fix(ui): clarify teacher help prompt and fit narrow panels

This commit is contained in:
鲨鱼辣椒
2026-09-29 11:57:29 +08:00
parent 3f5d1a8b65
commit e4b99d5e32
6 changed files with 56 additions and 40 deletions

View File

@@ -107,3 +107,18 @@
- Actual loaded Renderer is `index-DARbkmvA.js`, containing the new label and no “继续看看👀”. Route remains `#/chat`; all 10 persisted teacher drafts are unchanged. The trial was already hidden and signed out before this edit (pre-refresh persisted user/access token absent and refresh unavailable); it stays hidden and signed out, with `/api/auth/me` returning 401. No login was fabricated or restored, and no live teacher request was sent. The component behavior was checked in headless fixtures instead.
- Focus monitor recorded only the existing foreground WeChat process, no trial activation. Temporary inspector closed after verification. Formal installation remains untouched. This label follow-up is committed locally; no additional remote publication requested this turn.
- Required document structure passes; inherited drift diagnostic is still limited to the same five source task records imported by the prior merge, not new foreign edits. Retain this checkout for the live trial build.
## Follow-up: Explicit Unsure-What-To-Ask Button Copy
- User refines the same two button labels to exactly “不知道问什么,你先帮我看看👀”. Continue this owned UI/trial follow-up without subagents, foreground activation or remote publication.
- Project Context Loaded: official same-task start/status fields verified; own record/current peer scopes rechecked and unchanged entry/project/teacher context reused. Formal signed-app and unspecified language-skill peers do not conflict with this bounded copy change. Gate Passed.
- Plan: change labels and new help request text, preserving historical fixtures/pending payloads; update existing expectations, verify narrow/wide wrapping and stable send-button position, make only necessary layout accommodation, run focused validation/build and refresh the temporary Renderer with state protection.
### Longer Button Copy Outcome And Verification
- Both help entries and fresh suggestion text now use exactly “不知道问什么,你先帮我看看👀”. Preserve previous payloads in legacy fixtures/history and all request/intent/draft safeguards. Narrow buttons wrap after the comma with the second phrase and emoji kept together; the composer action row reserves 68px so hiding the two-line button while typing does not move Send. Existing warm-yellow styling remains.
- Initial narrow-layout regression exposed an 11px Send shift with two-line text. Reserved action height resolves it without changing dispatch. All 71 focused UI tests, typecheck, changed-file ESLint and final production build pass. All 3 targeted headless layout cases pass at 319px/508px, including clipping, position, click payload and draft preservation; inspected narrow screenshot confirms balanced semantic wrapping and attached emoji.
- Refreshed only the existing trial Renderer (Main remains PID 62668). Preflight found empty textareas, no stop controls and an idle Pi worker; saved current UI state/build backups under `help-long-label-update-20260929/`. Actual loaded `index-CaxXl4aZ.js` contains the full new copy. Current route and authenticated identity preserved, all 11 pre-refresh teacher drafts unchanged, window remains visible/unfocused. Consultation pane closes on page refresh; no extra teacher/operation model request was made to reopen it.
- Temporary inspector closed. No formal-app change, model/cloud configuration publication or remote push. Required doc structure and whitespace checks pass; same inherited five foreign-source doc diagnostics remain from the original integration base. Retain checkout for running trial.

View File

@@ -174,9 +174,9 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
- 主动发言采用紧贴小头像的短气泡;只有存在真实主动消息或运营欢迎语时才显示这组浮层,收起消息时头像和气泡一起消失,顶部入口仍保留。长消息在气泡中最多显示三行,点击接回原智能体对话查看全文。不提供“智能体偶尔来看看”开关或本地模拟巡看控制,既有自动跟进与真实消息接收逻辑保留。作品原生预览对整组气泡和头像测量避让,避免图片被原生页面遮住。
- 新的单会话入口停止旧版前端定时跟进派发;项目主动观察由独立功能衔接,不能重新创建可见话题。未下发目录的旧版入口仍保留原跟进合同:前端每 5 分钟请求一次智能体跟进;窗口隐藏、来源归档、主对话正在执行、智能体正在回复或学生正在智能体栏写草稿时延后。Main 再检查在线启停、来源归属、项目级冷却和已完成文本指纹,未变上下文按下述十五分钟冷却处理。跟进沿用运营模型、已发布 Skills 与当前智能体话题,生成符合所选配置的具体建议或引导;以主动智能体发言持久化,不伪造学生消息。模型调用沿用智能体计费规则。
- 进入项目时,智能体头像旁先显示运营发布的欢迎语气泡,不调用模型、不声称已检查项目;点击智能体进入咨询时立即收起欢迎语,并按账号记住已查看,关闭、重新打开、切换项目或刷新后不重复弹出;主动收起也保留同样记录。真实的未读主动建议优先替换欢迎语,打开咨询不会自动将这些消息标为已读。主动发言在头像旁直接显示气泡,不抢焦点、不盖住作品;作品区按气泡与原生预览的实际交叠范围避让,已有工具栏高度计入计算,气泡位于独立智能体栏时不额外留白,窗口/分栏尺寸变化和收起气泡时实时归还空间;“和智能体聊聊”接回同一段咨询,“等会儿聊”收起并保留历史。已有未处理气泡时不堆叠新邀请;咨询已展开时仍显示真实未读主动消息气泡,直到学生主动点击查看或收起,期间的其他回复不会吞掉未读邀请。五分钟检查计时在刷新和项目切换后保留;智能体咨询里的新讨论也算进展,即使操作对话没有变化也会继续跟进;上下文完全未变时放缓到十五分钟一次,不永久停止。事件连接丢失时,每十五秒只读同步未完成回复,收到结果后停止轮询,不重复调用模型。失败如实提示,不以本地文案替代模型回复。已接受请求恢复使用原身份,退出账号和来源删除仍沿用 Main 中止边界。
- 智能体支持自由提问;输入框为空时,底部显示暖黄色小按钮“帮我看看”,输入文字后收起,清空后重现,发送按钮保持原位。只有学生点击才调用模型,程序说明学生主动求助且尚未提出具体问题,智能体结合可用上下文按云端配置回应。点击快捷回复会主动开始讨论;回复下方不再附加固定的二次求助链接;失败或中断后,可点暖黄色小按钮“帮我看看”重试。这些快捷求助保留已有输入草稿,网络结果不确定时复用请求身份,已确认终态的请求不重复执行;解析失败可重新求助,不用固定问题伪装模型结果。服务端下发的推荐问题只填入草稿,由用户确认发送。界面不提供独立示范页面、“记一下”、共识或自动待办,智能体的回复下不再展示“我去试一试”和“复制”按钮,学生通过已有的操作对话/作品入口继续创作。
- 智能体支持自由提问;输入框为空时,底部显示暖黄色小按钮“不知道问什么,你先帮我看看👀”,输入文字后收起,清空后重现,发送按钮保持原位。只有学生点击才调用模型,程序说明学生主动求助且尚未提出具体问题,智能体结合可用上下文按云端配置回应。点击快捷回复会主动开始讨论;回复下方不再附加固定的二次求助链接;失败或中断后,可点暖黄色小按钮“不知道问什么,你先帮我看看👀”重试。这些快捷求助保留已有输入草稿,网络结果不确定时复用请求身份,已确认终态的请求不重复执行;解析失败可重新求助,不用固定问题伪装模型结果。服务端下发的推荐问题只填入草稿,由用户确认发送。界面不提供独立示范页面、“记一下”、共识或自动待办,智能体的回复下不再展示“我去试一试”和“复制”按钮,学生通过已有的操作对话/作品入口继续创作。
- 老师咨询只展示老师正文和可点击的快捷回复卡片,学生也可在“和老师聊聊”输入框自由输入。点击卡片原样发送该回复,保留当前输入草稿;旧话题的数据不会恢复想法板、结构图、流程图或对照表及其操作。
- 客户端只提供入口事实、可用上下文和界面协议。讨论切入点、教学方式、是否提问、正文长度、前缀和表达风格由所选智能体的云端配置决定;不在“帮我看看”、引导开口或后台检查时附加教学策略。当前新回复允许 0–3 条快捷回复,没有时用空数组,不要求凑满。
- 客户端只提供入口事实、可用上下文和界面协议。讨论切入点、教学方式、是否提问、正文长度、前缀和表达风格由所选智能体的云端配置决定;不在“不知道问什么,你先帮我看看👀”、引导开口或后台检查时附加教学策略。当前新回复允许 0–3 条快捷回复,没有时用空数组,不要求凑满。
- Main 对咨询回复统一使用 `{reply, quickReplies}`,本地读取工具的前言和云端非最终片段不当作最终回答;完整解析后展示正文和卡片,主动进展提醒仍直接展示正文。普通 Markdown、JSON 数据和代码示例保持正文。兼容读取旧 `{intro, questions}` 和带 `tool` 的回复,只提取正文与快捷回复,不恢复或更新组件。新输出超过 3 条快捷回复时保留完整正文、提示格式错误并保留原始回答,不展示超额卡片或静默截取;历史中已有的更多卡片在读取与后续保存时仍原样保留。独立资源上限只用于防护,不做旧式字数截断;格式错误的原文默认折叠、字面显示,不再次送进模型。
- 回复中的引号或换行格式有误时,仅在正文边界明确、修复后整体合法的情况下恢复完整文字,不把半句话当成完整回答。附加建议损坏时仍展示完整正文;正文无法确认完整时显示“这次回复未能完整显示”,由学生点击“重新回答”,沿用原问题、引用与来源;持续聊天须回到原项目和操作对话后重试,新一轮按当前发布版本执行,旧话题仍使用绑定版本,保留当前草稿,连续点击不重复发送。打开聊天或旧话题会从保留的原文中在本地恢复可确认的正文,不调用模型,不因显示修复而改写磁盘记录;原始内容继续折叠保留。不完整的回答和建议不进入后续模型上下文。
- 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按本次问题/工具调用身份合并名称和状态,跨云端暂停、续接仍只计一次;暂停读取不算失败,状态以实际读取结果或问题终态为准,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。

View File

@@ -471,7 +471,7 @@ export function TeacherChatPanel({
(request) => request.status === 'preparing' || request.status === '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 removeReferences = () => { setReferences([]); persistDraft(text, [], pending.current); };
const questions = definition?.suggested_questions ?? [];
@@ -541,7 +541,7 @@ export function TeacherChatPanel({
{request.suggestedQuestions?.map((question, index) => <button key={`${request.id}:${index}`} type="button" disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-follow-up"><span className="min-w-0 flex-1">{question}</span><ChevronRight className="h-4 w-4 shrink-0 opacity-70" aria-hidden="true" /></button>)}
</div>
</>}
{['failed', 'cancelled', 'interrupted'].includes(request.status) && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-retry">帮我看看</button>}
{['failed', 'cancelled', 'interrupted'].includes(request.status) && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-retry"><span>不知道问什么,<span className="whitespace-nowrap">你先帮我看看👀</span></span></button>}
</> : request.response ? <TeacherReply text={request.response} /> : <div className="text-[13px] leading-7">{['running', 'preparing'].includes(request.status) ? <span role="status" className="inline-flex items-center gap-2 text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />{label}正在想…</span> : ['cancelled', 'interrupted'].includes(request.status) ? '这次先停在这里。想继续时可以再说说。' : ''}</div>}
{request.intent !== 'suggestions' && request.status === 'completed' && !request.replyIncomplete && Boolean(request.suggestedQuestions?.length) && <div className="consultation-follow-ups" aria-label="接着聊">
{request.suggestedQuestions?.map((question, index) => <button type="button" key={`${request.id}:${index}`} disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-follow-up"><span className="min-w-0 flex-1">{question}</span><ChevronRight className="h-4 w-4 shrink-0 opacity-70" aria-hidden="true" /></button>)}
@@ -583,9 +583,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}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); void send(); } }}
className="min-h-[60px] w-full resize-none border-0 bg-transparent text-[13px] leading-6 outline-none placeholder:text-muted-foreground/65" />
<div className="mt-2 flex min-h-11 items-center justify-between gap-2" data-testid="consultation-composer-actions">
<div className="mt-2 flex min-h-[68px] items-center justify-between gap-2" data-testid="consultation-composer-actions">
<div className="min-w-0 flex-1">
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint">帮我看看</button>}
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint"><span>不知道问什么,<span className="whitespace-nowrap">你先帮我看看👀</span></span></button>}
</div>
{running ? <button type="button" className="rounded-lg border px-3 py-2 text-xs" onClick={() => void teacherApi.cancel(topicBase, endpointId, running.id).then(consume).catch(() => setError('暂时无法停止,请稍后重试。'))}>停止回复</button>
: <button type="submit" aria-label="提问" title={busy ? '正在发送…' : '发送'} disabled={busy || !topicEnabled || !text.trim()} className="consultation-send flex h-8 w-8 items-center justify-center rounded-full text-white disabled:opacity-30"><ArrowUp className="h-4 w-4" /></button>}

View File

@@ -1869,7 +1869,7 @@ test('project consultations preserve student drafts and switch between work and
await expect(page.getByRole('button', { name: '与朋友聊天' })).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('#coding-consultation-dock')).toHaveCSS('width', '508px');
await expect(teacher.locator('form .consultation-hint')).toBeVisible();
await expect(teacher.locator('form .consultation-hint')).toHaveText('帮我看看');
await expect(teacher.locator('form .consultation-hint')).toHaveText('不知道问什么,你先帮我看看👀');
await expect(teacher.getByText('把你的困惑说出来就好')).toHaveCount(0);
await expect(teacher.getByText('当前项目',{exact:true})).toHaveCount(0);
await expect(teacher.getByRole('button',{name:'解释当前代码',exact:true})).toBeVisible();
@@ -1924,7 +1924,7 @@ test('project consultations preserve student drafts and switch between work and
expect(request.body).not.toHaveProperty('discussion');
}
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?.intent).toBeUndefined();
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});

View File

@@ -29,7 +29,7 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
const panel = page.getByTestId('teacher-chat-panel');
const retry = panel.locator('.consultation-retry');
await expect(retry).toBeEnabled();
await expect(retry).toHaveText('帮我看看');
await expect(retry).toHaveText('不知道问什么,你先帮我看看👀');
expect(await retry.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
const [panelBounds, retryBounds] = await Promise.all([panel.boundingBox(), retry.boundingBox()]);
expect(retryBounds!.x).toBeGreaterThanOrEqual(panelBounds!.x);
@@ -41,7 +41,7 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
await expect(input).toHaveValue('我还有一点自己的想法');
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs)).toEqual([
{ requestId: expect.any(String), text: '帮我看看', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' },
{ requestId: expect.any(String), text: '不知道问什么,你先帮我看看👀', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' },
]);
}
});
@@ -89,7 +89,8 @@ for (const width of [319, 508]) {
const send = page.getByRole('button', { name: '提问', exact: true });
const sent = () => page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
await expect(hint).toBeEnabled();
await expect(hint).toHaveText('帮我看看');
await expect(hint).toHaveText('不知道问什么,你先帮我看看👀');
expect(await hint.evaluate(el => el.scrollWidth <= el.clientWidth && el.scrollHeight <= el.clientHeight)).toBe(true);
await expect(hint).toHaveCSS('background-color', 'rgb(255, 242, 201)');
const initialSend = (await send.boundingBox())!;
const hintBounds = (await hint.boundingBox())!;
@@ -109,9 +110,9 @@ for (const width of [319, 508]) {
expect(await sent()).toEqual([]);
await hint.click();
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
expect(await sent()).toEqual([{ requestId: expect.any(String), text: '帮我看看', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' }]);
expect(await sent()).toEqual([{ requestId: expect.any(String), text: '不知道问什么,你先帮我看看👀', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' }]);
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: '可以从哪个小地方开始?' });
await expect(card).toBeVisible();
await input.fill('留着我自己的草稿');

View File

@@ -496,13 +496,13 @@ describe('teacher side chat', () => {
}));
render(<TeacherChatPanel projectId="p" sourceId="c" />);
const input = screen.getByLabelText('向智能体提问');
const help = () => screen.getByText('帮我看看', { selector: 'form .consultation-hint' });
const help = () => screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' });
await waitFor(() => expect(help()).toBeEnabled());
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(help());
expect(help()).toHaveAccessibleName('帮我看看');
expect(help()).toHaveAccessibleName('不知道问什么,你先帮我看看👀');
expect(api.send).not.toHaveBeenCalled();
fireEvent.change(input, { target: { value: '我先自己想想' } });
expect(screen.queryByText('帮我看看', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(api.send).not.toHaveBeenCalled();
fireEvent.change(input, { target: { value: ' ' } });
expect(help()).toBeEnabled();
@@ -517,11 +517,11 @@ describe('teacher side chat', () => {
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
expect(api.send).toHaveBeenCalledTimes(1);
expect(api.send).toHaveBeenCalledWith('p/agent-topics', 'first', expect.objectContaining({
intent: 'suggestions', text: '帮我看看', sourceConversationId: 'c',
intent: 'suggestions', text: '不知道问什么,你先帮我看看👀', sourceConversationId: 'c',
}));
expect(help()).toBeDisabled();
fireEvent.change(input, { target: { value: '我自己还想问的另一件事' } });
expect(screen.queryByText('帮我看看', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
await waitFor(() => expect(streams.has('first')).toBe(true));
const sent = api.send.mock.calls[0][2] as TeacherSend;
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({
@@ -532,7 +532,7 @@ describe('teacher side chat', () => {
expect(screen.queryByText('我看看你最近做到了哪里…')).not.toBeInTheDocument();
for (const question of suggestedQuestions) expect(screen.getByRole('button', { name: question })).toBeEnabled();
expect(screen.queryByRole('button', { name: '我也说不清,你带我看看', exact: true })).not.toBeInTheDocument();
expect(screen.queryByText('帮我看看', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(api.send).toHaveBeenCalledTimes(1);
expect(input).toHaveValue('我自己还想问的另一件事');
});
@@ -567,8 +567,8 @@ describe('teacher side chat', () => {
it('keeps an uncertain suggestions request identity through reopening and draft edits', async () => {
api.send.mockRejectedValue(new Error('网络暂不可用'));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeEnabled());
fireEvent.click(screen.getByText('帮我看看', { selector: 'form .consultation-hint' }));
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
await screen.findByText('网络暂不可用');
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
@@ -578,10 +578,10 @@ describe('teacher side chat', () => {
render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByLabelText('向智能体提问')).toBeEnabled());
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还没写完的问题');
expect(screen.queryByText('帮我看看', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
fireEvent.click(screen.getByText('帮我看看', { selector: 'form .consultation-hint' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
await screen.findByText('网络暂不可用');
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
@@ -592,16 +592,16 @@ describe('teacher side chat', () => {
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
api.send.mockRejectedValue(new Error('网络暂不可用'));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeEnabled());
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
expect(api.send).not.toHaveBeenCalled();
fireEvent.click(screen.getByText('帮我看看', { selector: 'form .consultation-hint' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
await screen.findByText('网络暂不可用');
expect(JSON.parse(JSON.stringify(api.send.mock.calls[0][2]))).toEqual(original);
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeEnabled());
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('帮我看看', { selector: 'form .consultation-hint' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
await screen.findByText('网络暂不可用');
expect(JSON.parse(JSON.stringify(api.send.mock.calls[1][2]))).toEqual(original);
});
@@ -626,13 +626,13 @@ describe('teacher side chat', () => {
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({ id: original.requestId, text: original.text, status })] });
api.send.mockResolvedValue({ ...first, revision: 2 });
render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeEnabled());
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
expect(api.send).not.toHaveBeenCalled();
fireEvent.click(screen.getByText('帮我看看', { selector: 'form .consultation-hint' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
const sent = api.send.mock.calls[0][2] as TeacherSend;
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 => {
@@ -645,7 +645,7 @@ describe('teacher side chat', () => {
await screen.findByText('这次没有看清楚,请再试一次。');
expect(screen.queryByText('{"intro":"未完成')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: suggestedQuestions[0] })).not.toBeInTheDocument();
fireEvent.click(screen.getByText('帮我看看', { selector: '.consultation-retry' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: '.consultation-retry' }));
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
expect(api.send.mock.calls[0][2]).toMatchObject({ intent: 'suggestions' });
expect(api.send.mock.calls[0][2].requestId).not.toBe(failed.id);
@@ -657,9 +657,9 @@ describe('teacher side chat', () => {
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
}));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeEnabled());
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
await waitFor(() => expect(streams.has('first')).toBe(true));
fireEvent.click(screen.getByText('帮我看看', { selector: 'form .consultation-hint' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
await screen.findByText('响应中途断开');
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
@@ -673,10 +673,10 @@ describe('teacher side chat', () => {
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(recovered) })));
}
await screen.findByText('智能体已接到,但这次回复失败了');
await waitFor(() => expect(screen.getByText('帮我看看', { selector: '.consultation-retry' })).toBeEnabled());
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: '.consultation-retry' })).toBeEnabled());
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我的自由提问继续留着');
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('帮我看看', { selector: '.consultation-retry' }));
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: '.consultation-retry' }));
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
expect(api.send).toHaveBeenCalledTimes(2);
expect(api.send.mock.calls[1][2].requestId).not.toBe(originalRequest.requestId);
@@ -699,7 +699,7 @@ describe('teacher side chat', () => {
expect(api.send.mock.calls[0][2].intent).toBeUndefined();
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeEnabled();
expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled();
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" />);
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
@@ -787,7 +787,7 @@ describe('teacher side chat', () => {
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
expect(screen.getByLabelText('向智能体提问')).toHaveValue('运营配置的预览问题');
expect(api.send).not.toHaveBeenCalled();
expect(screen.queryByText('帮我看看', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '智能体新话题' }));
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码', undefined));
});
@@ -911,7 +911,7 @@ describe('merged cloud teacher classroom', () => {
api.sendConversation.mockRejectedValue(new Error('结果暂未确认'));
const props = { projectId: 'p', sourceId: 'original-source', selectedAgent: agent };
const view = render(<TeacherChatPanel {...props} />);
const hint = () => screen.getByText('帮我看看', { selector: 'form .consultation-hint' });
const hint = () => screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' });
await waitFor(() => expect(hint()).toBeEnabled());
expect(api.sendConversation).not.toHaveBeenCalled();
fireEvent.click(hint());
@@ -984,10 +984,10 @@ describe('merged cloud teacher classroom', () => {
render(<TeacherChatPanel projectId="p" sourceId="c" />);
await waitFor(() => expect(screen.getByText('智能体暂未开放,历史仍可查看。')).toBeVisible());
const input = screen.getByLabelText('向智能体提问');
expect(screen.getByText('帮我看看', { selector: 'form .consultation-hint' })).toBeDisabled();
expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeDisabled();
fireEvent.change(input, { target: { value: '继续讨论' } });
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
expect(screen.queryByText('帮我看看', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
fireEvent.keyDown(input, { key: 'Enter' });
expect(api.send).not.toHaveBeenCalled();
});