-
Notifications
You must be signed in to change notification settings - Fork 57
feat(editor): let copilot narrow the API references panel during tutorials #3304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { z } from 'zod' | ||
| import { defineComponent, watch } from 'vue' | ||
| import { useCodeEditorRef, parseDefinitionName } 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. \`names\` is a comma-separated list of API names, as named in the spx \ | ||
| API reference (e.g. \`onStart\`, \`say\`, \`move\`). Use \`names=""\` 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} names="onStart, say" /> keeps only "onStart" and "say" visible.` | ||
|
|
||
| export const attributes = z.object({ | ||
| names: z | ||
| .string() | ||
| .describe('Comma-separated API names to keep visible, e.g., `onStart, say, move`. Empty string shows all.') | ||
| }) | ||
|
|
||
| export type Props = { | ||
| /** Comma-separated API names to keep visible. Empty string shows all. */ | ||
| names: string | ||
| } | ||
|
|
||
| export default defineComponent<Props>( | ||
| (props) => { | ||
| const codeEditorRef = useCodeEditorRef() | ||
|
|
||
| function apply() { | ||
| const codeEditor = codeEditorRef.value | ||
| if (codeEditor == null) return | ||
| const names = props.names | ||
| .split(',') | ||
| .map((name) => name.trim().toLowerCase()) | ||
| .filter((name) => name !== '') | ||
| if (names.length === 0) { | ||
| codeEditor.setAPIReferenceFilter(null) | ||
| return | ||
| } | ||
| const allow = new Set(names) | ||
| codeEditor.setAPIReferenceFilter((item) => { | ||
| const fullName = item.definition.name | ||
| if (fullName == null) return false | ||
| // Match either the receiver-qualified name (e.g. `Sprite.say`) or the bare method name (`say`), | ||
| // so the model can use the plain names it knows from the spx API reference skill. | ||
| const [, method] = parseDefinitionName(fullName) | ||
| return allow.has(fullName.toLowerCase()) || allow.has(method.toLowerCase()) | ||
| }) | ||
| } | ||
|
|
||
| // 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 `names` prop. | ||
| watch([codeEditorRef, () => props.names], apply, { immediate: true }) | ||
|
|
||
| return function render() { | ||
| return null | ||
| } | ||
| }, | ||
| { | ||
| name: 'ApiReferenceFilter', | ||
| props: { | ||
| names: { | ||
| type: String, | ||
| default: '' | ||
| } | ||
| } | ||
| } | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ import { | |
| } from '../spx-code-editor' | ||
| import * as codeLink from './CodeLink' | ||
| import * as codeChange from './CodeChange.vue' | ||
| import * as apiReferenceFilter from './ApiReferenceFilter' | ||
| import CodeBlock from './CodeBlock.vue' | ||
|
|
||
| class Retriever { | ||
|
|
@@ -286,6 +287,15 @@ 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.registerCustomElement({ | ||
| tagName: apiReferenceFilter.tagName, | ||
| description: apiReferenceFilter.detailedDescription, | ||
| attributes: apiReferenceFilter.attributes, | ||
| isRaw: apiReferenceFilter.isRaw, | ||
| component: apiReferenceFilter.default | ||
| }) | ||
| ) | ||
| d.addDisposer( | ||
| copilot.registerCustomElement({ | ||
| tagName: codeLink.tagName, | ||
|
|
@@ -316,6 +326,15 @@ export function useSpxEditorCopilot(): void { | |
| }) | ||
| ) | ||
|
|
||
| // The API reference filter is transient state scoped to a copilot session: clear it whenever the | ||
| // session ends or switches (e.g. a tutorial course finishes), so a filter never leaks past its context. | ||
| d.addDisposer( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这边是把 apiReferenceFilter 的控制放在 copilot 这里,而不是 tutorial 中,考虑的是? 我感觉放 tutorial 中可能好点,主要是暂时没想到在 tutorial 外 copilot 需要控制 apiReferenceFilter 的合理用例 |
||
| watch( | ||
| () => copilot.currentSession, | ||
| () => codeEditor.setAPIReferenceFilter(null) | ||
| ) | ||
| ) | ||
|
|
||
| watch( | ||
| () => editorCtx.state.runtime, | ||
| (editorRuntime, _, onCleanup) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,21 @@ export class APIReferenceController extends Disposable { | |
| return this.itemsMgr.result.data | ||
| } | ||
|
|
||
| /** | ||
| * Items after applying `codeEditor.apiReferenceFilter`. This 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 filteredItems() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 建议 filter 后的数据叫 |
||
| const items = this.items | ||
| 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 | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不同的 API definition 可能有相同的 name(不过 ID 是各自唯一的),比如
say word跟say word, seconds的 name 都是say,通过 name 控制的话这俩要么都留要么都不留,是你预期的吗?