feat(ui): introduce each teacher with an individual welcome bubble
This commit is contained in:
@@ -7,8 +7,9 @@ import { TeacherChatPanel } from '@/pages/Chat/TeacherChatPanel';
|
||||
import { TEACHER_CHECK_IN_INTERVAL_MS, type TeacherCatalog, type TeacherDefinition, type TeacherTopic } from '../../shared/coding-teacher';
|
||||
|
||||
const api = vi.hoisted(() => ({ catalog: vi.fn(), config: vi.fn(), list: vi.fn(), read: vi.fn(), create: vi.fn(), send: vi.fn(), events: vi.fn(), checkIn: vi.fn(), conversation: vi.fn(), sendConversation: vi.fn(), seenConversation: vi.fn() }));
|
||||
const identity = vi.hoisted(() => ({ id: 'kid' }));
|
||||
vi.mock('@/lib/coding-teacher', () => ({ teacherApi: api, legacyTopicBase: (base: string) => base, teacherTopicsPath: (project: string) => project }));
|
||||
vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: unknown) => unknown) => selector({ user: { userId: 'kid' } }) }));
|
||||
vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: unknown) => unknown) => selector({ user: { userId: identity.id } }) }));
|
||||
const definition: TeacherDefinition = {
|
||||
schema_version: 1, teacher_id: 'coding-teacher', name: '运营配置的智能体', description: '', avatar_id: 'avatar-06', welcome_message: '', suggested_questions: [], system_prompt: '指导', skills: [],
|
||||
model: { model_id: 'model', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1500 },
|
||||
@@ -37,6 +38,7 @@ async function advance(ms = TEACHER_CHECK_IN_INTERVAL_MS) {
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks(); vi.useFakeTimers(); localStorage.clear();
|
||||
identity.id = 'kid';
|
||||
vi.stubGlobal('ResizeObserver', class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
@@ -52,6 +54,83 @@ beforeEach(() => {
|
||||
});
|
||||
afterEach(() => { cleanup(); vi.useRealTimers(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe('individual teacher introductions', () => {
|
||||
const agents = ['方法老师', '体验老师', '故事老师'].map((name, index) => ({
|
||||
teacher_id: `intro-${index}`, version: 1, is_default: index === 0,
|
||||
definition: { ...definition, config_id: `intro-${index}`, name, description: `${name}的自我介绍`, welcome_message: `${name}的聊天欢迎语` },
|
||||
}));
|
||||
|
||||
it('introduces every delivered teacher independently of the old shared welcome dismissal without sending requests', async () => {
|
||||
localStorage.setItem('makelore-teacher-presence:kid:welcome-dismissed', 'true');
|
||||
api.catalog.mockResolvedValue({ items: agents });
|
||||
await mount();
|
||||
const group = screen.getByTestId('teacher-welcomes');
|
||||
for (const agent of agents) expect(within(group).getByText(agent.definition.description)).toBeVisible();
|
||||
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起体验老师的介绍' })));
|
||||
expect(within(group).queryByText('体验老师的自我介绍')).not.toBeInTheDocument();
|
||||
expect(within(group).getByText('方法老师的自我介绍')).toBeVisible();
|
||||
expect(within(group).getByText('故事老师的自我介绍')).toBeVisible();
|
||||
await advance();
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
expect(api.sendConversation).not.toHaveBeenCalled();
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
expect(api.seenConversation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps dismissal across reloads for this account/project while leaving other projects and accounts independent', async () => {
|
||||
api.catalog.mockResolvedValue({ items: agents });
|
||||
const first = await mount();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起方法老师的介绍' })));
|
||||
first.unmount();
|
||||
const next = await mount();
|
||||
expect(screen.queryByRole('button', { name: '收起方法老师的介绍' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible();
|
||||
await act(async () => next.rerender(<Harness projectId="other" />));
|
||||
expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '先自己试试' })));
|
||||
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
|
||||
await act(async () => next.rerender(<Harness />));
|
||||
expect(screen.queryByRole('button', { name: '收起方法老师的介绍' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible();
|
||||
identity.id = 'another-kid';
|
||||
await act(async () => next.rerender(<Harness />));
|
||||
expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('opens the clicked teacher and consumes only their introduction, preserving the others for later', async () => {
|
||||
api.catalog.mockResolvedValue({ items: agents });
|
||||
await mount();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '和故事老师聊聊' })));
|
||||
expect(screen.getByRole('button', { name: '与故事老师聊天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.getByTestId('teacher-chat-panel')).toHaveTextContent('故事老师的聊天欢迎语');
|
||||
expect(screen.getByTestId('teacher-chat-panel')).not.toHaveTextContent('故事老师的自我介绍');
|
||||
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '与故事老师聊天' })));
|
||||
expect(screen.queryByRole('button', { name: '收起故事老师的介绍' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible();
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
expect(api.sendConversation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('introduces only newly delivered teachers after dismiss-all, without resetting seen replies or existing receipts on refresh', async () => {
|
||||
localStorage.setItem('makelore-teacher-presence:kid:seen', JSON.stringify(['keep-seen']));
|
||||
api.catalog.mockResolvedValue({ items: agents });
|
||||
await mount();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '先自己试试' })));
|
||||
const added = { ...agents[0], teacher_id: 'new', definition: { ...agents[0].definition, config_id: 'new', name: '新老师', description: '', welcome_message: '新老师的云端欢迎语' } };
|
||||
api.catalog.mockResolvedValue({ items: [...agents.map(agent => ({ ...agent, version: 2 })), added] });
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '刷新智能体' })));
|
||||
expect(screen.getByTestId('teacher-welcomes')).toHaveTextContent('新老师的云端欢迎语');
|
||||
for (const agent of agents) expect(screen.queryByRole('button', { name: `收起${agent.definition.name}的介绍` })).not.toBeInTheDocument();
|
||||
expect(JSON.parse(localStorage.getItem('makelore-teacher-presence:kid:seen')!)).toEqual(['keep-seen']);
|
||||
expect(api.sendConversation).not.toHaveBeenCalled();
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('manual agent refresh', () => {
|
||||
it('refreshes while busy and composing, shares one request, and sends through the same agent after publication updates', async () => {
|
||||
const original = { teacher_id: 'refresh-first-method', version: 3, is_default: true, definition: { ...definition, config_id: 'refresh-first-method', name: '方法老师', welcome_message: '一起想想办法' } };
|
||||
|
||||
Reference in New Issue
Block a user