feat(frontend): streamline task creation and preview workflows

- 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
This commit is contained in:
2026-03-27 02:23:56 -03:00
parent a83ea85857
commit f2a16b5cf6
10 changed files with 430 additions and 222 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { getEntity } from '../api/entitiesApi'
@@ -35,6 +35,12 @@ interface RegularitySuggestion {
cronParts: CronParts
}
interface GeneratedMessageItem {
id: string
label: string
content: string
}
const DEFAULT_CRON_PARTS: CronParts = {
minute: '0',
hour: '9',
@@ -115,8 +121,10 @@ export default function EditTaskPage() {
const queryClient = useQueryClient()
const [cronParts, setCronParts] = useState<CronParts>(DEFAULT_CRON_PARTS)
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
const [preview, setPreview] = useState('')
const [generatedMessages, setGeneratedMessages] = useState<GeneratedMessageItem[]>([])
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 (
<div className="min-h-screen overflow-y-auto bg-slate-950 py-8">
<div className="mx-auto max-w-2xl px-4 sm:px-6">
<div className="mx-auto max-w-7xl px-4 sm:px-6">
<nav className="mb-6">
<Link
to={`/entities/${entityId}`}
@@ -443,13 +465,72 @@ export default function EditTaskPage() {
{previewMutation.isPending ? 'Generating…' : 'Generate Test Message'}
</button>
<div className="mt-4">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
Final Prompt
</p>
<pre className="mt-2 whitespace-pre-wrap rounded-md bg-slate-950 p-4 text-xs text-slate-200">
{finalPrompt}
</pre>
<div className="mt-4 grid gap-4 lg:grid-cols-3">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
Final Prompt
</p>
<pre className="mt-2 h-full min-h-56 whitespace-pre-wrap rounded-md bg-slate-950 p-4 text-xs text-slate-200">
{finalPrompt}
</pre>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
Generated Message
</p>
<pre className="mt-2 min-h-56 whitespace-pre-wrap rounded-md bg-slate-950 p-4 text-xs text-slate-200">
{selectedMessage?.content ?? 'Generate a message and it will appear here.'}
</pre>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
Generated Message History
</p>
<ul
aria-label="Generated message history"
className="mt-2 space-y-2 rounded-md border border-slate-800 bg-slate-950 p-3"
>
{generatedMessages.length === 0 && (
<li className="text-xs text-slate-400">No generated messages yet.</li>
)}
{generatedMessages.map((message) => (
<li
key={message.id}
className="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900 p-2"
>
<button
type="button"
onClick={() => setSelectedMessageId(message.id)}
className="flex-1 text-left text-xs text-slate-200 hover:text-cyan-300"
aria-label={message.label}
>
<span className="block font-medium">{message.label}</span>
<span className="mt-1 block line-clamp-2 text-slate-400">
{message.content.split('\n')[0]}
</span>
</button>
<button
type="button"
onClick={() => {
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
</button>
</li>
))}
</ul>
</div>
</div>
{previewError && (
@@ -458,16 +539,6 @@ export default function EditTaskPage() {
</p>
)}
{preview && (
<div className="mt-4">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
Generated Message
</p>
<pre className="mt-2 whitespace-pre-wrap rounded-md bg-slate-950 p-4 text-xs text-slate-200">
{preview}
</pre>
</div>
)}
</div>
<div className="flex justify-end gap-3 pb-8">