Files
triqura-ecd/docs/intent/noshow-case/epics/NS-E4-document-artifact.md
colinislit 924988dd15 feat(cortex): no-show casus — intent flow, brief-rescript en document artifact
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>
2026-07-09 23:14:55 +02:00

337 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NS.E4 — Document Artifact Block
**Casus:** Cortex No Show Afhandeling
**Epic doel:** Een block component bouwen dat de herschreven huisartsbrief toont in het artifact paneel, inclusief accordering.
**Geschatte tijd:** ~2 uur
**Afhankelijkheden:** NS.E1 (BlockType), NS.E3 (dispatch API)
---
## Context & waarschuwingen
### Bestaande `renderArtifactBlock` heeft een `default` case
`artifact-container.tsx` heeft op regel 209:
```typescript
default:
return (
<div className="p-4 text-slate-500">
Onbekend artifact type: {artifact.type}
</div>
);
```
TypeScript heeft dit als `default` case, niet als een exhaustive check. Dat betekent: als we `register_no_show` vergeten toe te voegen aan de switch, valt de app niet over — maar toont wel "Onbekend artifact type". Visueel zichtbaar, maar geen compile-time fout.
### `getArtifactTitle` heeft ook een switch
De `getArtifactTitle` functie onderaan `artifact-container.tsx` heeft een eigen switch over `BlockType` met een `default: return 'Artifact'`. Ook hier: geen compile-time fout bij ontbrekende case, maar een generieke titel.
### Geen rich text editor in codebase
Er bestaat geen TipTap, Quill of Slate in de codebase. `dagnotitie-block.tsx` gebruikt een `<Textarea>` van shadcn/ui. Wij doen hetzelfde — dat is voldoende voor het prototype.
### BlockPrefillData is een open interface
`BlockPrefillData extends ChatEntities` — dat is een open interface. We kunnen er extra velden aan toevoegen voor no-show specifieke data (`documentId`, `originalContent`) zonder de store te wijzigen. De cast `as NoShowDocumentPrefill` in de container zorgt voor type safety binnen het block.
---
## NS.E4.S1 — NoShowDocumentBlock component
**Nieuw bestand:** `components/cortex/blocks/noshow-document-block.tsx`
### Props interface
```typescript
interface NoShowDocumentPrefill {
documentId: string;
content: string; // Herschreven inhoud van LLM
title: string;
originalContent?: string; // Originele inhoud voor "bekijk origineel"
rescriptWarning?: string; // Tonen als LLM fout had
}
```
### Component implementatie
```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(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 met Akkoord knop */}
<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 flex items-center gap-1">
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>
);
}
```
### Done criteria
- Component rendert de herschreven inhoud in een bewerkbare textarea
- "Originele brief bekijken" toggle werkt (collapsible)
- Textarea is editeerbaar — aanpassingen blijven behouden
- "Akkoord & Verzendklaar" knop roept dispatch API aan
- Na succesvolle dispatch: knop verdwijnt, "✓ Verzendklaar" verschijnt, chat ontvangt bevestiging
- Bij leeg content: knop is disabled
- Bij API fout: error message in chat, knop hergebruikbaar
---
## NS.E4.S2 — ArtifactContainer uitbreiden
**Bestand:** `components/cortex/artifacts/artifact-container.tsx`
### Wijziging 1 — Import toevoegen
Voeg toe na de bestaande block imports:
```typescript
import { NoShowDocumentBlock } from '../blocks/noshow-document-block';
```
### Wijziging 2 — Case toevoegen in `renderArtifactBlock`
Voeg toe in de switch, vóór de `default` case:
```typescript
case 'register_no_show': {
const nsPrefill = artifact.prefill as {
documentId: string;
content: string;
title: string;
originalContent?: string;
rescriptWarning?: string;
};
return (
<NoShowDocumentBlock
key={artifact.id}
prefill={nsPrefill}
/>
);
}
```
### Wijziging 3 — Case toevoegen in `getArtifactTitle`
Voeg toe in de switch, vóór de `default` case:
```typescript
case 'register_no_show':
return prefill?.title
? `Brief — ${prefill.title}`
: 'Huisartsbrief';
```
### Done criteria
- `openArtifact({ type: 'register_no_show', title: 'Huisartsbrief n.a.v. intake', prefill: { documentId: '...', content: '...', title: '...' } })` opent het `NoShowDocumentBlock`
- Tab titel in multi-artifact view toont "Brief — Huisartsbrief n.a.v. intake"
- Geen TypeScript errors in `artifact-container.tsx`
- Geen "Onbekend artifact type" fallback zichtbaar
---
## NS.E4.S3 — `openArtifact` aanroep in `handleNoShowRescriptStep`
**Bestand:** `components/cortex/chat/chat-panel.tsx`
Dit is de koppeling tussen de rescript API (NS.E3.S4) en het artifact (NS.E4.S1).
Voeg `handleNoShowRescriptStep` toe als `useCallback` in `ChatPanel`:
```typescript
const handleNoShowRescriptStep = useCallback(async () => {
const { noShowFlow } = useCortexStore.getState();
setNoShowStep('brief_open');
addChatMessage({ type: 'assistant', content: 'Huisartsbrief aanpassen...' });
try {
const res = await fetch('/api/cortex/noshow/rescript', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentId: noShowFlow.documentId ?? 'mock-brief-noshow-001',
originalContent: noShowFlow.originalContent ?? '',
patientId: activePatient?.id ?? 'demo-patient-001',
}),
});
const result = await res.json();
openArtifact({
type: 'register_no_show',
title: 'Huisartsbrief n.a.v. intake',
prefill: {
documentId: result.documentId,
content: result.rescriptedContent,
title: 'Huisartsbrief n.a.v. intake',
originalContent: result.originalContent,
rescriptWarning: result.warning, // undefined als LLM succesvol was
},
});
addChatMessage({
type: 'assistant',
content: 'Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent.',
});
} catch {
setNoShowStep('waiting_brief'); // Terug naar vorige stap
addChatMessage({
type: 'error',
content: 'Herschrijven mislukt. Probeer het opnieuw.',
});
}
}, [activePatient, setNoShowStep, addChatMessage, openArtifact]);
```
**Let op:** `useCortexStore.getState()` wordt hier gebruikt (niet de hook) omdat we binnen een `useCallback` zitten en de state op het moment van uitvoering nodig hebben, niet de state van de laatste render.
### Done criteria
- Klikken op `[Ja, pas brief aan]` → rescript API aangeroepen → artifact paneel opent met `NoShowDocumentBlock`
- Brief inhoud is de AI-herschreven versie (of origineel bij LLM fout)
- Als `rescriptWarning` aanwezig is: gele waarschuwingsbalk zichtbaar in het block
---
## Validatie na NS.E4
**Visuele test:**
1. Doorloop de flow tot en met nudge 2 acceptatie
2. Verwacht: artifact paneel schuift open met `NoShowDocumentBlock`
3. Inhoud is de AI-herschreven brief
4. Textarea is editeerbaar
5. Klik `[Akkoord & Verzendklaar]`
6. Verwacht: "✓ Verzendklaar" in het block + bevestiging in chat
7. `pnpm build` — geen errors