feat: integrate pixel teacher presence and resilient classroom preview

This commit is contained in:
鲨鱼辣椒
2026-09-23 18:23:53 +08:00
parent 12d800ec51
commit 7951cca700
60 changed files with 3313 additions and 180 deletions

View File

@@ -49,7 +49,7 @@ const first = topic('first'),
second = topic('second');
function request(overrides: Partial<TeacherRequest> = {}): TeacherRequest {
return {
id: 'request-1', text: '老师,帮我看看', references: [], createdAt: 'now',
id: 'request-1', text: '老师帮我看看', references: [], createdAt: 'now',
sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now',
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '',
...overrides,
@@ -198,12 +198,14 @@ describe('teacher side chat', () => {
expect(api.create).not.toHaveBeenCalled();
expect(api.send).not.toHaveBeenCalled();
if (role === 'teacher') {
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled();
expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeEnabled();
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(screen.getByRole('button', { name: '老师帮我看看', exact: true }));
expect(screen.queryByText('把你的困惑说出来就好')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '固定的通用问题' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '我还没想好下一步做什么' })).not.toBeInTheDocument();
} else {
expect(screen.getByRole('button', { name: '固定的通用问题' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: '老师,帮我看看', exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '老师帮我看看', exact: true })).not.toBeInTheDocument();
}
});
@@ -213,7 +215,7 @@ describe('teacher side chat', () => {
...first, revision: 2, requests: [suggestionsRequest({ id: input.requestId, status: 'running', response: streamingJson, suggestedQuestions: undefined })],
}));
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
const help = screen.getByRole('button', { name: '老师,帮我看看', exact: true });
const help = screen.getByRole('button', { name: '老师帮我看看', exact: true });
await waitFor(() => expect(help).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我自己还想问的另一件事' } });
fireEvent.click(help);
@@ -222,10 +224,10 @@ describe('teacher side chat', () => {
expect(screen.queryByText(streamingJson)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
expect(api.send).toHaveBeenCalledWith('p/teacher', 'first', expect.objectContaining({
intent: 'suggestions', text: '老师,帮我看看', sourceConversationId: 'c',
intent: 'suggestions', text: '老师帮我看看', sourceConversationId: 'c',
}));
expect(screen.getByLabelText('向老师提问')).toHaveValue('我自己还想问的另一件事');
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeDisabled();
expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeDisabled();
await waitFor(() => expect(streams.has('first')).toBe(true));
const sent = api.send.mock.calls[0][2] as TeacherSend;
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({
@@ -236,7 +238,7 @@ describe('teacher side chat', () => {
expect(screen.queryByText('我看看你最近做到了哪里…')).not.toBeInTheDocument();
for (const question of suggestedQuestions) expect(screen.getByRole('button', { name: question })).toBeEnabled();
expect(screen.getByRole('button', { name: '我也说不清,你带我看看', exact: true })).toBeEnabled();
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled();
expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeEnabled();
expect(api.send).toHaveBeenCalledTimes(1);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我自己还想问的另一件事');
});
@@ -273,20 +275,20 @@ describe('teacher side chat', () => {
it('retries an uncertain suggestions request after reopening without losing the student draft', async () => {
api.send.mockRejectedValue(new Error('网络暂不可用'));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled());
await waitFor(() => expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我还没写完的问题' } });
fireEvent.click(screen.getByRole('button', { name: '老师,帮我看看', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '老师帮我看看', exact: true }));
await screen.findByText('网络暂不可用');
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
expect(originalRequest.intent).toBe('suggestions');
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled());
await waitFor(() => expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeEnabled());
expect(screen.getByLabelText('向老师提问')).toHaveValue('我还没写完的问题');
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我又补了一点自己的想法' } });
fireEvent.click(screen.getByRole('button', { name: '老师,帮我看看', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '老师帮我看看', exact: true }));
await screen.findByText('网络暂不可用');
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我又补了一点自己的想法');
@@ -314,10 +316,10 @@ describe('teacher side chat', () => {
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
}));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled());
await waitFor(() => expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeEnabled());
await waitFor(() => expect(streams.has('first')).toBe(true));
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我的自由提问继续留着' } });
fireEvent.click(screen.getByRole('button', { name: '老师,帮我看看', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '老师帮我看看', exact: true }));
await screen.findByText('响应中途断开');
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
accepted = suggestionsRequest({ id: originalRequest.requestId, status: 'failed', response: '', suggestedQuestions: undefined, error: '老师已接到,但这次回复失败了' });
@@ -356,7 +358,7 @@ describe('teacher side chat', () => {
expect(api.send.mock.calls[0][2].intent).toBeUndefined();
expect(screen.getByLabelText('向老师提问')).toHaveValue('');
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled();
expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeEnabled();
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
expect(screen.getByLabelText('向老师提问')).toHaveValue('');
@@ -412,20 +414,19 @@ describe('teacher side chat', () => {
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
});
it('returns to the operation chat without importing advice or offering a notes workflow', async () => {
const returnToWork = vi.fn();
it.each(['teacher', 'friend'] as const)('shows %s replies without redundant actions or importing advice', async (role) => {
const bringBack = vi.fn();
api.read.mockResolvedValue({ ...first, requests: [{
id: 'reply', text: '游戏不好玩', references: [], createdAt: 'now',
sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now',
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '先观察别人在哪儿停下来。',
}] });
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" onReturnToWork={returnToWork} onBringBack={bringBack} />);
render(<TeacherChatPanel projectId="p" sourceId="c" role={role} onBringBack={bringBack} />);
await screen.findByText('先观察别人在哪儿停下来。');
expect(screen.queryByRole('button', { name: '带回主会话草稿' })).not.toBeInTheDocument();
expect(screen.queryByText(/记一下|项目共识|记入共识|待办/)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '我去试一试' }));
expect(returnToWork).toHaveBeenCalledOnce();
expect(screen.queryByRole('button', { name: '我去试一试' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '复制', exact: true })).not.toBeInTheDocument();
expect(bringBack).not.toHaveBeenCalled();
expect(api.send).not.toHaveBeenCalled();
});
@@ -442,7 +443,7 @@ describe('teacher side chat', () => {
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
expect(screen.getByLabelText('向老师提问')).toHaveValue('运营配置的预览问题');
expect(api.send).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '老师,帮我看看', exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '老师帮我看看', exact: true })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '老师新话题' }));
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码'));
});