Files
makelore/tests/e2e/fixtures/teacher-discussion-api.ts

76 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Deterministic responses for layout/interaction tests only; never imported by the app.
import type { TeacherDefinition, TeacherTopic, TeacherSend, TeacherDiscussionAction } from '../../../shared/coding-teacher';
import type { TeacherDiscussionContent } from '../../../shared/teacher-discussion';
const definition: TeacherDefinition = { schema_version: 1, teacher_id: 'coding-teacher', name: '麦洛智能体', description: '', avatar_id: 'avatar-01', welcome_message: '', suggested_questions: [], system_prompt: '测试', skills: [], model: { model_id: 'fixture', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1500 } };
const samples: Record<string, TeacherDiscussionContent> = {
ideas: { kind: 'ideas', title: '我的小狗世界', items: [{ id: 'dog', text: '养一只自己的小狗', state: 'kept' }, { id: 'breed', parentId: 'dog', text: '是一只柯基', state: 'kept' }, { id: 'garden', text: '有一片可以种东西的花园', state: 'kept' }, { id: 'food', parentId: 'garden', text: '种出来的东西可以喂小狗', state: 'suggested' }] },
structure: { kind: 'structure', title: '小狗和花园怎么连起来?', nodes: [{ id: 'world', label: '我的小狗世界' }, { id: 'dog', label: '照顾小狗', parentId: 'world' }, { id: 'breed', label: '一只柯基', parentId: 'dog', relation: '它的样子' }, { id: 'garden', label: '种植花园', parentId: 'world' }, { id: 'food', label: '收获食物,喂给小狗', parentId: 'garden', relation: '收获后' }] },
flow: { kind: 'flow', title: '我回家时,小狗会做什么?', nodes: [{ id: 'home', kind: 'event', label: '我回到家' }, { id: 'awake', kind: 'condition', label: '小狗醒着吗?' }, { id: 'welcome', kind: 'outcome', label: '跑过来欢迎我' }, { id: 'sleep', kind: 'outcome', label: '先睡觉,醒来再过来' }], edges: [{ id: 'one', from: 'home', to: 'awake' }, { id: 'yes', from: 'awake', to: 'welcome', label: '醒着' }, { id: 'no', from: 'awake', to: 'sleep', label: '睡着了' }] },
comparison: { kind: 'comparison', title: '哪一种更像你想要的小狗?', columns: [{ id: 'click', label: '点了才回应' }, { id: 'active', label: '会主动找我' }], rows: [{ id: 'home', label: '刚回到家的时候', cells: [{ columnId: 'click', text: '等我点一下,才跳起来。' }, { columnId: 'active', text: '看见我就跑过来,还带着玩具。' }] }, { id: 'busy', label: '我忙着种东西的时候', cells: [{ columnId: 'click', text: '安静等着,不打扰我。' }, { columnId: 'active', text: '有时来蹭一蹭,可能也会打断我。' }] }] },
};
const scenario = new URLSearchParams(location.search).get('kind') ?? 'ideas';
let current: TeacherTopic = { schemaVersion: 1, revision: 1, id: 'discussion-fixture', accountId: 'fixture', projectId: 'fixture', sourceConversationId: 'project', definition, version: 1, createdAt: 'now', updatedAt: 'now', requests: Array.from({ length: 8 }, (_, i) => ({ id: `r${i}`, text: i === 7 ? '我还没想好,我们边聊边想。' : '我希望这个游戏更有意思。', references: [], createdAt: 'now', sourceCursor: { workerGeneration: 0, seq: i }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: i === 7 ? '可以,我们先把已经想到的放在一起。哪里还不确定,就留着继续聊。' : '先从你最在意的一个小地方开始想。', ...(scenario === 'ordinary' && i === 7 ? { suggestedQuestions: ['有什么好玩的想法?'] } : {}) })), ...(samples[scenario] ? { discussion: { id: 'tool', revision: 1, status: 'active', content: structuredClone(samples[scenario]), ...(scenario === 'structure' ? { previousIdeas: structuredClone(samples.ideas) as Extract<TeacherDiscussionContent, { kind: 'ideas' }> } : {}) } } : {}) };
const stream = Object.assign(new EventTarget(), { close() {}, onerror: null, onopen: null });
if (scenario === 'rich-text') {
current.requests = [{
...current.requests[0], text: '帮我看看项目和最近的讨论',
response: '## 项目建议\n\n**先核对已有设计**,再决定下一步。\n\n- 检查入口文件\n- 保留当前玩法\n\n| 文件 | 作用 | 需要确认 |\n| --- | --- | --- |\n| src/main.ts | 初始化项目 | 游戏循环与计时 |\n| public/assets | 图片资源 | 构建后路径是否可用 |\n\n```js\nconst message = "保留字符串里的\\n,不把它变成实际换行";\nconst resourcePath = "/assets/" + "very-long-file-name-".repeat(15);\n```\n\n运动距离:$s = vt$。\n\n![项目截图](https://example.com/teacher-preview.png)\n\n[项目说明](https://example.com/readme)\n\n> 先验证观察到的现象。',
discussionError: '这次整理没有完成,先保留原来的内容。',
unparsedResponse: '{"reply":"正文已保留","tool":{"kind":"structure",',
}];
}
let timer: ReturnType<typeof setTimeout> | undefined;
const snapshot = () => { current.revision++; stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(current) })); };
export const teacherApi = {
catalog: async () => ({ items: [{ teacher_id: definition.teacher_id, version: 1, is_default: true, definition }] }),
config: async () => ({ enabled: true, definition }),
list: async () => ({ items: [{ id: current.id, title: '我的小狗世界' }], lastSelectedTopicId: current.id }),
read: async () => structuredClone(current),
create: async () => structuredClone(current),
events: async () => stream,
updateDiscussion: async (_base: string, _id: string, input: TeacherDiscussionAction) => {
const tool = current.discussion!;
if (input.action === 'pause' || input.action === 'finish') tool.status = input.action === 'pause' ? 'paused' : 'finished';
else tool.status = 'active';
if (input.action === 'back-ideas' && tool.previousIdeas) {
if (tool.content.kind === 'structure') tool.previousStructure = structuredClone(tool.content);
tool.content = structuredClone(tool.previousIdeas);
}
if (tool.content.kind === 'ideas') {
const item = tool.content.items.find(item => item.id === input.itemId);
if (item && input.action === 'keep-idea') item.state = 'kept';
if (item && input.action === 'aside-idea') item.state = 'aside';
if (item && input.action === 'first-idea') tool.content.firstItemId = item.id;
}
tool.revision++;
snapshot();
return structuredClone(current);
},
send: async (_base: string, _id: string, input: TeacherSend) => {
current.requests.push({ id: input.requestId, text: input.text, references: [], createdAt: 'now', status: 'running', sourceCursor: { workerGeneration: 0, seq: 1 }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, response: '', discussionContext: input.discussion });
snapshot();
timer = setTimeout(() => {
const request = current.requests.at(-1)!;
request.status = 'completed';
request.response = '好,已经把你的补充放在上面了。我们可以接着想。';
const tool = current.discussion;
if (tool?.status === 'active' && input.discussion) {
if (input.discussion.transition === 'structure' && tool.content.kind === 'ideas') {
tool.previousIdeas = structuredClone(tool.content);
tool.content = structuredClone(samples.structure);
} else if (tool.content.kind === 'ideas') tool.content.items.push({ id: `new-${tool.revision}`, text: input.text, state: 'kept' });
else if (tool.content.kind === 'structure') tool.content.nodes.push({ id: `new-${tool.revision}`, label: input.text, parentId: 'world' });
else if (tool.content.kind === 'flow') tool.content.nodes.push({ id: `new-${tool.revision}`, label: input.text, kind: 'outcome' });
else tool.content.rows.push({ id: `new-${tool.revision}`, label: '再想想这一点', cells: tool.content.columns.map(col => ({ columnId: col.id, text: '先观察实际玩的时候有什么不同。' })) });
tool.revision++;
}
snapshot();
}, 350);
return structuredClone(current);
},
cancel: async () => { clearTimeout(timer); current.requests.at(-1)!.status = 'cancelled'; snapshot(); return structuredClone(current); },
};
export const teacherTopicsPath = () => '/fixture';
export const legacyTopicBase = (base: string) => base;