Merge reply recovery and simplified consultation UI into ongoing chats
This commit is contained in:
commit
d032425521
37 files changed
+2089
-1940
No files matched your search
@@ -22,6 +22,7 @@ type HostConnection = {
|
||||
};
|
||||
|
||||
type TeacherCatalogFixture = {
|
||||
nextTeacherReplyIncomplete?: boolean;
|
||||
teacherCatalog: TeacherCatalog;
|
||||
teacherCatalogFailure: boolean;
|
||||
holdTeacherCatalog: boolean;
|
||||
@@ -590,8 +591,14 @@ async function installCodingFirstChatHost(
|
||||
}
|
||||
if (path.endsWith(`${topicBase}/${topicId}/messages`) && method === 'POST') {
|
||||
const suggestionRequest = body!.intent === 'suggestions';
|
||||
const teacherResponse = suggestionRequest ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
|
||||
const teacherResponse = suggestionRequest ? '我们可以从你最近试过的地方聊起。' : '先理解状态如何随点击变化,再修改代码。';
|
||||
consultationTopics[consultationPath]={...currentTopic,revision:Number(currentTopic?.revision??0)+1,requests:[...(currentTopic?.requests as unknown[]??[]),{id:body!.requestId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response:teacherResponse,...(suggestionRequest?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
if (state.nextTeacherReplyIncomplete) {
|
||||
state.nextTeacherReplyIncomplete = false;
|
||||
const reply = (consultationTopics[consultationPath].requests as Record<string, unknown>[]).at(-1)!;
|
||||
Object.assign(reply, { response: '它早就不是', replyIncomplete: true, replyParseError: '格式不完整',
|
||||
sourceConversationId: body!.sourceConversationId, unparsedResponse: '{"reply":"它早就不是"刚搭好架子' });
|
||||
}
|
||||
return respond(consultationTopics[consultationPath],202);
|
||||
}
|
||||
if (path.endsWith(`${topicBase}/${topicId}`)) return respond(currentTopic);
|
||||
@@ -1846,14 +1853,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: '与朋友聊天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
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('当前项目',{exact:true})).toHaveCount(0);
|
||||
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);
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
||||
await teacher.getByRole('button',{name:'帮我看看',exact:true}).click();
|
||||
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);
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('');
|
||||
await teacher.locator('form .consultation-hint').click();
|
||||
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
|
||||
await expect(teacher.getByTestId('teacher-discussion')).toHaveCount(0);
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
||||
await expect(teacher.locator('form .consultation-hint')).toHaveCount(0);
|
||||
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
|
||||
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
|
||||
await teacher.getByRole('button',{name:'怎样观察别人玩游戏?',exact:true}).click();
|
||||
@@ -1863,8 +1877,7 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await expect(teacher.getByRole('button',{name:'我去试一试'})).toHaveCount(0);
|
||||
await expect(teacher.getByRole('button',{name:'复制',exact:true})).toHaveCount(0);
|
||||
await expect(composer).toHaveValue('保留我的草稿');
|
||||
await teacher.getByRole('button',{name:'我也说不清,你带我看看',exact:true}).click();
|
||||
await expect(teacher.getByText('你最近做的哪一步,让你停下来想了一会儿?')).toBeVisible();
|
||||
await expect(teacher.getByRole('button',{name:'我也说不清,你带我看看',exact:true})).toHaveCount(0);
|
||||
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
|
||||
await page.getByRole('tab',{name:'操作对话',exact:true}).click();
|
||||
await expect(composer).toBeVisible();
|
||||
@@ -1886,16 +1899,19 @@ test('project consultations preserve student drafts and switch between work and
|
||||
const requests=(await readState(electronApp)).captured;
|
||||
expect(requests.filter(item=>item.path.endsWith('/prompt')&&item.method==='POST')).toHaveLength(0);
|
||||
const consultationRequests=requests.filter(item=>item.path.endsWith('/messages')&&item.method==='POST');
|
||||
expect(consultationRequests).toHaveLength(3);
|
||||
expect(consultationRequests).toHaveLength(2);
|
||||
expect(requests.filter(item => item.path.endsWith('/agent-topics') && item.method === 'POST')).toHaveLength(0);
|
||||
expect(consultationRequests.every(item => item.path === '/api/coding/agent-conversations/teacher-algorithm/messages')).toBe(true);
|
||||
await expect(teacher.getByRole('button', { name: '智能体新话题' })).toHaveCount(0);
|
||||
await expect(teacher.getByRole('combobox')).toHaveCount(0);
|
||||
for (const request of consultationRequests) {
|
||||
expect(request.body).not.toHaveProperty('presentation');
|
||||
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();
|
||||
expect(consultationRequests[2].body).toMatchObject({intent:'guided-help',text:'我也说不清,你带我看看'});
|
||||
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
|
||||
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();await expect(teacher).toHaveCount(0);
|
||||
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();
|
||||
@@ -1905,6 +1921,50 @@ test('project consultations preserve student drafts and switch between work and
|
||||
});
|
||||
|
||||
|
||||
test('incomplete consultation replies retry only on click and preserve both drafts', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(app);
|
||||
const connection = await page.evaluate(async () => ({
|
||||
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
|
||||
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
|
||||
}));
|
||||
await installCodingFirstChatHost(app, connection, true);
|
||||
await settleSnapshot(app);
|
||||
await disableCodingEventSource(page);
|
||||
await app.evaluate(() => {
|
||||
(globalThis as typeof globalThis & { __makelorePiFirstChatE2E: TeacherCatalogFixture })
|
||||
.__makelorePiFirstChatE2E.nextTeacherReplyIncomplete = true;
|
||||
});
|
||||
try {
|
||||
await page.reload();
|
||||
page = await getStableWindow(app);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const mainDraft = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
|
||||
await mainDraft.fill('主对话草稿');
|
||||
await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).click();
|
||||
const teacher = page.getByTestId('teacher-chat-panel');
|
||||
const draft = teacher.getByRole('textbox', { name: '向智能体提问' });
|
||||
await draft.fill('现在项目是什么阶段?');
|
||||
await teacher.getByRole('button', { name: '提问', exact: true }).click();
|
||||
await expect(teacher.getByRole('status')).toHaveText('这次回复未能完整显示');
|
||||
await expect(teacher.getByTestId('teacher-reply')).toHaveCount(0);
|
||||
await expect(teacher.getByText('查看收到的原始内容').locator('..')).not.toHaveAttribute('open');
|
||||
const sends = async () => (await readState(app)).captured.filter(item => item.path.endsWith('/messages') && item.method === 'POST');
|
||||
expect(await sends()).toHaveLength(1);
|
||||
await draft.fill('咨询里还没写完的草稿');
|
||||
await teacher.getByRole('button', { name: '重新回答' }).click();
|
||||
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
|
||||
await expect(draft).toHaveValue('咨询里还没写完的草稿');
|
||||
await expect(mainDraft).toHaveValue('主对话草稿');
|
||||
const inputs = await sends();
|
||||
expect(inputs).toHaveLength(2);
|
||||
expect(inputs[1].path).toBe(inputs[0].path);
|
||||
expect(inputs[1].body).toEqual({ ...inputs[0].body, requestId: expect.any(String) });
|
||||
expect(inputs[1].body!.requestId).not.toBe(inputs[0].body!.requestId);
|
||||
} finally { await releaseSnapshot(app); }
|
||||
});
|
||||
|
||||
test('manual agent refresh updates the catalog and retries failures without replacing the ongoing chat or drafts', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
@@ -2199,6 +2259,10 @@ test('work tab prepares the page automatically and keeps the student draft throu
|
||||
await expect(page.getByText('正在打开你的作品…', { exact: true })).toBeVisible();
|
||||
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workReady: boolean } }).__makelorePiFirstChatE2E.workReady = true; });
|
||||
await expect(page.getByRole('textbox', { name: '网页地址' })).toHaveValue('http://127.0.0.1:4173/');
|
||||
await expect(page.getByTestId('agent-browser-panel')).toHaveAccessibleName('作品预览');
|
||||
await expect(page.getByRole('heading', { name: '作品预览', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '收起作品', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByTestId('agent-browser-viewport')).toBeVisible();
|
||||
await expect(page.getByText('正在打开你的作品…', { exact: true })).toHaveCount(0);
|
||||
// A manual address open must not permanently disable recovery checks.
|
||||
await page.getByRole('textbox', { name: '网页地址' }).fill('http://127.0.0.1:4173/');
|
||||
|
||||
Reference in new issue
Block a user