feat(ui): introduce each teacher with an individual welcome bubble
This commit is contained in:
@@ -18,6 +18,8 @@ export function Layout() {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [refreshStatus, setRefreshStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [selected, setSelected] = useState(0);
|
||||
const welcomeMode = new URLSearchParams(location.search).has('welcomes');
|
||||
const [dismissed, setDismissed] = useState<string[]>([]);
|
||||
const agents = Array.from({ length: count }, (_, index) => ({
|
||||
teacher_id: `agent-${index}`, version: index + 1, is_default: index === 0,
|
||||
definition: { config_id: `agent-${index}`, name: `智能体${index + 1}`, avatar_id: 'avatar-06',
|
||||
@@ -27,8 +29,11 @@ export function Layout() {
|
||||
const companion = {
|
||||
agents, unreadAgentIds: [], selectedAgent: agents[selected],
|
||||
definition: agents[selected]?.definition ?? { name: '编程老师', avatar_id: 'avatar-06' },
|
||||
invitation: visible ? { response: long ? '你刚才说想做一个宠物游戏。我们一起想想,什么样的小动作,会让你愿意一直陪它玩下去?可以先挑一个最想试的想法。' : '有什么编程问题想一起讨论?' } : null,
|
||||
invitation: !welcomeMode && visible ? { response: long ? '你刚才说想做一个宠物游戏。我们一起想想,什么样的小动作,会让你愿意一直陪它玩下去?可以先挑一个最想试的想法。' : '有什么编程问题想一起讨论?' } : null,
|
||||
welcome: null, enabled: true, loading: false, checking: false, error: null,
|
||||
welcomes: welcomeMode && !open ? agents.flatMap((agent, index) => dismissed.includes(agent.teacher_id) ? [] : [{ agentId: agent.teacher_id, definition: agent.definition, text: agent.definition.description, tone: index % 3 }]) : [],
|
||||
dismissWelcome: (id: string) => setDismissed(ids => [...new Set([...ids, id])]),
|
||||
dismissWelcomes: () => setDismissed(agents.map(agent => agent.teacher_id)),
|
||||
refreshing, refreshStatus,
|
||||
refresh: async () => {
|
||||
setRefreshing(true);
|
||||
|
||||
@@ -2186,9 +2186,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();
|
||||
@@ -2205,6 +2205,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); }
|
||||
});
|
||||
@@ -2229,9 +2230,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') });
|
||||
@@ -2261,27 +2262,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');
|
||||
@@ -2303,7 +2305,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();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
|
||||
|
||||
let server: ViteDevServer;
|
||||
let baseUrl: string;
|
||||
test.use({ channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
||||
test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
||||
test.beforeAll(async () => {
|
||||
server = await createServer({
|
||||
configFile: false,
|
||||
@@ -35,6 +35,50 @@ test.beforeAll(async () => {
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
|
||||
for (const width of [375, 768, 1280]) {
|
||||
test(`all teacher introductions fit at ${width}px and keep the native preview clear`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 800 });
|
||||
await page.goto(baseUrl + '?agents=3&welcomes');
|
||||
const group = page.getByTestId('teacher-welcomes');
|
||||
await expect(group.locator('.teacher-welcome-card')).toHaveCount(3);
|
||||
await expect(group).toBeInViewport({ ratio: 1 });
|
||||
for (const card of await group.locator('.teacher-welcome-card').all()) {
|
||||
await expect(card).toBeInViewport({ ratio: 1 });
|
||||
expect(await card.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
|
||||
}
|
||||
await expect.poll(async () => {
|
||||
const [bubble, viewport] = await Promise.all([group.boundingBox(), page.getByTestId('agent-browser-viewport').boundingBox()]);
|
||||
return !!bubble && !!viewport && viewport.y >= bubble.y + bubble.height;
|
||||
}).toBe(true);
|
||||
await page.screenshot({ path: test.info().outputPath(`teacher-introductions-${width}.png`) });
|
||||
await group.getByRole('button', { name: '收起智能体2的介绍' }).click();
|
||||
await expect(group.locator('.teacher-welcome-card')).toHaveCount(2);
|
||||
await group.getByRole('button', { name: '和智能体3聊聊' }).click();
|
||||
await expect(page.getByRole('button', { name: '与智能体3聊天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(group).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '关闭智能体栏' }).click();
|
||||
await expect(group.locator('.teacher-welcome-card')).toHaveCount(1);
|
||||
await group.getByRole('button', { name: '先自己试试' }).click();
|
||||
await expect(group).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
test('many teacher introductions remain scrollable and honor reduced motion', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 520, height: 600 });
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(baseUrl + '?agents=8&welcomes');
|
||||
const group = page.getByTestId('teacher-welcomes');
|
||||
await expect(group).toBeInViewport({ ratio: 1 });
|
||||
await expect(group.locator('.teacher-welcome-card')).toHaveCount(8);
|
||||
await expect(group.locator('.teacher-welcome-card').first()).toHaveCSS('animation-name', 'none');
|
||||
const last = group.getByRole('button', { name: '和智能体8聊聊' });
|
||||
await last.scrollIntoViewIfNeeded();
|
||||
await expect(last).toBeInViewport({ ratio: 1 });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(520);
|
||||
await group.getByRole('button', { name: '先自己试试' }).click();
|
||||
await expect(group).toHaveCount(0);
|
||||
});
|
||||
|
||||
for (const width of [520, 1000]) {
|
||||
test(`distributed agent tabs scroll inside the title bar at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 800 });
|
||||
|
||||
Reference in New Issue
Block a user