Merge teacher composer height alignment

This commit is contained in:
鲨鱼辣椒
2026-09-30 00:27:06 +08:00
7 changed files with 158 additions and 13 deletions

View File

@@ -1,16 +1,36 @@
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { fixtureAgent } from './teacher-reply-api';
import { CodingComposer } from '../../../src/pages/Chat/CodingComposer';
import { TeacherChatPanel } from '../../../src/pages/Chat/TeacherChatPanel';
import '../../../src/styles/globals.css';
import '../../../src/pages/Chat/classroom-workspace.css';
import 'katex/dist/katex.min.css';
const chat = new URLSearchParams(location.search).get('chat');
const params = new URLSearchParams(location.search);
const chat = params.get('chat');
const continuous = chat !== 'legacy';
const width = Number(new URLSearchParams(location.search).get('width')) || 508;
const width = Number(params.get('width')) || 508;
const compareComposers = params.get('compareComposers') === '1';
export function OperationComposerComparison() {
const [draft, setDraft] = useState('');
return <main className="classroom-main flex min-h-0 min-w-0 flex-1 flex-col">
<div className="min-h-0 flex-1 p-8"><h1 className="text-lg">我的小狗世界</h1><p className="mt-4 text-sm text-muted-foreground">操作 Agent</p></div>
<div className="classroom-composer-view shrink-0">
<CodingComposer value={draft} onChange={setDraft} editable canSend={Boolean(draft.trim())}
preparing={false} recovering={false} runStatus="idle" mode="prompt" queue={{ items: [] }}
error={null} recoverableError={false} attachments={[]} submitting={false}
placeholder="告诉操作 Agent 你的想法" voiceScopeKey="composer-layout-fixture"
onModeChange={() => {}} onSubmit={() => setDraft('')} onRecover={() => {}}
onAddFiles={() => {}} onRemoveAttachment={() => {}} />
</div>
</main>;
}
createRoot(document.getElementById('root')!).render(<div className="classroom-workspace flex h-screen flex-col bg-white">
<header className="flex h-10 shrink-0 items-center border-b px-5 text-xs">智能体回复交互测试 · 真实页面组件 / 模拟回复</header>
<div className="flex min-h-0 flex-1 justify-end bg-[#f5f5f1]">
<main className="min-w-0 flex-1 p-8"><h1 className="text-lg">我的小狗世界</h1><p className="mt-4 text-sm text-muted-foreground">作品区域</p></main>
{compareComposers ? <OperationComposerComparison /> : <main className="min-w-0 flex-1 p-8"><h1 className="text-lg">我的小狗世界</h1><p className="mt-4 text-sm text-muted-foreground">作品区域</p></main>}
<div className="h-full shrink-0 border-l" style={{ width }}><TeacherChatPanel projectId="fixture" sourceId={chat === 'continuous' ? 'original-source' : 'source'} selectedAgent={continuous ? fixtureAgent : undefined} /></div>
</div>
</div>);

View File

@@ -2018,6 +2018,17 @@ test('incomplete consultation replies retry only on click and preserve both draf
const teacher = page.getByTestId('teacher-chat-panel');
const draft = teacher.getByRole('textbox', { name: '向智能体提问' });
await draft.fill('现在项目是什么阶段?');
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await expect(mainDraft).toBeVisible();
const [operationBox, teacherBox] = await Promise.all([
page.getByTestId('coding-message-composer').locator('.chat-composer-surface').boundingBox(),
teacher.locator('.consultation-composer').boundingBox(),
]);
expect(operationBox).not.toBeNull();
expect(teacherBox).not.toBeNull();
expect(Math.abs(operationBox!.height - teacherBox!.height)).toBeLessThanOrEqual(1);
expect(Math.abs(operationBox!.y + operationBox!.height - teacherBox!.y - teacherBox!.height)).toBeLessThanOrEqual(1);
await expect(teacher.getByRole('button', { name: /添加图片|添加文件|老师模型/ })).toHaveCount(0);
await teacher.getByRole('button', { name: '提问', exact: true }).click();
await expect(teacher.getByRole('status')).toHaveText('这次回复未能完整显示');
await expect(teacher.getByTestId('teacher-reply')).toHaveCount(0);

View File

@@ -256,17 +256,55 @@ for (const width of [319, 508]) for (const kind of ['ordinary', 'ideas', 'struct
});
}
for (const width of [319, 508]) test(`teacher composer shows only microphone and send controls at ${width}px`, async ({ page }) => {
for (const width of [319, 508]) test(`teacher composer matches the operation composer and shows only microphone and send controls at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width: 1180, height: 800 });
const savedModel = width === 508 ? 'test-second-model' : undefined;
await page.addInitScript((model) => {
localStorage.clear();
if (model) localStorage.setItem('makelore-teacher-model:fixture:coding-teacher:published', model);
}, savedModel);
await page.goto(`${url}?kind=rich-text&width=${width}`);
await page.route('https://example.com/teacher-preview.png', route => route.fulfill({
contentType: 'image/svg+xml',
body: '<svg xmlns="http://www.w3.org/2000/svg" width="900" height="250"><rect width="900" height="250" fill="#ebeedf"/></svg>',
}));
await page.goto(`${url}?kind=rich-text&width=${width}&compareComposers=1`);
const panel = page.getByTestId('teacher-chat-panel');
const input = page.getByRole('textbox', { name: '向智能体提问' });
const operationComposer = page.getByTestId('coding-message-composer');
const operationInput = operationComposer.getByRole('textbox');
const teacherSurface = panel.locator('.consultation-composer');
const operationSurface = operationComposer.locator('.chat-composer-surface');
const heights: number[] = [];
await expect(input).toBeEnabled();
for (const lines of [0, 1, 3, 6, 8]) {
const draft = Array.from({ length: lines }, (_, index) => `第 ${index + 1} 行想法`).join('\n');
await operationInput.fill(draft);
await input.fill(draft);
const [teacherInputBounds, operationInputBounds, teacherBounds, operationBounds] = await Promise.all([
input.boundingBox(), operationInput.boundingBox(), teacherSurface.boundingBox(), operationSurface.boundingBox(),
]);
expect(Math.abs(teacherInputBounds!.height - operationInputBounds!.height), `${lines} 行输入区域等高`).toBeLessThanOrEqual(1);
expect(Math.abs(teacherBounds!.height - operationBounds!.height), `${lines} 行输入框外框等高`).toBeLessThanOrEqual(1);
expect(Math.abs(teacherBounds!.y + teacherBounds!.height - operationBounds!.y - operationBounds!.height), `${lines} 行外框底边对齐`).toBeLessThanOrEqual(1);
expect(Math.abs(teacherInputBounds!.y + teacherInputBounds!.height - operationInputBounds!.y - operationInputBounds!.height), `${lines} 行文字区域底边对齐`).toBeLessThanOrEqual(1);
heights.push(teacherInputBounds!.height);
for (const element of [panel, operationComposer, teacherSurface, operationSurface, input, operationInput]) {
expect(await element.evaluate(el => el.scrollWidth <= el.clientWidth), `${lines} 行没有横向溢出`).toBe(true);
}
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await page.screenshot({ path: test.info().outputPath(`teacher-operation-composers-${width}-${lines}-lines.png`) });
}
expect(heights[0]).toBe(heights[1]);
expect(heights[2]).toBeGreaterThan(heights[1]);
expect(heights[3]).toBeGreaterThan(heights[2]);
expect(heights[4]).toBe(heights[3]);
expect(await input.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true);
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs)).toEqual([]);
await input.fill('保留麦克风和发送按钮');
await expect(panel.getByRole('button', { name: /老师模型/ })).toHaveCount(0);
await expect(panel.getByRole('button', { name: /添加图片|添加文件|添加附件/ })).toHaveCount(0);
await expect(panel.locator('input[type="file"]')).toHaveCount(0);
await expect(panel.getByRole('combobox')).toHaveCount(0);
await expect(panel.getByRole('button', { name: '语音输入' })).toBeVisible();
await expect(panel.getByRole('button', { name: '提问', exact: true })).toBeEnabled();
expect(await panel.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
@@ -274,7 +312,8 @@ for (const width of [319, 508]) test(`teacher composer shows only microphone and
await expect(controls.getByRole('button')).toHaveCount(2);
await expect(controls).not.toContainText(/老师配置|模型默认|fixture|test-second-model/);
expect(await controls.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
await panel.screenshot({ path: test.info().outputPath(`teacher-composer-${width}.png`) });
await operationInput.fill('保留麦克风和发送按钮');
await page.screenshot({ path: test.info().outputPath(`teacher-composer-${width}.png`) });
await panel.getByRole('button', { name: '提问', exact: true }).click();
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: Array<{text: string; modelId?: string}> }).__teacherReplyInputs.at(-1))).toMatchObject({ text: '保留麦克风和发送按钮' });
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: Array<{modelId?: string}> }).__teacherReplyInputs.at(-1)?.modelId)).toBe(savedModel);