- remove prompt and preview generation from task creation - create tasks as inactive and route directly to edit page - add generated message history UX to edit task - update entity/task views and related test coverage
232 lines
8.6 KiB
TypeScript
232 lines
8.6 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { useState } from 'react'
|
|
import { Link, useParams } from 'react-router-dom'
|
|
import { getEntity, updateEntity, VirtualEntityCreateDto, VirtualEntityResponse } from '../api/entitiesApi'
|
|
import { getEmailLookbackLabel, getTasksByEntity } from '../api/tasksApi'
|
|
|
|
function toEntityForm(entity: VirtualEntityResponse): VirtualEntityCreateDto {
|
|
return {
|
|
name: entity.name,
|
|
email: entity.email,
|
|
jobTitle: entity.jobTitle,
|
|
personality: entity.personality ?? '',
|
|
contextWindowDays: entity.contextWindowDays,
|
|
}
|
|
}
|
|
|
|
export default function EntityDetailPage() {
|
|
const { entityId = '' } = useParams()
|
|
const queryClient = useQueryClient()
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const [form, setForm] = useState<VirtualEntityCreateDto | null>(null)
|
|
|
|
const { data: entity, isLoading: isLoadingEntity } = useQuery({
|
|
queryKey: ['entity', entityId],
|
|
queryFn: () => getEntity(entityId),
|
|
enabled: Boolean(entityId),
|
|
})
|
|
|
|
const { data: tasks = [], isLoading: isLoadingTasks } = useQuery({
|
|
queryKey: ['entity-tasks', entityId],
|
|
queryFn: () => getTasksByEntity(entityId),
|
|
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 <div className="p-8 text-sm text-slate-300">Entity identifier is missing.</div>
|
|
}
|
|
|
|
if (isLoadingEntity || isLoadingTasks) {
|
|
return <div className="p-8 text-sm text-slate-300">Loading entity details...</div>
|
|
}
|
|
|
|
if (!entity) {
|
|
return (
|
|
<div className="p-8">
|
|
<p className="text-sm text-red-300">Entity not found.</p>
|
|
<Link
|
|
to="/entities"
|
|
className="mt-4 inline-block text-sm text-cyan-300 hover:text-cyan-200"
|
|
>
|
|
Back to Entities
|
|
</Link>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8 p-8">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-slate-100">{entity.name}</h1>
|
|
<p className="mt-2 text-sm text-slate-300">
|
|
{entity.jobTitle} - {entity.email}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
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
|
|
</button>
|
|
<Link
|
|
to={`/entities/${entityId}/tasks/new`}
|
|
className="rounded-md bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-950 hover:bg-cyan-400"
|
|
>
|
|
New Task
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<section>
|
|
<h2 className="text-lg font-semibold text-slate-100">Scheduled Tasks</h2>
|
|
<ul className="mt-3 divide-y divide-slate-800 overflow-hidden rounded-xl border border-slate-800 bg-slate-900/70">
|
|
{tasks.map((task) => (
|
|
<li
|
|
key={task.id}
|
|
className={`space-y-1 px-4 py-3 ${task.active ? '' : 'bg-slate-950/30 text-slate-500'}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<p className={`font-medium ${task.active ? 'text-slate-100' : 'text-slate-400'}`}>
|
|
{task.name}
|
|
</p>
|
|
{!task.active && (
|
|
<span className="inline-flex rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-amber-200">
|
|
Inactive
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className={`text-sm ${task.active ? 'text-slate-300' : 'text-slate-500'}`}>
|
|
Schedule: {task.scheduleCron}
|
|
</p>
|
|
<p className={`text-sm ${task.active ? 'text-slate-400' : 'text-slate-500'}`}>
|
|
Email context: {getEmailLookbackLabel(task.emailLookback)}
|
|
</p>
|
|
<div className="pt-2">
|
|
<Link
|
|
to={`/entities/${entityId}/tasks/${task.id}`}
|
|
className={`inline-flex rounded-md border px-3 py-1.5 text-sm ${
|
|
task.active
|
|
? 'border-slate-700 text-slate-200 hover:border-cyan-500 hover:text-cyan-300'
|
|
: 'border-slate-800 text-slate-400 hover:border-amber-500/60 hover:text-amber-200'
|
|
}`}
|
|
>
|
|
Details
|
|
</Link>
|
|
</div>
|
|
</li>
|
|
))}
|
|
{tasks.length === 0 && (
|
|
<li className="px-4 py-4 text-sm text-slate-400">No scheduled tasks yet.</li>
|
|
)}
|
|
</ul>
|
|
</section>
|
|
|
|
{dialogOpen && form && (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Edit Entity"
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70"
|
|
>
|
|
<div className="w-full max-w-md rounded-lg border border-slate-800 bg-slate-950 p-6 shadow-lg">
|
|
<h2 className="mb-4 text-lg font-semibold text-slate-100">Edit Entity</h2>
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
updateMutation.mutate(form)
|
|
}}
|
|
className="space-y-3"
|
|
>
|
|
<div>
|
|
<label htmlFor="entity-name" className="mb-1 block text-sm font-medium text-slate-200">
|
|
Entity Name
|
|
</label>
|
|
<input
|
|
id="entity-name"
|
|
value={form.name}
|
|
onChange={(e) => 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
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="entity-email" className="mb-1 block text-sm font-medium text-slate-200">
|
|
Sender Email
|
|
</label>
|
|
<input
|
|
id="entity-email"
|
|
type="email"
|
|
value={form.email}
|
|
onChange={(e) => 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
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="entity-job-title" className="mb-1 block text-sm font-medium text-slate-200">
|
|
Job Title
|
|
</label>
|
|
<input
|
|
id="entity-job-title"
|
|
value={form.jobTitle}
|
|
onChange={(e) => 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
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="entity-personality" className="mb-1 block text-sm font-medium text-slate-200">
|
|
Personality Notes
|
|
</label>
|
|
<textarea
|
|
id="entity-personality"
|
|
value={form.personality}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-2 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setDialogOpen(false)
|
|
setForm(null)
|
|
}}
|
|
className="rounded border border-slate-700 px-4 py-2 text-sm text-slate-200"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="rounded bg-cyan-500 px-4 py-2 text-sm font-medium text-slate-950 hover:bg-cyan-400"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|