diff --git a/packages/web/src/pages/chat-page.test.tsx b/packages/web/src/pages/chat-page.test.tsx index 1a067138..339f6a8b 100644 --- a/packages/web/src/pages/chat-page.test.tsx +++ b/packages/web/src/pages/chat-page.test.tsx @@ -63,6 +63,35 @@ function renderChatPage() { } describe('ChatPage', () => { + test('renames a changeable agent from its right-click menu', async () => { + fetchMock.mockImplementation((_input, init) => { + if (init?.method === 'PUT') { + return Promise.resolve(Response.json({persona: {...finance, name: 'Accounting'}})); + } + return Promise.resolve(new Response(null, {status: 204})); + }); + renderChatPage(); + + fireEvent.contextMenu(await screen.findByRole('link', {name: /Finance/})); + fireEvent.click(await screen.findByRole('menuitem', {name: 'Rename'})); + expect(screen.getByRole('heading', {name: 'Edit agent'})).toBeDefined(); + expect( + screen.getByText('Its name and its head. The conversation stays as it is.'), + ).toBeDefined(); + fireEvent.change(screen.getByLabelText('Name'), {target: {value: 'Accounting'}}); + fireEvent.click(screen.getByRole('button', {name: 'Save'})); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith( + '/api/orgs/acme/chat/personas/finance', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({name: 'Accounting', look, color: 'blue'}), + }), + ); + }); + }); + test('deletes a changeable agent from its right-click menu after confirmation', async () => { renderChatPage(); @@ -86,6 +115,7 @@ describe('ChatPage', () => { fireEvent.contextMenu(await screen.findByRole('link', {name: /General/})); + expect(screen.queryByRole('menuitem', {name: 'Rename'})).toBeNull(); expect(screen.queryByRole('menuitem', {name: 'Delete'})).toBeNull(); }); }); diff --git a/packages/web/src/pages/chat-page.tsx b/packages/web/src/pages/chat-page.tsx index c9cfa1eb..cd5b5c6c 100644 --- a/packages/web/src/pages/chat-page.tsx +++ b/packages/web/src/pages/chat-page.tsx @@ -1,5 +1,12 @@ import type {ChatEntry, ChatPersona, Persona, ThreadLog} from '@proxy/contracts/harness'; -import {ArrowUpIcon, ChevronDownIcon, MoonIcon, PlusIcon, Trash2Icon} from 'lucide-react'; +import { + ArrowUpIcon, + ChevronDownIcon, + MoonIcon, + PencilIcon, + PlusIcon, + Trash2Icon, +} from 'lucide-react'; import {type FormEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState} from 'react'; import {Link, useLoaderData, useNavigate, useRevalidator} from 'react-router'; import { @@ -123,12 +130,14 @@ function PersonaList({ current, currentLog, onNew, + onEdit, onDelete, }: { personas: ChatPersona[]; current: string; currentLog: ThreadLog; onNew: () => void; + onEdit: (persona: ChatPersona) => void; onDelete: (persona: ChatPersona) => void; }) { const org = useOrgSlug(); @@ -187,6 +196,10 @@ function PersonaList({ }>{row} + onEdit(persona)}> + + Rename + onDelete(persona)}> Delete @@ -232,8 +245,8 @@ function Conversation({loaded}: {loaded: Loaded}) { const [text, setText] = useState(''); const [sending, setSending] = useState(false); const [error, setError] = useState(null); - // The agent dialog: making a new one, or changing this one; closed as null. - const [editing, setEditing] = useState<'new' | 'current' | null>(null); + // The agent dialog: making a new one, or changing the selected one; closed as null. + const [editing, setEditing] = useState<'new' | ChatPersona | null>(null); const [deleting, setDeleting] = useState(null); const form = useRef(null); const personaId = loaded.log.persona.id; @@ -286,8 +299,11 @@ function Conversation({loaded}: {loaded: Loaded}) { } }; - // Saves the dialog's persona: a new one, opened; or this one, renamed or refaced. + // Saves the dialog's persona: a new one, opened; or the selected one, renamed or refaced. const savePersona = async (persona: Persona): Promise => { + if (editing === null) { + return 'This agent is no longer being edited.'; + } if (editing === 'new') { const made = await createPersona(org, persona); if (made.isErr()) { @@ -296,7 +312,7 @@ function Conversation({loaded}: {loaded: Loaded}) { void navigate(`/${org}/chat/${made.value.persona.id}`); return null; } - const changed = await updatePersona(org, personaId, persona); + const changed = await updatePersona(org, editing.id, persona); if (changed.isErr()) { return describeFetchError(changed.error); } @@ -335,7 +351,7 @@ function Conversation({loaded}: {loaded: Loaded}) { current.mayChange ? (