'use client' import { useRef, useState, useTransition } from 'react' import Link from 'next/link' import { toast } from 'sonner' import { Markdown } from '@/components/markdown' import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' import { entityDialogBodyClasses, entityDialogContentClasses, entityDialogFooterClasses, } from '@/components/shared/entity-dialog-layout' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useSoloStore } from '@/stores/solo-store' import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs' import { cn } from '@/lib/utils' import { getBranchUrl } from '@/lib/job-status-url' import { debugProps } from '@/lib/debug' import type { SoloTask } from './solo-board' const STATUS_COLORS: Record = { TO_DO: 'bg-status-todo/15 text-status-todo border-status-todo/30', IN_PROGRESS: 'bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30', REVIEW: 'bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30', DONE: 'bg-status-done/15 text-status-done border-status-done/30', } const STATUS_LABELS: Record = { TO_DO: 'To Do', IN_PROGRESS: 'Bezig', REVIEW: 'Review', DONE: 'Klaar', } interface TaskDetailDialogProps { task: SoloTask | null productId: string isDemo: boolean repoUrl?: string | null onClose: () => void } interface TaskDetailContentProps { task: SoloTask productId: string isDemo: boolean repoUrl?: string | null onClose: () => void } const VERIFY_RESULT_CONFIG: Record = { aligned: { label: 'Aligned', className: 'text-status-done' }, partial: { label: 'Gedeeltelijk', className: 'text-warning' }, divergent: { label: 'Divergent', className: 'text-error' }, empty: { label: 'Geen wijzigingen', className: 'text-muted-foreground' }, } type SaveState = 'idle' | 'saving' | 'saved' const VERIFY_REQUIRED_LABELS: Record = { ALIGNED: 'Strikt — alleen ALIGNED', ALIGNED_OR_PARTIAL: 'Standaard — ALIGNED of PARTIAL met uitleg', ANY: 'Vrij — geen verify-eis', } function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDetailContentProps) { const { updatePlan, updateVerifyOnly, updateVerifyRequired } = useSoloStore() const job = useSoloStore(s => s.claudeJobsByTaskId[task.id]) const connectedWorkers = useSoloStore(s => s.connectedWorkers) const [localPlan, setLocalPlan] = useState(task.implementation_plan ?? '') const [localVerifyOnly, setLocalVerifyOnly] = useState(task.verify_only) const [localVerifyRequired, setLocalVerifyRequired] = useState(task.verify_required) const [saveState, setSaveState] = useState('idle') const [, startTransition] = useTransition() const [jobPending, startJobTransition] = useTransition() const [verifyOnlyPending, startVerifyOnlyTransition] = useTransition() const [verifyRequiredPending, startVerifyRequiredTransition] = useTransition() const fadeTimer = useRef | null>(null) const savedPlanRef = useRef(task.implementation_plan ?? '') function handleEnqueue() { startJobTransition(async () => { const result = await enqueueClaudeJobAction(task.id) if ('error' in result) { toast.error(result.error) } else { toast.success('Agent ingeschakeld') } }) } function handleCancel() { if (!job) return startJobTransition(async () => { const result = await cancelClaudeJobAction(job.job_id) if ('error' in result) toast.error(result.error) }) } function handleBlur() { if (isDemo || localPlan === savedPlanRef.current) return setSaveState('saving') if (fadeTimer.current) clearTimeout(fadeTimer.current) // fetch naar Route Handler i.p.v. Server Action — Server Actions // kappen anders de open SSE-stream van het Solo Paneel af. Zie // notitie in solo-board.tsx handleDragEnd. startTransition(async () => { try { const res = await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ implementation_plan: localPlan }), }) if (!res.ok) { setSaveState('idle') toast.error('Implementatieplan opslaan mislukt') return } savedPlanRef.current = localPlan updatePlan(task.id, localPlan || null) setSaveState('saved') fadeTimer.current = setTimeout(() => setSaveState('idle'), 2000) } catch { setSaveState('idle') toast.error('Implementatieplan opslaan mislukt') } }) } function handleVerifyOnlyToggle() { if (isDemo) return const newValue = !localVerifyOnly setLocalVerifyOnly(newValue) startVerifyOnlyTransition(async () => { try { const res = await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ verify_only: newValue }), }) if (!res.ok) { setLocalVerifyOnly(!newValue) toast.error('Verify-only bijwerken mislukt') return } updateVerifyOnly(task.id, newValue) } catch { setLocalVerifyOnly(!newValue) toast.error('Verify-only bijwerken mislukt') } }) } function handleVerifyRequiredChange(e: React.ChangeEvent) { if (isDemo) return const newValue = e.target.value as typeof localVerifyRequired const prevValue = localVerifyRequired setLocalVerifyRequired(newValue) startVerifyRequiredTransition(async () => { try { const res = await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ verify_required: newValue }), }) if (!res.ok) { setLocalVerifyRequired(prevValue) toast.error('Verify-required bijwerken mislukt') return } updateVerifyRequired(task.id, newValue) } catch { setLocalVerifyRequired(prevValue) toast.error('Verify-required bijwerken mislukt') } }) } return ( <>
{task.title} {task.task_code && ( {task.task_code} )} {STATUS_LABELS[task.status]}

{task.story_code && {task.story_code}} {task.story_title}

{task.description && (

Beschrijving

{task.description}
)}

Implementatieplan