style(teacher): use the playful continue button in the composer
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore-teacher-reply-cleanup
|
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore-teacher-reply-cleanup
|
||||||
- Base commit: ae0f72d8fa0089f87b41d272da220e1c1397aec8
|
- Base commit: ae0f72d8fa0089f87b41d272da220e1c1397aec8
|
||||||
- Owner: codex
|
- Owner: codex
|
||||||
- Status: Ready for Integration
|
- Status: In progress
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -115,3 +115,11 @@
|
|||||||
- Verified same login/account/project/conversation/route, operation drafts, composer values and all persisted consultation draft text/references/pending payloads. One persisted absent pending field normalizes to null on mount; semantic comparison confirms no draft content or request identity was lost. Runtime verification contains only boolean/style/build metadata, no draft bodies or credentials. Temporary Main-memory backup and inspector removed.
|
- Verified same login/account/project/conversation/route, operation drafts, composer values and all persisted consultation draft text/references/pending payloads. One persisted absent pending field normalizes to null on mount; semantic comparison confirms no draft content or request identity was lost. Runtime verification contains only boolean/style/build metadata, no draft bodies or credentials. Temporary Main-memory backup and inspector removed.
|
||||||
- Foreground monitor: trialFocused=false, unchanged foreground PID 88503. No show/focus calls were needed. Local trial metadata and next-launch bootstrap updated; production app untouched. No cloud publication, deliberate model test request, main merge or push.
|
- Foreground monitor: trialFocused=false, unchanged foreground PID 88503. No show/focus calls were needed. Local trial metadata and next-launch bootstrap updated; production app untouched. No cloud publication, deliberate model test request, main merge or push.
|
||||||
- Documentation structure and whitespace checks pass; the task-aware drift diagnostic remains limited to the unchanged imported manual-refresh task record described above, reconfirmed identical to 4f6bda4.
|
- Documentation structure and whitespace checks pass; the task-aware drift diagnostic remains limited to the unchanged imported manual-refresh task record described above, reconfirmed identical to 4f6bda4.
|
||||||
|
|
||||||
|
## Visible Hint Clarification
|
||||||
|
|
||||||
|
- User could not see the prior rename. Read-only runtime inspection confirmed the new build was loaded but no failed/cancelled request was currently displayed; the visible empty-composer entry still said “给我一点提示”. Explained the distinction and user explicitly confirmed to rename/style the composer entry too.
|
||||||
|
- Same task identity/ownership resumed and verified; reuse this follow-up's loaded project/peer context with no new conflict. User clarification supersedes the earlier preference for a transparent text hint. Plan: share the warm sticker styling between hint and retry, send the matching new label through the existing suggestions intent, preserve empty/type/clear behavior and legacy pending-payload reuse. Increase the reserved action-row height to 44px so the send control remains still. Update existing tests and preview with a background Renderer refresh. No backend or cloud-policy change.
|
||||||
|
|
||||||
|
- Implemented shared hint/retry sticker styling and exact “继续看看👀” composer label/payload. The hint still hides on nonempty input; reserved 44px action height keeps the send button fixed. Existing request identity reuse covers both former labels (“帮我看看” and “给我一点提示”), without changing saved history.
|
||||||
|
- Verification passed: typecheck, full lint plus final test-file lint, production build, 59 UI tests and 15 headless browser checks. Existing tests distinguish the two same-label controls by composer/reply location. Screenshots at 319/508px show the visible empty-composer button and confirm no clipping or send-button movement. Shared native E2E assertions updated without launching a native test app.
|
||||||
|
|||||||
@@ -174,9 +174,9 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
|
|||||||
- 主动发言采用紧贴小头像的短气泡;只有存在真实主动消息或运营欢迎语时才显示这组浮层,收起消息时头像和气泡一起消失,顶部入口仍保留。长消息在气泡中最多显示三行,点击接回原智能体对话查看全文。不提供“智能体偶尔来看看”开关或本地模拟巡看控制,既有自动跟进与真实消息接收逻辑保留。作品原生预览对整组气泡和头像测量避让,避免图片被原生页面遮住。
|
- 主动发言采用紧贴小头像的短气泡;只有存在真实主动消息或运营欢迎语时才显示这组浮层,收起消息时头像和气泡一起消失,顶部入口仍保留。长消息在气泡中最多显示三行,点击接回原智能体对话查看全文。不提供“智能体偶尔来看看”开关或本地模拟巡看控制,既有自动跟进与真实消息接收逻辑保留。作品原生预览对整组气泡和头像测量避让,避免图片被原生页面遮住。
|
||||||
- 学生在当前项目创作时,前端每 5 分钟请求一次智能体跟进;窗口隐藏、来源归档、主对话正在执行、智能体正在回复或学生正在智能体栏写草稿时延后。Main 再检查在线启停、来源归属、项目级冷却和已完成文本指纹,未变上下文按下述十五分钟冷却处理。跟进沿用运营模型、已发布 Skills 与当前智能体话题,生成符合所选配置的具体建议或引导;以主动智能体发言持久化,不伪造学生消息。模型调用沿用智能体计费规则。
|
- 学生在当前项目创作时,前端每 5 分钟请求一次智能体跟进;窗口隐藏、来源归档、主对话正在执行、智能体正在回复或学生正在智能体栏写草稿时延后。Main 再检查在线启停、来源归属、项目级冷却和已完成文本指纹,未变上下文按下述十五分钟冷却处理。跟进沿用运营模型、已发布 Skills 与当前智能体话题,生成符合所选配置的具体建议或引导;以主动智能体发言持久化,不伪造学生消息。模型调用沿用智能体计费规则。
|
||||||
- 进入项目时,智能体头像旁先显示运营发布的欢迎语气泡,不调用模型、不声称已检查项目;每个账号/项目主动收起后不重复弹出。真实的未读主动建议优先替换欢迎语。主动发言在头像旁直接显示气泡,不抢焦点、不盖住作品;作品区按气泡与原生预览的实际交叠范围避让,已有工具栏高度计入计算,气泡位于独立智能体栏时不额外留白,窗口/分栏尺寸变化和收起气泡时实时归还空间;“和智能体聊聊”接回同一段咨询,“等会儿聊”收起并保留历史。已有未处理气泡时不堆叠新邀请;咨询已展开时也显示气泡,直到学生主动点击查看或收起,期间的其他回复不会吞掉未读邀请。五分钟检查计时在刷新和项目切换后保留;智能体咨询里的新讨论也算进展,即使操作对话没有变化也会继续跟进;上下文完全未变时放缓到十五分钟一次,不永久停止。事件连接丢失时,每十五秒只读同步未完成回复,收到结果后停止轮询,不重复调用模型。失败如实提示,不以本地文案替代模型回复。已接受请求恢复使用原身份,退出账号和来源删除仍沿用 Main 中止边界。
|
- 进入项目时,智能体头像旁先显示运营发布的欢迎语气泡,不调用模型、不声称已检查项目;每个账号/项目主动收起后不重复弹出。真实的未读主动建议优先替换欢迎语。主动发言在头像旁直接显示气泡,不抢焦点、不盖住作品;作品区按气泡与原生预览的实际交叠范围避让,已有工具栏高度计入计算,气泡位于独立智能体栏时不额外留白,窗口/分栏尺寸变化和收起气泡时实时归还空间;“和智能体聊聊”接回同一段咨询,“等会儿聊”收起并保留历史。已有未处理气泡时不堆叠新邀请;咨询已展开时也显示气泡,直到学生主动点击查看或收起,期间的其他回复不会吞掉未读邀请。五分钟检查计时在刷新和项目切换后保留;智能体咨询里的新讨论也算进展,即使操作对话没有变化也会继续跟进;上下文完全未变时放缓到十五分钟一次,不永久停止。事件连接丢失时,每十五秒只读同步未完成回复,收到结果后停止轮询,不重复调用模型。失败如实提示,不以本地文案替代模型回复。已接受请求恢复使用原身份,退出账号和来源删除仍沿用 Main 中止边界。
|
||||||
- 智能体支持自由提问;输入框为空时,底部显示轻量文字入口“给我一点提示”,输入文字后收起,清空后重现,发送按钮保持原位。只有学生点击才调用模型,结合当前操作对话与咨询历史推荐一个具体切入点,并生成围绕它的快捷回复。点击快捷回复会主动开始讨论;回复下方不再附加固定的二次求助链接;失败或中断后,可点暖黄色小按钮“继续看看👀”重试。这些快捷求助保留已有输入草稿,网络结果不确定时复用请求身份,已确认终态的请求不重复执行;解析失败可重新求助,不用固定问题伪装模型结果。服务端下发的推荐问题只填入草稿,由用户确认发送。界面不提供独立示范页面、“记一下”、共识或自动待办,智能体的回复下不再展示“我去试一试”和“复制”按钮,学生通过已有的操作对话/作品入口继续创作。
|
- 智能体支持自由提问;输入框为空时,底部显示暖黄色小按钮“继续看看👀”,输入文字后收起,清空后重现,发送按钮保持原位。只有学生点击才调用模型,结合当前操作对话与咨询历史推荐一个具体切入点,并生成围绕它的快捷回复。点击快捷回复会主动开始讨论;回复下方不再附加固定的二次求助链接;失败或中断后,可点暖黄色小按钮“继续看看👀”重试。这些快捷求助保留已有输入草稿,网络结果不确定时复用请求身份,已确认终态的请求不重复执行;解析失败可重新求助,不用固定问题伪装模型结果。服务端下发的推荐问题只填入草稿,由用户确认发送。界面不提供独立示范页面、“记一下”、共识或自动待办,智能体的回复下不再展示“我去试一试”和“复制”按钮,学生通过已有的操作对话/作品入口继续创作。
|
||||||
- 老师咨询只展示老师正文和可点击的快捷回复卡片,学生也可在“和老师聊聊”输入框自由输入。点击卡片原样发送该回复,保留当前输入草稿;旧话题的数据不会恢复想法板、结构图、流程图或对照表及其操作。
|
- 老师咨询只展示老师正文和可点击的快捷回复卡片,学生也可在“和老师聊聊”输入框自由输入。点击卡片原样发送该回复,保留当前输入草稿;旧话题的数据不会恢复想法板、结构图、流程图或对照表及其操作。
|
||||||
- “给我一点提示”由老师结合项目推荐一个具体切入点,快捷回复围绕这个切入点帮助学生接话;客户端不要求多个独立话题,也不固定正文长度、选项数量、前缀或表达风格。老师继续依据已配置职责帮助孩子整理想法、理解关系和承接已确认的共识,区分建议与已确认内容。
|
- “继续看看👀”由老师结合项目推荐一个具体切入点,快捷回复围绕这个切入点帮助学生接话;客户端不要求多个独立话题,也不固定正文长度、选项数量、前缀或表达风格。老师继续依据已配置职责帮助孩子整理想法、理解关系和承接已确认的共识,区分建议与已确认内容。
|
||||||
- Main 对咨询回复统一使用 `{reply, quickReplies}`,本地读取工具的前言和云端非最终片段不当作最终回答;完整解析后展示正文和卡片,主动进展提醒仍直接展示正文。普通 Markdown、JSON 数据和代码示例保持正文。兼容读取旧 `{intro, questions}` 和带 `tool` 的回复,只提取正文与快捷回复,不恢复或更新组件。资源上限只用于防护,不做旧式字数截断;格式错误保留可读正文及原始回答,原文默认折叠、字面显示,不再次送进模型。
|
- Main 对咨询回复统一使用 `{reply, quickReplies}`,本地读取工具的前言和云端非最终片段不当作最终回答;完整解析后展示正文和卡片,主动进展提醒仍直接展示正文。普通 Markdown、JSON 数据和代码示例保持正文。兼容读取旧 `{intro, questions}` 和带 `tool` 的回复,只提取正文与快捷回复,不恢复或更新组件。资源上限只用于防护,不做旧式字数截断;格式错误保留可读正文及原始回答,原文默认折叠、字面显示,不再次送进模型。
|
||||||
- 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按本次问题/工具调用身份合并名称和状态,跨云端暂停、续接仍只计一次;暂停读取不算失败,状态以实际读取结果或问题终态为准,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。
|
- 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按本次问题/工具调用身份合并名称和状态,跨云端暂停、续接仍只计一次;暂停读取不算失败,状态以实际读取结果或问题终态为准,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。
|
||||||
- 云端咨询在 local_context 声明 read_protocol=2;需先部署配套 Yuxi API 和 worker,再升级客户端。云端持久累计读取字节及批次,每轮告知模型剩余额度;预算耗尽后消费最后一批结果,并以 tool_choice=none 要求根据现有证据形成答案和说明缺口。Main 限制实际返回量并拒绝第十三批读取,区分读取达到上限、上下文失效及格式无效。服务端对未声明协议的已安装旧客户端保留原工具参数与六批边界。
|
- 云端咨询在 local_context 声明 read_protocol=2;需先部署配套 Yuxi API 和 worker,再升级客户端。云端持久累计读取字节及批次,每轮告知模型剩余额度;预算耗尽后消费最后一批结果,并以 tool_choice=none 要求根据现有证据形成答案和说明缺口。Main 限制实际返回量并拒绝第十三批读取,区分读取达到上限、上下文失效及格式无效。服务端对未声明协议的已安装旧客户端保留原工具参数与六批边界。
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export function TeacherChatPanel({
|
|||||||
(request) => request.status === 'preparing' || request.status === 'running'
|
(request) => request.status === 'preparing' || request.status === 'running'
|
||||||
);
|
);
|
||||||
const helpUnavailable = busy || !topicEnabled || Boolean(running);
|
const helpUnavailable = busy || !topicEnabled || Boolean(running);
|
||||||
const askForSuggestions = () => void send({ text: '给我一点提示', intent: 'suggestions' });
|
const askForSuggestions = () => void send({ text: '继续看看👀', intent: 'suggestions' });
|
||||||
const changeText = (next: string) => { setText(next); persistDraft(next, references, pending.current); };
|
const changeText = (next: string) => { setText(next); persistDraft(next, references, pending.current); };
|
||||||
const removeReferences = () => { setReferences([]); persistDraft(text, [], pending.current); };
|
const removeReferences = () => { setReferences([]); persistDraft(text, [], pending.current); };
|
||||||
const questions = definition?.suggested_questions ?? [];
|
const questions = definition?.suggested_questions ?? [];
|
||||||
@@ -444,9 +444,9 @@ export function TeacherChatPanel({
|
|||||||
<textarea ref={inputRef} aria-label={`向${label}提问`} value={text} disabled={busy} maxLength={6000} onChange={(e) => changeText(e.target.value)} placeholder="和老师聊聊" rows={2}
|
<textarea ref={inputRef} aria-label={`向${label}提问`} value={text} disabled={busy} maxLength={6000} onChange={(e) => changeText(e.target.value)} placeholder="和老师聊聊" rows={2}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); void send(); } }}
|
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); void send(); } }}
|
||||||
className="min-h-[60px] w-full resize-none border-0 bg-transparent text-[13px] leading-6 outline-none placeholder:text-muted-foreground/65" />
|
className="min-h-[60px] w-full resize-none border-0 bg-transparent text-[13px] leading-6 outline-none placeholder:text-muted-foreground/65" />
|
||||||
<div className="mt-2 flex min-h-9 items-center justify-between gap-2" data-testid="consultation-composer-actions">
|
<div className="mt-2 flex min-h-11 items-center justify-between gap-2" data-testid="consultation-composer-actions">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint flex min-h-9 cursor-pointer items-center gap-1 rounded-md px-1 text-[13px] transition-colors disabled:cursor-not-allowed disabled:opacity-50">给我一点提示<ChevronRight className="h-3.5 w-3.5" aria-hidden="true" /></button>}
|
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint">继续看看👀</button>}
|
||||||
</div>
|
</div>
|
||||||
{running ? <button type="button" className="rounded-lg border px-3 py-2 text-xs" onClick={() => void teacherApi.cancel(topicBase, topic!.id, running.id).then(consume).catch(() => setError('暂时无法停止,请稍后重试。'))}>停止回复</button>
|
{running ? <button type="button" className="rounded-lg border px-3 py-2 text-xs" onClick={() => void teacherApi.cancel(topicBase, topic!.id, running.id).then(consume).catch(() => setError('暂时无法停止,请稍后重试。'))}>停止回复</button>
|
||||||
: <button type="submit" aria-label="提问" title={busy ? '正在发送…' : '发送'} disabled={busy || !topicEnabled || !text.trim()} className="consultation-send flex h-8 w-8 items-center justify-center rounded-full text-white disabled:opacity-30"><ArrowUp className="h-4 w-4" /></button>}
|
: <button type="submit" aria-label="提问" title={busy ? '正在发送…' : '发送'} disabled={busy || !topicEnabled || !text.trim()} className="consultation-send flex h-8 w-8 items-center justify-center rounded-full text-white disabled:opacity-30"><ArrowUp className="h-4 w-4" /></button>}
|
||||||
|
|||||||
@@ -49,13 +49,11 @@
|
|||||||
.classroom-consultation-resizer { position: absolute; inset: 0 auto 0 -4px; width: 8px; z-index: 2; cursor: col-resize; }
|
.classroom-consultation-resizer { position: absolute; inset: 0 auto 0 -4px; width: 8px; z-index: 2; cursor: col-resize; }
|
||||||
.classroom-consultation-resizer:hover, .classroom-consultation-resizer:focus-visible { background: #7994b533; }
|
.classroom-consultation-resizer:hover, .classroom-consultation-resizer:focus-visible { background: #7994b533; }
|
||||||
.consultation-pane { background: #fffdf8; color: #30465f; }
|
.consultation-pane { background: #fffdf8; color: #30465f; }
|
||||||
.consultation-hint { color: #5c7087; background: transparent; border: 0; text-underline-offset: 4px; text-decoration: underline transparent; }
|
.consultation-hint, .consultation-retry { display: inline-flex; align-items: center; justify-content: center; align-self: flex-start; min-height: 44px; max-width: 100%; padding: 8px 16px; border: 1.5px solid #e3ce8f; border-radius: 16px 16px 16px 6px; background: #fff2c9; color: #6f5127; box-shadow: 0 3px 0 #e7d7a6, inset 0 1px 0 #fff9e8; font-size: 14px; font-weight: 600; line-height: 24px; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; }
|
||||||
.consultation-hint:hover:not(:disabled) { color: #30465f; text-decoration-color: #a9bbce; }
|
.consultation-hint:hover:not(:disabled), .consultation-retry:hover:not(:disabled) { background: #ffe9a9; border-color: #d6b76f; }
|
||||||
.consultation-retry { display: inline-flex; align-items: center; justify-content: center; align-self: flex-start; min-height: 44px; max-width: 100%; padding: 8px 16px; border: 1.5px solid #e3ce8f; border-radius: 16px 16px 16px 6px; background: #fff2c9; color: #6f5127; box-shadow: 0 3px 0 #e7d7a6, inset 0 1px 0 #fff9e8; font-size: 14px; font-weight: 600; line-height: 24px; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; }
|
.consultation-hint:active:not(:disabled), .consultation-retry:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 #e7d7a6, inset 0 1px 0 #fff9e8; }
|
||||||
.consultation-retry:hover:not(:disabled) { background: #ffe9a9; border-color: #d6b76f; }
|
.consultation-hint:disabled, .consultation-retry:disabled { cursor: not-allowed; opacity: 0.5; }
|
||||||
.consultation-retry:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 #e7d7a6, inset 0 1px 0 #fff9e8; }
|
@media (prefers-reduced-motion: reduce) { .consultation-hint, .consultation-retry { transition: none; } .consultation-hint:active:not(:disabled), .consultation-retry:active:not(:disabled) { transform: none; } }
|
||||||
.consultation-retry:disabled { cursor: not-allowed; opacity: 0.5; }
|
|
||||||
@media (prefers-reduced-motion: reduce) { .consultation-retry { transition: none; } .consultation-retry:active:not(:disabled) { transform: none; } }
|
|
||||||
.consultation-pane button:focus-visible { outline: 2px solid #71845f; outline-offset: 3px; }
|
.consultation-pane button:focus-visible { outline: 2px solid #71845f; outline-offset: 3px; }
|
||||||
.consultation-suggestion { border-color: #e0e6d6; color: #718163; }
|
.consultation-suggestion { border-color: #e0e6d6; color: #718163; }
|
||||||
.consultation-suggestion:hover { border-color: #bac9aa; background: #f4f7ee; }
|
.consultation-suggestion:hover { border-color: #bac9aa; background: #f4f7ee; }
|
||||||
|
|||||||
@@ -1829,20 +1829,21 @@ test('project consultations preserve student drafts and switch between work and
|
|||||||
await expect(page.getByRole('button', { name: '与代码智能体聊天' })).toBeVisible();
|
await expect(page.getByRole('button', { name: '与代码智能体聊天' })).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: '与朋友聊天' })).toHaveAttribute('aria-pressed', 'true');
|
await expect(page.getByRole('button', { name: '与朋友聊天' })).toHaveAttribute('aria-pressed', 'true');
|
||||||
await expect(page.locator('#coding-consultation-dock')).toHaveCSS('width', '508px');
|
await expect(page.locator('#coding-consultation-dock')).toHaveCSS('width', '508px');
|
||||||
await expect(teacher.getByTestId('consultation-composer-actions').getByRole('button', { name: '给我一点提示', exact: true })).toBeVisible();
|
await expect(teacher.locator('form .consultation-hint')).toBeVisible();
|
||||||
|
await expect(teacher.locator('form .consultation-hint')).toHaveText('继续看看👀');
|
||||||
await expect(teacher.getByText('把你的困惑说出来就好')).toHaveCount(0);
|
await expect(teacher.getByText('把你的困惑说出来就好')).toHaveCount(0);
|
||||||
await expect(teacher.getByText('当前项目',{exact:true})).toHaveCount(0);
|
await expect(teacher.getByText('当前项目',{exact:true})).toHaveCount(0);
|
||||||
await expect(teacher.getByRole('button',{name:'解释当前代码',exact:true})).toBeVisible();
|
await expect(teacher.getByRole('button',{name:'解释当前代码',exact:true})).toBeVisible();
|
||||||
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
|
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
|
||||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
||||||
await expect(teacher.getByRole('button',{name:'给我一点提示',exact:true})).toHaveCount(0);
|
await expect(teacher.locator('form .consultation-hint')).toHaveCount(0);
|
||||||
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
|
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
|
||||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('');
|
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('');
|
||||||
await teacher.getByRole('button',{name:'给我一点提示',exact:true}).click();
|
await teacher.locator('form .consultation-hint').click();
|
||||||
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
|
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
|
||||||
await expect(teacher.getByTestId('teacher-discussion')).toHaveCount(0);
|
await expect(teacher.getByTestId('teacher-discussion')).toHaveCount(0);
|
||||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
||||||
await expect(teacher.getByRole('button',{name:'给我一点提示',exact:true})).toHaveCount(0);
|
await expect(teacher.locator('form .consultation-hint')).toHaveCount(0);
|
||||||
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
|
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
|
||||||
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
|
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
|
||||||
await teacher.getByRole('button',{name:'怎样观察别人玩游戏?',exact:true}).click();
|
await teacher.getByRole('button',{name:'怎样观察别人玩游戏?',exact:true}).click();
|
||||||
@@ -1881,7 +1882,7 @@ test('project consultations preserve student drafts and switch between work and
|
|||||||
}
|
}
|
||||||
expect(requests.find(item => item.path.endsWith('/agent-topics') && item.method === 'POST')?.body?.teacherVersion).toBe(9);
|
expect(requests.find(item => item.path.endsWith('/agent-topics') && item.method === 'POST')?.body?.teacherVersion).toBe(9);
|
||||||
await expect(teacher).toHaveAccessibleName('朋友咨询');
|
await expect(teacher).toHaveAccessibleName('朋友咨询');
|
||||||
expect(consultationRequests[0].body).toMatchObject({intent:'suggestions',text:'给我一点提示'});
|
expect(consultationRequests[0].body).toMatchObject({intent:'suggestions',text:'继续看看👀'});
|
||||||
expect(consultationRequests[1].body).toMatchObject({text:'怎样观察别人玩游戏?'});
|
expect(consultationRequests[1].body).toMatchObject({text:'怎样观察别人玩游戏?'});
|
||||||
expect(consultationRequests[1].body?.intent).toBeUndefined();
|
expect(consultationRequests[1].body?.intent).toBeUndefined();
|
||||||
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
|
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
|
|||||||
for (const status of ['failed', 'cancelled']) {
|
for (const status of ['failed', 'cancelled']) {
|
||||||
await page.goto(`${url}?kind=retry&status=${status}&width=319`);
|
await page.goto(`${url}?kind=retry&status=${status}&width=319`);
|
||||||
const panel = page.getByTestId('teacher-chat-panel');
|
const panel = page.getByTestId('teacher-chat-panel');
|
||||||
const retry = page.getByRole('button', { name: '继续看看👀', exact: true });
|
const retry = panel.locator('.consultation-retry');
|
||||||
await expect(retry).toBeEnabled();
|
await expect(retry).toBeEnabled();
|
||||||
await expect(retry).toHaveText('继续看看👀');
|
await expect(retry).toHaveText('继续看看👀');
|
||||||
expect(await retry.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
|
expect(await retry.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
|
||||||
@@ -41,24 +41,28 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
|
|||||||
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
|
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
|
||||||
await expect(input).toHaveValue('我还有一点自己的想法');
|
await expect(input).toHaveValue('我还有一点自己的想法');
|
||||||
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs)).toEqual([
|
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs)).toEqual([
|
||||||
{ requestId: expect.any(String), text: '给我一点提示', intent: 'suggestions', references: [], sourceConversationId: 'source' },
|
{ requestId: expect.any(String), text: '继续看看👀', intent: 'suggestions', references: [], sourceConversationId: 'source' },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const width of [319, 508]) {
|
for (const width of [319, 508]) {
|
||||||
test(`empty-composer hint stays lightweight and keeps the send position stable at ${width}px`, async ({ page }) => {
|
test(`empty-composer hint stays readable and keeps the send position stable at ${width}px`, async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 1180, height: 800 });
|
await page.setViewportSize({ width: 1180, height: 800 });
|
||||||
await page.goto(`${url}?kind=ordinary&width=${width}`);
|
await page.goto(`${url}?kind=ordinary&width=${width}`);
|
||||||
const input = page.getByRole('textbox', { name: '向智能体提问' });
|
const input = page.getByRole('textbox', { name: '向智能体提问' });
|
||||||
const hint = page.getByRole('button', { name: '给我一点提示', exact: true });
|
const hint = page.locator('form .consultation-hint');
|
||||||
const send = page.getByRole('button', { name: '提问', exact: true });
|
const send = page.getByRole('button', { name: '提问', exact: true });
|
||||||
const sent = () => page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
|
const sent = () => page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
|
||||||
await expect(hint).toBeEnabled();
|
await expect(hint).toBeEnabled();
|
||||||
await expect(hint).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
await expect(hint).toHaveText('继续看看👀');
|
||||||
|
await expect(hint).toHaveCSS('background-color', 'rgb(255, 242, 201)');
|
||||||
const initialSend = (await send.boundingBox())!;
|
const initialSend = (await send.boundingBox())!;
|
||||||
const hintBounds = (await hint.boundingBox())!;
|
const hintBounds = (await hint.boundingBox())!;
|
||||||
expect(hintBounds.x).toBeLessThan(initialSend.x);
|
expect(hintBounds.x).toBeLessThan(initialSend.x);
|
||||||
|
expect(hintBounds.height).toBeGreaterThanOrEqual(44);
|
||||||
|
expect(hintBounds.x + hintBounds.width).toBeLessThanOrEqual(initialSend.x);
|
||||||
|
await page.getByTestId('teacher-chat-panel').screenshot({ path: test.info().outputPath(`composer-hint-${width}.png`) });
|
||||||
expect(await sent()).toEqual([]);
|
expect(await sent()).toEqual([]);
|
||||||
await input.fill('我想先自己说说');
|
await input.fill('我想先自己说说');
|
||||||
await expect(hint).toHaveCount(0);
|
await expect(hint).toHaveCount(0);
|
||||||
@@ -71,9 +75,9 @@ for (const width of [319, 508]) {
|
|||||||
expect(await sent()).toEqual([]);
|
expect(await sent()).toEqual([]);
|
||||||
await hint.click();
|
await hint.click();
|
||||||
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
|
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
|
||||||
expect(await sent()).toEqual([{ requestId: expect.any(String), text: '给我一点提示', intent: 'suggestions', references: [], sourceConversationId: 'source' }]);
|
expect(await sent()).toEqual([{ requestId: expect.any(String), text: '继续看看👀', intent: 'suggestions', references: [], sourceConversationId: 'source' }]);
|
||||||
await expect(page.getByRole('button', { name: '我也说不清,你带我看看', exact: true })).toHaveCount(0);
|
await expect(page.getByRole('button', { name: '我也说不清,你带我看看', exact: true })).toHaveCount(0);
|
||||||
await expect(page.locator('.consultation-messages').getByRole('button', { name: '给我一点提示', exact: true })).toHaveCount(0);
|
await expect(page.locator('.consultation-messages').getByRole('button', { name: '继续看看👀', exact: true })).toHaveCount(0);
|
||||||
const card = page.getByRole('button', { name: '可以从哪个小地方开始?' });
|
const card = page.getByRole('button', { name: '可以从哪个小地方开始?' });
|
||||||
await expect(card).toBeVisible();
|
await expect(card).toBeVisible();
|
||||||
await input.fill('留着我自己的草稿');
|
await input.fill('留着我自己的草稿');
|
||||||
@@ -82,7 +86,7 @@ for (const width of [319, 508]) {
|
|||||||
await expect(page.getByText('好,我们就从这一个问题聊起。')).toBeVisible();
|
await expect(page.getByText('好,我们就从这一个问题聊起。')).toBeVisible();
|
||||||
await expect(input).toHaveValue('留着我自己的草稿');
|
await expect(input).toHaveValue('留着我自己的草稿');
|
||||||
expect(await sent()).toHaveLength(2);
|
expect(await sent()).toHaveLength(2);
|
||||||
await page.screenshot({ path: test.info().outputPath(`composer-hint-${width}.png`) });
|
await page.screenshot({ path: test.info().outputPath(`composer-draft-${width}.png`) });
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`rich replies keep code, tables and images inside the panel at ${width}px`, async ({ page }) => {
|
test(`rich replies keep code, tables and images inside the panel at ${width}px`, async ({ page }) => {
|
||||||
|
|||||||
@@ -353,12 +353,13 @@ describe('teacher side chat', () => {
|
|||||||
}));
|
}));
|
||||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
const input = screen.getByLabelText('向智能体提问');
|
const input = screen.getByLabelText('向智能体提问');
|
||||||
const help = () => screen.getByRole('button', { name: '给我一点提示', exact: true });
|
const help = () => screen.getByText('继续看看👀', { selector: 'form .consultation-hint' });
|
||||||
await waitFor(() => expect(help()).toBeEnabled());
|
await waitFor(() => expect(help()).toBeEnabled());
|
||||||
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(help());
|
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(help());
|
||||||
|
expect(help()).toHaveAccessibleName('继续看看👀');
|
||||||
expect(api.send).not.toHaveBeenCalled();
|
expect(api.send).not.toHaveBeenCalled();
|
||||||
fireEvent.change(input, { target: { value: '我先自己想想' } });
|
fireEvent.change(input, { target: { value: '我先自己想想' } });
|
||||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||||
expect(api.send).not.toHaveBeenCalled();
|
expect(api.send).not.toHaveBeenCalled();
|
||||||
fireEvent.change(input, { target: { value: ' ' } });
|
fireEvent.change(input, { target: { value: ' ' } });
|
||||||
expect(help()).toBeEnabled();
|
expect(help()).toBeEnabled();
|
||||||
@@ -373,11 +374,11 @@ describe('teacher side chat', () => {
|
|||||||
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
|
||||||
expect(api.send).toHaveBeenCalledTimes(1);
|
expect(api.send).toHaveBeenCalledTimes(1);
|
||||||
expect(api.send).toHaveBeenCalledWith('p/agent-topics', 'first', expect.objectContaining({
|
expect(api.send).toHaveBeenCalledWith('p/agent-topics', 'first', expect.objectContaining({
|
||||||
intent: 'suggestions', text: '给我一点提示', sourceConversationId: 'c',
|
intent: 'suggestions', text: '继续看看👀', sourceConversationId: 'c',
|
||||||
}));
|
}));
|
||||||
expect(help()).toBeDisabled();
|
expect(help()).toBeDisabled();
|
||||||
fireEvent.change(input, { target: { value: '我自己还想问的另一件事' } });
|
fireEvent.change(input, { target: { value: '我自己还想问的另一件事' } });
|
||||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||||
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({
|
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({
|
||||||
@@ -388,7 +389,7 @@ describe('teacher side chat', () => {
|
|||||||
expect(screen.queryByText('我看看你最近做到了哪里…')).not.toBeInTheDocument();
|
expect(screen.queryByText('我看看你最近做到了哪里…')).not.toBeInTheDocument();
|
||||||
for (const question of suggestedQuestions) expect(screen.getByRole('button', { name: question })).toBeEnabled();
|
for (const question of suggestedQuestions) expect(screen.getByRole('button', { name: question })).toBeEnabled();
|
||||||
expect(screen.queryByRole('button', { name: '我也说不清,你带我看看', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: '我也说不清,你带我看看', exact: true })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||||
expect(api.send).toHaveBeenCalledTimes(1);
|
expect(api.send).toHaveBeenCalledTimes(1);
|
||||||
expect(input).toHaveValue('我自己还想问的另一件事');
|
expect(input).toHaveValue('我自己还想问的另一件事');
|
||||||
});
|
});
|
||||||
@@ -423,8 +424,8 @@ describe('teacher side chat', () => {
|
|||||||
it('keeps an uncertain suggestions request identity through reopening and draft edits', async () => {
|
it('keeps an uncertain suggestions request identity through reopening and draft edits', async () => {
|
||||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||||
await screen.findByText('网络暂不可用');
|
await screen.findByText('网络暂不可用');
|
||||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
|
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
|
||||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||||
@@ -434,30 +435,30 @@ describe('teacher side chat', () => {
|
|||||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByLabelText('向智能体提问')).toBeEnabled());
|
await waitFor(() => expect(screen.getByLabelText('向智能体提问')).toBeEnabled());
|
||||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还没写完的问题');
|
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还没写完的问题');
|
||||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||||
expect(api.send).toHaveBeenCalledTimes(1);
|
expect(api.send).toHaveBeenCalledTimes(1);
|
||||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
|
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||||
await screen.findByText('网络暂不可用');
|
await screen.findByText('网络暂不可用');
|
||||||
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
|
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
|
||||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([{ references: undefined }, { references: [{ kind: 'code' as const, text: '旧请求的引用' }] }])('retries an uncertain legacy hint payload unchanged with references $references', async ({ references }) => {
|
it.each([{ text: '帮我看看', references: undefined }, { text: '给我一点提示', references: [{ kind: 'code' as const, text: '旧请求的引用' }] }])('retries an uncertain legacy hint payload "$text" unchanged with references $references', async ({ text, references }) => {
|
||||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text: '帮我看看', intent: 'suggestions', sourceConversationId: 'previous-source', ...(references ? { references } : {}) };
|
const original: TeacherSend = { requestId: 'legacy-hint-request', text, intent: 'suggestions', sourceConversationId: 'previous-source', ...(references ? { references } : {}) };
|
||||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
||||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||||
expect(api.send).not.toHaveBeenCalled();
|
expect(api.send).not.toHaveBeenCalled();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||||
await screen.findByText('网络暂不可用');
|
await screen.findByText('网络暂不可用');
|
||||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[0][2]))).toEqual(original);
|
expect(JSON.parse(JSON.stringify(api.send.mock.calls[0][2]))).toEqual(original);
|
||||||
view.unmount();
|
view.unmount();
|
||||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||||
expect(api.send).toHaveBeenCalledTimes(1);
|
expect(api.send).toHaveBeenCalledTimes(1);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||||
await screen.findByText('网络暂不可用');
|
await screen.findByText('网络暂不可用');
|
||||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[1][2]))).toEqual(original);
|
expect(JSON.parse(JSON.stringify(api.send.mock.calls[1][2]))).toEqual(original);
|
||||||
});
|
});
|
||||||
@@ -482,13 +483,13 @@ describe('teacher side chat', () => {
|
|||||||
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({ id: original.requestId, text: original.text, status })] });
|
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({ id: original.requestId, text: original.text, status })] });
|
||||||
api.send.mockResolvedValue({ ...first, revision: 2 });
|
api.send.mockResolvedValue({ ...first, revision: 2 });
|
||||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||||
expect(api.send).not.toHaveBeenCalled();
|
expect(api.send).not.toHaveBeenCalled();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||||
expect(sent.requestId).not.toBe(original.requestId);
|
expect(sent.requestId).not.toBe(original.requestId);
|
||||||
expect(sent).toMatchObject({ text: '给我一点提示', intent: 'suggestions', references: [], sourceConversationId: 'c' });
|
expect(sent).toMatchObject({ text: '继续看看👀', intent: 'suggestions', references: [], sourceConversationId: 'c' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(['failed', 'cancelled', 'interrupted'] as const)('retries a %s suggestion request without leaking raw JSON or reusing its accepted request id', async status => {
|
it.each(['failed', 'cancelled', 'interrupted'] as const)('retries a %s suggestion request without leaking raw JSON or reusing its accepted request id', async status => {
|
||||||
@@ -501,7 +502,7 @@ describe('teacher side chat', () => {
|
|||||||
await screen.findByText('这次没有看清楚,请再试一次。');
|
await screen.findByText('这次没有看清楚,请再试一次。');
|
||||||
expect(screen.queryByText('{"intro":"未完成')).not.toBeInTheDocument();
|
expect(screen.queryByText('{"intro":"未完成')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: suggestedQuestions[0] })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: suggestedQuestions[0] })).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '继续看看👀', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: '.consultation-retry' }));
|
||||||
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
||||||
expect(api.send.mock.calls[0][2]).toMatchObject({ intent: 'suggestions' });
|
expect(api.send.mock.calls[0][2]).toMatchObject({ intent: 'suggestions' });
|
||||||
expect(api.send.mock.calls[0][2].requestId).not.toBe(failed.id);
|
expect(api.send.mock.calls[0][2].requestId).not.toBe(failed.id);
|
||||||
@@ -513,9 +514,9 @@ describe('teacher side chat', () => {
|
|||||||
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
|
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
|
||||||
}));
|
}));
|
||||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||||
await screen.findByText('响应中途断开');
|
await screen.findByText('响应中途断开');
|
||||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
|
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
|
||||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||||
@@ -529,10 +530,10 @@ describe('teacher side chat', () => {
|
|||||||
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(recovered) })));
|
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(recovered) })));
|
||||||
}
|
}
|
||||||
await screen.findByText('智能体已接到,但这次回复失败了');
|
await screen.findByText('智能体已接到,但这次回复失败了');
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '继续看看👀', exact: true })).toBeEnabled());
|
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: '.consultation-retry' })).toBeEnabled());
|
||||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我的自由提问继续留着');
|
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我的自由提问继续留着');
|
||||||
expect(api.send).toHaveBeenCalledTimes(1);
|
expect(api.send).toHaveBeenCalledTimes(1);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '继续看看👀', exact: true }));
|
fireEvent.click(screen.getByText('继续看看👀', { selector: '.consultation-retry' }));
|
||||||
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
||||||
expect(api.send).toHaveBeenCalledTimes(2);
|
expect(api.send).toHaveBeenCalledTimes(2);
|
||||||
expect(api.send.mock.calls[1][2].requestId).not.toBe(originalRequest.requestId);
|
expect(api.send.mock.calls[1][2].requestId).not.toBe(originalRequest.requestId);
|
||||||
@@ -555,7 +556,7 @@ describe('teacher side chat', () => {
|
|||||||
expect(api.send.mock.calls[0][2].intent).toBeUndefined();
|
expect(api.send.mock.calls[0][2].intent).toBeUndefined();
|
||||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||||
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled();
|
expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled();
|
||||||
view.unmount();
|
view.unmount();
|
||||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||||
@@ -643,7 +644,7 @@ describe('teacher side chat', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
|
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
|
||||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('运营配置的预览问题');
|
expect(screen.getByLabelText('向智能体提问')).toHaveValue('运营配置的预览问题');
|
||||||
expect(api.send).not.toHaveBeenCalled();
|
expect(api.send).not.toHaveBeenCalled();
|
||||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '智能体新话题' }));
|
fireEvent.click(screen.getByRole('button', { name: '智能体新话题' }));
|
||||||
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码', undefined));
|
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码', undefined));
|
||||||
});
|
});
|
||||||
@@ -778,10 +779,10 @@ describe('merged cloud teacher classroom', () => {
|
|||||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||||
await waitFor(() => expect(screen.getByText('智能体暂未开放,历史仍可查看。')).toBeVisible());
|
await waitFor(() => expect(screen.getByText('智能体暂未开放,历史仍可查看。')).toBeVisible());
|
||||||
const input = screen.getByLabelText('向智能体提问');
|
const input = screen.getByLabelText('向智能体提问');
|
||||||
expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeDisabled();
|
expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeDisabled();
|
||||||
fireEvent.change(input, { target: { value: '继续讨论' } });
|
fireEvent.change(input, { target: { value: '继续讨论' } });
|
||||||
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
|
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
|
||||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||||
fireEvent.keyDown(input, { key: 'Enter' });
|
fireEvent.keyDown(input, { key: 'Enter' });
|
||||||
expect(api.send).not.toHaveBeenCalled();
|
expect(api.send).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user