Skip to content

[Demo only DO NOT MERGE] Game window design#3330

Draft
qingqing-ux wants to merge 2 commits into
goplus:uifrom
qingqing-ux:game-window-design
Draft

[Demo only DO NOT MERGE] Game window design#3330
qingqing-ux wants to merge 2 commits into
goplus:uifrom
qingqing-ux:game-window-design

Conversation

@qingqing-ux

Copy link
Copy Markdown
Collaborator

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +100 to +103
if (course.entrypoint.includes('Coding-Course-3')) {
names.add('step')
names.add('turn')
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use optional chaining when accessing course.entrypoint to prevent a potential TypeError if entrypoint is undefined or null.

  if (course.entrypoint?.includes('Coding-Course-3')) {
    names.add('step')
    names.add('turn')
  }

Comment on lines +110 to +112
if (course.entrypoint.includes('Coding-Course-3')) {
return ['step distance', 'turn direction']
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use optional chaining when accessing course.entrypoint to prevent a potential TypeError if entrypoint is undefined or null.

  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 !== '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Provide a fallback empty string for item.overview to prevent normalizeAPIName from throwing a TypeError if overview is undefined or null.

  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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Provide a fallback empty string for item.overview to prevent normalizeAPIName from throwing a TypeError if overview is undefined or null.

  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' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use optional chaining when accessing editorCtx.state.runtime to prevent a potential TypeError if runtime is undefined or null.

    editorCtx.state.runtime?.setRunning({ mode: 'none' })

@qingqing-ux qingqing-ux changed the base branch from dev to ui July 7, 2026 03:04
@qingqing-ux qingqing-ux marked this pull request as draft July 7, 2026 03:04

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...) #78c966 block plus its background-size list is copy-pasted verbatim between spx-gui/src/components/editor/ProjectEditor.vue (.tutorial-preview-pane) and spx-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 isTutorialCourse predicate. 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 a useIsTutorialCourse() composable in tutorials/tutorial.ts so the definition lives in one place.
  • EditorPreview.vue — resize watch writes on the non-tutorial path too. watch(windowSize, ...) in CopilotUI.vue:110 now writes tutorialPanelHeight.value (a localStorageRef) on every window resize regardless of whether a tutorial course is active, causing needless localStorage writes. Consider gating the write behind isTutorialCourse.value and/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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant