feat(teacher): show a subtle hint action in the empty composer
This commit is contained in:
@@ -51,7 +51,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,
|
||||
@@ -346,25 +346,38 @@ describe('teacher side chat', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('requests contextual suggestions explicitly, hides streaming JSON and preserves the freeform draft', async () => {
|
||||
it('only requests suggestions from the empty composer and preserves a later draft while streaming', async () => {
|
||||
const streamingJson = '{"intro":"还没输出完","questions":["尚未确认的问题';
|
||||
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
|
||||
...first, revision: 2, requests: [suggestionsRequest({ id: input.requestId, status: 'running', response: streamingJson, suggestedQuestions: undefined })],
|
||||
}));
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
const help = screen.getByRole('button', { name: '帮我看看', exact: true });
|
||||
await waitFor(() => expect(help).toBeEnabled());
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我自己还想问的另一件事' } });
|
||||
fireEvent.click(help);
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
const help = () => screen.getByRole('button', { name: '给我一点提示', exact: true });
|
||||
await waitFor(() => expect(help()).toBeEnabled());
|
||||
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(help());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: '我先自己想想' } });
|
||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: ' ' } });
|
||||
expect(help()).toBeEnabled();
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
expect(help()).toBeEnabled();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(help());
|
||||
fireEvent.click(help());
|
||||
|
||||
await screen.findByText('我看看你最近做到了哪里…');
|
||||
expect(screen.queryByText(streamingJson)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(api.send).toHaveBeenCalledWith('p/agent-topics', '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(help()).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '我自己还想问的另一件事' } });
|
||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
||||
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({
|
||||
@@ -374,23 +387,21 @@ describe('teacher side chat', () => {
|
||||
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
||||
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.queryByRole('button', { name: '我也说不清,你带我看看', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我自己还想问的另一件事');
|
||||
expect(input).toHaveValue('我自己还想问的另一件事');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[suggestedQuestions[0], undefined],
|
||||
['我也说不清,你带我看看', 'guided-help'],
|
||||
] as const)('sends the selected help action "%s" without replacing the student draft', async (text, intent) => {
|
||||
const previous = suggestionsRequest();
|
||||
it.each([suggestedQuestions[0], '我也说不清,你带我看看'])('sends the Agent-generated card "%s" as an ordinary question without replacing the draft', async text => {
|
||||
const previous = suggestionsRequest({ suggestedQuestions: [text] });
|
||||
api.read.mockResolvedValue({ ...first, requests: [previous] });
|
||||
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
|
||||
...first, revision: 2, requests: [previous, request({ id: input.requestId, text: input.text, intent: input.intent, response: '我们先从你刚才做的那一步看起。' })],
|
||||
...first, revision: 2, requests: [previous, request({ id: input.requestId, text: input.text, response: '我们先从你刚才做的那一步看起。' })],
|
||||
}));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" quote={{ kind: 'code', text: '学生正在引用的一段代码' }} />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: text, exact: true })).toBeEnabled());
|
||||
expect(screen.getAllByRole('button', { name: text, exact: true })).toHaveLength(1);
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '还在整理的自由提问' } });
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: text, exact: true }));
|
||||
@@ -399,7 +410,7 @@ describe('teacher side chat', () => {
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent).toMatchObject({ text, sourceConversationId: 'c' });
|
||||
expect(sent.references).toEqual([]);
|
||||
expect(sent.intent).toBe(intent);
|
||||
expect(sent.intent).toBeUndefined();
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('还在整理的自由提问');
|
||||
expect(screen.getByText('学生正在引用的一段代码')).toBeInTheDocument();
|
||||
view.unmount();
|
||||
@@ -409,30 +420,79 @@ describe('teacher side chat', () => {
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries an uncertain suggestions request after reopening without losing the student draft', async () => {
|
||||
it('keeps an uncertain suggestions request identity through reopening and draft edits', async () => {
|
||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeEnabled());
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '帮我看看', exact: true }));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
|
||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(originalRequest.intent).toBe('suggestions');
|
||||
view.unmount();
|
||||
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByLabelText('向智能体提问')).toBeEnabled());
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还没写完的问题');
|
||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我又补了一点自己的想法' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '帮我看看', exact: true }));
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我又补了一点自己的想法');
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('shows a failed suggestion request without leaking raw JSON and starts a fresh request on another try', async () => {
|
||||
const failed = suggestionsRequest({ status: 'failed', response: '{"intro":"未完成', suggestedQuestions: undefined, error: '这次没有看清楚,请再试一次。' });
|
||||
it.each([{ references: undefined }, { references: [{ kind: 'code' as const, text: '旧请求的引用' }] }])('retries an uncertain legacy hint payload unchanged with references $references', async ({ references }) => {
|
||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text: '帮我看看', intent: 'suggestions', sourceConversationId: 'previous-source', ...(references ? { references } : {}) };
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[0][2]))).toEqual(original);
|
||||
view.unmount();
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[1][2]))).toEqual(original);
|
||||
});
|
||||
|
||||
it('does not reuse an uncertain legacy hint request for an ordinary typed question with the same text', async () => {
|
||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text: '帮我看看', intent: 'suggestions', references: [] };
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: original.text, references: [], pending: original }));
|
||||
api.send.mockResolvedValue({ ...first, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '提问', exact: true })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent.requestId).not.toBe(original.requestId);
|
||||
expect(sent.text).toBe(original.text);
|
||||
expect(sent.intent).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(['completed', 'failed'] as const)('starts a fresh hint when the old hint request is already confirmed %s', async status => {
|
||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text: '帮我看看', intent: 'suggestions' };
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
||||
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({ id: original.requestId, text: original.text, status })] });
|
||||
api.send.mockResolvedValue({ ...first, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeEnabled());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '给我一点提示', exact: true }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent.requestId).not.toBe(original.requestId);
|
||||
expect(sent).toMatchObject({ text: '给我一点提示', intent: 'suggestions', references: [], sourceConversationId: 'c' });
|
||||
});
|
||||
|
||||
it.each(['failed', 'cancelled', 'interrupted'] as const)('retries a %s suggestion request without leaking raw JSON or reusing its accepted request id', async status => {
|
||||
const failed = suggestionsRequest({ status, response: '{"intro":"未完成', suggestedQuestions: undefined, error: '这次没有看清楚,请再试一次。' });
|
||||
api.read.mockResolvedValue({ ...first, requests: [failed] });
|
||||
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
|
||||
...first, revision: 2, requests: [failed, suggestionsRequest({ id: input.requestId })],
|
||||
@@ -453,11 +513,11 @@ describe('teacher side chat', () => {
|
||||
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
|
||||
}));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
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('响应中途断开');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
|
||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||
accepted = suggestionsRequest({ id: originalRequest.requestId, status: 'failed', response: '', suggestedQuestions: undefined, error: '智能体已接到,但这次回复失败了' });
|
||||
const recovered = { ...first, revision: 2, requests: [accepted] };
|
||||
@@ -495,7 +555,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" />);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
@@ -583,7 +643,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, '学生的练习代码', undefined));
|
||||
});
|
||||
@@ -718,9 +778,10 @@ describe('merged cloud teacher classroom', () => {
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText('智能体暂未开放,历史仍可查看。')).toBeVisible());
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
expect(screen.getByRole('button', { name: '给我一点提示', exact: true })).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '继续讨论' } });
|
||||
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeDisabled();
|
||||
expect(screen.queryByRole('button', { name: '给我一点提示', exact: true })).not.toBeInTheDocument();
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user