From 40ce51dd70c596b22f6bd26f4a22f3b7c533c80c Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Tue, 23 Jun 2026 15:41:28 +0800 Subject: [PATCH 001/122] feat(tutorial): tailor leave-editor copy and route success modal to series - Show tutorial-specific copy in the leave-editor confirm dialog when a course is active; keep the original copy otherwise - Make the course success modal's "back to course series" button navigate to the current course's series instead of /tutorials Co-Authored-By: Claude Opus 4.8 --- spx-gui/src/apps/xbuilder/pages/editor/index.vue | 16 ++++++++++++---- .../tutorials/TutorialCourseSuccessModal.vue | 8 ++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/spx-gui/src/apps/xbuilder/pages/editor/index.vue b/spx-gui/src/apps/xbuilder/pages/editor/index.vue index e683866cbd..1bd952b7d5 100644 --- a/spx-gui/src/apps/xbuilder/pages/editor/index.vue +++ b/spx-gui/src/apps/xbuilder/pages/editor/index.vue @@ -58,6 +58,7 @@ import EditorContextProvider from '@/components/editor/EditorContextProvider.vue import ProjectEditor from '@/components/editor/ProjectEditor.vue' import { CodeEditorProvider, loadMonaco } from '@/components/editor/spx-code-editor' import { usePublishProject } from '@/components/project' +import { useTutorial } from '@/components/tutorials/tutorial' import { EditingMode, type ILocalCache } from '@/components/editor/editing' import { EditorState } from '@/components/editor/editor-state' import { cloudHelpers } from '@/models/common/cloud' @@ -84,6 +85,7 @@ const i18n = useI18n() const { t } = i18n const { isOnline } = useNetwork() const m = useMessage() +const tutorial = useTutorial() const confirmOpenTargetWithAnotherInCache = (targetName: string, cachedName: string): Promise => { return confirm({ @@ -215,15 +217,21 @@ onBeforeRouteLeave(async () => { async function checkChangesNotToBeSaved(es: EditorState) { const hasEdits = es.editing.mode === EditingMode.EffectFree && es.editing.dirty if (!hasEdits) return true + const inTutorial = tutorial.currentCourse != null return confirm({ title: t({ en: 'Leave editor', zh: '离开编辑器' }), - content: t({ - en: `Project edits will not be saved if you leave now. Are you sure to leave?`, - zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?` - }), + content: inTutorial + ? t({ + en: `Tutorial edits will not be saved if you leave now. Are you sure to leave?`, + zh: `教程中的修改不会被保存,确认要离开吗?` + }) + : t({ + en: `Project edits will not be saved if you leave now. Are you sure to leave?`, + zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?` + }), cancelText: t({ en: 'Keep editing', zh: '继续编辑' diff --git a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue index 4894b1177c..9c3563c26f 100644 --- a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue +++ b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue @@ -37,9 +37,9 @@ function handleCancel() { emit('cancelled') } -function handleBrowseTutorials() { +function handleBackToCourseSeries() { emit('cancelled') - router.push('/tutorials') + router.push(`/course-series/${props.series.id}`) } const hasNextCourse = computed(() => { @@ -96,8 +96,8 @@ const { fn: handleStartNextCourse } = useMessageHandle(
{{ courseCompleteMessage }}
- - {{ $t({ zh: '浏览所有课程', en: 'Browse all courses' }) }} + + {{ $t({ zh: '返回系列课程', en: 'Back to Series Courses' }) }} {{ $t({ zh: '学习下一个课程', en: 'Learn next course' }) }} From 81480734000e302ea416691a7ea60d88fee577d9 Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Tue, 23 Jun 2026 18:14:40 +0800 Subject: [PATCH 002/122] feat(tutorial): skip leave confirmation for course navigation Add a one-shot skip flag on Tutorial so explicit, expected navigations away from the editor do not trigger the leave confirmation: - "Back to course series" in the success modal requests the skip before navigating to the series page - startCourse requests the skip before pushing the course entrypoint, which may point to a non-editor route Co-Authored-By: Claude Opus 4.8 --- .../src/apps/xbuilder/pages/editor/index.vue | 1 + .../tutorials/TutorialCourseSuccessModal.vue | 1 + spx-gui/src/components/tutorials/tutorial.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/spx-gui/src/apps/xbuilder/pages/editor/index.vue b/spx-gui/src/apps/xbuilder/pages/editor/index.vue index 1bd952b7d5..f83000013d 100644 --- a/spx-gui/src/apps/xbuilder/pages/editor/index.vue +++ b/spx-gui/src/apps/xbuilder/pages/editor/index.vue @@ -215,6 +215,7 @@ onBeforeRouteLeave(async () => { * If it is OK to leave, return true, otherwise return false. */ async function checkChangesNotToBeSaved(es: EditorState) { + if (tutorial.consumeSkipLeaveConfirm()) return true const hasEdits = es.editing.mode === EditingMode.EffectFree && es.editing.dirty if (!hasEdits) return true const inTutorial = tutorial.currentCourse != null diff --git a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue index 9c3563c26f..fd9a95fdbc 100644 --- a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue +++ b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue @@ -39,6 +39,7 @@ function handleCancel() { function handleBackToCourseSeries() { emit('cancelled') + props.tutorial.requestSkipLeaveConfirm() router.push(`/course-series/${props.series.id}`) } diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index 1af158edbc..8c6bb5a873 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -61,6 +61,19 @@ export class Tutorial { this.abandonPredictionCountRef.value = 0 } + // One-shot flag to skip the editor's leave confirmation on the next navigation. + // Used when leaving the editor is an explicit, expected action (e.g. clicking + // "back to course series" in the course success modal), making a confirm redundant. + private skipLeaveConfirmOnce = false + requestSkipLeaveConfirm() { + this.skipLeaveConfirmOnce = true + } + consumeSkipLeaveConfirm() { + const skip = this.skipLeaveConfirmOnce + this.skipLeaveConfirmOnce = false + return skip + } + async startCourse(course: Course, series: CourseSeries): Promise { try { this.copilot.endCurrentSession() @@ -71,6 +84,10 @@ export class Tutorial { const { entrypoint } = course if (entrypoint) { + // A course entrypoint may point to a non-editor route. Navigating away from the + // editor would otherwise trigger its leave confirmation, but starting a course is + // an explicit, expected action, so skip the confirmation for this navigation. + this.requestSkipLeaveConfirm() await this.router.push(entrypoint) await until(this.isRouteLoaded) await timeout(100) // Wait for detailed UI rendering From 6aa0a0f30134d533ad41e06c0f1b0e56cb101c5d Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Tue, 23 Jun 2026 18:27:06 +0800 Subject: [PATCH 003/122] fix(tutorial): time-bound the leave-confirm skip to avoid a stuck flag A plain one-shot flag could leak: navigations that don't reach the editor's onBeforeRouteLeave (e.g. editor -> editor route updates when starting the next course, or starting a course from a non-editor page) never consume it, so a later genuine leave would silently skip its confirmation. Expire the skip request after skipLeaveConfirmTimeout instead. Co-Authored-By: Claude Opus 4.8 --- spx-gui/src/components/tutorials/tutorial.ts | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index 8c6bb5a873..ec3f44539f 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -15,6 +15,13 @@ import { tutorialCourseAbandonDismissal, tutorialCourseAbandonPrediction } from const tutorialKey: InjectionKey = Symbol('tutorial') +// Max age (ms) for a `requestSkipLeaveConfirm` request to still suppress the editor's +// leave confirmation. Time-bound so a request that is never consumed cannot get "stuck": +// some navigations (e.g. editor -> editor route updates, or starting a course from a +// non-editor page) never reach the editor's `onBeforeRouteLeave`, and an un-expiring +// flag would otherwise suppress a later, genuine leave confirmation. +const skipLeaveConfirmTimeout = 1000 + export function useTutorial() { const tutorial = inject(tutorialKey) if (tutorial == null) { @@ -61,16 +68,16 @@ export class Tutorial { this.abandonPredictionCountRef.value = 0 } - // One-shot flag to skip the editor's leave confirmation on the next navigation. - // Used when leaving the editor is an explicit, expected action (e.g. clicking - // "back to course series" in the course success modal), making a confirm redundant. - private skipLeaveConfirmOnce = false + // Skip the editor's leave confirmation for the navigation that immediately follows an + // explicit, expected action (e.g. clicking "back to course series" in the success modal, + // or starting a course). Time-bound via `skipLeaveConfirmTimeout` so it cannot get stuck. + private skipLeaveConfirmRequestedAt = 0 requestSkipLeaveConfirm() { - this.skipLeaveConfirmOnce = true + this.skipLeaveConfirmRequestedAt = Date.now() } consumeSkipLeaveConfirm() { - const skip = this.skipLeaveConfirmOnce - this.skipLeaveConfirmOnce = false + const skip = Date.now() - this.skipLeaveConfirmRequestedAt < skipLeaveConfirmTimeout + this.skipLeaveConfirmRequestedAt = 0 return skip } From dcd989bd4df61129c10af7b06c5090655123eed5 Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Wed, 24 Jun 2026 09:46:21 +0800 Subject: [PATCH 004/122] refactor(tutorial): address review feedback on success modal - Request the leave-confirm skip before emit('cancelled') so the time-bound window isn't shortened by the cancel handler - Wrap "back to course series" navigation in useMessageHandle to surface router.push rejections, consistent with handleStartNextCourse - Use sentence case for the button label: "Back to series courses" - Align the tutorial.ts comment with the actual button label Co-Authored-By: Claude Opus 4.8 --- .../tutorials/TutorialCourseSuccessModal.vue | 20 +++++++++++++------ spx-gui/src/components/tutorials/tutorial.ts | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue index fd9a95fdbc..7e84f0f5c9 100644 --- a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue +++ b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue @@ -37,11 +37,19 @@ function handleCancel() { emit('cancelled') } -function handleBackToCourseSeries() { - emit('cancelled') - props.tutorial.requestSkipLeaveConfirm() - router.push(`/course-series/${props.series.id}`) -} +const { fn: handleBackToCourseSeries } = useMessageHandle( + async () => { + // Request the skip before anything else so the time-bound window isn't shortened + // by work in the `cancelled` handler. + props.tutorial.requestSkipLeaveConfirm() + emit('cancelled') + await router.push(`/course-series/${props.series.id}`) + }, + { + en: 'Failed to go back to course series', + zh: '返回系列课程失败' + } +) const hasNextCourse = computed(() => { const currentCourse = props.course @@ -98,7 +106,7 @@ const { fn: handleStartNextCourse } = useMessageHandle(
- {{ $t({ zh: '返回系列课程', en: 'Back to Series Courses' }) }} + {{ $t({ zh: '返回系列课程', en: 'Back to series courses' }) }} {{ $t({ zh: '学习下一个课程', en: 'Learn next course' }) }} diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index ec3f44539f..8eb24997a3 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -69,7 +69,7 @@ export class Tutorial { } // Skip the editor's leave confirmation for the navigation that immediately follows an - // explicit, expected action (e.g. clicking "back to course series" in the success modal, + // explicit, expected action (e.g. clicking "Back to series courses" in the success modal, // or starting a course). Time-bound via `skipLeaveConfirmTimeout` so it cannot get stuck. private skipLeaveConfirmRequestedAt = 0 requestSkipLeaveConfirm() { From 414696ab28b19bea352e2058fa17a4d00042b7ab Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Thu, 25 Jun 2026 16:56:32 +0800 Subject: [PATCH 005/122] refactor(editor): introduce editor-owned leave-confirm extension point Invert the dependency so the editor no longer needs tutorial knowledge. Add an editor-owned `editorLeaveConfirm` controller exposing a generic message override and a one-shot, time-bound skip. Tutorial drives it the same way it drives copilot (TutorialRoot sets the tutorial-specific copy while a course is active; startCourse and the success modal request the skip), and the editor page's leave guard only reads the controller. The editor stays unaware of tutorials (no components/editor -> tutorials import), and pages/editor no longer imports useTutorial. Co-Authored-By: Claude Opus 4.8 --- .../src/apps/xbuilder/pages/editor/index.vue | 21 ++++----- .../src/components/editor/leave-confirm.ts | 47 +++++++++++++++++++ .../tutorials/TutorialCourseSuccessModal.vue | 3 +- .../src/components/tutorials/TutorialRoot.vue | 9 ++++ spx-gui/src/components/tutorials/tutorial.ts | 23 +-------- 5 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 spx-gui/src/components/editor/leave-confirm.ts diff --git a/spx-gui/src/apps/xbuilder/pages/editor/index.vue b/spx-gui/src/apps/xbuilder/pages/editor/index.vue index f83000013d..f0a9973c3d 100644 --- a/spx-gui/src/apps/xbuilder/pages/editor/index.vue +++ b/spx-gui/src/apps/xbuilder/pages/editor/index.vue @@ -58,8 +58,8 @@ import EditorContextProvider from '@/components/editor/EditorContextProvider.vue import ProjectEditor from '@/components/editor/ProjectEditor.vue' import { CodeEditorProvider, loadMonaco } from '@/components/editor/spx-code-editor' import { usePublishProject } from '@/components/project' -import { useTutorial } from '@/components/tutorials/tutorial' import { EditingMode, type ILocalCache } from '@/components/editor/editing' +import { editorLeaveConfirm } from '@/components/editor/leave-confirm' import { EditorState } from '@/components/editor/editor-state' import { cloudHelpers } from '@/models/common/cloud' import { localHelpers, type LocalHelpers } from '@/models/common/local' @@ -85,7 +85,6 @@ const i18n = useI18n() const { t } = i18n const { isOnline } = useNetwork() const m = useMessage() -const tutorial = useTutorial() const confirmOpenTargetWithAnotherInCache = (targetName: string, cachedName: string): Promise => { return confirm({ @@ -215,24 +214,20 @@ onBeforeRouteLeave(async () => { * If it is OK to leave, return true, otherwise return false. */ async function checkChangesNotToBeSaved(es: EditorState) { - if (tutorial.consumeSkipLeaveConfirm()) return true + if (editorLeaveConfirm.consumeSkipOnce()) return true const hasEdits = es.editing.mode === EditingMode.EffectFree && es.editing.dirty if (!hasEdits) return true - const inTutorial = tutorial.currentCourse != null return confirm({ title: t({ en: 'Leave editor', zh: '离开编辑器' }), - content: inTutorial - ? t({ - en: `Tutorial edits will not be saved if you leave now. Are you sure to leave?`, - zh: `教程中的修改不会被保存,确认要离开吗?` - }) - : t({ - en: `Project edits will not be saved if you leave now. Are you sure to leave?`, - zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?` - }), + content: t( + editorLeaveConfirm.messageOverride ?? { + en: `Project edits will not be saved if you leave now. Are you sure to leave?`, + zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?` + } + ), cancelText: t({ en: 'Keep editing', zh: '继续编辑' diff --git a/spx-gui/src/components/editor/leave-confirm.ts b/spx-gui/src/components/editor/leave-confirm.ts new file mode 100644 index 0000000000..c33ecf917a --- /dev/null +++ b/spx-gui/src/components/editor/leave-confirm.ts @@ -0,0 +1,47 @@ +import { shallowRef } from 'vue' + +import type { LocaleMessage } from '@/utils/i18n' + +// Max age (ms) for a skip request to still suppress the leave confirmation. Time-bound so a +// request that is never consumed cannot get "stuck": some navigations (e.g. editor -> editor +// route updates, or starting a course from a non-editor page) never reach the editor's +// `onBeforeRouteLeave`, and an un-expiring request would otherwise suppress a later, genuine +// leave confirmation. +const skipConfirmTimeout = 1000 + +/** + * Controls the editor's "leave editor" confirmation. + * + * This is an editor-owned extension point: other features (e.g. tutorials) can drive the + * editor's leave behavior without the editor depending on them. The editor only exposes + * generic capabilities here and stays ignorant of who is driving it. + * + * - `messageOverride`: replace the confirmation message (e.g. tutorial-specific copy). + * - `requestSkipOnce` / `consumeSkipOnce`: skip the confirmation for a single, expected + * navigation (e.g. starting a course, or returning to the course series). + */ +class EditorLeaveConfirm { + private messageOverrideRef = shallowRef(null) + + get messageOverride(): LocaleMessage | null { + return this.messageOverrideRef.value + } + setMessageOverride(message: LocaleMessage | null) { + this.messageOverrideRef.value = message + } + + private skipRequestedAt = 0 + requestSkipOnce() { + this.skipRequestedAt = Date.now() + } + consumeSkipOnce() { + const skip = Date.now() - this.skipRequestedAt < skipConfirmTimeout + this.skipRequestedAt = 0 + return skip + } +} + +// App-level single instance: there is only ever one editor leave confirmation in play, and +// the drivers (tutorial, success modal) live outside the editor component tree, so they reach +// it by import rather than provide/inject. +export const editorLeaveConfirm = new EditorLeaveConfirm() diff --git a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue index 7e84f0f5c9..58accbe808 100644 --- a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue +++ b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue @@ -8,6 +8,7 @@ import type { CourseSeries } from '@/apis/course-series' import { useI18n } from '@/utils/i18n' import { UIButton, UIImg, UIModal, UIModalClose } from '@/components/ui' +import { editorLeaveConfirm } from '@/components/editor/leave-confirm' import { DefaultException, useMessageHandle } from '@/utils/exception' import successImg from './success.png' @@ -41,7 +42,7 @@ const { fn: handleBackToCourseSeries } = useMessageHandle( async () => { // Request the skip before anything else so the time-bound window isn't shortened // by work in the `cancelled` handler. - props.tutorial.requestSkipLeaveConfirm() + editorLeaveConfirm.requestSkipOnce() emit('cancelled') await router.push(`/course-series/${props.series.id}`) }, diff --git a/spx-gui/src/components/tutorials/TutorialRoot.vue b/spx-gui/src/components/tutorials/TutorialRoot.vue index 1f39387300..892fb3e55f 100644 --- a/spx-gui/src/components/tutorials/TutorialRoot.vue +++ b/spx-gui/src/components/tutorials/TutorialRoot.vue @@ -7,6 +7,7 @@ import { useIsRouteLoaded } from '@/utils/route-loading' import { isTutorialTopic, provideTutorial, Tutorial } from './tutorial' import { useCopilot } from '@/components/copilot/context' +import { editorLeaveConfirm } from '@/components/editor/leave-confirm' import * as tutorialCourseSuccess from './TutorialCourseSuccess.vue' import * as tutorialCourseExitLink from './TutorialCourseExitLink' import * as tutorialStateIndicator from './TutorialStateIndicator.vue' @@ -27,7 +28,15 @@ watch( (currentCourse, _, onCleanup) => { if (currentCourse == null) return + // Drive the editor's leave confirmation with tutorial-specific copy while a course is + // active; the editor exposes the override and stays unaware of tutorials. + editorLeaveConfirm.setMessageOverride({ + en: 'Tutorial edits will not be saved if you leave now. Are you sure to leave?', + zh: '教程中的修改不会被保存,确认要离开吗?' + }) + const disposers = [ + () => editorLeaveConfirm.setMessageOverride(null), copilot.registerCustomElement({ tagName: tutorialCourseSuccess.tagName, description: tutorialCourseSuccess.detailedDescription, diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index 8eb24997a3..93a9e3a9e3 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -6,6 +6,7 @@ import { timeout, until } from '@/utils/utils' import { userSessionStorageRef } from '@/utils/user-storage' import type { Copilot, Topic } from '@/components/copilot/copilot' import { tagName as highlightLinkTagName } from '@/components/copilot/markdown-elements/HighlightLink.vue' +import { editorLeaveConfirm } from '@/components/editor/leave-confirm' import type { Course } from '@/apis/course' import type { CourseSeries } from '@/apis/course-series' @@ -15,13 +16,6 @@ import { tutorialCourseAbandonDismissal, tutorialCourseAbandonPrediction } from const tutorialKey: InjectionKey = Symbol('tutorial') -// Max age (ms) for a `requestSkipLeaveConfirm` request to still suppress the editor's -// leave confirmation. Time-bound so a request that is never consumed cannot get "stuck": -// some navigations (e.g. editor -> editor route updates, or starting a course from a -// non-editor page) never reach the editor's `onBeforeRouteLeave`, and an un-expiring -// flag would otherwise suppress a later, genuine leave confirmation. -const skipLeaveConfirmTimeout = 1000 - export function useTutorial() { const tutorial = inject(tutorialKey) if (tutorial == null) { @@ -68,19 +62,6 @@ export class Tutorial { this.abandonPredictionCountRef.value = 0 } - // Skip the editor's leave confirmation for the navigation that immediately follows an - // explicit, expected action (e.g. clicking "Back to series courses" in the success modal, - // or starting a course). Time-bound via `skipLeaveConfirmTimeout` so it cannot get stuck. - private skipLeaveConfirmRequestedAt = 0 - requestSkipLeaveConfirm() { - this.skipLeaveConfirmRequestedAt = Date.now() - } - consumeSkipLeaveConfirm() { - const skip = Date.now() - this.skipLeaveConfirmRequestedAt < skipLeaveConfirmTimeout - this.skipLeaveConfirmRequestedAt = 0 - return skip - } - async startCourse(course: Course, series: CourseSeries): Promise { try { this.copilot.endCurrentSession() @@ -94,7 +75,7 @@ export class Tutorial { // A course entrypoint may point to a non-editor route. Navigating away from the // editor would otherwise trigger its leave confirmation, but starting a course is // an explicit, expected action, so skip the confirmation for this navigation. - this.requestSkipLeaveConfirm() + editorLeaveConfirm.requestSkipOnce() await this.router.push(entrypoint) await until(this.isRouteLoaded) await timeout(100) // Wait for detailed UI rendering From e9eece93fb1465b02ffe3e41c138ff5b1d2a382f Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Mon, 6 Jul 2026 14:27:49 +0800 Subject: [PATCH 006/122] refactor(tutorial): scope leave-confirm skip to success-modal navigation Only the success modal's course navigations leave the editor as an explicit, expected action; other startCourse callers (course-start / course-series pages) are not leaving an editor. Request the skip at the success modal call site instead of inside startCourse, so it no longer applies to every caller. Co-Authored-By: Claude Opus 4.8 --- .../src/components/tutorials/TutorialCourseSuccessModal.vue | 5 +++++ spx-gui/src/components/tutorials/tutorial.ts | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue index 58accbe808..85997a5851 100644 --- a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue +++ b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue @@ -74,6 +74,11 @@ const { fn: handleStartNextCourse } = useMessageHandle( const tutorial = props.tutorial const nextCourse = await getCourse(currentSeries.courseIDs[findIndex + 1]) emit('cancelled') + // Skip the leave confirmation for the navigation `startCourse` performs: continuing to + // the next course is an explicit, expected action, and the course entrypoint may be a + // non-editor route. Requested right before `startCourse` so the time-bound window is + // not spent on the preceding `getCourse` request. + editorLeaveConfirm.requestSkipOnce() await tutorial.startCourse(nextCourse, currentSeries) }, { diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index 93a9e3a9e3..1af158edbc 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -6,7 +6,6 @@ import { timeout, until } from '@/utils/utils' import { userSessionStorageRef } from '@/utils/user-storage' import type { Copilot, Topic } from '@/components/copilot/copilot' import { tagName as highlightLinkTagName } from '@/components/copilot/markdown-elements/HighlightLink.vue' -import { editorLeaveConfirm } from '@/components/editor/leave-confirm' import type { Course } from '@/apis/course' import type { CourseSeries } from '@/apis/course-series' @@ -72,10 +71,6 @@ export class Tutorial { const { entrypoint } = course if (entrypoint) { - // A course entrypoint may point to a non-editor route. Navigating away from the - // editor would otherwise trigger its leave confirmation, but starting a course is - // an explicit, expected action, so skip the confirmation for this navigation. - editorLeaveConfirm.requestSkipOnce() await this.router.push(entrypoint) await until(this.isRouteLoaded) await timeout(100) // Wait for detailed UI rendering From c15609ab554a79ac16fbcf110e45ea8fea482f01 Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Tue, 7 Jul 2026 17:15:32 +0800 Subject: [PATCH 007/122] refactor(editor): unify leave-confirmation copy and drop message override Use a single leave-confirmation message for all effect-free cases instead of a tutorial-specific one. The copy also drops the "if you leave now" clause, which could imply the edits are saveable later. editorLeaveConfirm now only exposes the one-shot skip, and the tutorial layer no longer drives the confirmation copy. Co-Authored-By: Claude Opus 4.8 --- .../src/apps/xbuilder/pages/editor/index.vue | 10 ++++---- .../src/components/editor/leave-confirm.ts | 24 ++++--------------- .../src/components/tutorials/TutorialRoot.vue | 9 ------- 3 files changed, 9 insertions(+), 34 deletions(-) diff --git a/spx-gui/src/apps/xbuilder/pages/editor/index.vue b/spx-gui/src/apps/xbuilder/pages/editor/index.vue index f0a9973c3d..5f01eb2ea8 100644 --- a/spx-gui/src/apps/xbuilder/pages/editor/index.vue +++ b/spx-gui/src/apps/xbuilder/pages/editor/index.vue @@ -222,12 +222,10 @@ async function checkChangesNotToBeSaved(es: EditorState) { en: 'Leave editor', zh: '离开编辑器' }), - content: t( - editorLeaveConfirm.messageOverride ?? { - en: `Project edits will not be saved if you leave now. Are you sure to leave?`, - zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?` - } - ), + content: t({ + en: `Project edits will not be saved. Are you sure to leave?`, + zh: `对项目的修改将不会被保存,确定要离开吗?` + }), cancelText: t({ en: 'Keep editing', zh: '继续编辑' diff --git a/spx-gui/src/components/editor/leave-confirm.ts b/spx-gui/src/components/editor/leave-confirm.ts index c33ecf917a..0789d6b5da 100644 --- a/spx-gui/src/components/editor/leave-confirm.ts +++ b/spx-gui/src/components/editor/leave-confirm.ts @@ -1,7 +1,3 @@ -import { shallowRef } from 'vue' - -import type { LocaleMessage } from '@/utils/i18n' - // Max age (ms) for a skip request to still suppress the leave confirmation. Time-bound so a // request that is never consumed cannot get "stuck": some navigations (e.g. editor -> editor // route updates, or starting a course from a non-editor page) never reach the editor's @@ -12,24 +8,14 @@ const skipConfirmTimeout = 1000 /** * Controls the editor's "leave editor" confirmation. * - * This is an editor-owned extension point: other features (e.g. tutorials) can drive the - * editor's leave behavior without the editor depending on them. The editor only exposes - * generic capabilities here and stays ignorant of who is driving it. + * This is an editor-owned extension point: other features (e.g. tutorials) can skip the + * editor's leave confirmation without the editor depending on them. The editor only exposes + * a generic capability here and stays ignorant of who is driving it. * - * - `messageOverride`: replace the confirmation message (e.g. tutorial-specific copy). - * - `requestSkipOnce` / `consumeSkipOnce`: skip the confirmation for a single, expected - * navigation (e.g. starting a course, or returning to the course series). + * `requestSkipOnce` / `consumeSkipOnce`: skip the confirmation for a single, expected + * navigation (e.g. starting a course, or returning to the course series). */ class EditorLeaveConfirm { - private messageOverrideRef = shallowRef(null) - - get messageOverride(): LocaleMessage | null { - return this.messageOverrideRef.value - } - setMessageOverride(message: LocaleMessage | null) { - this.messageOverrideRef.value = message - } - private skipRequestedAt = 0 requestSkipOnce() { this.skipRequestedAt = Date.now() diff --git a/spx-gui/src/components/tutorials/TutorialRoot.vue b/spx-gui/src/components/tutorials/TutorialRoot.vue index 892fb3e55f..1f39387300 100644 --- a/spx-gui/src/components/tutorials/TutorialRoot.vue +++ b/spx-gui/src/components/tutorials/TutorialRoot.vue @@ -7,7 +7,6 @@ import { useIsRouteLoaded } from '@/utils/route-loading' import { isTutorialTopic, provideTutorial, Tutorial } from './tutorial' import { useCopilot } from '@/components/copilot/context' -import { editorLeaveConfirm } from '@/components/editor/leave-confirm' import * as tutorialCourseSuccess from './TutorialCourseSuccess.vue' import * as tutorialCourseExitLink from './TutorialCourseExitLink' import * as tutorialStateIndicator from './TutorialStateIndicator.vue' @@ -28,15 +27,7 @@ watch( (currentCourse, _, onCleanup) => { if (currentCourse == null) return - // Drive the editor's leave confirmation with tutorial-specific copy while a course is - // active; the editor exposes the override and stays unaware of tutorials. - editorLeaveConfirm.setMessageOverride({ - en: 'Tutorial edits will not be saved if you leave now. Are you sure to leave?', - zh: '教程中的修改不会被保存,确认要离开吗?' - }) - const disposers = [ - () => editorLeaveConfirm.setMessageOverride(null), copilot.registerCustomElement({ tagName: tutorialCourseSuccess.tagName, description: tutorialCourseSuccess.detailedDescription, From a9731380ab2f8128e640b43d104245be20dbe46c Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Mon, 6 Jul 2026 11:38:23 +0800 Subject: [PATCH 008/122] feat(editor): let copilot narrow the API references panel during tutorials Let a tutorial course focus the "API References" panel on just the APIs it uses, so learners are not distracted by the full catalog. - Generic editor: add an `apiReferenceFilter` predicate on CodeEditor; the API reference controller derives the shown items from it synchronously, so the panel updates reactively without re-running the async provider. - Copilot: an `api-reference-filter` custom element narrows the panel to a set of API definition ids, and a `list_api_reference_items` tool lets the copilot look up the exact ids to use. - Tutorial: the element is registered only while a course is active and the filter is reset when the course ends; the course prompt instructs the copilot to narrow the panel to the APIs the whole course uses. Co-Authored-By: Claude Opus 4.8 --- .../src/components/editor/copilot/index.ts | 26 +++++++ .../src/components/tutorials/TutorialRoot.vue | 12 ++++ .../tutorials/api-reference-filter.ts | 67 +++++++++++++++++++ spx-gui/src/components/tutorials/tutorial.ts | 3 + .../xgo-code-editor/api-reference.ts | 7 ++ .../components/xgo-code-editor/code-editor.ts | 11 ++- .../xgo-code-editor/ui/api-reference/index.ts | 18 ++++- 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 spx-gui/src/components/tutorials/api-reference-filter.ts diff --git a/spx-gui/src/components/editor/copilot/index.ts b/spx-gui/src/components/editor/copilot/index.ts index 3960c2b43b..3f29c47a0f 100644 --- a/spx-gui/src/components/editor/copilot/index.ts +++ b/spx-gui/src/components/editor/copilot/index.ts @@ -5,6 +5,7 @@ import { useCopilot } from '@/components/copilot/context' import { codeFilePathSchema, parseProjectIdentifier, projectIdentifierSchema } from '@/components/copilot/common' import { type ICopilotContextProvider, type ToolDefinition } from '@/components/copilot/copilot' import { skillSpxProject, skillXgoLanguage } from '@/components/copilot/skills/built-in' +import { stringifyDefinitionId } from '@/components/xgo-code-editor' import { cloudHelpers, type CloudHelpers } from '@/models/common/cloud' import { SpxProject } from '@/models/spx/project' import type { Sprite } from '@/models/spx/sprite' @@ -188,6 +189,30 @@ class GetCodeDiagnosticsTool implements ToolDefinition { } } +const listApiReferenceItemsParamsSchema = z.object({}) + +class ListApiReferenceItemsTool implements ToolDefinition { + name = 'list_api_reference_items' + description = + 'List the available API reference items (definition id and signature) for the code file the user is currently ' + + 'editing. Use the returned ids with the `api-reference-filter` element to narrow the "API References" panel.' + parameters = listApiReferenceItemsParamsSchema + + constructor(private codeEditor: CodeEditor) {} + + async implementation(_: z.infer, signal?: AbortSignal) { + const textDocument = this.codeEditor.getAttachedUI()?.activeTextDocument + if (textDocument == null) return [] + const items = await this.codeEditor.apiReferenceProvider.provideAPIReference({ + textDocument, + signal: signal ?? new AbortController().signal + }) + return items + .filter((item) => item.hiddenFromList !== true) + .map((item) => ({ id: stringifyDefinitionId(item.definition), overview: item.overview })) + } +} + class ProjectContextProvider implements ICopilotContextProvider { constructor(private editorCtx: EditorCtx) {} @@ -286,6 +311,7 @@ export function useSpxEditorCopilot(): void { d.addDisposer(copilot.registerTool(new GetSpriteContentTool(retriever))) d.addDisposer(copilot.registerTool(new GetProjectCodeTool(retriever))) d.addDisposer(copilot.registerTool(new GetCodeDiagnosticsTool(codeEditor))) + d.addDisposer(copilot.registerTool(new ListApiReferenceItemsTool(codeEditor))) d.addDisposer( copilot.registerCustomElement({ tagName: codeLink.tagName, diff --git a/spx-gui/src/components/tutorials/TutorialRoot.vue b/spx-gui/src/components/tutorials/TutorialRoot.vue index 1f39387300..8ef45c453d 100644 --- a/spx-gui/src/components/tutorials/TutorialRoot.vue +++ b/spx-gui/src/components/tutorials/TutorialRoot.vue @@ -7,15 +7,18 @@ import { useIsRouteLoaded } from '@/utils/route-loading' import { isTutorialTopic, provideTutorial, Tutorial } from './tutorial' import { useCopilot } from '@/components/copilot/context' +import { useCodeEditorRef } from '@/components/xgo-code-editor' import * as tutorialCourseSuccess from './TutorialCourseSuccess.vue' import * as tutorialCourseExitLink from './TutorialCourseExitLink' import * as tutorialStateIndicator from './TutorialStateIndicator.vue' +import * as apiReferenceFilter from './api-reference-filter' import { tutorialCourseAbandonPrediction, tutorialCourseAbandonDismissal } from './tutorial-course-abandon' const i18n = useI18n() const copilot = useCopilot() const router = useRouter() const isRouteLoaded = useIsRouteLoaded() +const codeEditorRef = useCodeEditorRef() const tutorial = new Tutorial(copilot, router, isRouteLoaded) @@ -44,6 +47,13 @@ watch( }), copilot.registerCustomElement(tutorialCourseAbandonPrediction), copilot.registerCustomElement(tutorialCourseAbandonDismissal), + copilot.registerCustomElement({ + tagName: apiReferenceFilter.tagName, + description: apiReferenceFilter.detailedDescription, + attributes: apiReferenceFilter.attributes, + isRaw: apiReferenceFilter.isRaw, + component: apiReferenceFilter.default + }), copilot.registerStateIndicatorComponent(tutorialStateIndicator.name, tutorialStateIndicator.default), copilot.registerQuickInputProvider({ provideQuickInput(lastCopilotMessage, topic) { @@ -75,6 +85,8 @@ watch( for (const dispose of disposers) { dispose() } + // Reset the API references panel when leaving the course, so the filter never outlives it. + codeEditorRef.value?.setAPIReferenceFilter(null) }) }, { diff --git a/spx-gui/src/components/tutorials/api-reference-filter.ts b/spx-gui/src/components/tutorials/api-reference-filter.ts new file mode 100644 index 0000000000..c654ffd756 --- /dev/null +++ b/spx-gui/src/components/tutorials/api-reference-filter.ts @@ -0,0 +1,67 @@ +import { z } from 'zod' +import { defineComponent, watch } from 'vue' +import { useCodeEditorRef, stringifyDefinitionId } from '@/components/xgo-code-editor' + +export const tagName = 'api-reference-filter' + +export const isRaw = false + +export const description = 'Narrow the "API References" panel to only the listed APIs.' + +export const detailedDescription = `Narrow the "API References" panel (left of the code editor) to only the listed \ +APIs, to focus the user during a guided step. \`ids\` is a comma-separated list of API definition IDs — get the exact \ +IDs from the \`list_api_reference_items\` tool. Use \`ids=""\` to show all APIs again. Re-emit this element with a new \ +list whenever the relevant APIs change or you previously made a mistake — the latest one wins. For example, \ +<${tagName} ids="xgo:github.com/goplus/spx/v2?Sprite.say#0,xgo:github.com/goplus/spx/v2?Game.onStart" /> keeps only \ +those two APIs visible.` + +export const attributes = z.object({ + ids: z + .string() + .describe( + 'Comma-separated API definition IDs to keep visible (from list_api_reference_items). Empty string shows all.' + ) +}) + +export type Props = { + /** Comma-separated API definition IDs to keep visible. Empty string shows all. */ + ids: string +} + +export default defineComponent( + (props) => { + const codeEditorRef = useCodeEditorRef() + + function apply() { + const codeEditor = codeEditorRef.value + if (codeEditor == null) return + const ids = props.ids + .split(',') + .map((id) => id.trim()) + .filter((id) => id !== '') + if (ids.length === 0) { + codeEditor.setAPIReferenceFilter(null) + return + } + const allow = new Set(ids) + codeEditor.setAPIReferenceFilter((item) => allow.has(stringifyDefinitionId(item.definition))) + } + + // Re-apply on initial render, when the editor becomes available (it may mount later than this + // element), and when the copilot updates the element with a new `ids` prop. + watch([codeEditorRef, () => props.ids], apply, { immediate: true }) + + return function render() { + return null + } + }, + { + name: 'ApiReferenceFilter', + props: { + ids: { + type: String, + default: '' + } + } + } +) diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index 1af158edbc..dc7215b49d 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -127,6 +127,8 @@ First do some preparation: * Clearly define the course completion criteria. +* If the course involves writing spx code, proactively narrow the "API References" panel (left of the code editor) at the start, before guiding the first coding step. Keep ALL the APIs the course uses anywhere — the union across every step, decided from the course goal and the reference project's code (the standard answer) — not just the current step's APIs, so the user can always find every API they will need throughout the course. Set this once and keep it stable for the whole course; only change it if the course genuinely needs a different set. This is expected for every coding course — do not wait for the user to ask. + Then guide the user through each step. For each step: 1. If extra information required, use appropriate tool to gather it. @@ -152,6 +154,7 @@ When coding tasks are involved: * Before offering coding suggestions, ensure you understand the current code. If not, use appropriate tools to review it first. * Avoid giving complete solution code directly. Instead, guide the user step-by-step with hints and explanations. * Prefer to insert code by dragging corresponding items (if available) from "API References" into the code editor over providing manual code snippets. +* Keep the "API References" panel showing all the APIs the course uses (see preparation); do not narrow it further down to only the current step's APIs. When tool result received: diff --git a/spx-gui/src/components/xgo-code-editor/api-reference.ts b/spx-gui/src/components/xgo-code-editor/api-reference.ts index 0c3c404057..20c93e0567 100644 --- a/spx-gui/src/components/xgo-code-editor/api-reference.ts +++ b/spx-gui/src/components/xgo-code-editor/api-reference.ts @@ -7,6 +7,13 @@ import type { BaseContext, DefinitionDocumentationItem } from './common' export type APIReferenceItem = DefinitionDocumentationItem +/** + * Predicate to narrow which API reference items are shown in the panel. + * `null` (no filter) means show all items. The editor stays agnostic about how + * the predicate is built; consumers decide the rule. + */ +export type APIReferenceFilter = (item: APIReferenceItem) => boolean + export type APIReferenceContext = BaseContext export type APICategoryViewInfo = { diff --git a/spx-gui/src/components/xgo-code-editor/code-editor.ts b/spx-gui/src/components/xgo-code-editor/code-editor.ts index 0b3962f447..c8e499c6ef 100644 --- a/spx-gui/src/components/xgo-code-editor/code-editor.ts +++ b/spx-gui/src/components/xgo-code-editor/code-editor.ts @@ -5,7 +5,7 @@ import type { History } from '@/components/editor/history' import type { IXGoProject } from './project' import { type IDocumentBase, DocumentBase } from './document-base' import type { ILSPClient } from './lsp/types' -import { EmptyAPIReferenceProvider } from './api-reference' +import { EmptyAPIReferenceProvider, type APIReferenceFilter } from './api-reference' import { type ICodeEditorUIController, type IDiagnosticsProvider, @@ -80,6 +80,7 @@ export class CodeEditor extends Disposable { this.inlayHintProviderRef = shallowRef(new InlayHintProvider(params.lspClient)) this.inputHelperProviderRef = shallowRef(new InputHelperProvider(params.lspClient, () => this.resourceAdapter)) this.apiReferenceProviderRef = shallowRef(new EmptyAPIReferenceProvider()) + this.apiReferenceFilterRef = shallowRef(null) this.completionProviderRef = shallowRef( new CompletionProvider(params.lspClient, documentBase, params.project.classFramework) ) @@ -135,6 +136,14 @@ export class CodeEditor extends Disposable { this.apiReferenceProviderRef.value = provider } + private apiReferenceFilterRef: ShallowRef + get apiReferenceFilter() { + return this.apiReferenceFilterRef.value + } + setAPIReferenceFilter(filter: APIReferenceFilter | null) { + this.apiReferenceFilterRef.value = filter + } + private completionProviderRef: ShallowRef get completionProvider() { return this.completionProviderRef.value diff --git a/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts b/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts index 6d891c8c7f..6df1b7f5de 100644 --- a/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts +++ b/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts @@ -23,10 +23,26 @@ export class APIReferenceController extends Disposable { return provider.provideAPIReference({ textDocument, signal }) }, true) - get items() { + /** Items as loaded from the provider, before applying the filter. */ + private get loadedItems() { return this.itemsMgr.result.data } + /** + * Items exposed to consumers, after applying `codeEditor.apiReferenceFilter`. Filtering is an + * internal concern of the controller: it is a synchronous derivation over the already-loaded items, + * so filter changes update the UI reactively without re-running the async provider. Falls back to + * the full list when the filter matches nothing, to avoid leaving the panel empty. + */ + get items() { + const items = this.loadedItems + if (items == null) return null + const filter = this.ui.codeEditor.apiReferenceFilter + if (filter == null) return items + const filtered = items.filter(filter) + return filtered.length > 0 ? filtered : items + } + get error() { return this.itemsMgr.result.error } From a13c8ce0cadf4c49bd754fcb0544b5ff98d2adcb Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Fri, 26 Jun 2026 11:10:52 +0800 Subject: [PATCH 009/122] feat(editor): add Copilot in-editor code guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of auto-applying patches or simply point to the editor area using a purple arrow, Copilot now guides the user to make the edit themselves in the editor, so children practice writing code by hand. A reply can render four guide elements(custom elements), each backed by an in-editor overlay: - [NEW] code-drag-hint — opens a drop target at a line and highlights the matching API reference item, for inserting a new statement by drag - [NEW] code-type-hint — opens a pre-indented blank line with a "Type the code here" chip and a green reference ghost, for typing a new statement - [NEW] code-delete-hint — highlights a range in red, for deleting it - [MODIFIED] code-change-hint — shows the deletion (red) and addition (green) at once, for replacing existing code (inline or whole-line) The CodeGuideController keeps a single active guide, navigates to it, and auto-dismisses it once the suggested edit is done (whitespace-insensitive). Completion for delete/change is region-local: an invisible Monaco decoration tracks the target range so it stays correct even when the user edits elsewhere. Shared logic lives in use-code-guide.ts (re-show on stream settle, reopen on click, skip-if-already-done, blank-line scaffolding) so all elements behave consistently. Changes to be committed: new file: node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json modified: spx-gui/src/components/copilot/CopilotRound.vue modified: spx-gui/src/components/copilot/context.ts modified: spx-gui/src/components/copilot/copilot.ts modified: spx-gui/src/components/editor/copilot/CodeChange.vue new file: spx-gui/src/components/editor/copilot/CodeDeleteHint.vue new file: spx-gui/src/components/editor/copilot/CodeDragHint.vue new file: spx-gui/src/components/editor/copilot/CodeTypeHint.vue modified: spx-gui/src/components/editor/copilot/index.ts new file: spx-gui/src/components/editor/copilot/use-code-guide.ts modified: spx-gui/src/components/xgo-code-editor/index.ts modified: spx-gui/src/components/xgo-code-editor/ui/CodeEditorUI.vue modified: spx-gui/src/components/xgo-code-editor/ui/api-reference/APIReferenceItem.vue modified: spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts modified: spx-gui/src/components/xgo-code-editor/ui/code-editor-ui.ts new file: spx-gui/src/components/xgo-code-editor/ui/code-guide/CodeGuideUI.vue new file: spx-gui/src/components/xgo-code-editor/ui/code-guide/index.test.ts new file: spx-gui/src/components/xgo-code-editor/ui/code-guide/index.ts modified: spx-gui/src/components/xgo-code-editor/ui/common.ts --- .../results.json | 1 + .../src/components/copilot/CopilotRound.vue | 4 + spx-gui/src/components/copilot/context.ts | 21 +- spx-gui/src/components/copilot/copilot.ts | 11 + .../components/editor/copilot/CodeChange.vue | 315 ++++++++++--- .../editor/copilot/CodeDeleteHint.vue | 111 +++++ .../editor/copilot/CodeDragHint.vue | 110 +++++ .../editor/copilot/CodeTypeHint.vue | 111 +++++ .../src/components/editor/copilot/index.ts | 42 +- .../editor/copilot/use-code-guide.ts | 207 +++++++++ .../src/components/xgo-code-editor/index.ts | 2 + .../xgo-code-editor/ui/CodeEditorUI.vue | 8 + .../ui/api-reference/APIReferenceItem.vue | 29 +- .../xgo-code-editor/ui/api-reference/index.ts | 47 +- .../xgo-code-editor/ui/code-editor-ui.ts | 26 ++ .../ui/code-guide/CodeGuideUI.vue | 428 ++++++++++++++++++ .../ui/code-guide/index.test.ts | 426 +++++++++++++++++ .../xgo-code-editor/ui/code-guide/index.ts | 291 ++++++++++++ .../components/xgo-code-editor/ui/common.ts | 45 ++ 19 files changed, 2159 insertions(+), 76 deletions(-) create mode 100644 node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json create mode 100644 spx-gui/src/components/editor/copilot/CodeDeleteHint.vue create mode 100644 spx-gui/src/components/editor/copilot/CodeDragHint.vue create mode 100644 spx-gui/src/components/editor/copilot/CodeTypeHint.vue create mode 100644 spx-gui/src/components/editor/copilot/use-code-guide.ts create mode 100644 spx-gui/src/components/xgo-code-editor/ui/code-guide/CodeGuideUI.vue create mode 100644 spx-gui/src/components/xgo-code-editor/ui/code-guide/index.test.ts create mode 100644 spx-gui/src/components/xgo-code-editor/ui/code-guide/index.ts diff --git a/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json new file mode 100644 index 0000000000..21c4bf8989 --- /dev/null +++ b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json @@ -0,0 +1 @@ +{"version":"4.1.9","results":[[":spx-gui/src/components/xgo-code-editor/ui/code-guide/index.test.ts",{"duration":0,"failed":true}]]} \ No newline at end of file diff --git a/spx-gui/src/components/copilot/CopilotRound.vue b/spx-gui/src/components/copilot/CopilotRound.vue index 58133e2f19..38bcc0cd17 100644 --- a/spx-gui/src/components/copilot/CopilotRound.vue +++ b/spx-gui/src/components/copilot/CopilotRound.vue @@ -1,6 +1,7 @@