diff --git a/prototypes/hel-790/README.md b/prototypes/hel-790/README.md new file mode 100644 index 0000000..0af09e0 --- /dev/null +++ b/prototypes/hel-790/README.md @@ -0,0 +1,49 @@ +# 月光里的相处 · HEL-790 独立动态小样 + +下载 `index.html`,用浏览器直接打开;图片、文字、样式和交互都在这一个文件里,不需要安装或联网。`evidence/` 是这份真实界面的截图、录屏与检查记录。此目录没有被产品入口导入。 + +这次请只判断对话的呈现和整体空间的感觉。两套主题都保留。所有人物画面是此前获批静帧,文字、生活、回忆和天气都是虚构示例;没有连接模型、麦克风、摄像头、生活或备份服务。 + +## 怎样试 + +1. 相处页保留右侧独立人物窗、左侧少量近况和底部透光导航。点击「和她说话」,看人物窗怎样在同一片空间里展开,再点「回到此刻」。 +2. 点击右上角「独立小样 · 演示数据」,选择「一段长回复」。当前只呈现一句;用左右箭头逐句读完,也可以选「自动续读」。点击「展开全文」或输入区左边的记录按钮,查看完整内容,关闭后回到刚才那一句。 +3. 同一试用浮层还可以体验短句、连续来两段话、生成中与取消。新来句会排在后面,不抢走正在读的内容。取消后已出现的片段明确标为未完成。 +4. 切换月光/雾光,再进入「日子」「我们的回忆」「设置」。从一条已分享生活进入对应聊天,可以返回这一天。新建临时演示身份后,这些区域为空。 +5. 手机可直接打开同一 HTML。收起或弹出键盘时,布局按可视高度重新给人物与正文分配空间。本次只验证了缩小视口,未在真实手机软键盘、iOS 或 Safari 上验证。 + +## 本次交互提案,仍待用户验收 + +- 默认手动续读;自动续读为可选,每句至少停留 3.8 秒,并随文字长度延长。输入、选择文字、打开记录或切到后台都会停止自动续读。 +- 句子按标点、可读长度分段,长句不会丢字;空间很小时段落会更短。上一句/下一句同时覆盖本轮用户与她的文字。身份每句注明。 +- 本原型按完整句子逐步模拟生成,不等同于已连接真实 SSE。真实词元流如何缓存成可读分句,需要在正式接入时验证。 +- 人物展开、浮层和背景的空间推进均可逆;文本以 100ms 退场、160ms 进入过渡,流式追加的同一句不重播整段动画。移动采用已有缓动与 280ms 预算。 +- 月光是打开小样的临时初始值,不代表用户已经选择产品默认主题。 + +## Before / After / Why + +对照对象是 PR#4 开始时实际 head `db571ba204c8372989f96e1ed9b5cd00d3a306c7` 的代码、8310 静态壳,以及 HEL-744/746 获批原型。访问 8310 时拦截全部 API,未请求真实档案与聊天;因此没有声称亲测真实聊天内容或模型表现。 + +| Before | After | Why | +| --- | --- | --- | +| 当前主区 `dialog-scroll` 同时容纳完整长消息 | 主区当前一句,前后查看;全文与历史独立按需展开 | 长回复完整保留,同时兑现 VIS-013 的人物为主 | +| 曾以局部文字效果和滚动修补作为主要验收依据 | 一个持续存在的人物窗,展开/收回、进入日子与设置都与背景共同改变层次 | 对照 VIS-001/006/007,验证整个相处空间 | +| 矮屏的固定人物高度会逼近正文 | 按真实正文位置计算人物窗剩余高度;缩小视口后重新分配空间 | 正文、身份、输入和导航互不覆盖 | +| 逐次替换文字可能产生生硬跳变 | 旧句先退、新句再进;同一句追加不重播;迟到内容受取消标记约束 | UX-002,阅读连续且取消可信 | +| 全局新旧状态容易争抢阅读位置 | 新来句排队,全文/历史开合保持当前位置 | 用户掌握阅读速度 | +| 组件逐块换色,容易脱离整体氛围 | 同一布局下同时切换天空、光雾、景物、阅读材质与微光 | 保留已认可的两主题,避免改变视觉方向 | + +## 复现与范围 + +```bash +python3 prototypes/hel-790/build.py +node --check prototypes/hel-790/src/space.js +python3 prototypes/hel-790/verify.py +python3 prototypes/hel-790/record.py +``` + +构建只用 Python 标准库,复用 `web/public/assets/her-room.jpg` 和 `her-talk.jpg`。验证与录屏使用环境已有的 Python Playwright / Chromium;必要的已有共享库路径由脚本检测,仅作用于本次进程。没有新增产品依赖。 + +结构化需求交接见 `handoff.json`,逐项映射与实际证据见 `REVIEW.md`。开始时从需求治理 PR#5 读取基准;交付前它已独立合入 main。本卡随后快进到 main `4242f8377e350e5f17e836d275c15b0f099bf109`,逐字节复核必读文件一致,并在本分支直接执行需求检查。相对 main 的改动只在本目录,不夹带产品 PR#4。 + +本小样未更换正式界面,未部署、未合并,也未改用户档案、出生底色、聊天与记忆。R1 的人格诠释/成长缺口、R2–R7 的真实功能接入和独立口型研究仍须后续完成。整项目 HEL-697 持续保留。 diff --git a/prototypes/hel-790/REVIEW.md b/prototypes/hel-790/REVIEW.md new file mode 100644 index 0000000..69e6845 --- /dev/null +++ b/prototypes/hel-790/REVIEW.md @@ -0,0 +1,64 @@ +# HEL-790 交接与体验自查 + +## 需求与来源 + +- 完整基准 v1.7;读取提交 `97aaefe5fac77e9746a4ce52d3af94adad15efc1`(需求治理 PR#5)。当时 `origin/main` 为 `498429399812a0776dadd4b56662d31b455a9ea5`,尚无需求入口。 +- 已读该提交的 AGENTS.md、需求 README/current/coverage、交接模板、原始产品与视觉访谈 `sources/comment-evidence.json`、获批视觉确认、完整设计规范、设计裁定和 R1–R7 接入方案。 +- 交付前 PR#5 已独立合入 main;快进到 `4242f8377e350e5f17e836d275c15b0f099bf109`,核对必读文件与已读版本逐字节一致。最终交接以此 main 提交为准,相对 main 只新增本原型目录。 +- 用户批准来源:HEL-697 评论 `01a1635b-b47c-79ed-ac66-9408c7905af9`;原两主题与首页比例批准 `01a15f1f-5a71-783c-a458-b326a0073d69`;完整页面与分享/关系/起名裁定 `01a15f42-2781-700e-b75a-3ae4b023f46c`。这些决定不重新提问。 +- 取回 HEL-744 两张获批实际样图、HEL-746 完整原型 R2 并实际查看;两个人物素材与仓库文件逐字节相同。没有新增生成图片。 +- 开始前经现有合法 SSH 身份认证 Gitea,重查产品 PR#4 实际 head:`db571ba204c8372989f96e1ed9b5cd00d3a306c7`,base main,open、未合并。仅 fetch/读取,不在其分支修改。 +- 实际打开 8310 页面时拦截全部 `/api/**`,只有 health 尝试被拦截,没有读取用户档案或聊天。当前长消息行为对照来自上述 head 的 `ChatPanel.tsx` 与样式,未冒充真实聊天实测。 + +`handoff.json` 逐条列出本卡关联编号、完整原意和来源评论,及本次可观察证据。 + +## 本次交付 + +`index.html` 是可离线打开的真实 DOM 原型,构建只复用获批静帧。相处 → 交流 → 全文/历史 → 收回交流 → 日子/回忆/设置 → 相处都可以实际操作。 + +首页的 30px 轻标题、右侧 27vw 拱窗、14vw 右边距和小巧底部导航沿用获批几何。交流的人物尺寸按可视高度和实际正文占用分配,不让长名字或缩高视口把文字挤到照片里。生活与设置用透光阅读浮层;人物窗和远近环境在同一空间里后退。 + +两套主题使用相同 DOM、入口与位置;月光有月面、月路和冷雾,雾光有蓝紫流光和更柔软的雾层,景物、材质与关键微光一起变化。没有新增粒子堆叠或整屏卧室背景。 + +## 动效自查(Before / After / Why) + +| Before | After | Why | +| --- | --- | --- | +| 第一轮试制的固定人物高度在小屏接近正文 | 按正文的实际边界分配人物窗高度 | 优先保证身份、正文、输入和导航不互挡 | +| 第一轮试制的图片默认居中,人物取景偏了 | 恢复获批 `88% 42%` 取景与 1.16 静态裁切 | 人物在相处状态也必须看得见 | +| 试制浮层的退出动画可能留下透明填充状态 | 关闭时取消该节点的全部动画,再恢复空间与焦点 | 反复展开记录仍可见、可操作 | +| 突然替换当前文字 | 100ms 退场、160ms 进入;相同句子追加不重播 | 过渡服务于语义变化,不让生成重复闪动 | +| 对几何属性直接做过渡会逐帧触发布局 | 几何一次落位,再用 FLIP 的 transform 运动 | 背景、人物和浮层的动效只播放 transform/opacity | +| 键盘操作也可能播放切换动效 | 键盘操作不启动位置动画;系统减少动效停止循环 | 遵守频率和可访问性约束 | + +入口与退出用已有 `cubic-bezier(.22,1,.36,1)`;空间移动与浮层进入 280ms、退出 180ms。背景的 32–64 秒循环只承担舒缓流动,在减少动效或页面隐藏时停止。正常关闭中可以重新打开,人物窗中途反向从当前可见位置衔接。没有 `transition: all`、零尺寸弹入、ease-in 进入或逐帧宽高动画。 + +工程自查结论:完成本卡的独立设计原型;视觉和阅读节奏仍待白栖知体验裁定,不能以自查代替认可。性能没有做帧率或低端设备实测,不声称稳定 60fps。 + +## 验证证据 + +- `evidence/verification.json`:浏览器验证的逐项结果,包含真实 DOM 逐句读取与完整原文比对、全文末句、连续来句不抢阅读位置、取消迟到消息、长名字、长输入、焦点与反复开合、两主题及可视区域边界。 +- 电脑四张:`desktop-home-moon/fog.png`、`desktop-talk-moon/fog.png`,1440×900。 +- 手机四张:`mobile-home-moon/fog.png`、`mobile-talk-moon/fog.png`,390×844。 +- 补充:长回复末尾、全文末尾、生活详情、共同回忆、设置,以及 `simulated-viewport-*.png`。 +- 动态:`desktop-walkthrough.webm` 与 `mobile-walkthrough.webm`,实际点击原型录制,展示对话、长回复、全文、记录、往返、主题、日子/回忆和设置。 +- Chromium / Linux;验证尺寸包括 1440×900、1280×720、844×390、390×844、390×600、390×420、360×400、320×568、320×360;两主题均做正文/输入/导航边界检查。缩小视口是等效模拟,不是真机键盘测试。 +- JavaScript 语法检查;标准库构建。运行环境已有浏览器共享库只设置到本次进程,没有更改全局库、认证或服务。 +- 离线原型用 CSP 禁止网络连接,浏览器检查 HTTP/WebSocket 请求为零,不调用媒体权限,不使用真实或浏览器既有产品存储。 +- 需求检查在已快进到最新 main 的本工作树运行,实际目标 `origin/main`;执行记录在 `evidence/requirements-check.txt`。没有把治理 PR 或产品 PR#4 搬进本小样分支。 + +## 本次不能作为完成结论的事项 + +1. 分句长度、默认手动翻看、自动续读的停留时长、过渡强度,是这次提出供用户验收的交互方案,不是此前已确认的产品规则。 +2. 文字生成只是完整句子的模拟,没有接入真实词元流;正式接入须继续验证流缓存、取消、重连、错误及阅读位置。 +3. 照片只证明同一人物/房间/衣服的展示关系,不证明真人活动、视频衔接、口型或语音同步已经实现。 +4. 四类入口和关系保留。出生排盘/底色、外观导入、备份恢复和外接服务在本卡主要保留入口与未接入说明;没有重做旧 R2 的所有五十多个状态,更不声称完整功能已补齐。后续正式界面继续继承 R2 完整范围及新小样认可结果。 +5. 演示生活只有两条已分享记录,没有携带任何未分享记录,也不模拟服务器权限已完成。R2/R3 仍须在响应前裁剪生活、记忆和关联引用。 +6. 临时演示身份只供测试长名字与关系选择;正式创建的出生、底色、关系确认、异常恢复等完整流程仍由原方案约束。本卡未改变、重建真实伴侣。 +7. 真机软键盘、Safari/iOS、屏幕阅读器的实际朗读、触摸手势与低端设备性能仍待验证。 + +## 未完成承接与下一道关卡 + +先由白栖知只验收这次的对话与空间小样。认可后才能安排正式界面实现、独立审核及测试环境发布;本卡没有部署或合并授权。 + +主卡 HEL-697 保留完整项目追踪。R1 的出生底色深度、模型诠释、初始喜好与持续成长仍有缺口;R2 生活、R3 记忆、R4 主动消息、R5 连续语音、R6 天气/素材/备份、R7 视觉观察及口型独立线继续按已有前置承接。没有新增、删除或降低产品需求,`requirement_changes` 为空。 diff --git a/prototypes/hel-790/build.py b/prototypes/hel-790/build.py new file mode 100644 index 0000000..39bb4f8 --- /dev/null +++ b/prototypes/hel-790/build.py @@ -0,0 +1,14 @@ +"""Build an offline, single-file prototype. No dependencies or network access.""" +import base64 +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +html = (ROOT / 'src/shell.html').read_text() +for key, filename in [('STYLE', 'space.css'), ('SCRIPT', 'space.js')]: + html = html.replace('{{' + key + '}}', (ROOT / 'src' / filename).read_text()) +for key, filename in [('ROOM', 'her-room.jpg'), ('TALK', 'her-talk.jpg')]: + data = (ROOT.parents[1] / 'web/public/assets' / filename).read_bytes() + html = html.replace('{{' + key + '}}', 'data:image/jpeg;base64,' + base64.b64encode(data).decode()) +assert '{{' not in html +(ROOT / 'index.html').write_text(html) +print(f'Built offline index.html ({len(html.encode()):,} bytes)') diff --git a/prototypes/hel-790/evidence/desktop-home-fog.png b/prototypes/hel-790/evidence/desktop-home-fog.png new file mode 100644 index 0000000..9b76d70 Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-home-fog.png differ diff --git a/prototypes/hel-790/evidence/desktop-home-moon.png b/prototypes/hel-790/evidence/desktop-home-moon.png new file mode 100644 index 0000000..3579e74 Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-home-moon.png differ diff --git a/prototypes/hel-790/evidence/desktop-life-moon.png b/prototypes/hel-790/evidence/desktop-life-moon.png new file mode 100644 index 0000000..1a96869 Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-life-moon.png differ diff --git a/prototypes/hel-790/evidence/desktop-management-fog.png b/prototypes/hel-790/evidence/desktop-management-fog.png new file mode 100644 index 0000000..0ccbe6c Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-management-fog.png differ diff --git a/prototypes/hel-790/evidence/desktop-memory-fog.png b/prototypes/hel-790/evidence/desktop-memory-fog.png new file mode 100644 index 0000000..d3b4503 Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-memory-fog.png differ diff --git a/prototypes/hel-790/evidence/desktop-playback-contact.png b/prototypes/hel-790/evidence/desktop-playback-contact.png new file mode 100644 index 0000000..042ac1d Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-playback-contact.png differ diff --git a/prototypes/hel-790/evidence/desktop-talk-fog.png b/prototypes/hel-790/evidence/desktop-talk-fog.png new file mode 100644 index 0000000..71f2a87 Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-talk-fog.png differ diff --git a/prototypes/hel-790/evidence/desktop-talk-moon.png b/prototypes/hel-790/evidence/desktop-talk-moon.png new file mode 100644 index 0000000..7957775 Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-talk-moon.png differ diff --git a/prototypes/hel-790/evidence/desktop-walkthrough.webm b/prototypes/hel-790/evidence/desktop-walkthrough.webm new file mode 100644 index 0000000..62ff4de Binary files /dev/null and b/prototypes/hel-790/evidence/desktop-walkthrough.webm differ diff --git a/prototypes/hel-790/evidence/mobile-full-end-fog.png b/prototypes/hel-790/evidence/mobile-full-end-fog.png new file mode 100644 index 0000000..8883b39 Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-full-end-fog.png differ diff --git a/prototypes/hel-790/evidence/mobile-home-fog.png b/prototypes/hel-790/evidence/mobile-home-fog.png new file mode 100644 index 0000000..618b9f5 Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-home-fog.png differ diff --git a/prototypes/hel-790/evidence/mobile-home-moon.png b/prototypes/hel-790/evidence/mobile-home-moon.png new file mode 100644 index 0000000..4e000f8 Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-home-moon.png differ diff --git a/prototypes/hel-790/evidence/mobile-long-end-fog.png b/prototypes/hel-790/evidence/mobile-long-end-fog.png new file mode 100644 index 0000000..567479a Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-long-end-fog.png differ diff --git a/prototypes/hel-790/evidence/mobile-playback-contact.png b/prototypes/hel-790/evidence/mobile-playback-contact.png new file mode 100644 index 0000000..c782a96 Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-playback-contact.png differ diff --git a/prototypes/hel-790/evidence/mobile-talk-fog.png b/prototypes/hel-790/evidence/mobile-talk-fog.png new file mode 100644 index 0000000..66f9047 Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-talk-fog.png differ diff --git a/prototypes/hel-790/evidence/mobile-talk-moon.png b/prototypes/hel-790/evidence/mobile-talk-moon.png new file mode 100644 index 0000000..8dd108f Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-talk-moon.png differ diff --git a/prototypes/hel-790/evidence/mobile-walkthrough.webm b/prototypes/hel-790/evidence/mobile-walkthrough.webm new file mode 100644 index 0000000..27f6da8 Binary files /dev/null and b/prototypes/hel-790/evidence/mobile-walkthrough.webm differ diff --git a/prototypes/hel-790/evidence/requirements-check.txt b/prototypes/hel-790/evidence/requirements-check.txt new file mode 100644 index 0000000..4e614fd --- /dev/null +++ b/prototypes/hel-790/evidence/requirements-check.txt @@ -0,0 +1,4 @@ +$ python3 scripts/check_requirements.py --base-ref origin/main +需求结构检查通过:58项;来源和覆盖完整。仍须人工核对授权语义与实际体验。 +$ python3 scripts/check_requirements.py --base-ref origin/main --handoff prototypes/hel-790/handoff.json +需求结构检查通过:58项;来源和覆盖完整。仍须人工核对授权语义与实际体验。 diff --git a/prototypes/hel-790/evidence/simulated-viewport-320-360.png b/prototypes/hel-790/evidence/simulated-viewport-320-360.png new file mode 100644 index 0000000..9caeeab Binary files /dev/null and b/prototypes/hel-790/evidence/simulated-viewport-320-360.png differ diff --git a/prototypes/hel-790/evidence/simulated-viewport-360-400.png b/prototypes/hel-790/evidence/simulated-viewport-360-400.png new file mode 100644 index 0000000..6931dcc Binary files /dev/null and b/prototypes/hel-790/evidence/simulated-viewport-360-400.png differ diff --git a/prototypes/hel-790/evidence/simulated-viewport-390-420.png b/prototypes/hel-790/evidence/simulated-viewport-390-420.png new file mode 100644 index 0000000..6746206 Binary files /dev/null and b/prototypes/hel-790/evidence/simulated-viewport-390-420.png differ diff --git a/prototypes/hel-790/evidence/verification.json b/prototypes/hel-790/evidence/verification.json new file mode 100644 index 0000000..1797381 --- /dev/null +++ b/prototypes/hel-790/evidence/verification.json @@ -0,0 +1,2227 @@ +{ + "browser": "Chromium / Linux", + "checks": [ + { + "check": "1440×900 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 240.00985717773438, + "y": 78.00181579589844, + "w": 959.99072265625, + "h": 500.00006103515625, + "b": 578.0018768310547, + "r": 1200.0005798339844 + }, + "reading": { + "x": 350, + "y": 592.9064331054688, + "w": 740, + "h": 90, + "b": 682.9064331054688, + "r": 1090 + }, + "sentence": { + "x": 376, + "y": 634.5939331054688, + "w": 688, + "h": 35, + "b": 669.5939331054688, + "r": 1064 + }, + "composer": { + "x": 350, + "y": 745.0001831054688, + "w": 740, + "h": 60, + "b": 805.0001831054688, + "r": 1090 + }, + "nav": { + "x": 578, + "y": 819, + "w": 284, + "h": 63, + "b": 882, + "r": 862 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1440, + "h": 900, + "b": 900, + "r": 1440 + }, + "speaker": { + "x": 701.984375, + "y": 606.2033081054688, + "w": 24.015625, + "h": 20.390625, + "b": 626.5939331054688, + "r": 726 + } + } + }, + { + "check": "1440×900 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "1440×900 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "1440×900 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 242.99102783203125, + "y": 78.55094146728516, + "w": 957.1974487304688, + "h": 500.0265808105469, + "b": 578.577522277832, + "r": 1200.1884765625 + }, + "reading": { + "x": 350, + "y": 592.955322265625, + "w": 740, + "h": 90, + "b": 682.955322265625, + "r": 1090 + }, + "sentence": { + "x": 376, + "y": 634.642822265625, + "w": 688, + "h": 35, + "b": 669.642822265625, + "r": 1064 + }, + "composer": { + "x": 350, + "y": 745.049072265625, + "w": 740, + "h": 60, + "b": 805.049072265625, + "r": 1090 + }, + "nav": { + "x": 578, + "y": 819, + "w": 284, + "h": 63, + "b": 882, + "r": 862 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1440, + "h": 900, + "b": 900, + "r": 1440 + }, + "speaker": { + "x": 701.984375, + "y": 606.252197265625, + "w": 24.015625, + "h": 20.390625, + "b": 626.642822265625, + "r": 726 + } + } + }, + { + "check": "1440×900 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "1440×900 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×844 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 68, + "w": 366, + "h": 449, + "b": 517, + "r": 378 + }, + "reading": { + "x": 12, + "y": 531.90625, + "w": 366, + "h": 115, + "b": 646.90625, + "r": 378 + }, + "sentence": { + "x": 26, + "y": 586.796875, + "w": 338, + "h": 30.59375, + "b": 617.390625, + "r": 364 + }, + "composer": { + "x": 12, + "y": 702, + "w": 366, + "h": 51, + "b": 753, + "r": 378 + }, + "nav": { + "x": 58, + "y": 772, + "w": 274, + "h": 57, + "b": 829, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 844, + "b": 844, + "r": 390 + }, + "speaker": { + "x": 176.984375, + "y": 561.40625, + "w": 24.015625, + "h": 20.390625, + "b": 581.796875, + "r": 201 + } + } + }, + { + "check": "390×844 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×844 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×844 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×844 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×844 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 68, + "w": 366, + "h": 449, + "b": 517, + "r": 378 + }, + "reading": { + "x": 12, + "y": 531.90625, + "w": 366, + "h": 115, + "b": 646.90625, + "r": 378 + }, + "sentence": { + "x": 26, + "y": 586.796875, + "w": 338, + "h": 30.59375, + "b": 617.390625, + "r": 364 + }, + "composer": { + "x": 12, + "y": 702, + "w": 366, + "h": 51, + "b": 753, + "r": 378 + }, + "nav": { + "x": 58, + "y": 772, + "w": 274, + "h": 57, + "b": 829, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 844, + "b": 844, + "r": 390 + }, + "speaker": { + "x": 176.984375, + "y": 561.40625, + "w": 24.015625, + "h": 20.390625, + "b": 581.796875, + "r": 201 + } + } + }, + { + "check": "390×844 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×844 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×844 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×844 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "逐句点击后的真实 DOM 文字与完整原文一致", + "passed": true, + "details": null + }, + { + "check": "长回复分句完整无缺字", + "passed": true, + "details": null + }, + { + "check": "长回复没有空白翻页", + "passed": true, + "details": null + }, + { + "check": "长回复末尾可从主界面翻到", + "passed": true, + "details": 13 + }, + { + "check": "全文保留末句", + "passed": true, + "details": null + }, + { + "check": "收回全文保持句子位置", + "passed": true, + "details": null + }, + { + "check": "记录第 1 次打开可见", + "passed": true, + "details": null + }, + { + "check": "浮层焦点在原生对话框内", + "passed": true, + "details": null + }, + { + "check": "Tab 保持在浮层内", + "passed": true, + "details": null + }, + { + "check": "收回记录保持句子位置", + "passed": true, + "details": null + }, + { + "check": "记录第 2 次打开可见", + "passed": true, + "details": null + }, + { + "check": "浮层焦点在原生对话框内", + "passed": true, + "details": null + }, + { + "check": "Tab 保持在浮层内", + "passed": true, + "details": null + }, + { + "check": "收回记录保持句子位置", + "passed": true, + "details": null + }, + { + "check": "记录第 3 次打开可见", + "passed": true, + "details": null + }, + { + "check": "浮层焦点在原生对话框内", + "passed": true, + "details": null + }, + { + "check": "Tab 保持在浮层内", + "passed": true, + "details": null + }, + { + "check": "收回记录保持句子位置", + "passed": true, + "details": null + }, + { + "check": "连续来句不抢走正在读的句子", + "passed": true, + "details": null + }, + { + "check": "两段来句均保留", + "passed": true, + "details": null + }, + { + "check": "取消后迟到内容不再写入", + "passed": true, + "details": null + }, + { + "check": "未完成片段明确标注", + "passed": true, + "details": null + }, + { + "check": "新一轮不混入取消的长回复", + "passed": true, + "details": null + }, + { + "check": "长用户输入与表情完整保留", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 240, + "y": 78, + "w": 960, + "h": 500, + "b": 578, + "r": 1200 + }, + "reading": { + "x": 350, + "y": 592.90625, + "w": 740, + "h": 90, + "b": 682.90625, + "r": 1090 + }, + "sentence": { + "x": 376, + "y": 634.59375, + "w": 688, + "h": 35, + "b": 669.59375, + "r": 1064 + }, + "composer": { + "x": 350, + "y": 745, + "w": 740, + "h": 60, + "b": 805, + "r": 1090 + }, + "nav": { + "x": 578, + "y": 819, + "w": 284, + "h": 63, + "b": 882, + "r": 862 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1440, + "h": 900, + "b": 900, + "r": 1440 + }, + "speaker": { + "x": 547.015625, + "y": 606.203125, + "w": 252.015625, + "h": 20.390625, + "b": 626.59375, + "r": 799.03125 + } + } + }, + { + "check": "1440×900 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 240, + "y": 78, + "w": 960, + "h": 500, + "b": 578, + "r": 1200 + }, + "reading": { + "x": 350, + "y": 592.90625, + "w": 740, + "h": 90, + "b": 682.90625, + "r": 1090 + }, + "sentence": { + "x": 376, + "y": 634.59375, + "w": 688, + "h": 35, + "b": 669.59375, + "r": 1064 + }, + "composer": { + "x": 350, + "y": 745, + "w": 740, + "h": 60, + "b": 805, + "r": 1090 + }, + "nav": { + "x": 578, + "y": 819, + "w": 284, + "h": 63, + "b": 882, + "r": 862 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1440, + "h": 900, + "b": 900, + "r": 1440 + }, + "speaker": { + "x": 547.015625, + "y": 606.203125, + "w": 252.015625, + "h": 20.390625, + "b": 626.59375, + "r": 799.03125 + } + } + }, + { + "check": "1440×900 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "1440×900 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 179.203125, + "y": 78, + "w": 921.59375, + "h": 320, + "b": 398, + "r": 1100.796875 + }, + "reading": { + "x": 270, + "y": 412.90625, + "w": 740, + "h": 90, + "b": 502.90625, + "r": 1010 + }, + "sentence": { + "x": 296, + "y": 454.59375, + "w": 688, + "h": 35, + "b": 489.59375, + "r": 984 + }, + "composer": { + "x": 270, + "y": 565, + "w": 740, + "h": 60, + "b": 625, + "r": 1010 + }, + "nav": { + "x": 498, + "y": 639, + "w": 284, + "h": 63, + "b": 702, + "r": 782 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1280, + "h": 720, + "b": 720, + "r": 1280 + }, + "speaker": { + "x": 467.015625, + "y": 426.203125, + "w": 252.015625, + "h": 20.390625, + "b": 446.59375, + "r": 719.03125 + } + } + }, + { + "check": "1280×720 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 179.203125, + "y": 78, + "w": 921.59375, + "h": 320, + "b": 398, + "r": 1100.796875 + }, + "reading": { + "x": 270, + "y": 412.90625, + "w": 740, + "h": 90, + "b": 502.90625, + "r": 1010 + }, + "sentence": { + "x": 296, + "y": 454.59375, + "w": 688, + "h": 35, + "b": 489.59375, + "r": 984 + }, + "composer": { + "x": 270, + "y": 565, + "w": 740, + "h": 60, + "b": 625, + "r": 1010 + }, + "nav": { + "x": 498, + "y": 639, + "w": 284, + "h": 63, + "b": 702, + "r": 782 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1280, + "h": 720, + "b": 720, + "r": 1280 + }, + "speaker": { + "x": 467.015625, + "y": 426.203125, + "w": 252.015625, + "h": 20.390625, + "b": 446.59375, + "r": 719.03125 + } + } + }, + { + "check": "1280×720 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "1280×720 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 118.1640625, + "y": 57, + "w": 607.671875, + "h": 73, + "b": 130, + "r": 725.8359375 + }, + "reading": { + "x": 92.84375, + "y": 144.90625, + "w": 658.3125, + "h": 68, + "b": 212.90625, + "r": 751.15625 + }, + "sentence": { + "x": 100.84375, + "y": 177.53125, + "w": 642.3125, + "h": 24.75, + "b": 202.28125, + "r": 743.15625 + }, + "composer": { + "x": 92.84375, + "y": 261, + "w": 658.3125, + "h": 54, + "b": 315, + "r": 751.15625 + }, + "nav": { + "x": 280, + "y": 321.609375, + "w": 284, + "h": 60.390625, + "b": 382, + "r": 564 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 844, + "h": 390, + "b": 390, + "r": 844 + }, + "speaker": { + "x": 259.53125, + "y": 156.1875, + "w": 231, + "h": 18.6875, + "b": 174.875, + "r": 490.53125 + } + } + }, + { + "check": "844×390 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 118.1640625, + "y": 57, + "w": 607.671875, + "h": 73, + "b": 130, + "r": 725.8359375 + }, + "reading": { + "x": 92.84375, + "y": 144.90625, + "w": 658.3125, + "h": 68, + "b": 212.90625, + "r": 751.15625 + }, + "sentence": { + "x": 100.84375, + "y": 177.53125, + "w": 642.3125, + "h": 24.75, + "b": 202.28125, + "r": 743.15625 + }, + "composer": { + "x": 92.84375, + "y": 261, + "w": 658.3125, + "h": 54, + "b": 315, + "r": 751.15625 + }, + "nav": { + "x": 280, + "y": 321.609375, + "w": 284, + "h": 60.390625, + "b": 382, + "r": 564 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 844, + "h": 390, + "b": 390, + "r": 844 + }, + "speaker": { + "x": 259.53125, + "y": 156.1875, + "w": 231, + "h": 18.6875, + "b": 174.875, + "r": 490.53125 + } + } + }, + { + "check": "844×390 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "844×390 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 68, + "w": 366, + "h": 449, + "b": 517, + "r": 378 + }, + "reading": { + "x": 12, + "y": 531.90625, + "w": 366, + "h": 115, + "b": 646.90625, + "r": 378 + }, + "sentence": { + "x": 26, + "y": 597, + "w": 338, + "h": 30.59375, + "b": 627.59375, + "r": 364 + }, + "composer": { + "x": 12, + "y": 702, + "w": 366, + "h": 51, + "b": 753, + "r": 378 + }, + "nav": { + "x": 58, + "y": 772, + "w": 274, + "h": 57, + "b": 829, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 844, + "b": 844, + "r": 390 + }, + "speaker": { + "x": 26, + "y": 551.21875, + "w": 244.0625, + "h": 40.78125, + "b": 592, + "r": 270.0625 + } + } + }, + { + "check": "390×844 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 68, + "w": 366, + "h": 449, + "b": 517, + "r": 378 + }, + "reading": { + "x": 12, + "y": 531.90625, + "w": 366, + "h": 115, + "b": 646.90625, + "r": 378 + }, + "sentence": { + "x": 26, + "y": 597, + "w": 338, + "h": 30.59375, + "b": 627.59375, + "r": 364 + }, + "composer": { + "x": 12, + "y": 702, + "w": 366, + "h": 51, + "b": 753, + "r": 378 + }, + "nav": { + "x": 58, + "y": 772, + "w": 274, + "h": 57, + "b": 829, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 844, + "b": 844, + "r": 390 + }, + "speaker": { + "x": 26, + "y": 551.21875, + "w": 244.0625, + "h": 40.78125, + "b": 592, + "r": 270.0625 + } + } + }, + { + "check": "390×844 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×844 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 366, + "h": 288, + "b": 345, + "r": 378 + }, + "reading": { + "x": 12, + "y": 359.3125, + "w": 366, + "h": 68, + "b": 427.3125, + "r": 378 + }, + "sentence": { + "x": 20, + "y": 391.9375, + "w": 350, + "h": 24.75, + "b": 416.6875, + "r": 370 + }, + "composer": { + "x": 12, + "y": 475.40625, + "w": 366, + "h": 49.59375, + "b": 525, + "r": 378 + }, + "nav": { + "x": 58, + "y": 538.3125, + "w": 274, + "h": 53.6875, + "b": 592, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 600, + "b": 600, + "r": 390 + }, + "speaker": { + "x": 32.53125, + "y": 370.59375, + "w": 231, + "h": 18.6875, + "b": 389.28125, + "r": 263.53125 + } + } + }, + { + "check": "390×600 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 366, + "h": 288, + "b": 345, + "r": 378 + }, + "reading": { + "x": 12, + "y": 359.3125, + "w": 366, + "h": 68, + "b": 427.3125, + "r": 378 + }, + "sentence": { + "x": 20, + "y": 391.9375, + "w": 350, + "h": 24.75, + "b": 416.6875, + "r": 370 + }, + "composer": { + "x": 12, + "y": 475.40625, + "w": 366, + "h": 49.59375, + "b": 525, + "r": 378 + }, + "nav": { + "x": 58, + "y": 538.3125, + "w": 274, + "h": 53.6875, + "b": 592, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 600, + "b": 600, + "r": 390 + }, + "speaker": { + "x": 32.53125, + "y": 370.59375, + "w": 231, + "h": 18.6875, + "b": 389.28125, + "r": 263.53125 + } + } + }, + { + "check": "390×600 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×600 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 366, + "h": 108, + "b": 165, + "r": 378 + }, + "reading": { + "x": 12, + "y": 179.3125, + "w": 366, + "h": 68, + "b": 247.3125, + "r": 378 + }, + "sentence": { + "x": 20, + "y": 211.9375, + "w": 350, + "h": 24.75, + "b": 236.6875, + "r": 370 + }, + "composer": { + "x": 12, + "y": 295.40625, + "w": 366, + "h": 49.59375, + "b": 345, + "r": 378 + }, + "nav": { + "x": 58, + "y": 358.3125, + "w": 274, + "h": 53.6875, + "b": 412, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 420, + "b": 420, + "r": 390 + }, + "speaker": { + "x": 32.53125, + "y": 190.59375, + "w": 231, + "h": 18.6875, + "b": 209.28125, + "r": 263.53125 + } + } + }, + { + "check": "390×420 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 366, + "h": 108, + "b": 165, + "r": 378 + }, + "reading": { + "x": 12, + "y": 179.3125, + "w": 366, + "h": 68, + "b": 247.3125, + "r": 378 + }, + "sentence": { + "x": 20, + "y": 211.9375, + "w": 350, + "h": 24.75, + "b": 236.6875, + "r": 370 + }, + "composer": { + "x": 12, + "y": 295.40625, + "w": 366, + "h": 49.59375, + "b": 345, + "r": 378 + }, + "nav": { + "x": 58, + "y": 358.3125, + "w": 274, + "h": 53.6875, + "b": 412, + "r": 332 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 390, + "h": 420, + "b": 420, + "r": 390 + }, + "speaker": { + "x": 32.53125, + "y": 190.59375, + "w": 231, + "h": 18.6875, + "b": 209.28125, + "r": 263.53125 + } + } + }, + { + "check": "390×420 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "390×420 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 336, + "h": 88, + "b": 145, + "r": 348 + }, + "reading": { + "x": 12, + "y": 159.3125, + "w": 336, + "h": 68, + "b": 227.3125, + "r": 348 + }, + "sentence": { + "x": 20, + "y": 200.625, + "w": 320, + "h": 24.75, + "b": 225.375, + "r": 340 + }, + "composer": { + "x": 12, + "y": 275.40625, + "w": 336, + "h": 49.59375, + "b": 325, + "r": 348 + }, + "nav": { + "x": 43, + "y": 338.3125, + "w": 274, + "h": 53.6875, + "b": 392, + "r": 317 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 360, + "h": 400, + "b": 400, + "r": 360 + }, + "speaker": { + "x": 20, + "y": 161.25, + "w": 226.0625, + "h": 37.375, + "b": 198.625, + "r": 246.0625 + } + } + }, + { + "check": "360×400 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 336, + "h": 88, + "b": 145, + "r": 348 + }, + "reading": { + "x": 12, + "y": 159.3125, + "w": 336, + "h": 68, + "b": 227.3125, + "r": 348 + }, + "sentence": { + "x": 20, + "y": 200.625, + "w": 320, + "h": 24.75, + "b": 225.375, + "r": 340 + }, + "composer": { + "x": 12, + "y": 275.40625, + "w": 336, + "h": 49.59375, + "b": 325, + "r": 348 + }, + "nav": { + "x": 43, + "y": 338.3125, + "w": 274, + "h": 53.6875, + "b": 392, + "r": 317 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 360, + "h": 400, + "b": 400, + "r": 360 + }, + "speaker": { + "x": 20, + "y": 161.25, + "w": 226.0625, + "h": 37.375, + "b": 198.625, + "r": 246.0625 + } + } + }, + { + "check": "360×400 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "360×400 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 296, + "h": 256, + "b": 313, + "r": 308 + }, + "reading": { + "x": 12, + "y": 327.3125, + "w": 296, + "h": 68, + "b": 395.3125, + "r": 308 + }, + "sentence": { + "x": 20, + "y": 368.625, + "w": 280, + "h": 24.75, + "b": 393.375, + "r": 300 + }, + "composer": { + "x": 12, + "y": 443.40625, + "w": 296, + "h": 49.59375, + "b": 493, + "r": 308 + }, + "nav": { + "x": 23, + "y": 506.3125, + "w": 274, + "h": 53.6875, + "b": 560, + "r": 297 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 320, + "h": 568, + "b": 568, + "r": 320 + }, + "speaker": { + "x": 20, + "y": 329.25, + "w": 186.0625, + "h": 37.375, + "b": 366.625, + "r": 206.0625 + } + } + }, + { + "check": "320×568 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 296, + "h": 256, + "b": 313, + "r": 308 + }, + "reading": { + "x": 12, + "y": 327.3125, + "w": 296, + "h": 68, + "b": 395.3125, + "r": 308 + }, + "sentence": { + "x": 20, + "y": 368.625, + "w": 280, + "h": 24.75, + "b": 393.375, + "r": 300 + }, + "composer": { + "x": 12, + "y": 443.40625, + "w": 296, + "h": 49.59375, + "b": 493, + "r": 308 + }, + "nav": { + "x": 23, + "y": 506.3125, + "w": 274, + "h": 53.6875, + "b": 560, + "r": 297 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 320, + "h": 568, + "b": 568, + "r": 320 + }, + "speaker": { + "x": 20, + "y": 329.25, + "w": 186.0625, + "h": 37.375, + "b": 366.625, + "r": 206.0625 + } + } + }, + { + "check": "320×568 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "320×568 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 moon 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 296, + "h": 48, + "b": 105, + "r": 308 + }, + "reading": { + "x": 12, + "y": 119.3125, + "w": 296, + "h": 68, + "b": 187.3125, + "r": 308 + }, + "sentence": { + "x": 20, + "y": 160.625, + "w": 280, + "h": 24.75, + "b": 185.375, + "r": 300 + }, + "composer": { + "x": 12, + "y": 235.40625, + "w": 296, + "h": 49.59375, + "b": 285, + "r": 308 + }, + "nav": { + "x": 23, + "y": 298.3125, + "w": 274, + "h": 53.6875, + "b": 352, + "r": 297 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 320, + "h": 360, + "b": 360, + "r": 320 + }, + "speaker": { + "x": 20, + "y": 121.25, + "w": 186.0625, + "h": 37.375, + "b": 158.625, + "r": 206.0625 + } + } + }, + { + "check": "320×360 长名 moon 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 moon 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 moon 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 moon 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 fog 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 12, + "y": 57, + "w": 296, + "h": 48, + "b": 105, + "r": 308 + }, + "reading": { + "x": 12, + "y": 119.3125, + "w": 296, + "h": 68, + "b": 187.3125, + "r": 308 + }, + "sentence": { + "x": 20, + "y": 160.625, + "w": 280, + "h": 24.75, + "b": 185.375, + "r": 300 + }, + "composer": { + "x": 12, + "y": 235.40625, + "w": 296, + "h": 49.59375, + "b": 285, + "r": 308 + }, + "nav": { + "x": 23, + "y": 298.3125, + "w": 274, + "h": 53.6875, + "b": 352, + "r": 297 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 320, + "h": 360, + "b": 360, + "r": 320 + }, + "speaker": { + "x": 20, + "y": 121.25, + "w": 186.0625, + "h": 37.375, + "b": 158.625, + "r": 206.0625 + } + } + }, + { + "check": "320×360 长名 fog 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 fog 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 fog 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "320×360 长名 fog 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "紧凑视口不丢失长回复文字", + "passed": true, + "details": null + }, + { + "check": "日子关联到对应的已分享对话", + "passed": true, + "details": null + }, + { + "check": "关联聊天能返回原来的一天", + "passed": true, + "details": null + }, + { + "check": "演示关系保存有明确反馈", + "passed": true, + "details": null + }, + { + "check": "初次创建关系不预选", + "passed": true, + "details": null + }, + { + "check": "名字复用已批准规则,拒绝尖括号", + "passed": true, + "details": null + }, + { + "check": "用户给的名字贯穿身份显示且按文字处理", + "passed": true, + "details": null + }, + { + "check": "语音演示持续标注没有收音", + "passed": true, + "details": null + }, + { + "check": "语音演示 人物与正文分开", + "passed": true, + "details": { + "portal": { + "x": 264.9891662597656, + "y": 84.19111633300781, + "w": 939.0146484375, + "h": 465.567626953125, + "b": 549.7587432861328, + "r": 1204.0038146972656 + }, + "reading": { + "x": 350, + "y": 563.80322265625, + "w": 740, + "h": 90, + "b": 653.80322265625, + "r": 1090 + }, + "sentence": { + "x": 376, + "y": 605.49072265625, + "w": 688, + "h": 35, + "b": 640.49072265625, + "r": 1064 + }, + "composer": { + "x": 350, + "y": 745.28759765625, + "w": 740, + "h": 60, + "b": 805.28759765625, + "r": 1090 + }, + "nav": { + "x": 578, + "y": 819, + "w": 284, + "h": 63, + "b": 882, + "r": 862 + }, + "viewport": { + "x": 0, + "y": 0, + "w": 1440, + "h": 900, + "b": 900, + "r": 1440 + }, + "speaker": { + "x": 695.984375, + "y": 577.10009765625, + "w": 36.015625, + "h": 20.390625, + "b": 597.49072265625, + "r": 732 + } + } + }, + { + "check": "语音演示 正文与输入分开", + "passed": true, + "details": null + }, + { + "check": "语音演示 输入与导航分开", + "passed": true, + "details": null + }, + { + "check": "语音演示 导航在可视范围", + "passed": true, + "details": null + }, + { + "check": "语音演示 无横向溢出", + "passed": true, + "details": null + }, + { + "check": "390×600 相处页人物不挡近况", + "passed": true, + "details": { + "title": { + "x": 24, + "y": 70, + "b": 130.78125, + "r": 226 + }, + "portal": { + "x": 246, + "y": 70, + "b": 210, + "r": 366 + }, + "actions": { + "x": 24, + "y": 466, + "b": 510, + "r": 206 + }, + "nav": { + "x": 58, + "y": 538.3125, + "b": 592, + "r": 332 + } + } + }, + { + "check": "390×600 相处入口不挡导航", + "passed": true, + "details": null + }, + { + "check": "390×420 相处页人物不挡近况", + "passed": true, + "details": { + "title": { + "x": 24, + "y": 70, + "b": 130.78125, + "r": 226 + }, + "portal": { + "x": 246, + "y": 70, + "b": 210, + "r": 366 + }, + "actions": { + "x": 24, + "y": 286, + "b": 330, + "r": 206 + }, + "nav": { + "x": 58, + "y": 358.3125, + "b": 412, + "r": 332 + } + } + }, + { + "check": "390×420 相处入口不挡导航", + "passed": true, + "details": null + }, + { + "check": "320×568 相处页人物不挡近况", + "passed": true, + "details": { + "title": { + "x": 24, + "y": 70, + "b": 161.171875, + "r": 156 + }, + "portal": { + "x": 176, + "y": 70, + "b": 210, + "r": 296 + }, + "actions": { + "x": 24, + "y": 434, + "b": 478, + "r": 206 + }, + "nav": { + "x": 23, + "y": 506.3125, + "b": 560, + "r": 297 + } + } + }, + { + "check": "320×568 相处入口不挡导航", + "passed": true, + "details": null + }, + { + "check": "320×360 相处页人物不挡近况", + "passed": true, + "details": { + "title": { + "x": 24, + "y": 70, + "b": 161.171875, + "r": 156 + }, + "portal": { + "x": 176, + "y": 70, + "b": 210, + "r": 296 + }, + "actions": { + "x": 24, + "y": 226, + "b": 270, + "r": 206 + }, + "nav": { + "x": 23, + "y": 298.3125, + "b": 352, + "r": 297 + } + } + }, + { + "check": "320×360 相处入口不挡导航", + "passed": true, + "details": null + }, + { + "check": "自动续读明确由用户选择", + "passed": true, + "details": null + }, + { + "check": "输入时停止自动续读", + "passed": true, + "details": null + }, + { + "check": "系统减少动效停止背景循环", + "passed": true, + "details": null + }, + { + "check": "减少动效不播放空间移动", + "passed": true, + "details": null + }, + { + "check": "无浏览器脚本错误", + "passed": true, + "details": [] + }, + { + "check": "离线原型没有 HTTP / WebSocket 请求", + "passed": true, + "details": [] + } + ], + "limitations": [ + "未测试真机软键盘", + "未测试 Safari / iOS", + "视口缩小是等效模拟", + "人物为获批静帧;服务全部未接入" + ] +} \ No newline at end of file diff --git a/prototypes/hel-790/evidence/video-review.json b/prototypes/hel-790/evidence/video-review.json new file mode 100644 index 0000000..7b6d159 --- /dev/null +++ b/prototypes/hel-790/evidence/video-review.json @@ -0,0 +1,26 @@ +[ + { + "file": "desktop-walkthrough.webm", + "duration": 35.84, + "currentTime": 35.84, + "ended": true, + "decodedCallbacks": 895, + "totalVideoFrames": 896, + "droppedVideoFrames": 0, + "videoWidth": 1280, + "videoHeight": 800, + "inspection": "完整浏览器播放至结束,每2秒取一帧供逐段检视;此统计不是产品性能测试。" + }, + { + "file": "mobile-walkthrough.webm", + "duration": 34.84, + "currentTime": 34.84, + "ended": true, + "decodedCallbacks": 870, + "totalVideoFrames": 871, + "droppedVideoFrames": 0, + "videoWidth": 390, + "videoHeight": 844, + "inspection": "完整浏览器播放至结束,每2秒取一帧供逐段检视;此统计不是产品性能测试。" + } +] diff --git a/prototypes/hel-790/handoff.json b/prototypes/hel-790/handoff.json new file mode 100644 index 0000000..0506638 --- /dev/null +++ b/prototypes/hel-790/handoff.json @@ -0,0 +1,328 @@ +{ + "baseline_version": "1.7", + "read_commit": "4242f8377e350e5f17e836d275c15b0f099bf109", + "requirement_ids": [ + "CG-004", + "CG-018", + "CG-024", + "CG-031", + "VIS-001", + "VIS-002", + "VIS-003", + "VIS-004", + "VIS-005", + "VIS-006", + "VIS-007", + "VIS-008", + "VIS-009", + "VIS-010", + "VIS-011", + "VIS-012", + "VIS-013", + "UX-001", + "UX-002", + "UX-003", + "UX-005", + "UX-006", + "AM-001", + "AM-002", + "AM-003", + "GOV-001", + "GOV-002", + "GOV-003", + "GOV-004", + "DEL-001" + ], + "deliverable": "HEL-790:独立离线HTML、两主题电脑/手机真实截图、实际动态录屏、可审查PR及本结构化交接。", + "acceptance": [ + "相处→交流→全文/历史→收回→生活/回忆/管理→相处的连续空间关系。", + "当前一句身份清楚、柔和进出;长回复可逐句读到末尾,全文与历史按需展开并保留阅读位置。", + "多视口两主题的人物/正文/输入/导航不互挡,明确未测真机键盘。", + "四类入口保留;未接入能力说明;用户只验收小样,不代表完整产品通过。" + ], + "deferred": [ + "正式界面实现和测试部署:小样认可后再安排独立审核发布。", + "R1出生底色深度、模型诠释、少量初始喜好与持续成长仍有缺口。", + "R2生活与未分享裁剪、R3记忆、R4主动消息、R5连续语音、R6天气/素材/备份、R7视觉观察及独立口型线。", + "R2旧完整设计的所有状态继续继承;本卡聚焦对话和整体空间,部分管理项仅入口说明。", + "真机软键盘、Safari/iOS、屏幕阅读器、低端设备性能未测。" + ], + "requirement_changes": [], + "verification": [ + "标准库离线构建;node --check语法通过。", + "verify.py真实浏览器检查,实际逐句DOM文字与完整原文比较、多视口遮挡/取消/身份/记录/焦点/离线隔离等;详evidence/verification.json。", + "电脑4张、手机4张两主题主要截图,以及长句/全文末尾/生活回忆/管理/缩小视口补图。", + "实际桌面与手机walkthrough录屏;复放检查另记evidence/video-review.json。", + "需求检查在快进到PR#5已合入的main之后,本分支直接执行--base-ref origin/main,含--handoff;详evidence/requirements-check.txt。", + "体验待白栖知验收。" + ], + "requirement_mapping": [ + { + "id": "CG-004", + "meaning": "日常可看她在环境中生活;交谈时允许自然切近到同一人物、同一房间、同一套衣服的半身或脸部。", + "source_comment_ids": [ + "01a15724-dcaa-7d2b-ae23-430f24ca36b9", + "01a15726-e178-72b0-9a3c-8b2041538730" + ], + "this_delivery": "同一持续存在的人物窗展开,复用同一人物/房间/衣服的获批静帧;录屏验证,非真人动态能力结论。" + }, + { + "id": "CG-018", + "meaning": "主要在电脑使用,优先大画面沉浸体验;适配手机看画面、文字聊天和连续语音,使用同一伴侣和同一份记忆。", + "source_comment_ids": [ + "01a1577b-b65c-787a-b436-b0a74c38fea4", + "01a1577c-d10f-7513-8f58-62b3b387240a" + ], + "this_delivery": "9种电脑/手机/矮屏/横屏视口,两个主题,人物/正文/输入/导航分别验证;真机与Safari未测。" + }, + { + "id": "CG-024", + "meaning": "先确认人物与房间的写实样图,再验收写实动态小样;验证环境与人物独立变化、同一人物连续性、动作循环衔接、半身/近景交谈与口型语音。用户认可后,再逐步接完整功能,并分别验收。", + "source_comment_ids": [ + "01a157a9-bd18-7533-9b21-e5f4c3a90d7c", + "01a157ab-209c-7c8f-956c-40ebb3340f4e", + "01a1582c-7386-7515-bf62-89dfadfea3d3", + "01a1599b-f137-71cc-8af2-a265499b4b6e", + "01a15e92-e689-754a-aad3-4566100962b3" + ], + "this_delivery": "仅独立设计小样与实际截图录屏,不把设计通过当实时口型或完整产品通过。" + }, + { + "id": "CG-031", + "meaning": "完整前端应覆盖四类要素:相处主界面、交流界面、她的生活与共同回忆、创建与管理。设计需建立在对整个项目的充分理解之上;此前认可的人物和高层卧室图片只属于人物/场景/口型等视觉子模块的参考,不能作为整体前端的固定基座,不能据此先铺满场景再往上叠界面。", + "source_comment_ids": [ + "01a15e8d-91ab-739e-84f9-fc4026bfcd70", + "01a15e92-e689-754a-aad3-4566100962b3" + ], + "this_delivery": "四类区域均有入口和返回关系;空间背景独立于人物/卧室,非全屏房间叠按钮。" + }, + { + "id": "VIS-001", + "meaning": "整体像只属于两人的数字空间,人物、交流、回忆融在其中。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "人物、对话和回忆在同一环境连续开合。" + }, + { + "id": "VIS-002", + "meaning": "静谧梦幻、安静亲密,有空间深度和柔和微光。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "静谧夜色、远近景物、柔和微光,等待用户亲眼验收。" + }, + { + "id": "VIS-003", + "meaning": "切换主题同时改变配色、背景、光影、材质和动效;页面结构与操作位置一致。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "同一DOM与入口,天空/光雾/材质/微光随主题整体改变。" + }, + { + "id": "VIS-004", + "meaning": "靛蓝月光与蓝紫雾光两套都保留;不把原型临时默认主题当成用户指定默认。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "月光与雾光均保留,临时初始主题不作为默认选择。" + }, + { + "id": "VIS-005", + "meaning": "首页只留少量当前状态与入口,其余内容按需展开,完整功能仍保留。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "首页只保留一句近况、已分享留言与入口,保留获批比例。" + }, + { + "id": "VIS-006", + "meaning": "光雾、景深与若隐若现的景物融合;具体画面通过样图验证。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "远近天际线、光路和两层雾构成独立背景,不铺满卧室。" + }, + { + "id": "VIS-007", + "meaning": "平时舒缓流动与景物微动;操作时有明显景深变化和空间过渡。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "一个人物窗通过FLIP连贯移动,背景共同推进;减少动效与隐藏页暂停。" + }, + { + "id": "VIS-008", + "meaning": "内容采用透光浮层,背后保留空间光影,文字清晰可读。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "日子/回忆/设置/历史都用主题透光阅读浮层,原生dialog管理焦点。" + }, + { + "id": "VIS-009", + "meaning": "现代轻盈、字形简洁、标题克制,正文易读。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "沿用30px轻标题和轻盈字体,正文清楚。" + }, + { + "id": "VIS-010", + "meaning": "少量微光集中于关键操作和选中状态,两主题各有强调色。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "微光仅集中于关键入口与选中状态。" + }, + { + "id": "VIS-011", + "meaning": "人物平时作为空间中独立视窗;交流时自然放大。已认可的人物位置、分量与首页文字分量沿用。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "右侧27vw拱窗与14vw右边距,恢复获批取景;交流时放大。" + }, + { + "id": "VIS-012", + "meaning": "底部小巧透光导航,图标配简短文字,选中亮起微光。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "底部四入口、图标与简短文字,两主题同布局。" + }, + { + "id": "VIS-013", + "meaning": "交流展开后以人物为主,只呈现当前一两句对话;完整记录按需展开,文字输入和语音操作均保留。", + "source_comment_ids": [ + "01a15f09-db1e-705c-8baf-cde63c7651fb", + "01a15f1f-5a71-783c-a458-b326a0073d69" + ], + "this_delivery": "当前一句,前后翻看,全文/历史按需展开,长回复完整保留;细节仍待用户验收。" + }, + { + "id": "UX-001", + "meaning": "手机对话不被人物、导航、底部区域遮挡,长句和键盘弹出时仍能阅读与输入;等效视口测试不能冒充真机验证。", + "source_comment_ids": [ + "01a162eb-1eab-7dad-8eff-66e0954f5e0b", + "01a16353-c93a-7745-8efa-2586f0ea3eee" + ], + "this_delivery": "当前正文实际边界决定剩余人物高度;多尺寸零重叠。键盘只做缩小视口等效验证。" + }, + { + "id": "UX-002", + "meaning": "文字有柔和的进入和退场,流式更新不重复闪烁;动态效果必须实际演示,不以 CSS 存在代替验收。", + "source_comment_ids": [ + "01a162eb-1eab-7dad-8eff-66e0954f5e0b", + "01a16353-c93a-7745-8efa-2586f0ea3eee" + ], + "this_delivery": "退场100ms/进入160ms;同一句追加不重播;真实DOM逐句完整读回并有动态录屏。" + }, + { + "id": "UX-003", + "meaning": "记录、详情、管理、首建等滚动区域与两套主题协调,保留清晰可操作性。", + "source_comment_ids": [ + "01a162eb-1eab-7dad-8eff-66e0954f5e0b", + "01a16353-c93a-7745-8efa-2586f0ea3eee" + ], + "this_delivery": "全文、记录、设置与日子同用主题scrollbar;两主题实际截图。" + }, + { + "id": "UX-005", + "meaning": "用户与实际起名的伴侣身份明确,当前对话与完整记录一致,不只依靠位置或颜色猜。", + "source_comment_ids": [ + "01a162eb-1eab-7dad-8eff-66e0954f5e0b", + "01a16353-c93a-7745-8efa-2586f0ea3eee" + ], + "this_delivery": "每句显示你或实际演示名字;历史身份相同;长名字验证。" + }, + { + "id": "UX-006", + "meaning": "按已认可的完整前端设计补齐功能和页面结构,未接通能力说明真实边界,不删成空壳也不伪造已运行。", + "source_comment_ids": [ + "01a162eb-1eab-7dad-8eff-66e0954f5e0b", + "01a16353-c93a-7745-8efa-2586f0ea3eee" + ], + "this_delivery": "保留相处/交流/生活回忆/创建管理入口和返回关系;未接功能有说明,R2完整状态及真实接入继续承接。" + }, + { + "id": "AM-001", + "meaning": "等她讲过才显示,覆盖列表、详情、摘要、关联回忆与消息引用;未知分享状态不展示,不能用链接或搜索绕开。真实接入必须在服务器响应前裁剪,前端过滤不能冒充权限控制。", + "source_comment_ids": [ + "01a15f42-2781-700e-b75a-3ae4b023f46c" + ], + "this_delivery": "离线包仅两条已经分享的虚构生活,关联聊天一致;新建临时身份后空生活。没有未分享数据;后端权限仍待R2/R3。" + }, + { + "id": "AM-002", + "meaning": "创建时由用户选择关系,不预选、不替用户决定;以后改变关系保留记忆与信任,底色不变。覆盖旧记录 T-02 中关系初始值仍待定的部分。", + "source_comment_ids": [ + "01a15f42-2781-700e-b75a-3ae4b023f46c" + ], + "this_delivery": "演示首建关系无预选,确认后进入小样;正式完整创建沿用原方案。" + }, + { + "id": "AM-003", + "meaning": "首次创建由用户提供有效名字,主页、交流、首建与设置统一;参考名不能硬编码成真实身份,不额外新增改名流程。", + "source_comment_ids": [ + "01a15f42-2781-700e-b75a-3ae4b023f46c" + ], + "this_delivery": "演示名字由用户输入并校验,在顶部、当前句子和历史复用;不新增正式改名流程。" + }, + { + "id": "GOV-001", + "meaning": "所有新卡、续办卡、设计卡、施工卡、审核卡从同一完整需求入口开始,不依赖会话记忆或最近一条反馈;现有未完成卡补齐对照项。", + "source_comment_ids": [ + "01a1635b-b47c-79ed-ac66-9408c7905af9" + ], + "this_delivery": "完整读取v1.7及原话来源,记录精确提交和对应编号。" + }, + { + "id": "GOV-002", + "meaning": "只有白栖知明确的新决定可以覆盖原要求,保留原话来源、旧结论、新结论及影响;不能把技术困难、阶段减项、智能体建议或演示通过当成需求修改。", + "source_comment_ids": [ + "01a1635b-b47c-79ed-ac66-9408c7905af9" + ], + "this_delivery": "没有产品需求变更;分句、速度明确是待用户验收提案。" + }, + { + "id": "GOV-003", + "meaning": "每卡列基准版本及提交位置、关联要求编号、交付验收、未完成承接、需求变更和验证证据;自动检查缺项,人工核对含义,用户验收体验。", + "source_comment_ids": [ + "01a1635b-b47c-79ed-ac66-9408c7905af9" + ], + "this_delivery": "结构化交接、真实截图/录屏/检查与未完成承接;结构检查不代替审美验收。" + }, + { + "id": "GOV-004", + "meaning": "先落实需求继承约束,再制作对话显示和整体沉浸界面的电脑/手机样图与可操作动态小样;小样认可前不替换正式界面,不合并当前产品 PR,不改现有伴侣和聊天数据,不切换队员全局模型。", + "source_comment_ids": [ + "01a1635b-b47c-79ed-ac66-9408c7905af9" + ], + "this_delivery": "独立原型分支,不改正式界面、不合并PR#4、不触碰真实档案和全局认证/模型。" + }, + { + "id": "DEL-001", + "meaning": "前端演示通过只代表该阶段通过;主卡保留追踪完整真实功能,后续接入按已批准 R1–R7 依赖顺序推进。阶段和技术方案不能降低完整需求。", + "source_comment_ids": [ + "01a161a4-842f-7c26-9be0-1812b720976a", + "01a1629c-732f-7559-8417-6f2ea05c7151" + ], + "this_delivery": "主项目不归档,R1修正与R2–R7及口型独立线保留;本卡无部署/合并/真实数据写入。" + } + ], + "initial_read_commit": "97aaefe5fac77e9746a4ce52d3af94adad15efc1" +} diff --git a/prototypes/hel-790/index.html b/prototypes/hel-790/index.html new file mode 100644 index 0000000..b54a388 --- /dev/null +++ b/prototypes/hel-790/index.html @@ -0,0 +1,427 @@ + + + + + + +CyberGril · 月光里的相处 · 独立动态小样 + + + + + + +
+
+
晚晚在家 · 醒着
+
上海 · 小雨 14° 示例
+
+
+
+

此 刻

书翻到一半,
窗外的雨声不大。

安静,有点倦 · 你们最近挺熟络

+ + +
+
+ +
+ 获批参考静帧:她坐在高层卧室沙发上看书 + 获批参考静帧:同一人物、房间和衣服的交流近景 +
窗边看书参考静帧
+
在你面前
+
+
+
+
晚晚
+

我在。你想说什么?

+
+
1 / 1
+ +
+
+
+ +
+

+

+ + diff --git a/prototypes/hel-790/record.py b/prototypes/hel-790/record.py new file mode 100644 index 0000000..a21f138 --- /dev/null +++ b/prototypes/hel-790/record.py @@ -0,0 +1,40 @@ +"""Record the real offline DOM, not a sequence of mocked screenshots.""" +import os +from pathlib import Path +from playwright.sync_api import sync_playwright + +ROOT=Path(__file__).resolve().parent +OUT=ROOT/'evidence' +LIBS=Path('/home/xiaomo/.local/pwlibs/usr/lib/x86_64-linux-gnu') +if LIBS.exists():os.environ['LD_LIBRARY_PATH']=str(LIBS) + +with sync_playwright() as p: + browser=p.chromium.launch(headless=True,args=['--no-sandbox']) + for label,width,height in [('desktop',1280,800),('mobile',390,844)]: + context=browser.new_context(viewport={'width':width,'height':height},record_video_dir=str(OUT),record_video_size={'width':width,'height':height}) + page=context.new_page();page.goto((ROOT/'index.html').as_uri());video=page.video + page.wait_for_timeout(1800) + page.click('#start-talk');page.wait_for_timeout(1700) + page.click('#next');page.wait_for_timeout(1400) + page.click('#rehearse');page.wait_for_timeout(1400) + page.locator('[data-scenario="long"]').click();page.wait_for_timeout(3400) + for _ in range(3):page.click('#next');page.wait_for_timeout(1700) + page.click('#read-all');page.wait_for_timeout(1800) + page.locator('#sheet-content').evaluate('(e)=>e.scrollTo({top:e.scrollHeight,behavior:"smooth"})');page.wait_for_timeout(1600) + page.click('#close-sheet');page.wait_for_timeout(850) + page.click('#history');page.wait_for_timeout(1300) + page.click('#close-sheet');page.wait_for_timeout(500) + page.click('#close-talk');page.wait_for_timeout(1500) + page.locator('.theme [data-theme="fog"]').click();page.wait_for_timeout(1800) + page.locator('[data-nav="days"]').click();page.wait_for_timeout(1500) + page.locator('[data-event="0"]').click();page.wait_for_timeout(1500) + page.locator('[data-action="days"]').click();page.wait_for_timeout(500) + page.locator('[data-action="memories"]').click();page.wait_for_timeout(1300) + page.click('#close-sheet');page.wait_for_timeout(1000) + page.locator('[data-nav="manage"]').click();page.wait_for_timeout(1400) + page.click('#close-sheet');page.wait_for_timeout(1300) + context.close();video.save_as(str(OUT/f'{label}-walkthrough.webm')) + original=Path(video.path()) + if original.name!=f'{label}-walkthrough.webm':original.unlink() + print(f'{label} walkthrough recorded') + browser.close() diff --git a/prototypes/hel-790/src/shell.html b/prototypes/hel-790/src/shell.html new file mode 100644 index 0000000..666fde3 --- /dev/null +++ b/prototypes/hel-790/src/shell.html @@ -0,0 +1,64 @@ + + + + + + +CyberGril · 月光里的相处 · 独立动态小样 + + + + + + +
+
+
晚晚在家 · 醒着
+
上海 · 小雨 14° 示例
+
+
+
+

此 刻

书翻到一半,
窗外的雨声不大。

安静,有点倦 · 你们最近挺熟络

+ + +
+
+ +
+ 获批参考静帧:她坐在高层卧室沙发上看书 + 获批参考静帧:同一人物、房间和衣服的交流近景 +
窗边看书参考静帧
+
在你面前
+
+
+
+
晚晚
+

我在。你想说什么?

+
+
1 / 1
+ +
+
+
+ +
+

+

+ + diff --git a/prototypes/hel-790/src/space.css b/prototypes/hel-790/src/space.css new file mode 100644 index 0000000..62c5a4d --- /dev/null +++ b/prototypes/hel-790/src/space.css @@ -0,0 +1,34 @@ +/* Approved HEL-744/746 palette and home geometry. Motion is isolated here. */ +:root{color-scheme:dark;--font-sans:"PingFang SC","HarmonyOS Sans SC","MiSans","Microsoft YaHei","Noto Sans CJK SC",system-ui,sans-serif;--ease-out:cubic-bezier(.22,1,.36,1);--ease-in-out:cubic-bezier(.65,0,.35,1);--dur-fast:160ms;--dur-ui:280ms;--view-h:100dvh;--view-top:0px;--safe-bottom:env(safe-area-inset-bottom,0px)} +html[data-theme="moon"]{--bg-0:#060a1c;--bg-1:#0d1838;--bg-2:#1b2c5e;--text-1:#eaf0ff;--text-2:#b4c2e3;--text-3:#9aaaca;--accent:#a9dcff;--accent-soft:#a9dcff1f;--accent-glow:#96d2ff55;--on-accent:#08142e;--glass:#1c2c5c80;--glass-read:#0a122ede;--glass-line:#cddeff2e;--surface:#0b1330;--window-glow:#ffc48c22;--mist:#7698e630;--horizon:#83b7e438;--far:#1f3863;--near:#182c50;--glass-blur:18px} +html[data-theme="fog"]{--bg-0:#110b26;--bg-1:#22174a;--bg-2:#43307a;--text-1:#f3ecff;--text-2:#d6c4ed;--text-3:#bca7d9;--accent:#ebbcff;--accent-soft:#ebbcff24;--accent-glow:#e2aaff55;--on-accent:#2a0f40;--glass:#3c266c80;--glass-read:#221442de;--glass-line:#ecd8ff2e;--surface:#24154a;--window-glow:#ffaabe22;--mist:#d5a9ff42;--horizon:#e0abfa40;--far:#473267;--near:#36264d;--glass-blur:30px} +*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg-0)}body{font:15px/1.7 var(--font-sans);color:var(--text-1);-webkit-font-smoothing:antialiased}button,input,textarea,select{font:inherit;color:inherit}button{border:0;background:none;cursor:pointer}button:disabled{opacity:.35;cursor:default}button:active:not(:disabled){transform:scale(.97)}button{transition:transform var(--dur-fast) var(--ease-out),opacity var(--dur-fast) var(--ease-out)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:4px}button svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;flex:none}h1,h2,h3,p,figure{margin:0}h2{font-size:22px;font-weight:350}h3{font-size:18px;font-weight:400}strong{font-weight:500}[hidden]{display:none!important}.symbols,.sr{position:absolute;width:1px;height:1px;clip-path:inset(50%);overflow:hidden;white-space:nowrap}.glass{background:var(--glass);border:1px solid var(--glass-line);box-shadow:inset 0 1px 0 #ffffff08,0 20px 80px #02061620;backdrop-filter:blur(var(--glass-blur)) saturate(120%)}.round{width:44px;height:44px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex:none}.primary{display:inline-flex;gap:10px;align-items:center;justify-content:center;background:var(--accent);color:var(--on-accent);padding:13px 23px;border-radius:30px;box-shadow:0 0 30px var(--accent-glow)}.small-link{color:var(--text-2);padding:12px 0;font-size:13px;text-align:left}.eyebrow{font-size:12px;letter-spacing:.18em;color:var(--text-3);margin-bottom:15px}.quiet{color:var(--text-2);font-size:14px}.quiet span{margin:0 10px}.subtle{font-size:13px;color:var(--text-2)} +/* The environment is independent of the photo. Three depth planes and a light path. */ +.space-scale{position:fixed;inset:-3%;overflow:hidden;transform:scale(1);transition:transform var(--dur-ui) var(--ease-out);pointer-events:none}.sky{position:absolute;inset:0;transition:opacity var(--dur-ui) var(--ease-out)}.sky.moon{background:radial-gradient(ellipse at 22% 8%,#2c4178 0,transparent 49%),linear-gradient(#071027 10%,#102047 58%,#17294d 76%,#0b1530)}.sky.fog{opacity:0;background:radial-gradient(ellipse at 15% 80%,#68498685,transparent 53%),radial-gradient(ellipse at 90% 4%,#5556b66e,transparent 65%),linear-gradient(150deg,#161033,#2c1e59 58%,#46306b 80%,#201235)}html[data-theme="fog"] .sky.fog{opacity:1}html[data-theme="fog"] .sky.moon{opacity:0}.moon-disc{position:absolute;left:16.7%;top:13%;width:72px;height:72px;border-radius:50%;background:#edf2ff;box-shadow:0 0 4px #edf2ff,0 0 60px 15px #9ac2ff30;opacity:.85}.moon-halo{position:absolute;left:calc(16.7% - 220px);top:calc(13% - 220px);width:512px;height:512px;background:radial-gradient(circle,#87b5ff30,transparent 68%)}.moon-road{position:absolute;left:5%;top:63%;width:42%;height:36%;background:radial-gradient(ellipse,#9fcdf71a,transparent 67%);transform:rotate(-16deg)}.ribbon{position:absolute;border-radius:50%;filter:blur(45px);animation:flow 32s var(--ease-in-out) infinite alternate}.r1{left:-15%;top:12%;width:90%;height:28%;background:radial-gradient(ellipse,#c895f942,transparent 68%);transform:rotate(-12deg)}.r2{right:-15%;top:35%;width:70%;height:34%;background:radial-gradient(ellipse,#6d8fee45,transparent 68%);animation-duration:46s}.r3{left:30%;bottom:-10%;width:70%;height:45%;background:radial-gradient(ellipse,#e4a7d529,transparent 68%);animation-duration:60s}.skyline{position:absolute;left:0;right:0;bottom:7%;height:28%;opacity:.4;pointer-events:none}.skyline svg{width:100%;height:100%;fill:var(--far)}.skyline.far{filter:blur(2px)}.skyline.near{height:21%;bottom:-2%;opacity:.55}.skyline.near svg{fill:var(--near)}.horizon{position:absolute;left:-10%;right:-10%;top:67%;height:30%;background:radial-gradient(ellipse at 50% 20%,var(--horizon),transparent 67%);transform:rotate(-3deg)}.mist{position:absolute;left:-20%;width:140%;background:radial-gradient(ellipse,var(--mist),transparent 67%);filter:blur(30px);border-radius:50%;animation:flow 48s var(--ease-in-out) infinite alternate}.mist-back{height:26%;bottom:15%;opacity:.7}.mist-front{height:35%;bottom:-6%;animation-duration:64s;animation-direction:alternate-reverse}.foreground{position:absolute;inset:0;box-shadow:inset 0 -80px 130px #03071955;background:radial-gradient(ellipse at 50% 45%,transparent 35%,#02040c55 100%)}@keyframes flow{from{transform:translate3d(-3%,0,0)}to{transform:translate3d(4%,2%,0)}}.depth-veil{position:fixed;inset:0;background:#020614;opacity:0;transition:opacity var(--dur-ui) var(--ease-out);pointer-events:none}body[data-mode="talk"] .space-scale{transform:scale(1.05)}body[data-mode="talk"] .depth-veil{opacity:.2}body[data-mode="pane"] .space-scale{transform:scale(1.025)}body[data-mode="pane"] .depth-veil{opacity:.36} +#viewport{position:absolute;top:var(--view-top);left:0;right:0;height:var(--view-h);min-height:280px;overflow:hidden}.topbar{height:78px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 32px;position:relative;z-index:3}.identity{display:flex;align-items:center;gap:12px;min-width:0}.identity strong{font-size:18px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.presence{color:var(--text-3);font-size:13px;white-space:nowrap}.glint{width:6px;height:6px;background:var(--accent);border-radius:50%;box-shadow:0 0 14px var(--accent)}.top-actions{display:flex;align-items:center;gap:12px}.sample-tag{border:1px dashed var(--glass-line);border-radius:20px;font-size:11px;padding:4px 10px;color:var(--text-2)}.weather{font-size:12px;color:var(--text-2)}.weather small{color:var(--text-3)}.theme{display:flex;border:1px solid var(--glass-line);padding:3px;border-radius:25px;background:var(--glass)}.theme button{font-size:12px;color:var(--text-2);padding:5px 13px;border-radius:20px}.theme [aria-pressed="true"]{color:var(--text-1);background:var(--accent-soft);box-shadow:0 0 20px var(--accent-glow)} +.home-copy{position:absolute;left:9vw;top:50%;transform:translateY(-42%);width:min(440px,38vw);transition:opacity var(--dur-ui) var(--ease-out),transform var(--dur-ui) var(--ease-out)}h1{font-size:30px;font-weight:300;line-height:1.65;letter-spacing:.01em;margin-bottom:16px}.left-note{display:block;width:100%;text-align:left;padding:18px 20px;border-radius:20px;margin-top:32px;line-height:1.85}.note-meta{display:flex;justify-content:space-between;gap:8px;font-size:12px;color:var(--accent);margin-bottom:10px}.note-meta time{color:var(--text-3)}.home-actions{display:flex;align-items:center;gap:12px;margin-top:18px}.home-actions .round{width:50px;height:50px}.portal{position:absolute;right:14vw;top:calc(50% - 7px);transform:translateY(-50%);width:clamp(300px,27vw,430px);aspect-ratio:1/1.3;border-radius:48% 48% 26px 26px;overflow:hidden;isolation:isolate;border:1px solid #f1e2ce35;box-shadow:0 0 50px #ffdca70a,0 25px 70px #03081855;background:#1e2332;z-index:1}.portal img{position:absolute;width:100%;height:100%;object-fit:cover;object-position:center;transition:opacity var(--dur-ui) var(--ease-out)}.room-photo{opacity:1}.talk-photo{opacity:0}.portal figcaption{position:absolute;bottom:14px;left:17px;right:17px;display:flex;justify-content:space-between;gap:8px;color:#f3ede3;font-size:11px;text-shadow:0 1px 5px #000}.portal figcaption span{padding:3px 9px;border-radius:15px;background:#14132170}.portal-glow{position:absolute;right:4vw;top:28%;width:44vw;height:45%;pointer-events:none;background:radial-gradient(ellipse,var(--window-glow),transparent 70%);filter:blur(25px);opacity:.8;transition:opacity var(--dur-ui) var(--ease-out)}.portal-top{position:absolute;inset:14px 14px auto;display:flex;align-items:flex-start;justify-content:space-between;opacity:0;pointer-events:none;color:#fff}.portal-top>span,.close-talk{background:#10152594;border:1px solid #ffffff20;backdrop-filter:blur(12px);border-radius:24px;padding:7px 13px;font-size:12px}.close-talk{display:flex;align-items:center;gap:5px}.close-talk svg{width:16px;height:16px}body[data-mode="talk"] .portal{top:78px;left:50%;right:auto;transform:translateX(-50%);width:min(960px,72vw);height:calc(var(--view-h) - 365px - var(--safe-bottom));min-height:140px;aspect-ratio:auto;border-radius:28px}body[data-mode="talk"] .talk-photo{opacity:1}body[data-mode="talk"] .portal-top{opacity:1;pointer-events:auto}body:not([data-mode="home"]) .home-copy{opacity:0;transform:translate(-20px,-42%);pointer-events:none}body[data-mode="pane"] .portal{width:clamp(150px,16vw,240px);right:7vw}body[data-mode="pane"] .portal-glow{opacity:.4} +.conversation{position:absolute;bottom:calc(95px + var(--safe-bottom));left:50%;width:min(740px,78vw);transform:translate(-50%,10px);opacity:0;pointer-events:none;transition:opacity var(--dur-ui) var(--ease-out),transform var(--dur-ui) var(--ease-out);z-index:2}body[data-mode="talk"] .conversation{opacity:1;transform:translate(-50%,0);pointer-events:auto}.reading{min-height:90px;display:flex;flex-direction:column;justify-content:center;padding:2px 26px;text-align:center}.speaker-row{display:flex;justify-content:center;align-items:center;gap:12px;font-size:12px;color:var(--accent);margin-bottom:8px;min-height:20px}#speaker{overflow-wrap:anywhere}#message-status{font-size:11px;color:var(--text-2);white-space:nowrap}#sentence{font-size:20px;font-weight:350;line-height:1.75;overflow-wrap:anywhere;white-space:pre-wrap;text-shadow:0 2px 18px #030719;font-synthesis:none}.reading-controls{display:flex;align-items:center;justify-content:center;gap:10px;font-size:12px;color:var(--text-2);margin:7px 0 14px;min-height:28px}.reading-controls>button{min-height:30px;padding:1px 7px;white-space:nowrap}.reading-controls #previous,.reading-controls #next{font-size:23px;min-width:32px}.reading-controls #page-count{min-width:42px;text-align:center;font-variant-numeric:tabular-nums}#autoplay[aria-pressed="true"]{color:var(--accent)}#stop{color:var(--accent)}.composer{display:flex;gap:6px;align-items:center;padding:7px;border-radius:32px}.composer textarea{flex:1;min-width:0;resize:none;max-height:70px;border:0;background:transparent;line-height:1.6;padding:7px 4px;outline:none;color:var(--text-1);font-size:15px}.composer textarea:focus-visible{outline-offset:0;outline:1px solid var(--accent);border-radius:6px}.composer textarea::placeholder{color:var(--text-3)}.send{background:var(--accent-soft);color:var(--accent)}.voice-button{display:flex;align-items:center;justify-content:center;border-radius:50%;width:44px;height:44px;background:var(--accent);color:var(--on-accent);flex:none}.voice-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;color:var(--text-2);margin:7px 10px}.voice-row button{color:var(--accent)}.navigation{position:absolute;left:50%;bottom:calc(18px + var(--safe-bottom));transform:translateX(-50%);display:flex;padding:5px;border-radius:40px;z-index:3}.navigation button{min-width:68px;min-height:51px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px 15px;color:var(--text-2);border-radius:32px;font-size:12px}.navigation svg{width:18px;height:18px}.navigation [aria-current="page"]{background:var(--accent-soft);color:var(--accent);box-shadow:0 0 20px var(--accent-glow)} +/* Native dialog supplies modality and focus containment; reading surfaces stay in the space. */ +.sheet{position:fixed;inset:auto;margin:0;top:calc(var(--view-top) + 84px);left:5vw;width:min(950px,68vw);height:calc(var(--view-h) - 183px - var(--safe-bottom));max-height:none;max-width:calc(100vw - 24px);padding:0;border-radius:28px;color:var(--text-1);background:var(--glass-read);overflow:hidden;box-shadow:0 25px 100px #02041260}.sheet::backdrop{background:#02061422}.sheet[data-kind="history"],.sheet[data-kind="full"]{left:auto;right:4vw;width:min(460px,42vw)}.sheet-header{display:flex;justify-content:space-between;align-items:center;gap:20px;padding:24px 26px 18px;border-bottom:1px solid var(--glass-line);height:110px}.sheet-header .eyebrow{font-size:11px;margin-bottom:3px}.sheet-content{height:calc(100% - 110px);overflow:auto;overscroll-behavior:contain;padding:24px 26px 34px;scrollbar-gutter:stable;line-height:1.9}.sheet-content :is(p,ul){margin-bottom:16px}.sheet-content .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0}.sheet-content .pill{border:1px solid var(--glass-line);border-radius:24px;padding:8px 17px;font-size:13px}.sheet-content .pill[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}.sheet-content .item{width:100%;text-align:left;border-top:1px solid var(--glass-line);padding:22px 0;display:block}.item small{display:block;color:var(--text-2);margin-top:7px;font-size:13px}.item .date{font-size:11px;letter-spacing:.08em;color:var(--accent);margin-bottom:10px}.quote{font-size:20px;font-weight:350;border-left:1px solid var(--accent);padding-left:20px;margin:24px 0}.sheet-content .message{padding:18px 0;border-bottom:1px solid var(--glass-line)}.message .author{color:var(--accent);font-size:12px;margin-bottom:7px;overflow-wrap:anywhere}.message[data-role="user"] .author{color:var(--text-2)}.message p{font-size:15px;white-space:pre-wrap;overflow-wrap:anywhere}.message .incomplete{font-size:12px;color:var(--text-2)}.sheet-content label{display:block;font-size:13px;color:var(--text-2);margin:18px 0 7px}.sheet-content input,.sheet-content textarea,.sheet-content select{border:1px solid var(--glass-line);border-radius:12px;background:var(--surface);padding:11px 14px;width:100%;max-width:100%;font-size:15px}.sheet-content textarea{min-height:90px;resize:vertical}.sheet-content .notice{padding:14px 17px;border:1px solid var(--glass-line);border-radius:16px;color:var(--text-2);font-size:13px}.sheet-content .status{color:var(--accent);min-height:26px;font-size:13px}.sheet-content .guide{font-size:13px;color:var(--text-2)}.sheet-content summary{cursor:pointer;color:var(--accent);padding:10px 0}.sheet-content details{border-top:1px solid var(--glass-line);margin-top:20px}.sheet-content .error{color:#ffb8c8}*{scrollbar-width:thin;scrollbar-color:var(--accent-glow) transparent}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-thumb{background:var(--accent-glow);border-radius:10px}::-webkit-scrollbar-track{background:transparent} +@media(hover:hover) and (pointer:fine){button:hover:not(:disabled){opacity:.82}.sheet-content .item:hover{color:var(--accent)}} +@media(max-width:1100px){.weather{display:none}.presence{display:none}.sheet{width:86vw;left:7vw}body[data-mode="pane"] .portal{opacity:.35;right:3vw}.sheet[data-kind="history"],.sheet[data-kind="full"]{width:440px;right:4vw}.identity strong{max-width:180px}} +@media(max-width:720px){ + .topbar{height:57px;padding:0 16px;gap:8px;align-items:center}.identity{gap:8px}.identity strong{font-size:15px;max-width:100px}.top-actions{gap:6px}.sample-tag{font-size:10px;padding:4px 7px}.theme{padding:2px}.theme button{font-size:11px;padding:4px 9px}.theme button:first-letter{font-size:8px} + .moon-disc{left:13%;top:12%;width:46px;height:46px}.moon-halo{left:calc(13% - 140px);top:calc(12% - 140px);width:326px;height:326px}.skyline.far{height:30%}.skyline.near{height:20%} + .home-copy{left:24px;top:auto;bottom:calc(109px + var(--safe-bottom));width:calc(100% - 48px);transform:none}.home-copy .eyebrow{margin-bottom:10px}.home-copy h1{font-size:23px;line-height:1.55;margin-bottom:10px}.home-copy .quiet{font-size:12px}.left-note{padding:13px 15px;font-size:13px;margin-top:18px;border-radius:18px}.note-meta{font-size:11px;margin-bottom:5px}.home-copy .small-link{padding:9px 0;font-size:12px}.home-actions{margin-top:9px}.home-actions .primary{padding:10px 20px;font-size:14px}.home-actions .round{width:44px;height:44px} + .portal{right:23px;top:77px;transform:none;width:clamp(150px,45vw,220px);height:calc(var(--view-h) - 531px - var(--safe-bottom));min-height:190px;max-height:265px;aspect-ratio:auto;border-radius:48% 48% 18px 18px}.portal figcaption{left:8px;right:8px;bottom:9px;font-size:9px;gap:4px}.portal figcaption span{padding:2px 6px}.portal-glow{right:-20%;top:11%;width:95vw;height:42%} + body:not([data-mode="home"]) .home-copy{transform:translateY(12px)} + body[data-mode="talk"] .portal{left:12px;right:12px;top:68px;width:calc(100% - 24px);transform:none;height:calc(var(--view-h) - 354px - var(--safe-bottom));min-height:120px;max-height:none;border-radius:23px}.portal-top{inset:10px 10px auto}.portal-top>span,.close-talk{font-size:10px;padding:5px 9px}.close-talk svg{width:13px;height:13px} + .conversation{width:calc(100% - 24px);bottom:calc(91px + var(--safe-bottom))}.reading{padding:0 14px;min-height:115px}.speaker-row{margin-bottom:5px}.reading #sentence{font-size:17px;line-height:1.8}.reading-controls{gap:5px;font-size:11px;margin:4px 0 10px}.reading-controls>button{padding:1px 5px}.reading-controls #previous,.reading-controls #next{min-width:28px}.composer{padding:5px;gap:3px}.composer .round,.voice-button{width:39px;height:39px}.composer textarea{font-size:16px;padding:6px 3px}.navigation{bottom:calc(15px + var(--safe-bottom));padding:4px}.navigation button{min-width:66px;min-height:47px;padding:3px 15px;font-size:11px}.navigation svg{width:17px;height:17px} + body[data-mode="pane"] .portal{top:90px;right:16px;width:160px;min-height:200px;opacity:.18} + .sheet,.sheet[data-kind="history"],.sheet[data-kind="full"]{left:12px;right:12px;width:calc(100% - 24px);top:calc(var(--view-top) + 65px);height:calc(var(--view-h) - 151px - var(--safe-bottom));border-radius:24px}.sheet-header{padding:16px 18px;height:86px}.sheet-content{padding:19px 20px 28px;height:calc(100% - 86px)}h2{font-size:20px}.quote{font-size:18px} +} +/* A compact visual viewport is the keyboard/short-screen layout, not a fake keyboard. */ +@media(max-height:700px) and (max-width:720px){body[data-mode="home"] .portal{top:68px;min-height:140px;height:160px;width:145px}.home-copy{bottom:91px}.home-copy h1{font-size:20px}.home-copy .eyebrow{display:none}.home-copy .left-note{margin-top:12px;padding:9px 13px}.home-copy .quiet{display:none}.home-actions{margin-top:0}.home-copy .small-link{padding:6px 0}.home-copy .note-meta{margin-bottom:3px}.home-copy .left-note br{display:none}} +body.compact[data-mode="talk"] .portal{top:57px;height:calc(var(--view-h) - 260px - var(--safe-bottom));min-height:56px;border-radius:17px}body.compact .portal figcaption{display:none}body.compact .conversation{bottom:calc(75px + var(--safe-bottom))}body.compact .reading{min-height:68px;padding:0 8px}body.compact #sentence{font-size:15px;line-height:1.65}body.compact .speaker-row{font-size:11px;margin-bottom:2px}body.compact .reading-controls{margin:2px 0 5px;gap:4px}body.compact .navigation{bottom:calc(8px + var(--safe-bottom))}body.compact .navigation button{min-height:40px}body.compact .topbar{height:49px}body.compact .weather,body.compact .presence{display:none}body.compact .voice-row{margin:2px 9px;font-size:11px}body.compact .composer .round,body.compact .voice-button{height:36px;width:36px}body.compact .sheet{top:calc(var(--view-top) + 54px);height:calc(var(--view-h) - 125px - var(--safe-bottom))}body.compact .sheet-header{height:74px;padding:10px 18px}body.compact .sheet-content{height:calc(100% - 74px)}body.has-voice[data-mode="talk"] .portal{height:calc(var(--view-h) - 392px - var(--safe-bottom))}body.compact.has-voice[data-mode="talk"] .portal{height:calc(var(--view-h) - 290px - var(--safe-bottom))} +.room-photo{object-position:88% 42%!important;transform:scale(1.16);transform-origin:80% 42%}.horizon{border-radius:50%;filter:blur(24px)} +#sentence{white-space:normal} +.top-actions{flex:none}.sample-tag,.theme button{white-space:nowrap}body.compact .talk-photo{object-position:54% 20%} +@media(max-width:720px){body.compact[data-mode="home"] .home-copy{top:70px;bottom:90px;transform:none}body.compact[data-mode="home"] h1{width:calc(100% - 140px);font-size:19px;line-height:1.6}body.compact[data-mode="home"] .portal{top:70px;right:24px;width:120px;height:140px;min-height:100px}body.compact[data-mode="home"] .left-note{position:absolute;top:auto;bottom:62px;margin:0;padding:9px 12px}body.compact[data-mode="home"] .left-note>.note-meta{margin:0}body.compact[data-mode="home"] .left-note>span:last-child{display:none}body.compact[data-mode="home"] .small-link{display:none}body.compact[data-mode="home"] .home-actions{position:absolute;bottom:0;margin:0}body.micro[data-mode="home"] .left-note{display:none}} +html.no-motion *,html.keyboard-action *{transition-duration:0ms!important}html.no-motion .mist,html.no-motion .ribbon,body.suspended .mist,body.suspended .ribbon{animation-play-state:paused}html.no-motion .space-scale{transition:none;transform:none}html.no-motion .sky{transition:opacity 160ms var(--ease-out)} +@media(prefers-reduced-motion:reduce){.mist,.ribbon{animation:none}.space-scale{transition:none;transform:none!important}.home-copy,.conversation{transition:opacity 160ms var(--ease-out)}.sky{transition:opacity 160ms var(--ease-out)}} diff --git a/prototypes/hel-790/src/space.js b/prototypes/hel-790/src/space.js new file mode 100644 index 0000000..a8c6742 --- /dev/null +++ b/prototypes/hel-790/src/space.js @@ -0,0 +1,329 @@ +'use strict'; +// Local fixtures only. CSP also forbids connections; nothing is persisted. +const $ = s => document.querySelector(s); +const $$ = s => [...document.querySelectorAll(s)]; +const html = document.documentElement; +const sheet = $('#sheet'); +const content = $('#sheet-content'); +const reduced = matchMedia('(prefers-reduced-motion: reduce)'); +const state = {mode:'home',name:'晚晚',relation:'朋友',turn:0,cursor:1,auto:false,busy:false,voice:false,token:0,dialog:null,returnMode:'home',memoryTab:false,readKey:'',renderEpoch:0}; +let messages = [ + {id:-3,turn:-2,role:'user',text:'淡色的花看起来很安静。',status:'complete',life:0}, + {id:-2,turn:-2,role:'her',text:'楼下花店的洋桔梗开了。我挑了一小束,放在窗边。',status:'complete',life:0}, + {id:-1,turn:-1,role:'her',text:'我们就这样一起听一会儿雨吧。',status:'complete',life:1}, + {id:1,turn:0,role:'user',text:'你今天在读什么?',status:'complete'}, + {id:2,turn:0,role:'her',text:'一本旧散文集。读到写雨的那页,突然很想和你说说话。',status:'complete'} +]; +let nextId = 3, autoTimer = 0, streamTimer = 0, animatingPortal = null, closing = null, trigger = null; +const SHORT = '刚刚看完一小节,正好想歇一会儿。你呢,今天过得怎么样?'; +const LONG = '今天读到一段写雨的文字,想到小时候放学回家的那条路。那时候总觉得路很长,下雨时又不舍得走快,想听雨落在不同屋檐上的声音。后来搬到高一点的地方,雨声变远了,能看见的灯却多了。我还是会在这种时候把书放一放,看看窗外,给自己泡一杯热茶。\n\n有时候,一天里也没有发生什么大事。可能只是花店换了橱窗,或者读到一个特别喜欢的句子。但如果能把这些细小的事情讲给你听,这一天就像多了一盏亮着的灯。\n\n我也想知道你那边的天气。如果你今天有点累,我们可以慢慢聊,不急着把所有的话一次说完。等你想说的时候,我就在这里。最后这一句也完整保留:愿今晚的雨声让你睡得安稳。'; +const FIXTURES = [ + {title:'窗边的洋桔梗',time:'今天 · 15:52',text:'楼下花店的洋桔梗开了。我挑了一小束,放在窗边。',memory:'你说,淡色的花看起来很安静。'}, + {title:'一起听过的雨',time:'昨天 · 21:16',text:'昨晚我们都没有急着说话,只是听了一会儿雨。',memory:'没有话的时候,也可以一起待一会儿。'} +]; +const escapeHTML = value => String(value).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const icon = name => ``; +const motionOff = () => reduced.matches || html.classList.contains('no-motion') || html.classList.contains('keyboard-action'); + +// Small generated skyline: code-native scenery, never a replacement photo or full UI image. +for (const [i,node] of $$('.skyline').entries()) { + let x=0, rects=''; + for(let n=0;n<36;n++) { + const w=24+((n*37+i*17)%46),h=30+((n*53+i*31)%140); + rects+=``; + if(n%3===0) for(let k=0;k<3;k++) rects+=``; + x+=w+5; + } + node.innerHTML=`${rects}`; +} + +function setName(name) { + state.name=name; + $$('[data-name]').forEach(n=>n.textContent=name); + $$('[data-name]').forEach(n=>n.title=name); + renderSentence(true); +} +function setMode(mode) { + if(mode!=='talk') pauseAuto(); + const portal=$('#portal'),before=portal.getBoundingClientRect(); + // Cancel at current visual position; FLIP moves one persistent portal, not duplicate scenes. + if(animatingPortal){animatingPortal.cancel();animatingPortal=null;} + state.mode=mode;document.body.dataset.mode=mode; + $('#scene-label').textContent=mode==='talk'?'交流近景':'窗边看书'; + if(mode!=='talk')portal.style.height=''; + $('.home-copy').inert=mode!=='home';$('#conversation').inert=mode!=='talk'; + if(mode==='talk')fitTalkStage(); + const after=portal.getBoundingClientRect(); + if(!motionOff() && before.width && after.width) { + // Fold the resting CSS translation into FLIP before starting an animation. + const base=getComputedStyle(portal).transform; + const matrix=new DOMMatrix(base==='none'?undefined:base); + const end=`translate(${matrix.e}px,${matrix.f}px)`; + animatingPortal=portal.animate([ + {transformOrigin:'0 0',transform:`translate(${matrix.e+before.x-after.x}px,${matrix.f+before.y-after.y}px) scale(${before.width/after.width},${before.height/after.height})`}, + {transformOrigin:'0 0',transform:end} + ],{duration:280,easing:'cubic-bezier(.22,1,.36,1)'}); + } + $$('.navigation button').forEach(n=>{if(n.dataset.nav===mode)n.setAttribute('aria-current','page');else n.removeAttribute('aria-current');}); + if(mode==='talk')renderSentence(true); +} +function openTalk(focus=false) { + setMode('talk');if(focus)$('#draft').focus({preventScroll:true}); +} + +function graphemes(text) { + return typeof Intl.Segmenter==='function'?[...new Intl.Segmenter('zh',{granularity:'grapheme'}).segment(text)].map(s=>s.segment):Array.from(text); +} +function chunks(text) { + const compact=document.body.classList.contains('compact'); + const width=Math.max(200,$('#reading').clientWidth-48),font=compact?15:innerWidth<=720?17:20; + const max=Math.max(16,Math.min(compact?28:58,Math.floor(width/font)*(compact?1.5:1.65))); + const units=graphemes(text),out=[];let line=''; + for(const char of units){ + // Keep punctuation and paragraph breaks attached; never make a blank reading page. + if(!line&&out.length&&/[。!?!?\n]/.test(char)){out[out.length-1]+=char;continue;} + line+=char; + if((/[。!?!?\n]/.test(char)&&line.trim())||graphemes(line).length>=max){out.push(line);line='';} + } + if(line)out.push(line); + return out; +} +function pages() { + return messages.filter(m=>m.turn===state.turn).flatMap(m=>chunks(m.text).map((text,index)=>({message:m,text,index,key:`${m.id}:${index}`}))); +} +function announce(text){$('#announcement').textContent=text;} +function fitTalkStage(){ + if(state.mode!=='talk')return; + const portal=$('#portal'),conversation=$('#conversation'),reading=conversation.getBoundingClientRect(); + const transform=getComputedStyle(conversation).transform; + const offset=new DOMMatrix(transform==='none'?undefined:transform).f; + const top=$('#viewport').getBoundingClientRect().top+parseFloat(getComputedStyle(portal).top); + portal.style.minHeight='48px'; + portal.style.height=`${Math.max(48,Math.floor(reading.top-offset-top-14))}px`; +} +function updateControls(list) { + $('#previous').disabled=state.cursor<=0; + $('#next').disabled=state.cursor>=list.length-1; + $('#page-count').textContent=list.length?`${state.cursor+1} / ${list.length}`:'0 / 0'; + $('#stop').hidden=!state.busy; + $('#autoplay').setAttribute('aria-pressed',String(state.auto)); + $('#autoplay').textContent=state.auto?'暂停续读':'自动续读'; + $('#portal-state').textContent=state.busy?'正在回应 · 演示':state.voice?'语音界面演示':'在你面前'; +} +function renderSentence(immediate=false) { + const list=pages();state.cursor=Math.max(0,Math.min(state.cursor,list.length-1)); + updateControls(list); + const page=list[state.cursor];if(!page)return; + const who=page.message.role==='user'?'你':state.name; + $('#message-status').textContent=state.busy?'她正在回应…':page.message.status==='cancelled'?'未完成 · 已取消':''; + const apply=()=>{ + $('#speaker').textContent=who; + $('#sentence').textContent=page.text; + fitTalkStage(); + announce(`${who}:${page.text}`); + }; + if(page.key===state.readKey){apply();return;} + const epoch=++state.renderEpoch; + state.readKey=page.key; + const node=$('#reading');node.getAnimations().forEach(a=>a.cancel()); + if(immediate || motionOff() || state.mode!=='talk'){apply();return;} + node.animate([{opacity:1,transform:'translateY(0)'},{opacity:0,transform:'translateY(-4px)'}],{duration:100,easing:'cubic-bezier(.22,1,.36,1)'}).finished.then(()=>{ + if(epoch!==state.renderEpoch)return; + apply(); + node.animate([{opacity:0,transform:'translateY(5px)'},{opacity:1,transform:'translateY(0)'}],{duration:160,easing:'cubic-bezier(.22,1,.36,1)'}); + }).catch(()=>{}); +} +function movePage(delta){state.cursor=Math.max(0,Math.min(state.cursor+delta,pages().length-1));renderSentence();scheduleAuto();} +function pauseAuto(){state.auto=false;clearTimeout(autoTimer);$('#autoplay').setAttribute('aria-pressed','false');$('#autoplay').textContent='自动续读';} +function scheduleAuto(){ + clearTimeout(autoTimer); + if(!state.auto||state.dialog||document.hidden||state.mode!=='talk')return; + if(document.activeElement===$('#draft')||String(getSelection()).length)return; + const page=pages()[state.cursor]; + if(page)autoTimer=setTimeout(()=>{ + if(state.cursorm.status==='generating').forEach(m=>{m.status='cancelled';if(!m.text)m.text='(回复在内容出现前取消)';}); + updateControls(pages());renderSentence(true); +} +function beginReply(text,reply=SHORT,after=null) { + cancelReply();pauseAuto(); + state.turn++;const user={id:nextId++,turn:state.turn,role:'user',text,status:'complete'}; + messages.push(user);state.cursor=0;state.readKey='';renderSentence(); + streamReply(reply,true,after); +} +function streamReply(reply,followFirst=false,after=null) { + const token=++state.token; + const message={id:nextId++,turn:state.turn,role:'her',text:'',status:'generating'}; + messages.push(message);state.busy=true;updateControls(pages()); + const parts=reply.match(/[^。!?!?\n]+[。!?!?\n]*|\n+/gu)||[reply];let index=0; + function append(){ + if(token!==state.token)return; + if(document.hidden){streamTimer=setTimeout(append,500);return;} + message.text+=parts[index++]; + if(index===1&&followFirst&&state.cursor===0&&!state.dialog){ + const list=pages();state.cursor=list.findIndex(p=>p.message.id===message.id); + } + if(index>=parts.length){state.busy=false;message.status='complete';} + renderSentence(); + if(state.dialog==='history')renderHistory(); + if(state.busy)streamTimer=setTimeout(append,650); + else if(after)after(); + } + streamTimer=setTimeout(append,1100); +} + +// Native dialog restores focus. WAAPI only controls its visible entry/exit. +function showSheet(kind,title,kicker,markup){ + pauseAuto(); + if(closing){closing.cancel();closing=null;} + if(!sheet.open){trigger=document.activeElement;state.returnMode=state.mode;setMode('pane');sheet.showModal();} + state.dialog=kind;sheet.dataset.kind=kind; + $('#sheet-title').textContent=title;$('#sheet-kicker').textContent=kicker;content.innerHTML=markup;content.scrollTop=0; + if(!motionOff())sheet.animate([{opacity:0,transform:'translateY(14px)'},{opacity:1,transform:'translateY(0)'}],{duration:280,easing:'cubic-bezier(.22,1,.36,1)'}); + $('#close-sheet').focus({preventScroll:true}); +} +function finishClose(){ + sheet.getAnimations().forEach(a=>a.cancel());sheet.close();state.dialog=null;closing=null;setMode(state.returnMode); + if(trigger&&trigger.isConnected&&!trigger.inert)trigger.focus({preventScroll:true}); +} +function closeSheet(){ + if(closing)return; + if(motionOff()){finishClose();return;} + closing=sheet.animate([{opacity:1,transform:'translateY(0)'},{opacity:0,transform:'translateY(14px)'}],{duration:180,easing:'cubic-bezier(.22,1,.36,1)',fill:'forwards'}); + closing.finished.then(finishClose).catch(()=>{}); +} +function renderHistory(){ + const previousScroll=content.scrollTop; + content.innerHTML='

本小样中的文字均为虚构。内容完整保留;关闭后回到刚才那一句。

'+messages.map(m=>`
${escapeHTML(m.role==='user'?'你':state.name)}

${escapeHTML(m.text||'正在回应…')}

${m.status==='cancelled'?'已取消 · 这是未完成片段':''}
`).join(''); + content.scrollTop=previousScroll; +} +function showHistory(){showSheet('history','你们说过的话','完整记录 · 演示', '');renderHistory();} +function showFull(){ + const current=pages()[state.cursor];if(!current)return; + const m=current.message; + showSheet('full','这一段,慢慢看',`${m.role==='user'?'你':state.name} · 演示全文`,`

这里可以滚动读完整段。返回后仍停在刚才那一句。

${escapeHTML(m.text)}

${m.status==='cancelled'?'这段回复已取消,内容未完成。':''}
`); +} +function showDays(memory=false){ + state.memoryTab=memory; + if(state.createdNew){showSheet('days',memory?'我们的回忆':'她的日子','初次见面 · 演示',`

刚刚见面,还没有分享过的日子与共同回忆。

真实生活和记忆尚未接入,小样不会自动编出她未分享的经历。

`);return;} + showSheet('days','留在日子里的微光','生活与共同回忆 · 演示',`

只放她已经向你分享的事。以下两条都是虚构的演示记录。

${FIXTURES.map((f,i)=>``).join('')}

更多真实生活、记忆和成长,等待后续功能接入。

`); +} +function eventDetail(index){ + const f=FIXTURES[index];if(!f)return; + showSheet('event',f.title,'已分享的生活 · 演示',`

${f.time}

${f.text}

与你有关的回忆

${f.memory}

演示故事,不是真实的后台生活。

`); +} +function showManage(){ + showSheet('manage','慢慢安排你们的世界','设置 · 演示',`

这里只体验入口和操作关系。所有更改只在这次小样中保留。

`); +} +function showInfo(kind){ + const info={ + profile:['她与最初的底色','出生资料在创建后保留为只读底色,每轮重新提供给模型。个性的诠释、少量初始喜好与后来的成长分别保留。','本小样不生成排盘、性格或成长,也不读取真实档案。相关真实能力仍须后续补齐。'], + appearance:['外观素材','替换外观前应先检查素材、预览新旧效果,再明确启用。记忆、生日、关系和成长仍属于同一个她。','本小样只使用已认可的两张参考静帧,没有导入素材或实时口型。'], + backup:['备份与恢复','产品仍需每日自动备份,并支持包含人物、场景、设定与记忆的完整导出和恢复。','备份尚未接入。本小样没有执行或伪装任何导出、备份、恢复操作。'], + services:['语音与视觉观察','语音由你主动开启,结束后停止收音。视觉观察可关闭,过期观察不再当作眼前事实。','本小样不申请麦克风或摄像头权限,不采集声音与影像。'], + }[kind]; + showSheet(kind,info[0],'入口示意 · 尚未接入',`

${info[1]}

${info[2]}

${kind==='services'?'':''}`); +} +function showRelationship(){ + showSheet('relationship','你们的关系','演示表单',`

修改关系不会改写出生底色、过去的记忆与信任。

`); +} +function showCity(){ + showSheet('city','她窗外的城市','城市选择 · 演示',`

正式产品将按城市跟随当地天气与作息。这里仅演示选择反馈,没有查询天气。

`); +} +function showDisplay(){ + showSheet('display','空间与显示','显示偏好 · 小样',`

保留同一布局,切换整片空间的光与材质。

系统的“减少动态效果”同样生效。切换主题不会移动入口。

`); +} +function showRehearsal(){ + showSheet('rehearsal','试试这些相处片段','小样试用 · 全部为演示',`

先回到此刻,再和她说话。也可以从下面选择一段情境。

回复分句保留,可手动前后翻看,也可选自动续读。分句方式、停留时间和整体动效都是本次待你验收的交互方案。

试试初次见面与长名字

只创建临时演示身份,不连接或重置现有伴侣。

验证说明与边界

人物为获批参考静帧;文字、生活与回忆都是虚构。没有连接模型、语音、摄像头、天气或备份服务。

缩小浏览器高度只能模拟键盘占用后的视口,不能证明真机键盘已通过。电脑与手机两套主题均可操作。

“独立小样 · 演示数据”随时可以再打开这里。

`); +} +function showCreation(){ + showSheet('create','先给她一个名字','初次创建 · 临时演示',`

出生资料、排盘与底色说明沿用既有创建流程,本次不模拟生成。确认仅进入这次虚构小样,不会接触真实档案。

`); +} +function showVoice(){ + showSheet('voice','体验语音界面','没有收音的演示',`

这里仅演示语音开启、取消与结束的界面。不会申请麦克风权限,也不会播放或识别声音。

`); +} +function runScenario(kind){ + finishClose();openTalk(); + if(kind==='incoming'){ + beginReply('你刚才想和我说什么?','窗边的花开了一点。',()=>streamReply('还有,今天的雨声很轻。我想把这片安静也分一点给你。')); + }else if(kind==='long')beginReply('慢慢讲,我想听听你今天的想法。',LONG); + else if(kind==='waiting')beginReply('可以和我多说一会儿吗?',LONG); + else beginReply('你在做什么?',SHORT); +} +function applyTheme(theme){ + if(!['moon','fog'].includes(theme))return; + html.dataset.theme=theme;$$('[data-theme]').filter(n=>n.tagName==='BUTTON').forEach(n=>n.setAttribute('aria-pressed',String(n.dataset.theme===theme))); +} + +document.addEventListener('click',e=>{ + const button=e.target.closest('button');if(!button)return; + if(button.dataset.theme){applyTheme(button.dataset.theme);return;} + if(button.dataset.nav){ + const mode=button.dataset.nav; + if(sheet.open){finishClose();} + if(mode==='home'){setMode('home');}else if(mode==='talk')openTalk();else if(mode==='days')showDays();else showManage();return; + } + if(button.dataset.event!==undefined){eventDetail(Number(button.dataset.event));return;} + if(button.dataset.eventHistory!==undefined){ + const index=Number(button.dataset.eventHistory);showHistory(); + content.insertAdjacentHTML('afterbegin',``); + content.querySelector(`[data-life="${index}"]`)?.scrollIntoView({block:'center'});return; + } + if(button.dataset.scenario){runScenario(button.dataset.scenario);return;} + const action=button.dataset.action; + const actions={days:()=>showDays(),memories:()=>showDays(true),manage:showManage,relationship:showRelationship,city:showCity,display:showDisplay,create:showCreation,voice:showVoice,dismiss:closeSheet, + motion:()=>{html.classList.toggle('no-motion');showDisplay();}, + related:()=>{showHistory();const target=[...content.querySelectorAll('.message')][0];target?.scrollIntoView({block:'start'});}, + 'start-voice':()=>{finishClose();openTalk();state.voice=true;document.body.classList.add('has-voice');$('#voice-row').hidden=false;renderSentence(true);} + }; + if(actions[action])actions[action]();else if(['profile','appearance','backup','services'].includes(action))showInfo(action); +}); +$('#start-talk').onclick=()=>openTalk();$('#shared-note').onclick=()=>{openTalk();showHistory();};$('#days-link').onclick=()=>showDays();$('#close-talk').onclick=()=>setMode('home'); +$('#history').onclick=showHistory;$('#read-all').onclick=showFull;$('#close-sheet').onclick=closeSheet;$('#rehearse').onclick=showRehearsal; +$('#home-voice').onclick=showVoice;$('#voice').onclick=showVoice;$('#end-voice').onclick=()=>{state.voice=false;document.body.classList.remove('has-voice');$('#voice-row').hidden=true;cancelReply();}; +$('#previous').onclick=()=>{pauseAuto();movePage(-1);};$('#next').onclick=()=>{pauseAuto();movePage(1);};$('#autoplay').onclick=()=>{state.auto=!state.auto;updateControls(pages());scheduleAuto();};$('#stop').onclick=cancelReply; +$('#composer').addEventListener('submit',e=>{e.preventDefault();const text=$('#draft').value.trim();if(!text)return;$('#draft').value='';beginReply(text,/长|故事|慢慢|完整/.test(text)?LONG:SHORT);}); +$('#draft').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!e.isComposing&&e.keyCode!==229){e.preventDefault();$('#composer').requestSubmit();}}); +$('#draft').addEventListener('focus',pauseAuto); +document.addEventListener('selectionchange',()=>{if(String(getSelection()).length)pauseAuto();}); +sheet.addEventListener('cancel',e=>{e.preventDefault();closeSheet();}); +document.addEventListener('submit',e=>{ + if(e.target.id==='relation-form'){ + e.preventDefault();state.relation=$('#relation').value;state.boundaries=$('#boundaries').value;$('#form-status').textContent='演示选择已确认。记忆和底色保持原样。'; + }else if(e.target.id==='city-form'){ + e.preventDefault();$('.weather').innerHTML=`${escapeHTML($('#city').value)} · 小雨 14° 示例`;$('#form-status').textContent='已保存演示选择;这里没有查询真实天气。'; + }else if(e.target.id==='create-form'){ + e.preventDefault();const name=$('#new-name').value.trim(),relation=$('#new-relation').value; + if(!name||Array.from(name).length>20||/[<>\x00-\x1f\x7f]/.test(name)||!relation){$('#creation-error').textContent='名字请用 1–20 字,不含尖括号和控制符,并选择关系。';return;} + cancelReply();messages=[];state.createdNew=true;state.turn++;messages.push({id:nextId++,turn:state.turn,role:'her',text:'你好。很高兴在这里见到你。',status:'complete'});state.cursor=0;state.relation=relation;setName(name);finishClose();openTalk(); + } +}); +document.addEventListener('keydown',e=>{ + html.classList.add('keyboard-action'); + if(e.key==='Escape'&&!sheet.open&&state.mode==='talk'){setMode('home');$('#start-talk').focus();} +}); +document.addEventListener('pointerdown',()=>html.classList.remove('keyboard-action')); +document.addEventListener('visibilitychange',()=>{ + document.body.classList.toggle('suspended',document.hidden); + if(document.hidden)pauseAuto(); +}); +function fitViewport(){ + const viewport=window.visualViewport,h=viewport?.height||innerHeight,t=viewport?.offsetTop||0; + html.style.setProperty('--view-h',`${h}px`);html.style.setProperty('--view-top',`${t}px`); + document.body.classList.toggle('compact',h<610); + document.body.classList.toggle('micro',h<400); + // Keep the same content offset when wrapping changes; never drop characters. + const current=pages()[state.cursor];const anchor=current?{id:current.message.id,offset:chunks(current.message.text).slice(0,current.index).join('').length}:null; + requestAnimationFrame(()=>{ + if(anchor){const list=pages();let consumed=0;for(let i=0;ianchor.offset){state.cursor=i;break;}consumed+=list[i].text.length;}} + renderSentence(true); + }); +} +window.visualViewport?.addEventListener('resize',fitViewport);window.visualViewport?.addEventListener('scroll',fitViewport);window.addEventListener('resize',fitViewport); +reduced.addEventListener('change',()=>{if(reduced.matches)animatingPortal?.cancel();}); +fitViewport();renderSentence(true); diff --git a/prototypes/hel-790/verify.py b/prototypes/hel-790/verify.py new file mode 100644 index 0000000..4255bec --- /dev/null +++ b/prototypes/hel-790/verify.py @@ -0,0 +1,188 @@ +"""Focused browser verification of the offline prototype, with actual screenshots. + +Requires the host's Python Playwright + Chromium. No production services used. +Run: python3 prototypes/hel-790/build.py && python3 prototypes/hel-790/verify.py +""" +import json +import os +from pathlib import Path +from playwright.sync_api import sync_playwright + +ROOT = Path(__file__).resolve().parent +OUT = ROOT / 'evidence' +OUT.mkdir(exist_ok=True) +LIBS = Path('/home/xiaomo/.local/pwlibs/usr/lib/x86_64-linux-gnu') +if LIBS.exists(): + os.environ['LD_LIBRARY_PATH'] = str(LIBS) + (':' + os.environ['LD_LIBRARY_PATH'] if os.environ.get('LD_LIBRARY_PATH') else '') +results = [] + + +def check(condition, label, details=None): + assert condition, f'{label}: {details}' + results.append({'check': label, 'passed': True, 'details': details}) + + +def settle(page, ms=330): + page.wait_for_timeout(ms) + + +def geometry(page): + return page.evaluate('''() => { + const r=s=>{const x=document.querySelector(s).getBoundingClientRect();return {x:x.x,y:x.y,w:x.width,h:x.height,b:x.bottom,r:x.right}}; + return {portal:r('#portal'),reading:r('#reading'),sentence:r('#sentence'),composer:r('#composer'),nav:r('.navigation'),viewport:r('#viewport'),speaker:r('#speaker')}; + }''') + + +def no_overlap(page, label): + g=geometry(page) + check(g['portal']['b']+8 <= g['reading']['y'], label+' 人物与正文分开', g) + check(g['sentence']['b'] <= g['composer']['y'], label+' 正文与输入分开') + check(g['composer']['b']+4 <= g['nav']['y'], label+' 输入与导航分开') + check(g['nav']['b'] <= g['viewport']['b'], label+' 导航在可视范围') + check(page.evaluate('document.documentElement.scrollWidth<=innerWidth'),label+' 无横向溢出') + + +with sync_playwright() as p: + browser=p.chromium.launch(headless=True,args=['--no-sandbox']) + context=browser.new_context(viewport={'width':1440,'height':900}) + page=context.new_page() + errors=[];requests=[] + page.on('pageerror',lambda error:errors.append(str(error))) + page.on('request',lambda request: requests.append(request.url) if request.url.startswith(('http:','https:','ws:','wss:')) else None) + page.goto((ROOT/'index.html').as_uri()) + settle(page) + # All four desktop states plus both themes on mobile, using real DOM. + for theme in ['moon','fog']: + page.locator(f'.theme [data-theme="{theme}"]').click() + settle(page) + page.screenshot(path=str(OUT/f'desktop-home-{theme}.png')) + page.click('#start-talk');settle(page) + no_overlap(page,f'1440×900 {theme}') + page.screenshot(path=str(OUT/f'desktop-talk-{theme}.png')) + page.click('#close-talk');settle(page) + for theme in ['moon','fog']: + page.set_viewport_size({'width':390,'height':844});settle(page) + page.locator(f'.theme [data-theme="{theme}"]').click();settle(page) + page.screenshot(path=str(OUT/f'mobile-home-{theme}.png')) + page.click('#start-talk');settle(page) + no_overlap(page,f'390×844 {theme}') + page.screenshot(path=str(OUT/f'mobile-talk-{theme}.png')) + page.click('#close-talk');settle(page) + + # Complete text is recoverable from the main sequence, not just from history. + page.set_viewport_size({'width':390,'height':844});settle(page) + page.click('#rehearse');settle(page);page.locator('[data-scenario="long"]').click() + page.wait_for_function('() => !state.busy',timeout=20000);settle(page) + expected=page.evaluate("messages.filter(m=>m.turn===state.turn).map(m=>m.text).join('')") + page.evaluate('state.cursor=0;renderSentence(true)');collected=[] + while True: + collected.append(page.locator('#sentence').text_content()) + if page.locator('#next').is_disabled():break + page.click('#next');settle(page,290) + actual=page.evaluate("pages().map(p=>p.text).join('')") + check(''.join(collected)==expected,'逐句点击后的真实 DOM 文字与完整原文一致') + check(actual==expected,'长回复分句完整无缺字') + check(page.evaluate('pages().every(p=>p.text.trim())'),'长回复没有空白翻页') + check(len(collected)>8 and '最后这一句' in actual,'长回复末尾可从主界面翻到',len(collected)) + page.screenshot(path=str(OUT/'mobile-long-end-fog.png')) + cursor=page.locator('#page-count').inner_text() + page.click('#read-all');settle(page) + check('愿今晚的雨声让你睡得安稳。' in page.locator('#sheet-content').inner_text(),'全文保留末句') + page.locator('#sheet-content').evaluate('(e)=>e.scrollTop=e.scrollHeight') + page.screenshot(path=str(OUT/'mobile-full-end-fog.png')) + page.click('#close-sheet');settle(page) + check(page.locator('#page-count').inner_text()==cursor,'收回全文保持句子位置') + # Repeated open/close catches stale fill-forwards animations and focus restoration. + for n in range(3): + page.click('#history');settle(page) + check(page.evaluate('getComputedStyle(sheet).opacity')=='1',f'记录第 {n+1} 次打开可见') + check(page.evaluate('sheet.contains(document.activeElement)'),'浮层焦点在原生对话框内') + for _ in range(12):page.keyboard.press('Tab') + check(page.evaluate('sheet.contains(document.activeElement)'),'Tab 保持在浮层内') + page.keyboard.press('Escape');settle(page) + check(page.locator('#page-count').inner_text()==cursor,'收回记录保持句子位置') + + # New incoming content queues while the user reads, with no forced jump. + page.click('#rehearse');settle(page);page.locator('[data-scenario="incoming"]').click() + settle(page,1800);sentence=page.locator('#sentence').inner_text();settle(page,3000) + check(page.locator('#sentence').inner_text()==sentence,'连续来句不抢走正在读的句子') + check(page.evaluate('messages.filter(m=>m.turn===state.turn&&m.role==="her").length')==2,'两段来句均保留') + + # Cancel after a partial response; wait past old timers, then issue a fresh turn. + page.click('#rehearse');settle(page);page.locator('[data-scenario="waiting"]').click() + settle(page,1300);page.click('#stop');settle(page,300) + partial=page.evaluate('messages[messages.length-1].text');settle(page,1500) + check(page.evaluate('messages[messages.length-1].text')==partial,'取消后迟到内容不再写入') + check(page.evaluate('messages[messages.length-1].status')=='cancelled','未完成片段明确标注') + page.fill('#draft','新的话题');page.click('.send');page.wait_for_function('() => !state.busy',timeout=10000) + check(page.evaluate('messages[messages.length-1].text')==page.evaluate('SHORT'),'新一轮不混入取消的长回复') + + # Long user input, long name and reduced viewports, with both themes. + long_user='这是很长的一段话,包含标点、换行和表情🌧️。\n' * 25 + page.fill('#draft',long_user);page.click('.send');settle(page,300) + check(page.evaluate("pages().filter(p=>p.message.role==='user').map(p=>p.text).join('')")==long_user.strip(),'长用户输入与表情完整保留') + page.click('#stop');settle(page) + page.evaluate("setName('一个很长很长但仍然需要完整显示的演示名字🌙');state.cursor=pages().findIndex(p=>p.message.role==='her');renderSentence(true)") + for width,height in [(1440,900),(1280,720),(844,390),(390,844),(390,600),(390,420),(360,400),(320,568),(320,360)]: + for theme in ['moon','fog']: + page.set_viewport_size({'width':width,'height':height});settle(page) + page.locator(f'.theme [data-theme="{theme}"]').click();settle(page) + no_overlap(page,f'{width}×{height} 长名 {theme}') + if height<=420 and width<=390: + page.screenshot(path=str(OUT/f'simulated-viewport-{width}-{height}.png')) + check(page.evaluate('chunks(LONG).join("")===LONG'),'紧凑视口不丢失长回复文字') + + # Life / memory / linked chat / management stay navigable with shared fixtures only. + page.set_viewport_size({'width':1440,'height':900});page.evaluate("setName('晚晚');applyTheme('moon')");settle(page) + page.click('#close-talk');settle(page);page.locator('[data-nav="days"]').click();settle(page) + page.locator('[data-event="0"]').click();settle(page) + page.screenshot(path=str(OUT/'desktop-life-moon.png')) + page.locator('[data-event-history="0"]').click();settle(page) + check(page.locator('[data-life="0"]').count()==2,'日子关联到对应的已分享对话') + page.locator('[data-event="0"]').click();settle(page) + check(page.locator('#sheet-title').inner_text()=='窗边的洋桔梗','关联聊天能返回原来的一天') + page.locator('[data-action="days"]').click();settle(page);page.locator('[data-action="memories"]').click();settle(page) + page.evaluate("applyTheme('fog')");settle(page) + page.screenshot(path=str(OUT/'desktop-memory-fog.png')) + page.click('#close-sheet');settle(page) + page.locator('[data-nav="manage"]').click();settle(page);page.screenshot(path=str(OUT/'desktop-management-fog.png')) + page.locator('[data-action="relationship"]').click();settle(page) + page.select_option('#relation','恋人');page.locator('#relation-form button[type="submit"]').click() + check('已确认' in page.locator('#form-status').inner_text(),'演示关系保存有明确反馈') + page.click('#close-sheet');settle(page) + + # No preselected creation relationship; user-supplied name is used safely. + page.click('#rehearse');settle(page);page.locator('summary').first.click();page.locator('[data-action="create"]').click();settle(page) + check(page.locator('#new-relation').input_value()=='','初次创建关系不预选') + page.fill('#new-name','小雨 <🌙>');page.select_option('#new-relation','朋友');page.locator('#create-form button[type="submit"]').click();settle(page) + check(page.locator('#creation-error').inner_text()!='','名字复用已批准规则,拒绝尖括号') + page.fill('#new-name','小雨🌙');page.locator('#create-form button[type="submit"]').click();settle(page) + check(page.locator('.identity strong').inner_text()=='小雨🌙','用户给的名字贯穿身份显示且按文字处理') + page.click('#voice');settle(page);page.locator('[data-action="start-voice"]').click();settle(page) + check(page.locator('#voice-row').is_visible(),'语音演示持续标注没有收音') + no_overlap(page,'语音演示') + page.click('#end-voice');settle(page) + + # Short home layouts keep the portrait beside, rather than over, the opening words. + page.click('#close-talk');settle(page) + for width,height in [(390,600),(390,420),(320,568),(320,360)]: + page.set_viewport_size({'width':width,'height':height});settle(page) + g=page.evaluate('''() => {const r=s=>{const e=document.querySelector(s).getBoundingClientRect();return {x:e.x,y:e.y,b:e.bottom,r:e.right}};return {title:r('h1'),portal:r('#portal'),actions:r('.home-actions'),nav:r('.navigation')}}''') + check(g['title']['r']+4<=g['portal']['x'] or g['portal']['b']+4<=g['title']['y'],f'{width}×{height} 相处页人物不挡近况',g) + check(g['actions']['b']+4<=g['nav']['y'],f'{width}×{height} 相处入口不挡导航') + page.set_viewport_size({'width':390,'height':844});settle(page);page.click('#start-talk');settle(page) + page.click('#autoplay');check(page.locator('#autoplay').get_attribute('aria-pressed')=='true','自动续读明确由用户选择') + page.focus('#draft');check(page.locator('#autoplay').get_attribute('aria-pressed')=='false','输入时停止自动续读') + + page.emulate_media(reduced_motion='reduce') + check(page.evaluate("getComputedStyle(document.querySelector('.mist')).animationName")=='none','系统减少动效停止背景循环') + page.click('#close-talk');settle(page);page.click('#start-talk');settle(page) + check(page.evaluate("!document.querySelector('#portal').getAnimations().some(a=>a.playState==='running')"),'减少动效不播放空间移动') + # Keyboard mode must not initiate WAAPI animations. + page.keyboard.press('Escape');page.keyboard.press('Tab');page.keyboard.press('Enter');settle(page) + check(not errors,'无浏览器脚本错误',errors) + check(not requests,'离线原型没有 HTTP / WebSocket 请求',requests) + browser.close() + +(OUT/'verification.json').write_text(json.dumps({'browser':'Chromium / Linux','checks':results,'limitations':['未测试真机软键盘','未测试 Safari / iOS','视口缩小是等效模拟','人物为获批静帧;服务全部未接入']},ensure_ascii=False,indent=2)) +print(f'{len(results)} meaningful checks passed; evidence saved to evidence/')