diff --git a/frontend/src/__tests__/api/tasksApi.test.ts b/frontend/src/__tests__/api/tasksApi.test.ts index ec682be..d04c3fb 100644 --- a/frontend/src/__tests__/api/tasksApi.test.ts +++ b/frontend/src/__tests__/api/tasksApi.test.ts @@ -229,6 +229,7 @@ TASK DETAILS INSTRUCTIONS - Write exactly one email message. +- The message must be written by Milton Fiscal (Director of Ceremonial Logistics) as the sender persona. - Use an extremely formal corporate tone. - Keep the content casual, mundane, and slightly nonsensical. - Reflect the entity personality and task prompt faithfully. diff --git a/frontend/src/__tests__/pages/CreateTaskPage.test.tsx b/frontend/src/__tests__/pages/CreateTaskPage.test.tsx index 5c94f72..eba692b 100644 --- a/frontend/src/__tests__/pages/CreateTaskPage.test.tsx +++ b/frontend/src/__tests__/pages/CreateTaskPage.test.tsx @@ -39,11 +39,6 @@ const mockEntity = { describe('CreateTaskPage', () => { beforeEach(() => { - vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week') - vi.mocked(tasksApi.buildTaskPreviewPrompt).mockImplementation( - (entity, task) => - `PROMPT FOR ${entity.name}: ${task.name} | ${task.prompt} | ${task.scheduleCron} | ${task.emailLookback}` - ) vi.mocked(entitiesApi.getEntity).mockResolvedValue(mockEntity) mockNavigate.mockClear() }) @@ -54,25 +49,36 @@ describe('CreateTaskPage', () => { await screen.findByRole('link', { name: /back to entity a/i }) expect(screen.getByLabelText(/task name/i)).toBeInTheDocument() - expect(screen.getByLabelText(/task prompt/i)).toBeInTheDocument() + expect(screen.queryByLabelText(/task prompt/i)).not.toBeInTheDocument() expect(screen.getByLabelText(/^Email Period$/i)).toBeInTheDocument() expect(screen.getByLabelText(/^Minute$/i)).toBeInTheDocument() expect(screen.getByLabelText(/^Hour$/i)).toBeInTheDocument() expect(screen.getByLabelText(/^Day of Month$/i)).toBeInTheDocument() expect(screen.getByLabelText(/^Month$/i)).toBeInTheDocument() expect(screen.getByLabelText(/^Day of Week$/i)).toBeInTheDocument() + expect(screen.queryByRole('button', { name: /generate test message/i })).not.toBeInTheDocument() + expect(screen.queryByText(/Final Prompt/i)).not.toBeInTheDocument() }) - it('should_generatePreviewAndCreateTask_when_formSubmitted', async () => { - vi.mocked(tasksApi.generateTaskPreview).mockResolvedValue('SUBJECT: Preview\nBODY:\nFormal nonsense') + it('should_createInactiveTaskAndNavigateToEditTask_when_formSubmitted', async () => { vi.mocked(tasksApi.createTask).mockResolvedValue({ id: 'task-2', entityId: 'entity-1', name: 'Morning Blast', - prompt: 'Talk about coffee', + prompt: '', scheduleCron: '0 8 * * 1-5', emailLookback: 'last_week', - active: true, + active: false, + createdAt: '2026-03-26T10:00:00Z', + }) + vi.mocked(tasksApi.inactivateTask).mockResolvedValue({ + id: 'task-2', + entityId: 'entity-1', + name: 'Morning Blast', + prompt: '', + scheduleCron: '0 8 * * 1-5', + emailLookback: 'last_week', + active: false, createdAt: '2026-03-26T10:00:00Z', }) @@ -97,45 +103,10 @@ describe('CreateTaskPage', () => { expect(screen.getByRole('button', { name: /Monthly/i })).toBeInTheDocument() fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Morning Blast' } }) - fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Talk about coffee' } }) fireEvent.click(screen.getByRole('button', { name: /Weekdays/i })) fireEvent.change(screen.getByLabelText(/^Hour$/i), { target: { value: '8' } }) - expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument() - expect( - screen.getByText('PROMPT FOR Entity A: Morning Blast | Talk about coffee | 0 8 * * 1-5 | last_week') - ).toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', { name: /generate test message/i })) - - await waitFor(() => { - expect(tasksApi.generateTaskPreview).toHaveBeenCalled() - expect(vi.mocked(tasksApi.buildTaskPreviewPrompt)).toHaveBeenCalledWith( - mockEntity, - expect.objectContaining({ - entityId: 'entity-1', - name: 'Morning Blast', - prompt: 'Talk about coffee', - scheduleCron: '0 8 * * 1-5', - emailLookback: 'last_week', - }) - ) - expect(vi.mocked(tasksApi.generateTaskPreview).mock.calls[0][0]).toEqual( - expect.objectContaining({ - entity: mockEntity, - task: expect.objectContaining({ - entityId: 'entity-1', - name: 'Morning Blast', - prompt: 'Talk about coffee', - scheduleCron: '0 8 * * 1-5', - emailLookback: 'last_week', - }), - }) - ) - expect(screen.getByText(/Formal nonsense/i)).toBeInTheDocument() - }) - fireEvent.click(screen.getByRole('button', { name: /create task/i })) await waitFor(() => { @@ -144,32 +115,13 @@ describe('CreateTaskPage', () => { expect.objectContaining({ entityId: 'entity-1', name: 'Morning Blast', - prompt: 'Talk about coffee', + prompt: '', scheduleCron: '0 8 * * 1-5', emailLookback: 'last_week', }) ) - expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1') - }) - }) - - it('should_showReadablePreviewError_when_generationFails', async () => { - vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue( - new Error('Unable to generate a test message from the local model. Connection refused') - ) - - render(, { wrapper }) - await screen.findByRole('link', { name: /back to entity a/i }) - - fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Morning Blast' } }) - fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Talk about coffee' } }) - - fireEvent.click(screen.getByRole('button', { name: /generate test message/i })) - - await waitFor(() => { - expect( - screen.getByText(/Unable to generate a test message from the local model. Connection refused/i) - ).toBeInTheDocument() + expect(tasksApi.inactivateTask).toHaveBeenCalledWith('task-2') + expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1/tasks/task-2') }) }) diff --git a/frontend/src/__tests__/pages/EditTaskPage.test.tsx b/frontend/src/__tests__/pages/EditTaskPage.test.tsx index 5462e79..f1c36a7 100644 --- a/frontend/src/__tests__/pages/EditTaskPage.test.tsx +++ b/frontend/src/__tests__/pages/EditTaskPage.test.tsx @@ -186,6 +186,59 @@ describe('EditTaskPage', () => { }) }) + it('should_renderGeneratedMessagePanels_when_pageLoads', async () => { + renderPage() + + await screen.findByRole('link', { name: /back to entity a/i }) + + expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument() + expect(screen.getByText(/^Generated Message$/i)).toBeInTheDocument() + expect(screen.getByText(/^Generated Message History$/i)).toBeInTheDocument() + expect(screen.getByText(/Generate a message and it will appear here./i)).toBeInTheDocument() + expect(screen.getByRole('list', { name: /generated message history/i })).toBeInTheDocument() + expect(screen.getByText(/No generated messages yet./i)).toBeInTheDocument() + }) + + it('should_showAndManageGeneratedMessageHistory_when_multipleMessagesGenerated', async () => { + vi.mocked(tasksApi.generateTaskPreview) + .mockResolvedValueOnce('SUBJECT: First\nBODY:\nFirst output') + .mockResolvedValueOnce('SUBJECT: Second\nBODY:\nSecond output') + + renderPage() + await screen.findByRole('link', { name: /back to entity a/i }) + + const generateButton = screen.getByRole('button', { name: /generate test message/i }) + + fireEvent.click(generateButton) + await screen.findByText(/First output/i) + + fireEvent.click(generateButton) + + await waitFor(() => { + expect(screen.getByText(/Second output/i)).toBeInTheDocument() + }) + + const history = screen.getByRole('list', { name: /generated message history/i }) + expect(history).toBeInTheDocument() + + const firstMessageHistoryItem = screen.getByRole('button', { name: /^message #1$/i }) + const secondMessageHistoryItem = screen.getByRole('button', { name: /^message #2$/i }) + + fireEvent.click(firstMessageHistoryItem) + expect(screen.getByText(/First output/i)).toBeInTheDocument() + + fireEvent.click( + screen.getByRole('button', { + name: /delete message #1/i, + }) + ) + + await waitFor(() => { + expect(firstMessageHistoryItem).not.toBeInTheDocument() + expect(secondMessageHistoryItem).toBeInTheDocument() + }) + }) + it('should_renderActivateButton_when_taskIsInactive', async () => { renderPage({ task: { ...mockTask, active: false } }) diff --git a/frontend/src/__tests__/pages/EntitiesPage.test.tsx b/frontend/src/__tests__/pages/EntitiesPage.test.tsx index 1321703..9167270 100644 --- a/frontend/src/__tests__/pages/EntitiesPage.test.tsx +++ b/frontend/src/__tests__/pages/EntitiesPage.test.tsx @@ -41,6 +41,37 @@ describe('EntitiesPage', () => { fireEvent.click(addButton) await waitFor(() => { expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.queryByLabelText(/default email context window/i)).not.toBeInTheDocument() + }) + }) + + it('should_submitDefaultContextWindow_when_createEntitySubmitted', async () => { + vi.mocked(entitiesApi.getEntities).mockResolvedValue([]) + vi.mocked(entitiesApi.createEntity).mockResolvedValue(mockEntity) + + render(, { wrapper }) + + fireEvent.click(screen.getByRole('button', { name: /add|create|new/i })) + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /create entity/i })).toBeInTheDocument() + }) + + fireEvent.change(screen.getByLabelText(/entity name/i), { target: { value: 'Test Entity' } }) + fireEvent.change(screen.getByLabelText(/sender email/i), { target: { value: 'test@condado.com' } }) + fireEvent.change(screen.getByLabelText(/job title/i), { target: { value: 'Tester' } }) + fireEvent.change(screen.getByLabelText(/personality notes/i), { target: { value: 'Formal' } }) + fireEvent.click(screen.getByRole('button', { name: /create/i })) + + await waitFor(() => { + expect(entitiesApi.createEntity).toHaveBeenCalled() + expect(vi.mocked(entitiesApi.createEntity).mock.calls[0]?.[0]).toEqual({ + name: 'Test Entity', + email: 'test@condado.com', + jobTitle: 'Tester', + personality: 'Formal', + contextWindowDays: 3, + }) }) }) diff --git a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx index 4bf02c5..3038d2e 100644 --- a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx +++ b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen, waitFor, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' @@ -20,6 +20,84 @@ const wrapper = ({ children }: { children: React.ReactNode }) => ( ) describe('EntityDetailPage', () => { + it('should_updateEntity_when_editSubmittedFromDetailPage', async () => { + vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week') + + vi.mocked(entitiesApi.getEntity) + .mockResolvedValueOnce({ + id: 'entity-1', + name: 'Entity A', + email: 'a@a.com', + jobTitle: 'Ops', + personality: 'Formal', + scheduleCron: '0 9 * * 1', + contextWindowDays: 3, + active: true, + createdAt: '', + }) + .mockResolvedValueOnce({ + id: 'entity-1', + name: 'Entity A Updated', + email: 'updated@a.com', + jobTitle: 'Operations Lead', + personality: 'Still formal', + scheduleCron: '0 9 * * 1', + contextWindowDays: 5, + active: true, + createdAt: '', + }) + + vi.mocked(entitiesApi.updateEntity).mockResolvedValue({ + id: 'entity-1', + name: 'Entity A Updated', + email: 'updated@a.com', + jobTitle: 'Operations Lead', + personality: 'Still formal', + scheduleCron: '0 9 * * 1', + contextWindowDays: 5, + active: true, + createdAt: '', + }) + vi.mocked(tasksApi.getTasksByEntity).mockResolvedValue([]) + + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText(/Entity A/i)).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /edit entity/i })) + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /edit entity/i })).toBeInTheDocument() + expect(screen.getByLabelText(/entity name/i)).toHaveValue('Entity A') + expect(screen.getByLabelText(/sender email/i)).toHaveValue('a@a.com') + expect(screen.getByLabelText(/job title/i)).toHaveValue('Ops') + expect(screen.getByLabelText(/personality notes/i)).toHaveValue('Formal') + expect(screen.queryByLabelText(/default email context window/i)).not.toBeInTheDocument() + }) + + fireEvent.change(screen.getByLabelText(/entity name/i), { target: { value: 'Entity A Updated' } }) + fireEvent.change(screen.getByLabelText(/sender email/i), { target: { value: 'updated@a.com' } }) + fireEvent.change(screen.getByLabelText(/job title/i), { target: { value: 'Operations Lead' } }) + fireEvent.change(screen.getByLabelText(/personality notes/i), { target: { value: 'Still formal' } }) + + fireEvent.click(screen.getByRole('button', { name: /^save$/i })) + + await waitFor(() => { + expect(entitiesApi.updateEntity).toHaveBeenCalledWith('entity-1', { + name: 'Entity A Updated', + email: 'updated@a.com', + jobTitle: 'Operations Lead', + personality: 'Still formal', + contextWindowDays: 3, + }) + expect(screen.queryByRole('dialog', { name: /edit entity/i })).not.toBeInTheDocument() + expect(screen.getByRole('heading', { name: /Entity A Updated/i })).toBeInTheDocument() + expect(screen.getByText(/Operations Lead - updated@a.com/i)).toBeInTheDocument() + }) + }) + it('should_renderEntityAndTasks_when_pageLoads', async () => { vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week') diff --git a/frontend/src/api/tasksApi.ts b/frontend/src/api/tasksApi.ts index 1666bc2..df1c3b5 100644 --- a/frontend/src/api/tasksApi.ts +++ b/frontend/src/api/tasksApi.ts @@ -108,6 +108,7 @@ export function buildTaskPreviewPrompt( '', 'INSTRUCTIONS', '- Write exactly one email message.', + `- The message must be written by ${entity.name} (${entity.jobTitle}) as the sender persona.`, '- Use an extremely formal corporate tone.', '- Keep the content casual, mundane, and slightly nonsensical.', '- Reflect the entity personality and task prompt faithfully.', diff --git a/frontend/src/pages/CreateTaskPage.tsx b/frontend/src/pages/CreateTaskPage.tsx index e00341a..16c64d9 100644 --- a/frontend/src/pages/CreateTaskPage.tsx +++ b/frontend/src/pages/CreateTaskPage.tsx @@ -3,15 +3,13 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Link, useNavigate, useParams } from 'react-router-dom' import { getEntity } from '../api/entitiesApi' import { - buildTaskPreviewPrompt, createTask, - generateTaskPreview, + inactivateTask, type EmailLookback, } from '../api/tasksApi' interface TaskFormState { name: string - prompt: string scheduleCron: string emailLookback: EmailLookback } @@ -72,7 +70,6 @@ function buildCron(parts: CronParts): string { const DEFAULT_TASK_FORM: TaskFormState = { name: '', - prompt: '', scheduleCron: buildCron(DEFAULT_CRON_PARTS), emailLookback: 'last_week', } @@ -83,8 +80,6 @@ export default function CreateTaskPage() { const queryClient = useQueryClient() const [cronParts, setCronParts] = useState(DEFAULT_CRON_PARTS) const [taskForm, setTaskForm] = useState(DEFAULT_TASK_FORM) - const [preview, setPreview] = useState('') - const [previewError, setPreviewError] = useState('') const { data: entity, isLoading } = useQuery({ queryKey: ['entity', entityId], @@ -94,51 +89,18 @@ export default function CreateTaskPage() { const createTaskMutation = useMutation({ mutationFn: createTask, - onSuccess: async () => { + onSuccess: async (createdTask) => { + await inactivateTask(createdTask.id) await queryClient.invalidateQueries({ queryKey: ['entity-tasks', entityId] }) - navigate(`/entities/${entityId}`) - }, - }) - - const previewMutation = useMutation({ - mutationFn: generateTaskPreview, - onMutate: () => { - setPreview('') - setPreviewError('') - }, - onSuccess: (value) => setPreview(value), - onError: (error) => { - setPreviewError( - error instanceof Error - ? error.message - : 'Unable to generate a test message from the local model.' - ) + await queryClient.invalidateQueries({ queryKey: ['entity-task', createdTask.id] }) + navigate(`/entities/${entityId}/tasks/${createdTask.id}`) }, }) const canSubmit = useMemo(() => { const hasFilledCronParts = Object.values(cronParts).every((value) => value.trim().length > 0) - return Boolean(taskForm.name.trim() && taskForm.prompt.trim() && hasFilledCronParts) - }, [cronParts, taskForm.name, taskForm.prompt]) - - const currentTask = useMemo( - () => ({ - entityId, - name: taskForm.name, - prompt: taskForm.prompt, - scheduleCron: taskForm.scheduleCron, - emailLookback: taskForm.emailLookback, - }), - [entityId, taskForm.emailLookback, taskForm.name, taskForm.prompt, taskForm.scheduleCron] - ) - - const finalPrompt = useMemo(() => { - if (!entity) { - return 'Entity details unavailable.' - } - - return buildTaskPreviewPrompt(entity, currentTask) - }, [currentTask, entity]) + return Boolean(taskForm.name.trim() && hasFilledCronParts) + }, [cronParts, taskForm.name]) const applyCronParts = (nextCronParts: CronParts) => { setCronParts(nextCronParts) @@ -186,7 +148,7 @@ export default function CreateTaskPage() { createTaskMutation.mutate({ entityId, name: taskForm.name, - prompt: taskForm.prompt, + prompt: '', scheduleCron: taskForm.scheduleCron, emailLookback: taskForm.emailLookback, }) @@ -205,21 +167,6 @@ export default function CreateTaskPage() { /> - - - Task Prompt - - - setTaskForm((prev) => ({ ...prev, prompt: event.target.value })) - } - className="mt-1 min-h-36 w-full rounded-md border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" - required - /> - - Email Period @@ -340,47 +287,6 @@ export default function CreateTaskPage() { Cron: {taskForm.scheduleCron} - - { - if (!canSubmit) return - if (!entity) return - previewMutation.mutate({ entity, task: currentTask }) - }} - disabled={!canSubmit || previewMutation.isPending} - className="rounded-md border border-cyan-500 px-3 py-2 text-sm font-medium text-cyan-300 hover:bg-cyan-500/10 disabled:opacity-50" - > - {previewMutation.isPending ? 'Generating…' : 'Generate Test Message'} - - - - - Final Prompt - - - {finalPrompt} - - - - {previewError && ( - - {previewError} - - )} - - {preview && ( - - - Generated Message - - - {preview} - - - )} - - (DEFAULT_CRON_PARTS) const [taskForm, setTaskForm] = useState(DEFAULT_TASK_FORM) - const [preview, setPreview] = useState('') + const [generatedMessages, setGeneratedMessages] = useState([]) + const [selectedMessageId, setSelectedMessageId] = useState('') const [previewError, setPreviewError] = useState('') + const generatedMessageCounter = useRef(0) const { data: entity, isLoading: isLoadingEntity } = useQuery({ queryKey: ['entity', entityId], @@ -187,10 +195,19 @@ export default function EditTaskPage() { const previewMutation = useMutation({ mutationFn: generateTaskPreview, onMutate: () => { - setPreview('') setPreviewError('') }, - onSuccess: (value) => setPreview(value), + onSuccess: (value) => { + generatedMessageCounter.current += 1 + const nextMessage: GeneratedMessageItem = { + id: `message-${generatedMessageCounter.current}`, + label: `Message #${generatedMessageCounter.current}`, + content: value, + } + + setGeneratedMessages((prev) => [nextMessage, ...prev]) + setSelectedMessageId(nextMessage.id) + }, onError: (error) => { setPreviewError( error instanceof Error @@ -224,6 +241,11 @@ export default function EditTaskPage() { return buildTaskPreviewPrompt(entity, currentTask) }, [currentTask, entity]) + const selectedMessage = useMemo( + () => generatedMessages.find((message) => message.id === selectedMessageId), + [generatedMessages, selectedMessageId] + ) + const applyCronParts = (nextCronParts: CronParts) => { setCronParts(nextCronParts) setTaskForm((prev) => ({ ...prev, scheduleCron: buildCron(nextCronParts) })) @@ -256,7 +278,7 @@ export default function EditTaskPage() { return ( - + - - - Final Prompt - - - {finalPrompt} - + + + + Final Prompt + + + {finalPrompt} + + + + + + Generated Message + + + {selectedMessage?.content ?? 'Generate a message and it will appear here.'} + + + + + + Generated Message History + + + {generatedMessages.length === 0 && ( + No generated messages yet. + )} + {generatedMessages.map((message) => ( + + setSelectedMessageId(message.id)} + className="flex-1 text-left text-xs text-slate-200 hover:text-cyan-300" + aria-label={message.label} + > + {message.label} + + {message.content.split('\n')[0]} + + + { + setGeneratedMessages((prev) => { + const nextMessages = prev.filter((item) => item.id !== message.id) + if (selectedMessageId === message.id) { + setSelectedMessageId(nextMessages[0]?.id ?? '') + } + return nextMessages + }) + }} + className="rounded border border-red-500/40 px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-red-300 hover:bg-red-500/10" + aria-label={`Delete ${message.label.toLowerCase()}`} + > + Delete + + + ))} + + {previewError && ( @@ -458,16 +539,6 @@ export default function EditTaskPage() { )} - {preview && ( - - - Generated Message - - - {preview} - - - )} diff --git a/frontend/src/pages/EntitiesPage.tsx b/frontend/src/pages/EntitiesPage.tsx index ca9c239..0f79c6e 100644 --- a/frontend/src/pages/EntitiesPage.tsx +++ b/frontend/src/pages/EntitiesPage.tsx @@ -8,25 +8,29 @@ import { VirtualEntityCreateDto, } from '../api/entitiesApi' +const DEFAULT_CONTEXT_WINDOW_DAYS = 3 + +const initialFormState: VirtualEntityCreateDto = { + name: '', + email: '', + jobTitle: '', + personality: '', + contextWindowDays: DEFAULT_CONTEXT_WINDOW_DAYS, +} + export default function EntitiesPage() { const navigate = useNavigate() const queryClient = useQueryClient() const { data: entities = [] } = useQuery({ queryKey: ['entities'], queryFn: getEntities }) const [dialogOpen, setDialogOpen] = useState(false) - const [form, setForm] = useState({ - name: '', - email: '', - jobTitle: '', - personality: '', - contextWindowDays: 3, - }) + const [form, setForm] = useState(initialFormState) const createMutation = useMutation({ mutationFn: createEntity, onSuccess: (createdEntity) => { queryClient.invalidateQueries({ queryKey: ['entities'] }) setDialogOpen(false) - setForm({ name: '', email: '', jobTitle: '', personality: '', contextWindowDays: 3 }) + setForm(initialFormState) navigate(`/entities/${createdEntity.id}`) }, }) @@ -140,20 +144,6 @@ export default function EntitiesPage() { className="block w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" /> - - - Default Email Context Window (days) - - setForm({ ...form, contextWindowDays: Number(e.target.value) })} - className="block w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" - min={1} - required - /> - (null) const { data: entity, isLoading: isLoadingEntity } = useQuery({ queryKey: ['entity', entityId], @@ -18,6 +32,18 @@ export default function EntityDetailPage() { enabled: Boolean(entityId), }) + const updateMutation = useMutation({ + mutationFn: (data: VirtualEntityCreateDto) => updateEntity(entityId, data), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['entity', entityId] }), + queryClient.invalidateQueries({ queryKey: ['entities'] }), + ]) + setDialogOpen(false) + setForm(null) + }, + }) + if (!entityId) { return Entity identifier is missing. } @@ -49,12 +75,24 @@ export default function EntityDetailPage() { {entity.jobTitle} - {entity.email} - - New Task - + + { + setForm(toEntityForm(entity)) + setDialogOpen(true) + }} + className="rounded-md border border-slate-700 px-4 py-2 text-sm font-semibold text-slate-100 hover:border-cyan-500 hover:text-cyan-300" + > + Edit Entity + + + New Task + + @@ -100,6 +138,93 @@ export default function EntityDetailPage() { )} + + {dialogOpen && form && ( + + + Edit Entity + { + e.preventDefault() + updateMutation.mutate(form) + }} + className="space-y-3" + > + + + Entity Name + + setForm({ ...form, name: e.target.value })} + className="block w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" + required + /> + + + + Sender Email + + setForm({ ...form, email: e.target.value })} + className="block w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" + required + /> + + + + Job Title + + setForm({ ...form, jobTitle: e.target.value })} + className="block w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" + required + /> + + + + Personality Notes + + setForm({ ...form, personality: e.target.value })} + className="block w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100" + /> + + + { + setDialogOpen(false) + setForm(null) + }} + className="rounded border border-slate-700 px-4 py-2 text-sm text-slate-200" + > + Cancel + + + Save + + + + + + )} ) }
Cron: {taskForm.scheduleCron}
- Final Prompt -
- {finalPrompt} -
- {previewError} -
- Generated Message -
- {preview} -
+ Final Prompt +
+ {finalPrompt} +
+ Generated Message +
+ {selectedMessage?.content ?? 'Generate a message and it will appear here.'} +
+ Generated Message History +