From 79b1dc189250cab5ddb303e53e43c47ab48ba4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=A8=E9=B1=BC=E8=BE=A3=E6=A4=92?= Date: Tue, 29 Sep 2026 20:20:58 +0800 Subject: [PATCH] Clarify teacher prompt button click affordance --- .../20260929-teacher-prompt-buttons-a682d30f.md | 9 +++++++++ README.md | 2 +- src/pages/Chat/classroom-workspace.css | 11 ++++++----- tests/e2e/teacher-reply-layout.spec.ts | 14 +++++++++++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260929-teacher-prompt-buttons-a682d30f.md b/.project-docs/30-worklog/tasks/20260929-teacher-prompt-buttons-a682d30f.md index 9f0b366e..19f496f4 100644 --- a/.project-docs/30-worklog/tasks/20260929-teacher-prompt-buttons-a682d30f.md +++ b/.project-docs/30-worklog/tasks/20260929-teacher-prompt-buttons-a682d30f.md @@ -64,3 +64,12 @@ - Completed: prompt button minimum height 30px (was 36px), padding 4px/10px, 8px corners, no border, soft 2px/6px shadow and pale cream background. Font remains 13px/400. - Verification: typecheck, lint (0 errors, 8 existing warnings), source/staged production builds and six 319/508px prompt/model/microphone layout scenarios passed. Existing checks now assert no border, a shadow and 30px height. - Updated the production stylesheet link in the live trial, with new built assets/index installed for subsequent reload. No Renderer remount or Main restart; live JS behavior is unchanged. Native capture confirms 30px, zero border and soft shadow. All localStorage, input text, operation messages, teacher turn text and route remained byte-identical; window stayed unfocused. + +## Click Affordance Follow-up + +- Same-task ownership and existing planning context verified again at dc8f766; peer scope decisions remain compatible. Gate passed. User wants the compact borderless prompts to look unmistakably clickable. +- Plan: retain 30px/13px sizing, strengthen the warm fill and bottom shadow, add a small decorative chevron, and provide hover/pressed depth feedback with reduced-motion support. Update existing layout checks and hot-swap the built stylesheet in the trial, preserving live conversations. + +- Completed: 30px borderless buttons now have a warmer pale-yellow fill, 2px bottom shadow and decorative chevron. Hover raises 1px and pressed state lowers 1px; reduced-motion keeps position stable. Exact configured text and accessible name unchanged. +- Verification: typecheck, lint (0 errors / 8 existing warnings), source and combined-stage production builds, and six focused browser cases passed. Existing cases now exercise hover, pressed and reduced-motion feedback as well as 319/508px layout and exact prompt sends. +- Trial stylesheet index-ydJGBX8O.css installed and activated without reload. Native screenshot inspected; computed height 30px, font 13px, border 0 and raised shadow confirmed. All storage, inputs, 14 operation messages, teacher message text and route unchanged; focused=false before/after. No cloud changes or live messages sent. diff --git a/README.md b/README.md index f3290699..d88b14c5 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,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,只添加真实工具能力、上下文边界和本轮界面协议。智能体名称叫“老师”“朋友”或“代码顾问”不改变调用路径或权限。 -- 输入框为空时,在输入框外上方显示当前老师已发布的快捷提示;采用与回复正文一致的 13px 常规字重、无边框、浅色底与柔和阴影的小巧圆角按钮,横向排列并自动换行。按 Yuxi 中配置的顺序展示文案,点击直接发送该项实际提示词。Yuxi“编辑 → 快捷提示”支持最多 8 项,每项文案最多 40 字、提示词最多 6000 字;保存并发布后,目录更新即可生效,历史和未发送草稿保留。明确发布空列表隐藏全部按钮;缺少字段的历史版本保留“帮我整体看🧠”。多个长文案在限定高度内滚动,运行中禁止重复发送。上线需先更新 WS 登记接口,再更新 Yuxi 和客户端。 +- 输入框为空时,在输入框外上方显示当前老师已发布的快捷提示;采用与回复正文一致的 13px 常规字重、无边框、浅色底与底部立体阴影的小巧圆角按钮,配有小箭头和悬停/按下反馈,横向排列并自动换行。按 Yuxi 中配置的顺序展示文案,点击直接发送该项实际提示词。Yuxi“编辑 → 快捷提示”支持最多 8 项,每项文案最多 40 字、提示词最多 6000 字;保存并发布后,目录更新即可生效,历史和未发送草稿保留。明确发布空列表隐藏全部按钮;缺少字段的历史版本保留“帮我整体看🧠”。多个长文案在限定高度内滚动,运行中禁止重复发送。上线需先更新 WS 登记接口,再更新 Yuxi 和客户端。 - `reply.ts` 只声明正文与快捷回复的输出格式,语言、长度、前缀与表达风格由所选智能体的云端配置决定。客户端不再发送选择、生成、转换或维护展示组件的指令。旧话题内的组件状态、快照和解析诊断作为不透明历史数据原样保留,不校验删除、不恢复交互;旧组件修改路由已移除。 - 当前输入是按预算截取的所选操作对话已完成分支文本、明确引用、咨询正文和快捷回复,并支持上述只读原文恢复;旧组件状态、快照和未解析原文不加入模型上下文或咨询历史读取结果。没有跨全部操作会话自动读取,也未向操作 Agent 注入咨询共识。提示词不声称看过未提供的画面或试玩过作品。Yuxi 话题经云端原生执行,旧版本地智能体由 Main 调用计费网关;均不创建本地 Pi 编程 worker。 - 智能体输入裁剪区分两种计量:本地模型以约每 2 个 UTF-8 字节估算 1 Token,并计入工具与读取预留;这不是精确分词或扣费依据,模型上限与实际 usage 仍由服务端拥有。云端沿用 query 字节上限,以完整 JSON 转义后的大小裁剪来源节选。智能体指令、当前问题和明确引用不会被静默裁掉;固定配置或回复协议超限时明确提示调整配置,避免让学生反复缩短短问题或新建话题。完整来源仍可由只读工具按需读取。 diff --git a/src/pages/Chat/classroom-workspace.css b/src/pages/Chat/classroom-workspace.css index 7545eebb..149ccb18 100644 --- a/src/pages/Chat/classroom-workspace.css +++ b/src/pages/Chat/classroom-workspace.css @@ -52,13 +52,14 @@ .consultation-retry { display: inline-flex; align-items: center; justify-content: center; align-self: flex-start; min-height: 44px; max-width: 100%; padding: 8px 16px; border: 1.5px solid #e3ce8f; border-radius: 16px 16px 16px 6px; background: #fff2c9; color: #6f5127; box-shadow: 0 3px 0 #e7d7a6, inset 0 1px 0 #fff9e8; font-size: 14px; font-weight: 600; line-height: 24px; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; } .consultation-retry:hover:not(:disabled) { background: #ffe9a9; border-color: #d6b76f; } .consultation-retry:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 #e7d7a6, inset 0 1px 0 #fff9e8; } -.consultation-shortcuts { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; max-height: min(192px, 24vh); overflow-y: auto; margin-bottom: 10px; padding: 3px; } -.consultation-hint { display: inline-flex; align-items: center; max-width: 100%; min-height: 30px; padding: 4px 10px; border: 0; border-radius: 8px; background: #f8f1e2; box-shadow: 0 2px 6px #8b75491a; color: #5b503b; font-size: 13px; font-weight: 400; line-height: 22px; text-align: left; white-space: normal; overflow-wrap: anywhere; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease; } -.consultation-hint:hover:not(:disabled) { background: #f2e8d2; box-shadow: 0 3px 8px #8b754924; } -.consultation-hint:active:not(:disabled) { background: #eee3c7; } +.consultation-shortcuts { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; max-height: min(192px, 24vh); overflow-y: auto; margin-bottom: 10px; padding: 3px 4px 6px; } +.consultation-hint { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; min-height: 30px; padding: 4px 10px; border: 0; border-radius: 8px; background: #f8e6b7; box-shadow: 0 2px 0 #d8c492, 0 3px 6px #8b75491a; color: #65491f; font-size: 13px; font-weight: 400; line-height: 22px; text-align: left; white-space: normal; overflow-wrap: anywhere; cursor: pointer; transition: background-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; } +.consultation-hint::after { content: ''; width: 5px; height: 5px; flex-shrink: 0; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); opacity: 0.7; } +.consultation-hint:hover:not(:disabled) { background: #f7dfa0; box-shadow: 0 3px 0 #d1b979, 0 5px 8px #8b754924; transform: translateY(-1px); } +.consultation-hint:active:not(:disabled) { background: #efd28b; box-shadow: 0 1px 0 #c7ad70; transform: translateY(1px); } .consultation-hint:focus-visible { outline: 2px solid #7891af; outline-offset: 2px; } .consultation-hint:disabled, .consultation-retry:disabled { cursor: not-allowed; opacity: 0.5; } -@media (prefers-reduced-motion: reduce) { .consultation-hint, .consultation-retry { transition: none; } .consultation-hint:active:not(:disabled), .consultation-retry:active:not(:disabled) { transform: none; } } +@media (prefers-reduced-motion: reduce) { .consultation-hint, .consultation-retry { transition: none; } .consultation-hint:hover:not(:disabled), .consultation-hint:active:not(:disabled), .consultation-retry:active:not(:disabled) { transform: none; } } .consultation-pane button:focus-visible { outline: 2px solid #71845f; outline-offset: 3px; } .consultation-suggestion { border-color: #e0e6d6; color: #718163; } .consultation-suggestion:hover { border-color: #bac9aa; background: #f4f7ee; } diff --git a/tests/e2e/teacher-reply-layout.spec.ts b/tests/e2e/teacher-reply-layout.spec.ts index 2e1da537..c052fbfc 100644 --- a/tests/e2e/teacher-reply-layout.spec.ts +++ b/tests/e2e/teacher-reply-layout.spec.ts @@ -66,6 +66,18 @@ for (const width of [319, 508]) { expect((await row.boundingBox())!.y + (await row.boundingBox())!.height).toBeLessThan((await composer.boundingBox())!.y); await shortcuts.first().focus(); await expect(shortcuts.first()).toHaveCSS('outline-style', 'solid'); + await expect(shortcuts.first()).toHaveCSS('cursor', 'pointer'); + await shortcuts.first().hover(); + await expect(shortcuts.first()).toHaveCSS('transform', 'matrix(1, 0, 0, 1, 0, -1)'); + await page.mouse.down(); + await expect(shortcuts.first()).toHaveCSS('transform', 'matrix(1, 0, 0, 1, 0, 1)'); + await page.mouse.move(0, 0); + await page.mouse.up(); + await page.emulateMedia({ reducedMotion: 'reduce' }); + await shortcuts.first().hover(); + await expect(shortcuts.first()).toHaveCSS('transform', 'none'); + await page.mouse.move(0, 0); + await page.emulateMedia({ reducedMotion: 'no-preference' }); 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); @@ -130,7 +142,7 @@ for (const width of [319, 508]) { await expect(hint).toBeEnabled(); await expect(hint).toHaveText('帮我整体看🧠'); expect(await hint.evaluate(el => el.scrollWidth <= el.clientWidth && el.scrollHeight <= el.clientHeight)).toBe(true); - await expect(hint).toHaveCSS('background-color', 'rgb(248, 241, 226)'); + await expect(hint).toHaveCSS('background-color', 'rgb(248, 230, 183)'); const initialSend = (await send.boundingBox())!; const hintBounds = (await hint.boundingBox())!; expect(hintBounds.x).toBeLessThan(initialSend.x);