Compare commits

...
2 Commits
Author SHA1 Message Date
brother7 f443f3bc66 docs: 记录快捷提示主分支集成与工作树清理
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
2026-09-29 17:23:55 +08:00
brother7 53d5b6ba5b feat: 按老师发布配置渲染快捷提示 2026-09-29 17:04:55 +08:00
13 changed files with 215 additions and 4 deletions

No files matched your search

@@ -10,6 +10,8 @@
## Decision
- 2026-09-29 用户要求并授权合并可配置快捷提示。老师快捷提示由 Yuxi 的有序 `teacher_shortcuts` 配置拥有,每项分别保存展示文案 `label` 和实际提示词 `prompt`;最多 8 项,去除首尾空白后文案 1–40 字符、提示词 1–6000 字符。普通保存只改草稿,显式发布固定列表并以 `shortcuts` 下发;同一发布重试读取原快照。明确的空列表隐藏所有入口,历史快照缺字段或 null 保留客户端旧入口,未配置草稿提供可编辑的默认项。Makelore 按顺序渲染文案,点击通过普通提问发送实际提示词,聊天记录显示实际提问;目录刷新保留项目聊天和草稿。
- 2026-09-29 用户确认一次显式发布和及时刷新并授权合并。Yuxi 管理员在可管理的原生智能体编辑页一次“保存并发布”即可更新 Makelore;普通保存只改草稿。Yuxi 固定不可变快照并携同一操作身份交给 WS,只有 WS 登记提交后才报成功,失败重试不发布后来编辑的草稿。WS 按来源绑定稳定老师 ID,首次复用唯一启用旧项,歧义需显式选择;保留默认、停用和历史,不自动合并旧身份,重新启用需要显式动作。
- 前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。 五秒是检查间隔,实际可见时间还包含网络请求,不承诺离线或故障时延迟。
@@ -1,5 +1,7 @@
# Decision Index
- [老师可配置快捷提示](ADR-2026-09-22-coding-teacher.md):2026-09-29 已合并;Yuxi 有序展示文案/实际提示词,显式发布冻结,WS 版本化下发,客户端普通提问;空列表隐藏入口,历史缺字段保持旧入口。
- 2026-09-29 修订[下发智能体咨询决定](ADR-2026-09-22-coding-teacher.md):Yuxi 一次发布保持稳定老师身份;客户端五秒前台 revision 检查、入场/焦点刷新独立于聊天状态,保留项目连续聊天和运行快照。
- 2026-09-26 已确认[老师 Agent 产品定位](../00-brief/project-positioning.md#老师-agent-的产品定位):某一领域杰出人物的能力转化而成的智能体,供平台学生使用;蒸馏属于实现方法,以真实案例验证领域能力;补充下方既有咨询技术决定。
@@ -1,5 +1,7 @@
# System Overview
咨询输入区从当前老师发布定义的 `shortcuts` 按序渲染展示文案,并通过已有普通提问路径发送实际提示词;不附加旧 suggestions 意图。明确的空列表隐藏入口,缺字段/null 保留旧入口。多项长文案在限定高度内滚动,正在回答时不可点击;目录更新不替换项目聊天或草稿,同一未确认提问重试保留请求身份。详见[老师决定](../10-decisions/ADR-2026-09-22-coding-teacher.md)。
云端咨询的 Main 声明 `read_protocol=2`:目录按层分页(默认深度 4、最多 6,跳过生成缓存),文件与捕获会话返回连续文本及准确的下一页行/Unicode 字符列。文件增量解码,不受 UI 预览大小限制。每问题最多 12 批、累计成功结果 64 KiB、单页 8 KiB;剩余不足 512 字节时停止读取。Yuxi 消费最后一批结果后通过 `tool_choice: none` 请求最终答案,保留原身份、版本、时限与幂等续接;未声明新协议的旧客户端保持六批。工具活动以问题与逻辑 tool_call_id 合并跨 Run 重放,原始 interrupt 工具错误不单独判为失败。Main 区分读取额度、上下文失效与协议错误;失败问题不自动重试。须先更新 Yuxi API/worker,再更新客户端;生产供应商与安装版验收仍待完成。
智能体图标跟随发布定义:Yuxi 将原生 `icon` 固定到发布快照,Works Square 转为公开地址 `icon_url` 下发。Makelore 顶栏、邀请、面板标题、欢迎区和消息行统一读取该字段;缺失或加载失败时回退到既有 `avatar_id`。每个新问题取当前发布定义,已接受问题保持原版本;自定义图标使用正常图像渲染,内置像素头像保持像素样式。
@@ -4,6 +4,8 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
## Integrated Through
- 2026-09-29:按用户要求,将已独立审查的快捷提示源 `53d5b6b` 无冲突快进合入本地 `main`。Yuxi 配置多项展示文案和实际提示词,显式发布冻结后经 WS 下发,Makelore 按序显示并通过普通提问发送;空列表隐藏入口,历史缺字段保留旧入口。源验证 73 项 UI 单测、21 项浏览器布局场景、类型检查、lint 和生产构建通过;独立 Reviewer PASS。合并保留已审查产品与测试,新增修改仅为项目记忆;原有跨仓 7 份外来文档保持未提交。快捷提示不新增迁移;上线顺序为 WS API、Yuxi、Makelore,本次未推送、部署或打包。见[源记录](tasks/20260929-teacher-shortcuts-ml-8ce941d7.md)及[集成记录](tasks/20260929-integrate-shortcuts-ml-b75d3c29.md)。
- 2026-09-29:用户确认“一次发布,自动更新 Makelore”并要求合并。源 `913ccad5774df9a8e1f8259394658a524f8255d4` 无冲突快进合入本地 `main`。Yuxi 管理员在可管理的原生智能体编辑页一次“保存并发布”即可更新 Makelore;普通保存只改草稿。Yuxi 固定不可变快照并携同一操作身份交给 WS,只有 WS 登记提交后才报成功,失败重试不发布后来编辑的草稿。WS 按来源绑定稳定老师 ID,首次复用唯一启用旧项,歧义需显式选择;保留默认、停用和历史,不自动合并旧身份,重新启用需要显式动作。 前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。 独立 Reviewer 发现的历史字符串 ID `coding-teacher` 被 UUID 约束拒绝问题已修复并复核通过。WS 33 项(含真实 PostgreSQL 迁移/并发)、Yuxi 2260 后端单测/1 跳过及发布 HTTP/PG、worker/Redis、浏览器和前端构建,MakeLore 208 项及类型/lint/构建通过;具体分批证据见源记录。必须先部署 WS `20260929_0099`/API,再更新 Yuxi 和客户端;本次仅本地合并,未推送、部署或测量生产延迟。原有跨仓共 7 份外来文档保留未提交。此条更新旧运营必经和旧话题固定版本的发布说明,下方日期条目保留为历史。见[源记录](tasks/20260929-teacher-publish-ml-e6b32168.md)、[集成记录](tasks/20260929-integrate-teacher-ml-a19f72c4.md)。
- 2026-09-29:用户明确选择与其他官方插件相同的“客户端内置,默认安装”。已合入源 `97c3e45`,合并提交 `7a42de875bacb79fe6d660194b58f22c7d081830`。MakeLore 版本更新为 `2.0.2`,内置“对孩子说清楚”`1.0.1`,原 Skill 文件字节不变;登录/刷新时默认领取,无插件 ZIP 下载或单独签名。手动移除保持,项目启用后对操作 Agent 生效,无需另行分配 Skill。配套服务端 `f15cc46f` / 迁移 0098 创建新 bundled Release,保留旧 Artifact 和账号记录,并阻止不含 Skill 的旧客户端使用新版。113 项相关单测、类型检查、lint、构建和 1 项 Electron 场景通过;源产品与主目录一致,3 份外来文档原样保留。未推送、打包、发布或更新安装版,需两端发布。本条取代下方该插件从服务端下载并验签的历史指导;其他 Artifact 插件验签不变。见[源任务](tasks/20260928-default-child-replies-0108dd01.md)及[集成记录](tasks/20260928-merge-default-child-b4d62e81.md#bundled-delivery-resume)。
@@ -0,0 +1,57 @@
# Task: Integrate published teacher shortcuts (ml)
## Identity
- Task ID: 20260929-integrate-shortcuts-ml-b75d3c29
- Mode: Integration
- Branch: main
- Worktree: D:\Datas\OthersProjects\makelore
- Base commit: 4103754b80b57b67b787ab4a6bc24bbf8591be4d
- Owner: codex
- Status: Ready for Integration
## Scope
- Fast-forward reviewed teacher shortcuts source 53d5b6b from task 20260929-teacher-shortcuts-ml-8ce941d7 into local main; promote the accepted shortcut contract into canonical memory.
- Product source, tests, public mechanism docs and imported source task records remain byte-identical to reviewed source.
## Intent And Constraints
- User requested merge into main. Prior explicit permission to hand off these same three primary directories and preserve the same seven foreign documents applies; official release/start with adoption and exclusive integration ownership succeeded.
- Concurrent and Planning Gates passed. Read own task, entry, positioning, current-state, teacher decisions, architecture/domain, relevant evidence/reflection/commitments and source/review task scopes; reuse unchanged peer scope assessments from feature work. Historical placeholders/malformed peer records remain unknown and read-only; no concrete dependency or semantic conflict identified.
- Yuxi positioning/success templates contain placeholders; actual accepted teacher mechanisms and AGENTS define this task's boundary. This integration does not change positioning or fill unrelated templates.
- Preserve these adopted foreign documents unchanged and exclude them from commits:
- .project-docs/30-worklog/tasks/20260901-package-122-c5e8.md
- .project-docs/30-worklog/tasks/20260901-package-123-d7f3.md
- .project-docs/30-worklog/tasks/20260902-client-hang-diagnosis-a47c9e2b.md
- Merge authorization covers local integration; the user subsequently explicitly approved removing the three listed source worktrees. No push, deployment or production mutation. Existing fresh independent reviewer PASS covers the exact unchanged source.
## Plan
1. Fast-forward source after confirming main is its ancestor.
2. Integrate ordered label/prompt publication facts into relevant canonical documents.
3. Verify source equivalence, main ancestry, whitespace and task document boundaries; stop for any new product difference or changed adopted document.
4. Commit only canonical updates and this integration record, complete official task, and offer optional source-worktree cleanup.
## Outcome
- Successfully fast-forwarded 53d5b6b into local main; accepted shortcut contract promoted to canonical memory. Product files, tests and imported source task records remain identical to the independently reviewed source.
- User explicitly approved cleanup of the three named feature paths. Official retire failed with Directory not empty. Git removed worktree registration and .git before returning failure, but D:/Datas/OthersProjects/.codex-worktrees/makelore/20260929-teacher-shortcuts-ml-8ce941d7 retains partial files and its task ownership reservation remains. The path is no longer an eligible clean linked worktree. Do not manually delete/relabel registry state or bypass retire; cleanup is blocked pending supported recovery. Source branch codex/20260929-teacher-shortcuts-ml-8ce941d7-teacher-shortcuts and commit 53d5b6b are preserved and integrated.
## Verification
- Source commits and main bases verified; all three main branches are ancestors of their source commits.
- Source evidence: WS 36 SQLite/TestClient and 3 real PostgreSQL tests; Yuxi 2270 unit / 1 skipped, 3 real HTTP/PostgreSQL integration, 319 web tests, actual Vue editor browser proof, lint/build/docs/engineering checks; Makelore 73 UI tests, 21 browser cases, type/lint/production compile. Fresh independent reviewer PASS across all three.
- Integration verification passed: git diff --exit-code against source excluding .project-docs, unchanged imported source task, source ancestry in actual main, git diff --check and official check_doc_drift. The drift gate confirms all adopted foreign document states remain unchanged. Source tests/builds are reused because the product tree is identical; only canonical/task documentation was edited here.
- Confirmed all three source branch references still resolve to their source commits after cleanup attempts. Representative linked runtime dependencies remain available; no claim of a fresh full dependency audit.
## Follow-ups
- Release WS API first, then Yuxi and Makelore. No new migration from shortcuts; prerequisite direct-publication migration remains required.
- Cleanup result: blocked by partial Windows Git removal; residual path and stale task reservation preserved for supported recovery. User consent already covers this exact path; do not ask again merely because this same cleanup resumes.
## Promotion Candidates
- Accepted: Yuxi owns ordered teacher_shortcuts entries with label/prompt, explicit publication freezes values, WS persists/delivers shortcuts in version JSON, Makelore renders labels and sends actual prompts.
- Explicit [] hides shortcuts; missing historical field preserves legacy entry. Limits 8 / 40 / 6000, trimmed text. Existing stable chat/current version rules hold.
- Promoted to canonical decision index, teacher ADR where present, architecture, domain rules and current-state. Additive to accepted explicit publication, no contradictory source or new human product decision.
@@ -0,0 +1,46 @@
# Task: Render and send published teacher shortcuts
## Identity
- Task ID: 20260929-teacher-shortcuts-ml-8ce941d7
- Mode: Feature
- Branch: codex/20260929-teacher-shortcuts-ml-8ce941d7-teacher-shortcuts
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260929-teacher-shortcuts-ml-8ce941d7
- Base commit: 4103754b80b57b67b787ab4a6bc24bbf8591be4d
- Owner: codex
- Status: Ready for Integration
## Scope
- Render and send published teacher shortcuts. One linked worktree per repository; this feature is separate from already merged teacher publication work. Primary and peer files remain unchanged.
## Intent And Constraints
- Concurrent/Planning Gates passed through official scripts before implementation, with canonical teacher context loaded and peer scopes assessed. Historical incomplete peer records stay unknown; no concrete dependency conflict.
- User requested ordered shortcuts with separate display label and actual prompt, edited in Yuxi and activated through explicit publish. Limits: 8 entries, label 1–40 and prompt 1–6000 after trim. Explicit [] hides all buttons; missing historical field keeps old entry. No new table, migration, dependency or runtime model path.
- User authorized one fresh independent reviewer and feature commits for this task. Merge, push, production writes and worktree deletion are not authorized for this new feature.
## Outcome
- Implementation and verification complete. Fresh independent Reviewer passed the complete three-end feature; no actionable findings. Ready for Integration after feature commit.
- Yuxi owns draft/default/editing; publication snapshot owns frozen list; WS persists/delivers versioned JSON; Makelore renders label and sends prompt as an ordinary question. Existing stable teacher identity, catalog refresh, in-flight questions and historical messages retain their existing paths.
## Verification
- Changed shared TeacherDefinition accepts optional/null ordered shortcuts. Composer renders current published labels and sends actual prompts via ordinary question path, preserves old missing-field suggestions entry, hides explicit empty list, disables while busy/running and keeps buttons hidden for nonempty text.
- UI unit: node node_modules/vitest/vitest.mjs run tests/unit/coding-teacher-ui.test.tsx: 73 passed. New tests prove label/prompt separation/order, same-prompt uncertain retry identity, different prompt independent identity, catalog refresh preserves existing chat/draft and clear hides buttons.
- TypeScript --noEmit and changed-file ESLint passed. Vite production compile including Renderer/Main/Preload/utility passed. No dependencies added or changed; installed primary dependencies linked for build.
- Existing Playwright browser layout suite tests/e2e/teacher-reply-layout.spec.ts: 21 passed, including new 319px/508px cases for eight long configured shortcuts, exact prompt sends, empty list and existing hint send-position regression. This suite uses real components/Chromium with explicit fixture API, not production Electron cloud transport.
- Local real-component browser .tmp/check-shortcuts.mjs additionally verified 340px fit, reaching/clicking last shortcut and [] clear. Detected large list pushing actions below viewport; capped shortcut area at 192px with scrolling and kept send position stable. Screenshots .tmp/teacher-shortcuts-client.png and teacher-shortcuts-client-narrow.png; retained ignored local evidence.
- README updated for configuration and rollout. git diff --check passed. Not verified: production deployment, signed installer or real teacher model invocation; existing Main request path unchanged.
- Independent Reviewer teacher_shortcuts_review_replacement (fresh fork, no inherited development history) passed. Reviewer inspected all diffs/new files, cross-end boundaries, tests and real-browser evidence; independently ran WS publication 10 tests, Makelore UI 73 tests and Yuxi shortcut validation probes. Reviewer records: 20260929-teacher-shortcuts-review-ws-b6f3, 20260929-teacher-shortcuts-review-yx-d92a, 20260929-teacher-shortcuts-review-ml-e7c1; their official drift/complete passed. Production deployment/model provider and signed package remain outside this evidence.
## Follow-ups
- Feature result is ready for a future explicitly authorized integration. No merge/push/deploy requested for this feature.
- Rollout: WS registration accepts shortcuts, then Yuxi publishes it, then upgraded Makelore renders it. Existing teachers can edit default or clear then publish. Draft-only save has no student effect.
## Promotion Candidates
- Target: canonical teacher architecture/domain decision and current-state. Proposal: Yuxi-owned ordered label/prompt shortcuts with explicit publish; optional legacy snapshot field and [] hiding; stable chat and per-question latest config continue. Evidence: tests/browser results above. Future impact: one configuration source, no new synchronization path or migration. Semantic conflicts: none identified with accepted explicit-publication decision. Human confirmation: behavior already requested; canonical promotion requires a future authorized integration task.
+3 -1
View File
@@ -2,6 +2,8 @@
## Project conversations and distributed agents
- 老师快捷提示由 Yuxi 的有序 `teacher_shortcuts` 配置拥有,每项分别保存展示文案 `label` 和实际提示词 `prompt`;最多 8 项,去除首尾空白后文案 1–40 字符、提示词 1–6000 字符。普通保存只改草稿,显式发布固定列表并以 `shortcuts` 下发;同一发布重试读取原快照。明确的空列表隐藏所有入口,历史快照缺字段或 null 保留客户端旧入口,未配置草稿提供可编辑的默认项。Makelore 按顺序渲染文案,点击通过普通提问发送实际提示词,聊天记录显示实际提问;目录刷新保留项目聊天和草稿。
- 前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。
- 每个新项目创建内部默认 Agent,普通界面无 Agent 创建步骤或分组列。defaultAgentId 选取已存默认、置顶或首个可用 Agent;历史 Agent 与会话绑定不重写,不重新启用停用/归档 Agent。
@@ -15,7 +17,7 @@
- 新咨询由 Main 统一声明 `{reply, quickReplies}`,正文和 0–3 条可选快捷回复按所选智能体云端配置生成,客户端只提供入口事实、上下文与输出格式。超额新卡片保留正文及原文诊断,不截断或展示;历史卡片数量不改写。旧组件状态和快照作为不透明档案保留,不恢复交互、主动导入或进入模型上下文,旧组件修改路由已移除。
- 完整正文可从保留原文中在本地恢复;无法确认完整时显示缺失提示,由用户明确点击重新回答,保持原问题、引用和项目/Pi 来源,保留现有草稿,采用当前发布版本。被动恢复不调用模型或改写已完成原始轮文件;不完整回答和未解析原文不进入后续模型上下文。
- 进入咨询消费静态欢迎语并按账号/项目保存,真实未读消息不随打开自动已读;空输入框的“继续看看👀”仅在点击时调用模型。
- 进入咨询消费静态欢迎语并按账号/项目保存,真实未读消息不随打开自动已读;空输入框显示已发布的快捷提示,只有点击才调用模型。历史缺字段的版本保留“继续看看👀”入口。
依据:[老师决定](../10-decisions/ADR-2026-09-22-coding-teacher.md)及 [ADR-008](../10-decisions/adr-008-interactive-ai-app-scaffold.md)。
+1
View File
@@ -184,6 +184,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
- 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按本次问题/工具调用身份合并名称和状态,跨云端暂停、续接仍只计一次;暂停读取不算失败,状态以实际读取结果或问题终态为准,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。
- 云端咨询在 local_context 声明 read_protocol=2;需先部署配套 Yuxi API 和 worker,再升级客户端。云端持久累计读取字节及批次,每轮告知模型剩余额度;预算耗尽后消费最后一批结果,并以 tool_choice=none 要求根据现有证据形成答案和说明缺口。Main 限制实际返回量并拒绝第十三批读取,区分读取达到上限、上下文失效及格式无效。服务端对未声明协议的已安装旧客户端保留原工具参数与六批边界。
- 智能体人设、职责、提示词和 Skills 由发布配置决定。Main 不追加固定教学基线、不生成“小麦”、不清空所选智能体的 Skills,只添加真实工具能力、上下文边界和本轮界面协议。智能体名称叫“老师”“朋友”或“代码顾问”不改变调用路径或权限。
- 输入框为空时显示当前老师已发布的快捷提示,按 Yuxi 中配置的顺序展示文案,点击直接发送该项实际提示词。Yuxi“编辑 → 快捷提示”支持最多 8 项,每项文案最多 40 字、提示词最多 6000 字;保存并发布后,目录更新即可生效,历史和未发送草稿保留。明确发布空列表隐藏全部按钮;缺少字段的历史版本保留“继续看看👀”。多个长文案在限定高度内滚动,运行中禁止重复发送。上线需先更新 WS 登记接口,再更新 Yuxi 和客户端。
- `reply.ts` 只声明正文与快捷回复的输出格式,语言、长度、前缀与表达风格由所选智能体的云端配置决定。客户端不再发送选择、生成、转换或维护展示组件的指令。旧话题内的组件状态、快照和解析诊断作为不透明历史数据原样保留,不校验删除、不恢复交互;旧组件修改路由已移除。
- 当前输入是按预算截取的所选操作对话已完成分支文本、明确引用、咨询正文和快捷回复,并支持上述只读原文恢复;旧组件状态、快照和未解析原文不加入模型上下文或咨询历史读取结果。没有跨全部操作会话自动读取,也未向操作 Agent 注入咨询共识。提示词不声称看过未提供的画面或试玩过作品。Yuxi 话题经云端原生执行,旧版本地智能体由 Main 调用计费网关;均不创建本地 Pi 编程 worker。
- 智能体输入裁剪区分两种计量:本地模型以约每 2 个 UTF-8 字节估算 1 Token,并计入工具与读取预留;这不是精确分词或扣费依据,模型上限与实际 usage 仍由服务端拥有。云端沿用 query 字节上限,以完整 JSON 转义后的大小裁剪来源节选。智能体指令、当前问题和明确引用不会被静默裁掉;固定配置或回复协议超限时明确提示调整配置,避免让学生反复缩短短问题或新建话题。完整来源仍可由只读工具按需读取。
+6
View File
@@ -6,6 +6,11 @@ export const TEACHER_CHECK_IN_INTERVAL_MS = 5 * 60 * 1000;
export const TEACHER_UNCHANGED_CHECK_IN_INTERVAL_MS = 15 * 60 * 1000;
export type TeacherRequestIntent = 'question' | 'suggestions' | 'guided-help' | 'check-in';
export interface TeacherShortcut {
label: string;
prompt: string;
}
export interface TeacherDefinition {
config_id?: string;
runtime?: 'local' | 'yuxi';
@@ -18,6 +23,7 @@ export interface TeacherDefinition {
icon_url?: string | null;
welcome_message: string;
suggested_questions: string[];
shortcuts?: TeacherShortcut[] | null;
system_prompt: string;
skills: Array<{
id: string;
+9 -3
View File
@@ -595,9 +595,15 @@ function ProjectTeacherChatPanel({
<textarea ref={inputRef} aria-label={`向${label}提问`} value={text} disabled={busy} maxLength={6000} onChange={(e) => changeText(e.target.value)} placeholder="和老师聊聊" rows={2}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); void send(); } }}
className="min-h-[60px] w-full resize-none border-0 bg-transparent text-[13px] leading-6 outline-none placeholder:text-muted-foreground/65" />
<div className="mt-2 flex min-h-11 items-center justify-between gap-2" data-testid="consultation-composer-actions">
<div className="min-w-0 flex-1">
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint">继续看看👀</button>}
<div className="mt-2 flex min-h-12 items-center justify-between gap-2" data-testid="consultation-composer-actions">
<div className="flex max-h-48 min-w-0 flex-1 flex-wrap gap-2 overflow-y-auto pb-1">
{!draftRevision && !text.trim() && (definition?.shortcuts == null
? <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint">继续看看👀</button>
: definition.shortcuts.map((shortcut, index) => <button
key={index} type="button" disabled={helpUnavailable}
onClick={() => void send({ text: shortcut.prompt })}
className="consultation-hint max-w-full whitespace-normal break-words text-left"
>{shortcut.label}</button>))}
</div>
{running ? <button type="button" className="rounded-lg border px-3 py-2 text-xs" onClick={() => void teacherApi.cancel(topicBase, endpointId, running.id).then(consume).catch(() => setError('暂时无法停止,请稍后重试。'))}>停止回复</button>
: <button type="submit" aria-label="提问" title={busy ? '正在发送…' : '发送'} disabled={busy || !topicEnabled || !text.trim()} className="consultation-send flex h-8 w-8 items-center justify-center rounded-full text-white disabled:opacity-30"><ArrowUp className="h-4 w-4" /></button>}
+6
View File
@@ -9,6 +9,12 @@ const samples: Record<string, unknown> = {
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';
if (scenario === 'shortcuts') definition.shortcuts = [
{ label: '看看进展', prompt: '请结合当前项目,分析我最近完成的工作。' },
{ label: '解释代码', prompt: '请解释当前项目最近的代码改动,用一个具体例子帮助我理解。' },
...Array.from({ length: 6 }, (_, i) => ({ label: `入口${i + 3}:看看我的项目进展,帮我理解当前代码并提出下一步的编程建议`, prompt: `请讨论第 ${i + 3} 个主题。` })),
];
if (scenario === 'shortcuts-empty') definition.shortcuts = [];
export const fixtureAgent = { teacher_id: definition.teacher_id, version: 1, is_default: true, definition };
const continuous = new URLSearchParams(location.search).get('chat') !== 'legacy';
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: [], projectId: 'fixture', sourceConversationId: 'source', teacherVersion: 1, createdAt: `2026-09-28T10:00:${String(i).padStart(2, '0')}Z`, sourceCursor: { workerGeneration: 0, seq: i }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: i === 7 ? '可以,我们先把已经想到的放在一起。哪里还不确定,就留着继续聊。' : '先从你最在意的一个小地方开始想。', ...(i === 7 ? { suggestedQuestions: ['有什么好玩的想法?'] } : {}) })), ...(samples[scenario] ? { discussion: { id: 'tool', revision: 1, status: 'active', content: structuredClone(samples[scenario]) } } : {}) };
+29
View File
@@ -47,6 +47,35 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
});
for (const width of [319, 508]) {
test(`published shortcuts send their prompts and keep eight long labels scrollable at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width: 1180, height: 800 });
await page.goto(`${url}?kind=shortcuts&width=${width}`);
const panel = page.getByTestId('teacher-chat-panel');
const shortcuts = page.locator('form .consultation-hint');
await expect(shortcuts).toHaveCount(8);
await expect(shortcuts.first()).toHaveText('看看进展');
await expect(shortcuts.nth(1)).toHaveText('解释代码');
await expect(shortcuts.nth(1)).toBeEnabled();
const form = (await panel.locator('form').boundingBox())!;
expect(form.y + form.height).toBeLessThanOrEqual(800);
expect(await panel.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
await shortcuts.last().scrollIntoViewIfNeeded();
await panel.screenshot({ path: test.info().outputPath(`shortcuts-${width}.png`) });
await shortcuts.last().click();
await expect(page.getByText('请讨论第 8 个主题。', { exact: true })).toBeVisible();
await expect(page.getByText('好,我们就从这一个问题聊起。')).toBeVisible();
await shortcuts.nth(1).click();
await expect(page.getByText('请解释当前项目最近的代码改动,用一个具体例子帮助我理解。', { exact: true })).toBeVisible();
const inputs = await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
expect(inputs).toEqual([
{ requestId: expect.any(String), text: '请讨论第 8 个主题。', references: [], projectId: 'fixture', sourceConversationId: 'source' },
{ requestId: expect.any(String), text: '请解释当前项目最近的代码改动,用一个具体例子帮助我理解。', references: [], projectId: 'fixture', sourceConversationId: 'source' },
]);
await page.goto(`${url}?kind=shortcuts-empty&width=${width}`);
await expect(page.getByRole('textbox', { name: '向智能体提问' })).toBeEnabled();
await expect(page.locator('form .consultation-hint')).toHaveCount(0);
});
for (const chat of ['legacy', 'continuous']) test(`quoted ${chat} replies recover fully and incomplete replies retry without losing the draft at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width: 1180, height: 800 });
const sent = () => page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
+50
View File
@@ -100,6 +100,56 @@ async function ready() {
await waitFor(() => expect(streams.has('first')).toBe(true));
}
describe('teacher side chat', () => {
it('renders published shortcut labels in order and sends each actual prompt with independent retry identity', async () => {
const shortcuts = [{ label: '看看进展', prompt: '请分析当前项目的进展。' }, { label: '解释代码', prompt: '请解释刚才的代码改动。' }];
const agent = { teacher_id: 'shortcuts', version: 1, is_default: true, definition: { ...definition, config_id: 'shortcuts', shortcuts } };
api.catalog.mockResolvedValue({ items: [agent] });
api.sendConversation.mockRejectedValue(new Error('结果暂未确认'));
render(<TeacherChatPanel projectId="p" sourceId="c" selectedAgent={agent} />);
await waitFor(() => expect(screen.getByRole('button', { name: '看看进展' })).toBeEnabled());
expect([...screen.getByTestId('consultation-composer-actions').querySelectorAll('.consultation-hint')].map(el => el.textContent)).toEqual(shortcuts.map(item => item.label));
expect(screen.queryByRole('button', { name: '继续看看👀' })).not.toBeInTheDocument();
expect(screen.queryByText(shortcuts[0].prompt)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '看看进展' }));
await screen.findByText('结果暂未确认');
const original = api.sendConversation.mock.calls[0][2];
expect(original).toMatchObject({ text: shortcuts[0].prompt, projectId: 'p', sourceConversationId: 'c', references: [] });
expect(original.intent).toBeUndefined();
fireEvent.click(screen.getByRole('button', { name: '看看进展' }));
await waitFor(() => expect(api.sendConversation).toHaveBeenCalledTimes(2));
await waitFor(() => expect(screen.getByRole('button', { name: '解释代码' })).toBeEnabled());
expect(api.sendConversation.mock.calls[1][2]).toEqual(original);
fireEvent.click(screen.getByRole('button', { name: '解释代码' }));
await waitFor(() => expect(api.sendConversation).toHaveBeenCalledTimes(3));
expect(api.sendConversation.mock.calls[2][2].text).toBe(shortcuts[1].prompt);
expect(api.sendConversation.mock.calls[2][2].requestId).not.toBe(original.requestId);
});
it('updates published shortcuts without replacing chat history or a composed draft, and supports clearing the list', async () => {
const old = { teacher_id: 'shortcut-refresh', version: 1, is_default: true, definition: { ...definition, config_id: 'shortcut-refresh', shortcuts: [{ label: '旧入口', prompt: '旧提示词' }] } };
const conversation = { ...topic('shortcut-chat'), definition: old.definition,
conversation: { agentId: old.teacher_id, segmentTurns: 1, discussions: {}, seenThrough: 'past' },
requests: [request({ id: 'past', text: '之前的问题', response: '之前的回答' })] };
api.conversation.mockResolvedValue({ topic: conversation, before: null });
const state = (agent: typeof old) => ({ catalog: { revision: agent.version, items: [agent] }, config: { enabled: true, published_version: agent.version, revision: agent.version, definition: agent.definition } });
const view = render(<TeacherChatPanel projectId="p" sourceId="c" selectedAgent={old} catalogState={state(old)} />);
await screen.findByText('之前的回答');
await waitFor(() => expect(screen.getByRole('button', { name: '旧入口' })).toBeEnabled());
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '尚未发出的草稿' } });
const updated = { ...old, version: 2, definition: { ...old.definition, shortcuts: [{ label: '新入口', prompt: '新的实际提示词' }] } };
view.rerender(<TeacherChatPanel projectId="p" sourceId="c" selectedAgent={updated} catalogState={state(updated)} />);
expect(screen.getByLabelText('向智能体提问')).toHaveValue('尚未发出的草稿');
expect(screen.queryByRole('button', { name: '新入口' })).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
expect(screen.getByRole('button', { name: '新入口' })).toBeVisible();
expect(screen.queryByRole('button', { name: '旧入口' })).not.toBeInTheDocument();
const cleared = { ...updated, version: 3, definition: { ...updated.definition, shortcuts: [] } };
view.rerender(<TeacherChatPanel projectId="p" sourceId="c" selectedAgent={cleared} catalogState={state(cleared)} />);
expect(screen.getByTestId('consultation-composer-actions').querySelectorAll('.consultation-hint')).toHaveLength(0);
expect(screen.getByText('之前的回答')).toBeVisible();
expect(api.sendConversation).not.toHaveBeenCalled();
});
it('opens directly into the consultation and puts new topics inside expanded history', async () => {
render(<TeacherChatPanel projectId="p" sourceId="c" />);
const history = await screen.findByRole('button', { name: '以往讨论' });