feat(frontend): generate task previews with local ollama

Replace the local preview stub with a real Ollama-backed test message flow using the configured local model.

Show the exact final prompt live on create and edit task pages, render generated output below it, and cover the integration with frontend tests.
This commit is contained in:
2026-03-27 01:28:29 -03:00
parent 1a7f5d706a
commit a83ea85857
6 changed files with 394 additions and 42 deletions

View File

@@ -3,6 +3,7 @@ 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,
type EmailLookback,
@@ -83,6 +84,7 @@ export default function CreateTaskPage() {
const [cronParts, setCronParts] = useState<CronParts>(DEFAULT_CRON_PARTS)
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
const [preview, setPreview] = useState('')
const [previewError, setPreviewError] = useState('')
const { data: entity, isLoading } = useQuery({
queryKey: ['entity', entityId],
@@ -100,7 +102,18 @@ export default function CreateTaskPage() {
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.'
)
},
})
const canSubmit = useMemo(() => {
@@ -108,6 +121,25 @@ export default function CreateTaskPage() {
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])
const applyCronParts = (nextCronParts: CronParts) => {
setCronParts(nextCronParts)
setTaskForm((prev) => ({ ...prev, scheduleCron: buildCron(nextCronParts) }))
@@ -313,13 +345,8 @@ export default function CreateTaskPage() {
type="button"
onClick={() => {
if (!canSubmit) return
previewMutation.mutate({
entityId,
name: taskForm.name,
prompt: taskForm.prompt,
scheduleCron: taskForm.scheduleCron,
emailLookback: taskForm.emailLookback,
})
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"
@@ -327,10 +354,30 @@ export default function CreateTaskPage() {
{previewMutation.isPending ? 'Generating…' : 'Generate Test Message'}
</button>
{preview && (
<pre className="mt-4 whitespace-pre-wrap rounded-md bg-slate-950 p-4 text-xs text-slate-200">
{preview}
<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>
{previewError && (
<p role="alert" className="mt-4 text-sm text-red-300">
{previewError}
</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>

View File

@@ -4,6 +4,7 @@ import { Link, useNavigate, useParams } from 'react-router-dom'
import { getEntity } from '../api/entitiesApi'
import {
activateTask,
buildTaskPreviewPrompt,
deleteTask,
generateTaskPreview,
getTask,
@@ -115,6 +116,7 @@ export default function EditTaskPage() {
const [cronParts, setCronParts] = useState<CronParts>(DEFAULT_CRON_PARTS)
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
const [preview, setPreview] = useState('')
const [previewError, setPreviewError] = useState('')
const { data: entity, isLoading: isLoadingEntity } = useQuery({
queryKey: ['entity', entityId],
@@ -184,7 +186,18 @@ export default function EditTaskPage() {
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.'
)
},
})
const canSubmit = useMemo(() => {
@@ -192,6 +205,25 @@ export default function EditTaskPage() {
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])
const applyCronParts = (nextCronParts: CronParts) => {
setCronParts(nextCronParts)
setTaskForm((prev) => ({ ...prev, scheduleCron: buildCron(nextCronParts) }))
@@ -402,13 +434,8 @@ export default function EditTaskPage() {
type="button"
onClick={() => {
if (!canSubmit) return
previewMutation.mutate({
entityId,
name: taskForm.name,
prompt: taskForm.prompt,
scheduleCron: taskForm.scheduleCron,
emailLookback: taskForm.emailLookback,
})
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"
@@ -416,10 +443,30 @@ export default function EditTaskPage() {
{previewMutation.isPending ? 'Generating…' : 'Generate Test Message'}
</button>
{preview && (
<pre className="mt-4 whitespace-pre-wrap rounded-md bg-slate-950 p-4 text-xs text-slate-200">
{preview}
<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>
{previewError && (
<p role="alert" className="mt-4 text-sm text-red-300">
{previewError}
</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>