diff --git a/docs/frontend/browser-results.json b/docs/frontend/browser-results.json index a5f8604..41ff899 100644 --- a/docs/frontend/browser-results.json +++ b/docs/frontend/browser-results.json @@ -18,5 +18,5 @@ "No uncaught browser errors, API/external requests, microphone or camera requests" ], "errors": [], - "requests": 59 + "requests": 56 } \ No newline at end of file diff --git a/docs/requirements/handoff-HEL-824.json b/docs/requirements/handoff-HEL-824.json new file mode 100644 index 0000000..4dbec2b --- /dev/null +++ b/docs/requirements/handoff-HEL-824.json @@ -0,0 +1,32 @@ +{ + "task": "HEL-824 视觉总监:HEL-822 审核两项必须修复缺陷返工(紧凑高度聚焦后首击失灵;停止回复后立即新轮续读迟滞),并修正 rework 检查错误断言、核对 Viewport 无效逻辑", + "baseline_version": "1.10", + "read_commit": "4321aebfdc491993e0648f68a0375047c3eb6359", + "read_notes": "唯一返工基底为远端 hel-822-candidate 提交 4321aeb(总管 git archive 附件 HEL-822-candidate-4321aeb.zip,SHA256 d18d54891f365f4b7999b4defb8cc4f92dace3e296b96707c617ac08f3b3e870,本机复算一致)。已读根 AGENTS.md、docs/requirements/README.md、current.md(59项)、coverage.md、changes.md、task-handoff-template.md、sources/dynamic-sample-effect-acceptance-20261023.md,并按编号核对 CG-018、VIS-003/004/007/011/013、UX-001/002/005、GOV-001–005、DEL-001 原文与边界;模板中的旧1.7字样不覆盖当前入口 v1.10。审核原文:HEL-822 评论 01a16afe-0cb1-7146-b561-c348c9fc8ed8。", + "requirement_ids": ["CG-018", "VIS-003", "VIS-004", "VIS-007", "VIS-011", "VIS-013", "UX-001", "UX-002", "UX-005", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"], + "deliverable": "在 4321aeb 上修改 6 个文件、新增本交接:(1) web/src/styles/components.css 删除 html.compact.typing{--nav-space:6px}:聚焦输入只让导航淡出让位,对话区几何不随焦点变化,按下与松开落在同一按钮;交流视觉、两主题与导航淡出保留。(2) web/src/chat/reading.ts 新增 ReadingClock,阅读停留按真实时间计(每帧最多计 STALL=1000ms,页面隐藏后的间隔不计),同一句 key 不变即不重置;web/src/chat/ChatPanel.tsx 改用它(goTo→forget、setFlow→restart、visibilitychange→pause),自动续读、暂停、回看、回到进度、墨入只播一次的逻辑不变。(3) web/src/shared/Viewport.tsx 删除没有任何 CSS 对应的 viewport-resizing 类及其定时器,注释改为实际行为。(4) web/tests/reading.test.ts 新增 3 项时钟回归;web/tests/rework_browser_check.py 三处整行 to_have_text('答复') 改为只对正文
精确断言,新增 390×420/390×430 双主题聚焦后首击回归与 700ms 慢帧下停止→立即新轮回归。总工在 browser_check / import_check 的机械修正未触碰。",
+ "root_causes": [
+ "缺陷1:compact 下输入聚焦时 --nav-space 由 54px 变 6px;按下按钮使输入失焦,对话区在按下与松开之间下移 48px,click 落到两者共同祖先 SECTION.conversation。修法:对话区几何不随焦点改变(只保留导航淡出),未用先 blur、强制点击、脚本调 handler 或 pointerdown 绕过。",
+ "缺陷2:阅读时钟每帧把 dt 截到 100ms(ChatPanel.tsx 原 113 行 Math.min(100, now - last)),停留时长随帧率缩放。停止→立即新轮时交叉淡化、模糊、墨入与房间画面同时进行,审核环境帧间隔约 1 秒,『新轮』1400ms 的停留被拉长为 14 帧≈14 秒;审核读到的 0.06…0.93 正是 1/14…13/14 的逐帧步进,满格后进入下一句再次从 0 起,看起来像反复重置。逐行核对:同一轮内 key 只由轮次与位置决定,流式增长、落库替换、旧轮迟到事件(TurnGate 拒收)都不会改变当前句 key,也没有其他自动调用 forget/restart 的路径。修法:按真实时间计时,不缩短任何设计停留、不取消自动阅读、不放宽测试等待(8s 超时保持)。"
+ ],
+ "acceptance": [
+ "正式 React 月光/雾光,390×420 与 390×430:输入框聚焦后直接点发送、上一句、暂停/继续、全文,均首击生效;草稿真正发出并落库(用户+她各1条)、全文浮层真正打开;按下与松开时对话区 top 相同、click 送达该按钮(rework_browser_check 新节,真实指针点击,脚本不 blur 不派发)。",
+ "停止回复→立即新轮:取消即时、旧轮迟到不串入(当前句状态为空),新轮先显示你的一句(设计停留1.4s)后她的回复上台;在 rAF 每 700ms 一帧的慢帧下同样在原 8s 断言内完成(旧实现需约 9.8s 必然失败)。",
+ "正常 60fps 下各句停留与修改前一致(每帧 dt≈16ms 远小于原 100ms 上限,行为不变);暂停/恢复、前后回看、回到读到的地方、全文、完整记录、同句墨入只播一次、历史恢复锚点逻辑未改动。",
+ "reading.test.ts:任意帧间隔(16/100/250/700/1000ms)下你的一句在 1400ms 至 1400ms+一帧之间到期;流式增长与落库不回退进度;新句、回看、恢复从零开始,暂停/悬停与隐藏期间不计,单帧停顿最多计 1 秒。",
+ "320×568、390×844、1024、1600×1000 布局:本次只删除 compact+typing 时的位移规则,非 compact 尺寸 CSS 无变化;仍需总工以 browser_check / rework 12 组视口复核。"
+ ],
+ "deferred": [
+ "本机(Windows 运行时)没有 node/npm、python、git,未能执行 typecheck/lint/test/build、四份浏览器检查、check_requirements.py 与 git diff --check;全部交总工机械执行,未运行即未通过,不以静态阅读代替。",
+ "修前修后实测计时未在本机取得:修前数据沿用总工 HEL-822 观测(约10–15秒、0.06→0.93),修后应由总工以 rework 慢帧节的 slow_frames_new_turn_to_reply_s 及受控慢流原复现记录。",
+ "未开 PR、未提交:本机无 git;按 changes 清单落到 hel-822-candidate 之上,PR 目标仍为 PR#4 产品线 agent/agent/de8874a3803b(原产品基底 db571ba),不指 main;远端引用名称须由总工用 git 实查。",
+ "真机键盘、Safari/Firefox、读屏未测;等效视口不冒充真机(UX-001)。人物仍为参考静帧,真实视频/语音/口型/生活/记忆未接入(DEL-001)。A3 日子/回忆/创建/设置由 HEL-823 承接;R1 人格深度与 R2–R7 在主卡 HEL-797 追踪。"
+ ],
+ "requirement_changes": [],
+ "verification": [
+ "附件 SHA256 复算 d18d54891f365f4b7999b4defb8cc4f92dace3e296b96707c617ac08f3b3e870,与卡片一致;解包为只读 before 与可写 work 两份,修改只在 work。",
+ "修改前后文件哈希与新增/删除清单见交付包 CHANGES-HEL-824.json(6 改、1 增、0 删)。",
+ "未执行任何构建、测试或浏览器检查(工具缺失);待总工执行:cd web && npm ci && npm run typecheck && npm run lint && npm test && npm run build;PYTHONPATH=src python3.12 web/tests/{browser_check,import_check,real_browser_check,rework_browser_check}.py;python3 scripts/check_requirements.py --base-ref origin/main --handoff docs/requirements/handoff-HEL-824.json 及实际 PR 目标引用;git diff --check。",
+ "真实网关 SSE 证据仍为 HEL-822 已有记录(本次未改后端、协议、SQLite、RealChatAdapter/TurnGate/epoch/AbortController);rework 新节使用隔离临时库与受控模型,二者分别标明。"
+ ]
+}
diff --git a/docs/rework/appearance-fog.png b/docs/rework/appearance-fog.png
index efc6cff..70a5e3c 100644
Binary files a/docs/rework/appearance-fog.png and b/docs/rework/appearance-fog.png differ
diff --git a/docs/rework/backup-fog.png b/docs/rework/backup-fog.png
index 37cfbe9..e4c8756 100644
Binary files a/docs/rework/backup-fog.png and b/docs/rework/backup-fog.png differ
diff --git a/docs/rework/birth-calendar-help-moon.png b/docs/rework/birth-calendar-help-moon.png
index 982f5a1..c6dce27 100644
Binary files a/docs/rework/birth-calendar-help-moon.png and b/docs/rework/birth-calendar-help-moon.png differ
diff --git a/docs/rework/birth-preview-moon.png b/docs/rework/birth-preview-moon.png
index db2269d..67aadc3 100644
Binary files a/docs/rework/birth-preview-moon.png and b/docs/rework/birth-preview-moon.png differ
diff --git a/docs/rework/browser-results.json b/docs/rework/browser-results.json
index 480fbe4..24665d3 100644
--- a/docs/rework/browser-results.json
+++ b/docs/rework/browser-results.json
@@ -2,7 +2,13 @@
"checks": [
"Real creation: lunar leap month, all six explanations, same final confirmation; preview writes no rows",
"Long Chinese stream: shown lines never change or re-key, ink plays once per line; pause/keyboard review is instant; record keeps everything",
- "12 viewport/theme combinations: lines stay inside the stage, stage/reader/composer/navigation never overlap; Esc closes full text with focus back; full history reachable"
+ "12 viewport/theme combinations: lines stay inside the stage, stage/reader/composer/navigation never overlap; Esc closes full text with focus back; full history reachable",
+ "390x420 and 390x430, both themes: with the input focused, the first press on send, review, pause/continue and full text acts; the dialogue does not move between press and release",
+ "Cancel then immediate new turn, model error and same-turn retry preserve roles and avoid duplicate user messages",
+ "Stop then new turn with busy (700 ms) frames: your line keeps its own time and her reply follows; the reading clock counts real time",
+ "Restored real pages: city search/empty, appearance/check/compare, backup/recovery, four services/live health, life/memory groups; unsaved close guard",
+ "Escape navigation; browser display preference survives reload; settings/system reduced motion uses gentle opacity",
+ "Demo has its own profile, voice simulation, life/detail/chat/return path and zero API requests"
],
"viewports": [
{
@@ -580,12 +586,175 @@
],
"motion": {
"inked_lines": {
- "351d619e-f034-4908-9340-aa384d74003f:1:0": 1
- }
+ "a7926846-51f6-4d1a-8057-565c2350c71d:1:0": 1
+ },
+ "slow_frames_new_turn_to_reply_s": 2.32
},
"console_errors": [],
"limits": [
"Chromium only; physical phone keyboard, Safari and screen reader not tested.",
"LLM is controlled for deterministic long-stream/error/cancel checks, not a live gateway."
+ ],
+ "compact_first_press": [
+ {
+ "size": "390x420",
+ "theme": "moon",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "发送"
+ },
+ {
+ "size": "390x420",
+ "theme": "moon",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "上一句"
+ },
+ {
+ "size": "390x420",
+ "theme": "moon",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "暂停"
+ },
+ {
+ "size": "390x420",
+ "theme": "moon",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "继续"
+ },
+ {
+ "size": "390x420",
+ "theme": "moon",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "全文"
+ },
+ {
+ "size": "390x420",
+ "theme": "fog",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "发送"
+ },
+ {
+ "size": "390x420",
+ "theme": "fog",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "上一句"
+ },
+ {
+ "size": "390x420",
+ "theme": "fog",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "暂停"
+ },
+ {
+ "size": "390x420",
+ "theme": "fog",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "继续"
+ },
+ {
+ "size": "390x420",
+ "theme": "fog",
+ "down": 182,
+ "up": 182,
+ "typing": false,
+ "target": "全文"
+ },
+ {
+ "size": "390x430",
+ "theme": "moon",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "发送"
+ },
+ {
+ "size": "390x430",
+ "theme": "moon",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "上一句"
+ },
+ {
+ "size": "390x430",
+ "theme": "moon",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "暂停"
+ },
+ {
+ "size": "390x430",
+ "theme": "moon",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "继续"
+ },
+ {
+ "size": "390x430",
+ "theme": "moon",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "全文"
+ },
+ {
+ "size": "390x430",
+ "theme": "fog",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "发送"
+ },
+ {
+ "size": "390x430",
+ "theme": "fog",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "上一句"
+ },
+ {
+ "size": "390x430",
+ "theme": "fog",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "暂停"
+ },
+ {
+ "size": "390x430",
+ "theme": "fog",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "继续"
+ },
+ {
+ "size": "390x430",
+ "theme": "fog",
+ "down": 192,
+ "up": 192,
+ "typing": false,
+ "target": "全文"
+ }
]
}
diff --git a/docs/rework/city-fog.png b/docs/rework/city-fog.png
index 785c24d..631c2c2 100644
Binary files a/docs/rework/city-fog.png and b/docs/rework/city-fog.png differ
diff --git a/docs/rework/compact-first-press-390-420-moon.png b/docs/rework/compact-first-press-390-420-moon.png
new file mode 100644
index 0000000..60422e5
Binary files /dev/null and b/docs/rework/compact-first-press-390-420-moon.png differ
diff --git a/docs/rework/compact-first-press-390-430-moon.png b/docs/rework/compact-first-press-390-430-moon.png
new file mode 100644
index 0000000..b0d04c7
Binary files /dev/null and b/docs/rework/compact-first-press-390-430-moon.png differ
diff --git a/docs/rework/demo-voice-390.png b/docs/rework/demo-voice-390.png
index 75ef125..a660b32 100644
Binary files a/docs/rework/demo-voice-390.png and b/docs/rework/demo-voice-390.png differ
diff --git a/docs/rework/existing-birth-fog.png b/docs/rework/existing-birth-fog.png
index e237294..ba08cc7 100644
Binary files a/docs/rework/existing-birth-fog.png and b/docs/rework/existing-birth-fog.png differ
diff --git a/docs/rework/existing-growth-fog.png b/docs/rework/existing-growth-fog.png
index 8bab7b5..c8bf17a 100644
Binary files a/docs/rework/existing-growth-fog.png and b/docs/rework/existing-growth-fog.png differ
diff --git a/docs/rework/full-history-fog.png b/docs/rework/full-history-fog.png
index 5802664..ceaf3e0 100644
Binary files a/docs/rework/full-history-fog.png and b/docs/rework/full-history-fog.png differ
diff --git a/docs/rework/full-history-moon.png b/docs/rework/full-history-moon.png
index 78e7132..947e1b8 100644
Binary files a/docs/rework/full-history-moon.png and b/docs/rework/full-history-moon.png differ
diff --git a/docs/rework/life-empty-fog.png b/docs/rework/life-empty-fog.png
index 883e54e..8e29daf 100644
Binary files a/docs/rework/life-empty-fog.png and b/docs/rework/life-empty-fog.png differ
diff --git a/docs/rework/live-gateway/browser-results.json b/docs/rework/live-gateway/browser-results.json
index 0e99ee4..d366fa5 100644
--- a/docs/rework/live-gateway/browser-results.json
+++ b/docs/rework/live-gateway/browser-results.json
@@ -17,7 +17,7 @@
},
{
"input": "刚才我们的暗号是什么?请直接回答。",
- "reply": "蓝色风铃。",
+ "reply": "蓝色风铃",
"source": "real gateway"
},
{
diff --git a/docs/rework/live-gateway/desktop-history-moon.png b/docs/rework/live-gateway/desktop-history-moon.png
index 6e5842e..1c543fa 100644
Binary files a/docs/rework/live-gateway/desktop-history-moon.png and b/docs/rework/live-gateway/desktop-history-moon.png differ
diff --git a/docs/rework/live-gateway/desktop-model-error.png b/docs/rework/live-gateway/desktop-model-error.png
index 7c230f0..4518b90 100644
Binary files a/docs/rework/live-gateway/desktop-model-error.png and b/docs/rework/live-gateway/desktop-model-error.png differ
diff --git a/docs/rework/live-gateway/desktop-preview-moon.png b/docs/rework/live-gateway/desktop-preview-moon.png
index e30d8ee..56495bd 100644
Binary files a/docs/rework/live-gateway/desktop-preview-moon.png and b/docs/rework/live-gateway/desktop-preview-moon.png differ
diff --git a/docs/rework/live-gateway/desktop-real-chat-moon.png b/docs/rework/live-gateway/desktop-real-chat-moon.png
index ece1e2e..4ef7d03 100644
Binary files a/docs/rework/live-gateway/desktop-real-chat-moon.png and b/docs/rework/live-gateway/desktop-real-chat-moon.png differ
diff --git a/docs/rework/live-gateway/mobile-chat-fog.png b/docs/rework/live-gateway/mobile-chat-fog.png
index aacc48e..f61b89f 100644
Binary files a/docs/rework/live-gateway/mobile-chat-fog.png and b/docs/rework/live-gateway/mobile-chat-fog.png differ
diff --git a/docs/rework/live-gateway/mobile-history-moon.png b/docs/rework/live-gateway/mobile-history-moon.png
index 012f673..3f5b54f 100644
Binary files a/docs/rework/live-gateway/mobile-history-moon.png and b/docs/rework/live-gateway/mobile-history-moon.png differ
diff --git a/docs/rework/live-gateway/mobile-relation-fog.png b/docs/rework/live-gateway/mobile-relation-fog.png
index ff98aec..641ca5c 100644
Binary files a/docs/rework/live-gateway/mobile-relation-fog.png and b/docs/rework/live-gateway/mobile-relation-fog.png differ
diff --git a/docs/rework/long-line-360-640-fog.png b/docs/rework/long-line-360-640-fog.png
index 5ea677a..dd0784a 100644
Binary files a/docs/rework/long-line-360-640-fog.png and b/docs/rework/long-line-360-640-fog.png differ
diff --git a/docs/rework/long-line-360-640-moon.png b/docs/rework/long-line-360-640-moon.png
index f015618..e5e71a1 100644
Binary files a/docs/rework/long-line-360-640-moon.png and b/docs/rework/long-line-360-640-moon.png differ
diff --git a/docs/rework/long-line-390-420-fog.png b/docs/rework/long-line-390-420-fog.png
index 44e98ed..3010b74 100644
Binary files a/docs/rework/long-line-390-420-fog.png and b/docs/rework/long-line-390-420-fog.png differ
diff --git a/docs/rework/long-line-390-420-moon.png b/docs/rework/long-line-390-420-moon.png
index c7ca3e5..5d69509 100644
Binary files a/docs/rework/long-line-390-420-moon.png and b/docs/rework/long-line-390-420-moon.png differ
diff --git a/docs/rework/long-line-390-844-fog.png b/docs/rework/long-line-390-844-fog.png
index 67486ff..8ead6ab 100644
Binary files a/docs/rework/long-line-390-844-fog.png and b/docs/rework/long-line-390-844-fog.png differ
diff --git a/docs/rework/long-line-390-844-moon.png b/docs/rework/long-line-390-844-moon.png
index f73a5e1..8e48ea3 100644
Binary files a/docs/rework/long-line-390-844-moon.png and b/docs/rework/long-line-390-844-moon.png differ
diff --git a/docs/rework/memory-groups-empty-fog.png b/docs/rework/memory-groups-empty-fog.png
index a86c7d0..9719ffa 100644
Binary files a/docs/rework/memory-groups-empty-fog.png and b/docs/rework/memory-groups-empty-fog.png differ
diff --git a/docs/rework/services-fog.png b/docs/rework/services-fog.png
index 0b4832f..fd65287 100644
Binary files a/docs/rework/services-fog.png and b/docs/rework/services-fog.png differ
diff --git a/web/src/chat/ChatPanel.tsx b/web/src/chat/ChatPanel.tsx
index 54a8213..4e527a9 100644
--- a/web/src/chat/ChatPanel.tsx
+++ b/web/src/chat/ChatPanel.tsx
@@ -8,7 +8,7 @@ import { Icon, Modal } from '../shared/ui';
import { snap } from '../shared/motion';
import { motionOff } from '../stage/roomScene';
import { LineStage, type PresentHow } from './lineStage';
-import { anchorOf, cursorAt, dwellFor, groupTurns, pagesOf, type Anchor, type Metrics, type ShownMessage } from './reading';
+import { anchorOf, cursorAt, groupTurns, pagesOf, ReadingClock, type Anchor, type Metrics, type ShownMessage } from './reading';
const START: Anchor = { mi: 0, offset: 0 };
const metricsFor = (width: number): Metrics => { const compact = document.documentElement.classList.contains('compact'); return { width, font: compact ? 16 : innerWidth <= 720 ? 18 : 23, compact }; };
@@ -62,7 +62,7 @@ export function ChatPanel({ state, onChange, onPresence, onVoice, onObservation,
const cursor = cursorAt(pages, anchor), page = pages[cursor], atEnd = cursor >= pages.length - 1;
const headIndex = cursorAt(pages, head.current);
if (cursor > headIndex) head.current = anchor;
- const held = useRef(new Set .
+HER_WORDS = CURRENT + '[data-role="companion"] p'
+# HEL-824: one real press while the input has focus. Records where the dialogue sat at press and at release and
+# which button the browser delivered the click to; nothing is blurred or dispatched by the script.
+TAP = """() => { const c = document.querySelector('.conversation'), top = () => Math.round(c.getBoundingClientRect().top);
+ window.tap = null; let down = null;
+ addEventListener('pointerdown', () => { down = top(); }, {capture: true, once: true});
+ addEventListener('click', e => { const b = e.target.closest('button');
+ window.tap = {down, up: top(), typing: document.documentElement.classList.contains('typing'), target: b ? (b.getAttribute('aria-label') || b.textContent.trim()) : e.target.tagName}; }, {capture: true, once: true}); }"""
+# Busy frames (software rendering, a heavy crossfade) arrive far apart; the reading clock must still keep real time.
+SLOW_FRAMES = "window.requestAnimationFrame = cb => setTimeout(() => cb(performance.now()), 700); window.cancelAnimationFrame = id => clearTimeout(id);"
try:
@@ -169,6 +182,7 @@ try:
assert metrics['composer']['bottom'] < metrics['nav']['top'], metrics
report['viewports'].append({'width': width, 'height': height, 'theme': theme, **metrics})
if width in (360, 390) and height in (640, 420, 844): shot(page, f'long-line-{width}-{height}-{theme}')
+ if page.evaluate("document.documentElement.classList.contains('micro')"): continue # micro (<420px) intentionally hides 全文/status; the design protects the dialogue area
click(page, '全文')
expect(page.locator('.record.full')).to_be_visible()
page.keyboard.press('Escape')
@@ -182,13 +196,59 @@ try:
if page.get_by_role('button', name='继续', exact=True).count(): click(page, '继续')
passed('12 viewport/theme combinations: lines stay inside the stage, stage/reader/composer/navigation never overlap; Esc closes full text with focus back; full history reachable')
+ # HEL-824 defect 1: keyboard-height screens, input focused, then a direct press on send / full text /
+ # pause / review. The press blurs the input; the dialogue must not move between press and release.
+ provider.mode = 'normal'
+ box, flow = page.get_by_role('textbox', name='想和她说的话'), page.locator('.reader-bar .flow')
+ report['compact_first_press'] = []
+
+ def first_press(button, label):
+ box.focus()
+ assert page.evaluate("document.documentElement.classList.contains('typing')")
+ page.evaluate(TAP)
+ button.click()
+ tap = page.evaluate('window.tap')
+ assert tap and tap['target'] == label and tap['down'] == tap['up'] and tap['typing'] is False, tap
+ report['compact_first_press'].append({'size': f'{width}x{height}', 'theme': theme, **tap})
+
+ for width, height in [(390, 420), (390, 430)]:
+ page.set_viewport_size({'width': width, 'height': height})
+ for theme, label in [('moon', '靛蓝月光'), ('fog', '蓝紫雾光')]:
+ click(page, label)
+ expect(page.locator('html')).to_have_class(re.compile(r'\bcompact\b'))
+ words = f'小屏首击{width}{theme}'
+ count = app.state.store.query_one('SELECT count(*) n FROM chat_message')['n']
+ box.fill(words)
+ first_press(page.get_by_role('button', name='发送', exact=True), '发送')
+ expect(box).to_have_value('')
+ expect(page.get_by_role('button', name='停止回复')).not_to_be_visible()
+ expect(page.locator(HER_WORDS)).to_have_text('答复', timeout=8000)
+ assert app.state.store.query_one('SELECT count(*) n FROM chat_message')['n'] == count + 2
+ first_press(page.get_by_role('button', name='上一句', exact=True), '上一句')
+ expect(page.locator(CURRENT)).to_have_attribute('data-role', 'user')
+ expect(page.locator(CURRENT + ' p')).to_have_text(words)
+ page.locator('.reader-bar .latest').click()
+ expect(page.locator(HER_WORDS)).to_have_text('答复')
+ was = flow.get_attribute('aria-pressed')
+ first_press(flow, flow.inner_text().strip())
+ expect(flow).not_to_have_attribute('aria-pressed', was)
+ first_press(flow, flow.inner_text().strip())
+ expect(flow).to_have_attribute('aria-pressed', was)
+ first_press(page.get_by_role('button', name='全文', exact=True), '全文')
+ expect(page.locator('.record.full')).to_be_visible()
+ expect(page.locator('.record.full')).to_contain_text('答复')
+ page.keyboard.press('Escape')
+ expect(page.locator('.record.full')).to_have_count(0)
+ if theme == 'moon': shot(page, f'compact-first-press-{width}-{height}-moon')
+ passed('390x420 and 390x430, both themes: with the input focused, the first press on send, review, pause/continue and full text acts; the dialogue does not move between press and release')
+
page.set_viewport_size({'width': 390, 'height': 700})
provider.mode = 'slow'; send(page, '取消并重发')
expect(page.get_by_role('button', name='停止回复')).to_be_visible()
click(page, '停止回复')
provider.mode = 'normal'; send(page, '新轮')
expect(page.get_by_role('button', name='停止回复')).not_to_be_visible()
- expect(page.locator(CURRENT + '[data-role="companion"]')).to_have_text('答复', timeout=8000)
+ expect(page.locator(HER_WORDS)).to_have_text('答复', timeout=8000)
provider.mode = 'error'; send(page, '失败再试')
expect(page.locator('.talk-status .retry')).to_be_visible()
expect(page.get_by_role('textbox', name='想和她说的话')).to_have_value('失败再试')
@@ -199,6 +259,24 @@ try:
assert app.state.store.query_one('SELECT count(*) n FROM chat_message')['n'] == count + 1
passed('Cancel then immediate new turn, model error and same-turn retry preserve roles and avoid duplicate user messages')
+ # HEL-824 defect 2: the same stop -> immediate new turn with frames 700 ms apart. Your line keeps its
+ # designed 1.4 s, then her reply is on stage; before the fix each frame counted 100 ms (about 10 s).
+ slow = browser.new_context(viewport={'width': 390, 'height': 700})
+ slow.add_init_script(SLOW_FRAMES)
+ sp = slow.new_page(); sp.on('pageerror', lambda e: report['console_errors'].append(str(e)))
+ sp.goto(url); nav(sp, '交谈')
+ provider.mode = 'slow'; send(sp, '慢帧下取消')
+ expect(sp.get_by_role('button', name='停止回复')).to_be_visible()
+ click(sp, '停止回复')
+ provider.mode = 'normal'; send(sp, '慢帧下新轮')
+ sent = time.monotonic()
+ expect(sp.locator(CURRENT)).to_have_attribute('data-role', 'user')
+ expect(sp.locator(HER_WORDS)).to_have_text('答复', timeout=8000)
+ report['motion']['slow_frames_new_turn_to_reply_s'] = round(time.monotonic() - sent, 2)
+ assert sp.locator(CURRENT + ' .state').inner_text() == ''
+ slow.close()
+ passed('Stop then new turn with busy (700 ms) frames: your line keeps its own time and her reply follows; the reading clock counts real time')
+
nav(page, '设置'); click(page, '她')
expect(page.locator('.color-sections section')).to_have_count(6)
expect(page.locator('.calendar')).to_contain_text('闰四月初一')
@@ -240,12 +318,12 @@ try:
assert page.locator('html').evaluate("el=>el.classList.contains('reduced')")
provider.mode = 'normal'; send(page, '减少动效检查')
expect(page.get_by_role('button', name='停止回复')).not_to_be_visible()
- expect(page.locator(CURRENT + '[data-role="companion"]')).to_have_text('答复', timeout=8000)
+ expect(page.locator(HER_WORDS)).to_have_text('答复', timeout=8000)
assert page.evaluate(STILL) and page.locator('.stage p.ink').count() == 0
reduced = browser.new_context(viewport={'width': 390, 'height': 700}, reduced_motion='reduce')
rp = reduced.new_page(); rp.goto(url); nav(rp, '交谈'); send(rp, '系统减少动效')
expect(rp.get_by_role('button', name='停止回复')).not_to_be_visible()
- expect(rp.locator(CURRENT + '[data-role="companion"]')).to_have_text('答复', timeout=8000)
+ expect(rp.locator(HER_WORDS)).to_have_text('答复', timeout=8000)
assert rp.evaluate(STILL) and rp.locator('.stage p.ink').count() == 0
reduced.close()
passed('Escape navigation; browser display preference survives reload; settings/system reduced motion uses gentle opacity')
@@ -266,7 +344,7 @@ try:
click(dp, '结束语音')
dp.locator('.demo > button').click(); click(dp, '载入三天记录')
nav(dp, '日子'); dp.locator('.ev').first.click(); click(dp, '看那段聊天')
- expect(dp.locator('.log-panel')).to_be_visible(); click(dp, '关闭记录'); click(dp, '返回日子')
+ expect(dp.locator('.log-panel')).to_be_visible(); click(dp, '关闭记录'); click(dp, '结束交谈 · 回到日子')
expect(dp.locator('.pane[aria-label="日子"]')).to_have_class('pane glass read open has-detail')
assert not requests
passed('Demo has its own profile, voice simulation, life/detail/chat/return path and zero API requests')