Cortex handelt een no-show af vanuit één chatcommando: afspraak annuleren (declarabiliteits-nudge), en de openstaande concept- huisartsbrief wordt via LLM herschreven en ter review aangeboden in een document artifact (human-in-the-loop, PATCH dispatch zet status op verzendklaar). - API-routes: context, rescript, cancel, dispatch - NoShowDocumentBlock: review/edit UI met origineel-vergelijk - Mock-data voor concept huisartsbrief - PRD, FO, bouwplan en epics in docs/intent/noshow-case/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { FileText, ChevronDown, ChevronUp } from 'lucide-react';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Button } from '@/components/ui/button';
|
||
import { useCortexStore } from '@/stores/cortex-store';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
interface NoShowDocumentPrefill {
|
||
documentId: string;
|
||
content: string;
|
||
title: string;
|
||
originalContent?: string;
|
||
rescriptWarning?: string;
|
||
}
|
||
|
||
interface NoShowDocumentBlockProps {
|
||
prefill: NoShowDocumentPrefill;
|
||
}
|
||
|
||
export function NoShowDocumentBlock({ prefill }: NoShowDocumentBlockProps) {
|
||
const [content, setContent] = useState<string>(prefill.content ?? '');
|
||
const [showOriginal, setShowOriginal] = useState(false);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [isDone, setIsDone] = useState(false);
|
||
|
||
const addChatMessage = useCortexStore((s) => s.addChatMessage);
|
||
const setNoShowStep = useCortexStore((s) => s.setNoShowStep);
|
||
|
||
const handleDispatch = async () => {
|
||
setIsSubmitting(true);
|
||
|
||
try {
|
||
const res = await fetch('/api/cortex/noshow/dispatch', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
documentId: prefill.documentId,
|
||
finalContent: content,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Dispatch mislukt');
|
||
|
||
setIsDone(true);
|
||
setNoShowStep('done');
|
||
addChatMessage({
|
||
type: 'assistant',
|
||
content: 'Brief is verzendklaar gemaakt. De no-show afhandeling is compleet.',
|
||
});
|
||
} catch {
|
||
addChatMessage({
|
||
type: 'error',
|
||
content: 'Opslaan mislukt. Probeer het opnieuw.',
|
||
});
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="w-full max-w-2xl">
|
||
{/* Header */}
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<FileText className="w-5 h-5 text-slate-600" />
|
||
<h2 className="text-lg font-semibold text-slate-800">
|
||
{prefill.title || 'Huisartsbrief'}
|
||
</h2>
|
||
<span className="ml-auto text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded font-medium">
|
||
Aangepast door Cortex
|
||
</span>
|
||
</div>
|
||
|
||
{/* LLM waarschuwing bij fallback naar origineel */}
|
||
{prefill.rescriptWarning && (
|
||
<div className="mb-3 p-3 bg-amber-50 border border-amber-200 rounded text-sm text-amber-800">
|
||
⚠️ {prefill.rescriptWarning}
|
||
</div>
|
||
)}
|
||
|
||
{/* Bewerkbare tekstinhoud */}
|
||
<Textarea
|
||
value={content}
|
||
onChange={(e) => setContent(e.target.value)}
|
||
disabled={isDone}
|
||
className={cn(
|
||
'min-h-[320px] font-mono text-sm resize-y',
|
||
isDone && 'opacity-60 cursor-not-allowed'
|
||
)}
|
||
placeholder="Brief inhoud..."
|
||
/>
|
||
|
||
{/* Origineel bekijken — collapsible */}
|
||
{prefill.originalContent && (
|
||
<div className="mt-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowOriginal(!showOriginal)}
|
||
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-700 transition-colors"
|
||
>
|
||
{showOriginal ? (
|
||
<ChevronUp className="w-3 h-3" />
|
||
) : (
|
||
<ChevronDown className="w-3 h-3" />
|
||
)}
|
||
{showOriginal ? 'Origineel verbergen' : 'Originele brief bekijken'}
|
||
</button>
|
||
|
||
{showOriginal && (
|
||
<div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-500 font-mono whitespace-pre-wrap">
|
||
{prefill.originalContent}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Footer */}
|
||
<div className="mt-4 flex items-center justify-between border-t border-slate-200 pt-4">
|
||
<span className="text-xs text-slate-400">
|
||
{isDone ? '✓ Brief is verzendklaar' : 'Controleer en pas aan indien nodig'}
|
||
</span>
|
||
|
||
{isDone ? (
|
||
<span className="text-sm font-medium text-green-600">
|
||
✓ Verzendklaar
|
||
</span>
|
||
) : (
|
||
<Button
|
||
onClick={handleDispatch}
|
||
disabled={isSubmitting || content.trim().length === 0}
|
||
className="bg-slate-800 hover:bg-slate-700 text-white"
|
||
>
|
||
{isSubmitting ? 'Opslaan...' : 'Akkoord & Verzendklaar →'}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|