feat(backend): persist tasks and generated message history
- add EntityTask domain and CRUD API backed by PostgreSQL - relate generated messages directly to tasks and delete on task removal - move preview generation to backend Llama endpoint - migrate frontend task APIs from localStorage to backend endpoints - update tests and CLAUDE rules for backend-owned LLM/persistence
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { getEntity } from '../api/entitiesApi'
|
||||
import {
|
||||
activateTask,
|
||||
buildTaskPreviewPrompt,
|
||||
deleteTaskGeneratedMessage,
|
||||
deleteTask,
|
||||
generateTaskPreview,
|
||||
getTaskGeneratedMessages,
|
||||
getTask,
|
||||
inactivateTask,
|
||||
updateTask,
|
||||
@@ -35,12 +37,6 @@ interface RegularitySuggestion {
|
||||
cronParts: CronParts
|
||||
}
|
||||
|
||||
interface GeneratedMessageItem {
|
||||
id: string
|
||||
label: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const DEFAULT_CRON_PARTS: CronParts = {
|
||||
minute: '0',
|
||||
hour: '9',
|
||||
@@ -121,10 +117,8 @@ export default function EditTaskPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const [cronParts, setCronParts] = useState<CronParts>(DEFAULT_CRON_PARTS)
|
||||
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
|
||||
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],
|
||||
@@ -138,6 +132,26 @@ export default function EditTaskPage() {
|
||||
enabled: Boolean(taskId),
|
||||
})
|
||||
|
||||
const { data: generatedMessages = [] } = useQuery({
|
||||
queryKey: ['task-generated-messages', taskId],
|
||||
queryFn: () => getTaskGeneratedMessages(taskId),
|
||||
enabled: Boolean(taskId),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (generatedMessages.length === 0) {
|
||||
if (selectedMessageId) {
|
||||
setSelectedMessageId('')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const hasCurrentSelection = generatedMessages.some((message) => message.id === selectedMessageId)
|
||||
if (!hasCurrentSelection) {
|
||||
setSelectedMessageId(generatedMessages[0].id)
|
||||
}
|
||||
}, [generatedMessages, selectedMessageId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!task) {
|
||||
return
|
||||
@@ -193,20 +207,13 @@ export default function EditTaskPage() {
|
||||
})
|
||||
|
||||
const previewMutation = useMutation({
|
||||
mutationFn: generateTaskPreview,
|
||||
mutationFn: (payload: Parameters<typeof generateTaskPreview>[1]) =>
|
||||
generateTaskPreview(taskId, payload),
|
||||
onMutate: () => {
|
||||
setPreviewError('')
|
||||
},
|
||||
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)
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['task-generated-messages', taskId] })
|
||||
},
|
||||
onError: (error) => {
|
||||
setPreviewError(
|
||||
@@ -246,6 +253,13 @@ export default function EditTaskPage() {
|
||||
[generatedMessages, selectedMessageId]
|
||||
)
|
||||
|
||||
const deleteGeneratedMessageMutation = useMutation({
|
||||
mutationFn: (messageId: string) => deleteTaskGeneratedMessage(taskId, messageId),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['task-generated-messages', taskId] })
|
||||
},
|
||||
})
|
||||
|
||||
const applyCronParts = (nextCronParts: CronParts) => {
|
||||
setCronParts(nextCronParts)
|
||||
setTaskForm((prev) => ({ ...prev, scheduleCron: buildCron(nextCronParts) }))
|
||||
@@ -514,13 +528,7 @@ export default function EditTaskPage() {
|
||||
<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
|
||||
})
|
||||
deleteGeneratedMessageMutation.mutate(message.id)
|
||||
}}
|
||||
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()}`}
|
||||
|
||||
Reference in New Issue
Block a user