Merge teacher composer and compact prompt UI into main

This commit is contained in:
鲨鱼辣椒
2026-09-29 20:40:09 +08:00
62 changed files with 2327 additions and 158 deletions

View File

@@ -42,6 +42,36 @@ const props = { projectId: 'project-a', projectName: '天气', sourceId: 'source
async function mount() { let view!: ReturnType<typeof render>; await act(async () => { view = render(<TeacherChatPanel {...props} />); }); return view; }
describe('continuous agent panel', () => {
it.each([undefined, 'suggestions', 'check-in'] as const)('shows question/check-in and reply times for %s across teacher reloads', async intent => {
const current = { ...request('timestamped'), createdAt: '2026-09-28T08:01:02Z', finishedAt: '2026-09-28T08:01:42Z', intent,
suggestedQuestions: ['接着聊'], response: '这一轮的回答' };
const saved = topic([current, request('legacy')]);
api.conversation.mockResolvedValue({ topic: saved, before: null });
api.seenConversation.mockImplementation(async () => saved);
const view = await mount();
expect(view.container.querySelectorAll('time')).toHaveLength(2);
expect(view.container.querySelectorAll('time')[0]).toHaveAttribute('datetime', '2026-09-28T08:01:02.000Z');
expect(view.container.querySelectorAll('time')[1]).toHaveAttribute('datetime', '2026-09-28T08:01:42.000Z');
expect(screen.getAllByText('时间未记录')).toHaveLength(2);
expect(screen.getByRole('button', { name: '接着聊' })).toBeVisible();
view.unmount();
const reloaded = await mount();
expect(reloaded.container.querySelectorAll('time')[1]).toHaveAttribute('datetime', '2026-09-28T08:01:42.000Z');
expect(api.sendConversation).not.toHaveBeenCalled();
});
it('waits for the terminal reply timestamp while preserving the question time', async () => {
const current = { ...request('streaming'), status: 'running' as const, createdAt: '2026-09-28T08:01:02Z' };
const saved = topic([current]);
api.conversation.mockResolvedValue({ topic: saved, before: null });
api.seenConversation.mockImplementation(async () => saved);
const view = await mount();
expect(view.container.querySelectorAll('time')).toHaveLength(1);
expect(view.container.querySelector('[data-message-time="pending"]')).toBeVisible();
await act(async () => stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({ ...saved, revision: 2, requests: [{ ...current, status: 'completed', finishedAt: '2026-09-28T08:01:42Z' }] }) })));
expect(view.container.querySelectorAll('time')).toHaveLength(2);
expect(view.container.querySelector('[data-message-time="pending"]')).toBeNull();
});
it('retries incomplete prose only in its original project/source and keeps current drafts and one send identity', async () => {
const original = { ...request('broken', '现在项目是什么阶段?'), response: '它早就不是', replyIncomplete: true,
references: [{ kind: 'code' as const, text: '原项目代码' }] };