Merge teacher composer and compact prompt UI into main
This commit is contained in:
commit
2b2d39449c
62 files changed
+2327
-158
No files matched your search
@@ -229,6 +229,7 @@ async function installCodingFirstChatHost(
|
||||
kind: 'message',
|
||||
id: 'message-user-e2e',
|
||||
sourceEntryId: 'entry-user-e2e',
|
||||
timestamp: Date.parse(now),
|
||||
role: 'user',
|
||||
status: 'complete',
|
||||
blocks: [{ kind: 'text', id: 'message-user-e2e:content:0', text: 'Durable user fork source', status: 'complete' }],
|
||||
@@ -585,7 +586,7 @@ async function installCodingFirstChatHost(
|
||||
if (action === 'messages' && method === 'POST') {
|
||||
const suggestions = body!.intent === 'suggestions';
|
||||
const response = suggestions ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
|
||||
consultationTopics[chatKey] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:new Date().toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
consultationTopics[chatKey] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:now,finishedAt:new Date(Date.parse(now)+40000).toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
const last = (consultationTopics[chatKey].requests as Record<string, unknown>[]).at(-1)!;
|
||||
Object.assign(last, consultationReply);
|
||||
if (state.nextTeacherReplyIncomplete) {
|
||||
@@ -1124,6 +1125,19 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
|
||||
await appendFile(performanceFragment, `${JSON.stringify({ editableMs })}\n`);
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('coding-message-composer')).toBeInViewport({ ratio: 1 });
|
||||
expect(await page.getByTestId('coding-message-composer').evaluate(el => innerHeight - el.getBoundingClientRect().bottom)).toBeLessThan(40);
|
||||
await expect(page.getByTestId('community-invitation')).toBeVisible();
|
||||
expect(await page.getByTestId('coding-message-composer').evaluate(el => {
|
||||
const body = document.querySelector('.coding-chat-body')!.getBoundingClientRect();
|
||||
const input = el.getBoundingClientRect();
|
||||
return Math.abs(input.left + input.width / 2 - body.left - body.width / 2);
|
||||
})).toBeLessThan(2);
|
||||
expect(await page.getByTestId('community-invitation').evaluate(el => {
|
||||
const composer = document.querySelector('[data-testid="coding-message-composer"]')!;
|
||||
return composer.getBoundingClientRect().top - el.getBoundingClientRect().bottom;
|
||||
})).toBeCloseTo(12, 0);
|
||||
|
||||
const pixelPng = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
'base64',
|
||||
@@ -1135,10 +1149,25 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
|
||||
buffer: largePng,
|
||||
});
|
||||
await composer.fill('Build the first PI scene');
|
||||
await electronApp.evaluate(({ shell }) => {
|
||||
const scope = globalThis as typeof globalThis & { __communityOpened?: string[] };
|
||||
scope.__communityOpened = [];
|
||||
shell.openExternal = async url => { scope.__communityOpened!.push(url); };
|
||||
});
|
||||
await page.getByRole('button', { name: '作品广场', exact: true }).click();
|
||||
await page.getByRole('button', { name: '去作品广场逛逛' }).click();
|
||||
await expect.poll(() => electronApp.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __communityOpened?: string[] }).__communityOpened
|
||||
))).toEqual(['https://square.nianxx.cn/#explore', 'https://square.nianxx.cn/#explore']);
|
||||
await expect(composer).toHaveValue('Build the first PI scene');
|
||||
await expect(page.getByText('large.png', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
expect((await readState(electronApp)).captured.filter(item => item.path.endsWith('/prompt') && item.method === 'POST')).toHaveLength(0);
|
||||
await expect(page.getByRole('button', { name: '发送' })).toBeEnabled();
|
||||
await page.getByTestId('coding-message-composer').evaluate(
|
||||
(form: HTMLFormElement) => form.requestSubmit(),
|
||||
);
|
||||
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
|
||||
|
||||
await expect.poll(async () => {
|
||||
const state = await readState(electronApp);
|
||||
@@ -1456,11 +1485,12 @@ test('Windows Code titlebar keeps browser actions clear of the logo and window c
|
||||
if (width === 1280 && zoom === 1) {
|
||||
await page.screenshot({ path: test.info().outputPath('windows-code-titlebar.png') });
|
||||
}
|
||||
await header.getByRole('button', { name: '打开开发浏览器' }).click();
|
||||
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
|
||||
await header.getByRole('button', { name: '展开老师对话栏' }).click();
|
||||
await expect(page.locator('#coding-consultation-dock')).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '操作对话' })).toHaveAttribute('aria-selected', 'true');
|
||||
await assertChromeClear();
|
||||
await header.getByRole('button', { name: '关闭开发浏览器' }).click();
|
||||
await expect(page.getByTestId('agent-browser-panel')).toHaveCount(0);
|
||||
await header.getByRole('button', { name: '收起老师对话栏' }).click();
|
||||
await expect(page.locator('#coding-consultation-dock')).toHaveCount(0);
|
||||
}
|
||||
await page.getByTestId('titlebar-sidebar-toggle').click();
|
||||
await assertChromeClear();
|
||||
@@ -1523,14 +1553,19 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
await expect(conversationHeader.getByRole('button', { name: '归档' })).toHaveCount(0);
|
||||
await expect(conversationHeader.getByRole('button', { name: '智能体设置' })).toHaveCount(0);
|
||||
await expect(conversationHeader.getByRole('button', { name: '打开编程工具' })).toHaveCount(0);
|
||||
const browserToggle = conversationHeader.getByRole('button', { name: '打开开发浏览器' });
|
||||
await expect(browserToggle).toBeVisible();
|
||||
const teacherToggle = conversationHeader.getByRole('button', { name: '展开老师对话栏' });
|
||||
await expect(teacherToggle).toBeVisible();
|
||||
if (process.platform === 'win32') {
|
||||
const browserBounds = await browserToggle.boundingBox();
|
||||
const browserBounds = await teacherToggle.boundingBox();
|
||||
expect(browserBounds).not.toBeNull();
|
||||
expect(browserBounds!.x + browserBounds!.width).toBeLessThanOrEqual(logoBounds!.x);
|
||||
}
|
||||
await browserToggle.click();
|
||||
await teacherToggle.click();
|
||||
await expect(page.locator('#coding-consultation-dock')).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '操作对话' })).toHaveAttribute('aria-selected', 'true');
|
||||
await conversationHeader.getByRole('button', { name: '收起老师对话栏' }).click();
|
||||
await expect(page.locator('#coding-consultation-dock')).toHaveCount(0);
|
||||
await page.getByRole('tab', { name: '作品', exact: true }).click();
|
||||
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
|
||||
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveCount(0);
|
||||
await expect.poll(async () => {
|
||||
@@ -1540,7 +1575,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
expect((await readState(electronApp)).captured.some(
|
||||
(request) => request.path === '/api/agent-browser/diagnostics',
|
||||
)).toBe(false);
|
||||
await page.getByRole('button', { name: '关闭开发浏览器' }).last().click();
|
||||
await page.getByRole('tab', { name: '操作对话' }).click();
|
||||
await expect(page.getByTestId('agent-browser-panel')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '项目设置' })).toHaveCount(1);
|
||||
await expect(page.getByTestId('project-conversations')).toBeVisible();
|
||||
@@ -1883,7 +1918,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();
|
||||
@@ -1894,6 +1929,11 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('');
|
||||
await teacher.locator('form .consultation-hint').click();
|
||||
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
|
||||
const sentTime = teacher.locator('time').nth(0), replyTime = teacher.locator('time').nth(1);
|
||||
await expect(sentTime).toHaveAttribute('aria-label', /^发送时间:/);
|
||||
await expect(replyTime).toHaveAttribute('aria-label', /^回复完成时间:/);
|
||||
const originalReplyTime = await replyTime.getAttribute('datetime');
|
||||
expect(Date.parse(originalReplyTime!) - Date.parse((await sentTime.getAttribute('datetime'))!)).toBe(40000);
|
||||
await expect(teacher.getByTestId('teacher-discussion')).toHaveCount(0);
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
||||
await expect(teacher.locator('form .consultation-hint')).toHaveCount(0);
|
||||
@@ -1938,13 +1978,14 @@ 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')});
|
||||
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();await expect(teacher).toHaveCount(0);
|
||||
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();
|
||||
await expect(page.getByText('先理解状态如何随点击变化,再修改代码。',{exact:true})).toBeVisible();
|
||||
await expect(teacher.locator('time').nth(1)).toHaveAttribute('datetime', originalReplyTime!);
|
||||
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
|
||||
} finally {await releaseSnapshot(electronApp);}
|
||||
});
|
||||
@@ -2238,6 +2279,49 @@ test('settled prompt releases the composer without a client receipt and keeps th
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
|
||||
test('teacher introductions collapse after three operation replies and remain available on hover', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
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(electronApp, connection, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await disableCodingEventSource(page);
|
||||
try {
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const introductions = page.getByTestId('teacher-welcomes');
|
||||
await expect(introductions.locator('.teacher-welcome-card')).toHaveCount(2);
|
||||
await expect(introductions).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await expect(introductions.getByText('来认识一下')).toHaveCount(0);
|
||||
await expect(introductions.getByRole('button', { name: '先自己试试' })).toHaveCount(0);
|
||||
const startedAt = await page.evaluate(() => Date.now());
|
||||
for (let round = 1; round <= 3; round++) {
|
||||
for (const status of ['running', 'idle'] as const) {
|
||||
const seq = round * 2 + (status === 'idle' ? 1 : 0);
|
||||
const snapshot = {
|
||||
schemaVersion: 1,
|
||||
conversation: { id: 'conversation-pi-first-chat', projectId: 'project-pi-first-chat', agentId: 'builder', title: '新对话', model: { model: { accountId: 'account-e2e', modelId: 'model-a', thinkingLevel: 'off' }, modelResolution: 'resolved' } },
|
||||
nodes: [], run: { status, mode: 'prompt', runId: `intro-round-${round}`, startedAt,
|
||||
...(status === 'idle' ? { terminalReason: 'completed', settledAt: startedAt + round } : {}) },
|
||||
worker: { status: 'ready', generation: 1 }, cursor: { workerGeneration: 1, seq },
|
||||
queue: { items: [] }, context: { usedTokens: 0, contextWindow: 4096, compaction: 'idle' }, pendingInteractions: [],
|
||||
};
|
||||
await emitCodingEvent(page, 'snapshot', { type: 'snapshot', conversationId: snapshot.conversation.id, workerGeneration: 1, seq, snapshot });
|
||||
if (round < 3 || status === 'running') await expect(introductions).toBeVisible();
|
||||
}
|
||||
}
|
||||
await expect(introductions).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).hover();
|
||||
await expect(page.getByTestId('teacher-introduction')).toContainText('一起发现代码里的小奥秘');
|
||||
await page.screenshot({ path: test.info().outputPath('teacher-introduction-after-three-rounds.png') });
|
||||
await page.reload();
|
||||
await expect(page.getByRole('button', { name: '与代码智能体聊天', exact: true })).toBeVisible();
|
||||
await expect(introductions).toHaveCount(0);
|
||||
expect((await readState(electronApp)).captured.filter(item => /\/(agent-topics|messages|teacher-check-in|prompt)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
|
||||
test('teacher avatar consumes the welcome bubble through close, reopen and reload without a model request', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
@@ -2250,9 +2334,9 @@ test('teacher avatar consumes the welcome bubble through close, reopen and reloa
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const teacherButton = page.getByRole('button', { name: '与代码智能体聊天', exact: true });
|
||||
const invitation = page.getByTestId('teacher-invitation');
|
||||
const invitation = page.locator('[data-testid="teacher-welcomes"] .teacher-welcome-card[data-agent-id="teacher-code"]');
|
||||
const teacher = page.getByTestId('teacher-chat-panel');
|
||||
await expect(invitation).toHaveAttribute('data-bubble-kind', 'welcome');
|
||||
await expect(invitation).toBeVisible();
|
||||
await teacherButton.click();
|
||||
await expect(teacher).toBeVisible();
|
||||
await expect(teacher.getByText('一起理解代码', { exact: true })).toBeVisible();
|
||||
@@ -2269,6 +2353,7 @@ test('teacher avatar consumes the welcome bubble through close, reopen and reloa
|
||||
await page.reload();
|
||||
await expect(teacherButton).toBeVisible();
|
||||
await expect(invitation).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '收起朋友的介绍' })).toBeVisible();
|
||||
expect((await readState(electronApp)).captured.filter(item => /\/(agent-topics|messages|teacher-check-in|prompt)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
@@ -2293,9 +2378,9 @@ test('teacher stays in the top-right header without legacy check-ins or taking o
|
||||
await expect(teacherButton.locator('img')).toHaveAttribute('src', /avatar-01/);
|
||||
await expect(page.getByRole('checkbox', { name: '智能体偶尔来看看' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('teacher-companion')).toHaveCount(1);
|
||||
const invitation = companion.getByTestId('teacher-invitation');
|
||||
await expect(invitation).toHaveAttribute('data-bubble-kind', 'welcome');
|
||||
await expect(invitation).toContainText('一起理解代码');
|
||||
const invitation = companion.locator('.teacher-welcome-card[data-agent-id="teacher-code"]');
|
||||
await expect(invitation).toBeVisible();
|
||||
await expect(invitation).toContainText('一起发现代码里的小奥秘,把想法变成作品。');
|
||||
await expect(invitation).toBeInViewport({ ratio: 1 });
|
||||
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(0);
|
||||
await page.screenshot({ path: test.info().outputPath('teacher-entry-greeting.png') });
|
||||
@@ -2325,27 +2410,28 @@ test('teacher stays in the top-right header without legacy check-ins or taking o
|
||||
expect(introductionViewportBounds!.y).toBeGreaterThanOrEqual(introductionBounds!.y + introductionBounds!.height);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(introduction).toHaveCount(0);
|
||||
await expect(invitation).toHaveAttribute('data-bubble-kind', 'welcome');
|
||||
await expect(invitation).toBeVisible();
|
||||
expect((await readState(electronApp)).captured.filter(item => /\/(messages|teacher-check-in)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
|
||||
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
|
||||
await composer.focus();
|
||||
await page.clock.fastForward(300_000);
|
||||
await expect(invitation).toContainText('一起理解代码');
|
||||
await expect(invitation).toHaveAttribute('data-bubble-kind', 'welcome');
|
||||
await expect(invitation).toContainText('一起发现代码里的小奥秘,把想法变成作品。');
|
||||
await expect(invitation).toBeVisible();
|
||||
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(0);
|
||||
await expect(invitation).toBeInViewport({ ratio: 1 });
|
||||
const presence = companion.getByTestId('teacher-presence');
|
||||
await expect(presence.getByRole('button', { name: '打开这条智能体消息' })).toBeVisible();
|
||||
await expect(presence.getByRole('button', { name: '打开这条智能体消息' }).locator('img')).toHaveAttribute('src', await teacherButton.locator('img').getAttribute('src') as string);
|
||||
const presence = companion.getByTestId('teacher-welcomes');
|
||||
await expect(presence.getByRole('button', { name: '认识代码智能体' })).toBeVisible();
|
||||
await expect(presence.getByRole('button', { name: '认识代码智能体' }).locator('img')).toHaveAttribute('src', await teacherButton.locator('img').getAttribute('src') as string);
|
||||
const [invitationBounds, companionBounds, avatarBounds, presenceBounds, speakerBounds] = await Promise.all([
|
||||
invitation.boundingBox(), companion.boundingBox(), teacherButton.locator('img').boundingBox(), presence.boundingBox(), presence.getByRole('button', { name: '打开这条智能体消息' }).boundingBox(),
|
||||
invitation.boundingBox(), companion.boundingBox(), teacherButton.locator('img').boundingBox(), presence.boundingBox(), presence.getByRole('button', { name: '认识代码智能体' }).boundingBox(),
|
||||
]);
|
||||
expect(invitationBounds).not.toBeNull();
|
||||
expect(companionBounds).not.toBeNull();
|
||||
expect(avatarBounds).not.toBeNull();
|
||||
expect(invitationBounds!.y).toBeGreaterThanOrEqual(avatarBounds!.y + avatarBounds!.height);
|
||||
expect(Math.abs(presenceBounds!.x + presenceBounds!.width - companionBounds!.x - companionBounds!.width)).toBeLessThanOrEqual(2);
|
||||
expect(speakerBounds!.x - invitationBounds!.x - invitationBounds!.width).toBeLessThanOrEqual(10);
|
||||
expect(speakerBounds!.x).toBeGreaterThanOrEqual(presenceBounds!.x);
|
||||
expect(speakerBounds!.x + speakerBounds!.width).toBeLessThan(invitationBounds!.x + invitationBounds!.width);
|
||||
await expect(composer).toBeFocused();
|
||||
await expect(composer).toHaveValue('我还在写自己的想法');
|
||||
await expect(page.getByRole('tab', { name: '操作对话', exact: true })).toHaveAttribute('aria-selected', 'true');
|
||||
@@ -2367,7 +2453,7 @@ test('teacher stays in the top-right header without legacy check-ins or taking o
|
||||
await expect(composer).toHaveValue('我还在写自己的想法');
|
||||
await page.clock.fastForward(300_000);
|
||||
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(0);
|
||||
await invitation.getByRole('button', { name: '和智能体聊聊' }).click();
|
||||
await invitation.getByRole('button', { name: '和代码智能体聊聊' }).click();
|
||||
await expect(invitation).toHaveCount(0);
|
||||
await expect(presence).toHaveCount(0);
|
||||
await expect(teacherButton).toBeVisible();
|
||||
|
||||
Reference in new issue
Block a user