[Demo only DO NOT MERGE] Game window design#3330
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a tutorial course mode to the SPX editor, adapting various UI components (such as the main editor layout, Copilot panel, preview controls, navbar, and code editor) to provide a streamlined, focused learning experience. It also adds comprehensive design engineering workflow documentation and Pencil design assets, while removing obsolete prototype files. The review feedback focuses on improving code robustness by recommending optional chaining and fallback values to prevent potential TypeErrors when accessing course properties or API reference overviews.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (course.entrypoint.includes('Coding-Course-3')) { | ||
| names.add('step') | ||
| names.add('turn') | ||
| } |
| if (course.entrypoint.includes('Coding-Course-3')) { | ||
| return ['step distance', 'turn direction'] | ||
| } |
| function getAPINameCandidates(item: DefinitionDocumentationItem) { | ||
| const definitionName = item.definition.name ?? '' | ||
| const methodName = definitionName.split('.').at(-1) ?? definitionName | ||
| return [definitionName, methodName, item.overview].map(normalizeAPIName).filter((name) => name !== '') |
| const names = normalizedAllowedNames.value | ||
| const overviews = normalizedAllowedOverviews.value | ||
| const filterText = normalizedFilterText.value | ||
| if (overviews.length > 0) return overviews.includes(normalizeAPIName(item.overview)) |
| lastPanicOutput.value = null | ||
| if (props.tutorialMode) { | ||
| runnerState.value = 'initial' | ||
| editorCtx.state.runtime.setRunning({ mode: 'none' }) |
There was a problem hiding this comment.
Review: Game window design (tutorial layout)
This is a demo PR ([Demo only DO NOT MERGE]). Almost all of the 129 changed files are design artifacts (ui/** .pen files, images, and Chinese docs) — those were not reviewed. Review focused on the 9 changed spx-gui/src source files that implement the tutorial "game window" layout.
No security findings. No high-severity performance regressions. The substantive findings are correctness/maintainability concerns in the API-reference filtering logic, which I've left inline.
Additional findings (no reliable single inline line)
- Duplicated decorative background CSS. The multi-stop
radial-gradient(...) #78c966block plus itsbackground-sizelist is copy-pasted verbatim betweenspx-gui/src/components/editor/ProjectEditor.vue(.tutorial-preview-pane) andspx-gui/src/components/editor/preview/EditorPreview.vue(.tutorial-preview .stage-viewer-container, .runner-host). Extract into a shared class / custom property so the tutorial look is defined once. - Duplicated
isTutorialCoursepredicate.const tutorial = useMaybeTutorial()+const isTutorialCourse = computed(() => tutorial?.currentCourse != null)is repeated across ~5 components (apps/xbuilder/pages/editor/index.vue,CopilotUI.vue,ProjectEditor.vue,EditorNavbar.vue,CodeEditorUI.vue). Consider auseIsTutorialCourse()composable intutorials/tutorial.tsso the definition lives in one place. EditorPreview.vue— resize watch writes on the non-tutorial path too.watch(windowSize, ...)inCopilotUI.vue:110now writestutorialPanelHeight.value(alocalStorageRef) on every window resize regardless of whether a tutorial course is active, causing needless localStorage writes. Consider gating the write behindisTutorialCourse.valueand/or only writing when the clamped value changed.
Note: the useMaybeTutorial() addition, named layout constants, and the final-commit simplification of the preview header back to a static UICardHeader are all clean.
| if (course == null) return null | ||
|
|
||
| const names = new Set<string>() | ||
| const rawCourse = course as Record<string, unknown> |
There was a problem hiding this comment.
This loop probes five speculative keys (apis, apiReferences, apiReferenceIds, allowedApis, allowedAPIReferenceIds) via a course as Record<string, unknown> cast, but the Course type (spx-gui/src/apis/course.ts) only has id, owner, title, thumbnail, entrypoint, references, prompt. None of these keys exist on Course or are returned by the backend, so every lookup is undefined and the loop is dead code — and the cast silently defeats type-checking that would otherwise flag it. Remove it, or if per-course allowed-API metadata is really needed, add it to the Course type and API layer explicitly (with a comment explaining intent).
| } | ||
| } | ||
|
|
||
| if (course.entrypoint.includes('Coding-Course-3')) { |
There was a problem hiding this comment.
Hardcoded course.entrypoint.includes('Coding-Course-3') matching (repeated at line 110) plus hardcoded api names (step, turn) and overviews (step distance, turn direction) couples generic layout code to one specific course by an entrypoint-URL substring. It's brittle (renaming the course silently breaks filtering), non-scalable (every new course needs a code change here), and undocumented. Prefer driving the allowed-API list from the course/backend data; at minimum add a comment marking this as temporary demo scaffolding with a tracking issue.
| if (overviews.length > 0) return overviews.includes(normalizeAPIName(item.overview)) | ||
| if (names.length === 0 && filterText === '') return true | ||
| return getAPINameCandidates(item).some((candidate) => { | ||
| return names.includes(candidate) || filterText.includes(candidate) |
There was a problem hiding this comment.
filterText.includes(candidate) does substring matching of a normalized API-name candidate against the concatenated normalized course text (id + title + entrypoint + prompt). Since normalizeAPIName strips all non-alphanumerics and lowercases, short candidates cause false positives — e.g. an api/method fragment like on, go, or set will match almost any prompt containing those letters (python, message, settings). That defeats the filter's purpose of restricting shown APIs. Use token/word matching or an explicit allow-list instead of free-text substring containment. Also consider making normalizedAllowedNames a Set for O(1) membership rather than Array.includes in the .some().
|
|
||
| const props = defineProps<{ | ||
| controller: APIReferenceController | ||
| variant?: 'sidebar' | 'strip' |
There was a problem hiding this comment.
The four new public props (variant, filterText, allowedNames, allowedOverviews) have no JSDoc, and their interaction in isAllowedInTutorial is non-obvious: if allowedOverviews is non-empty it short-circuits and wins outright, so the allowedNames values (step/turn) supplied alongside it from CodeEditorUI are effectively dead. Document the precedence between the three filter inputs (or simplify to one clear strategy), since this is a shared component being extended.
| return Math.min(getMaxTutorialPanelHeight(), Math.max(tutorialPanelMinHeight, height)) | ||
| } | ||
|
|
||
| // resize the panel to fit the window size |
There was a problem hiding this comment.
This comment (// resize the panel to fit the window size) now under-describes the block: the watch body was extended to also re-clamp tutorialPanelHeight on resize, not just call updatePanelClampedPosition(). Update the comment to reflect both behaviors.
No description provided.