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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user