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>
This commit is contained in:
689
docs/intent/noshow-case/bouwplan-cortex-casus-noshow.md
Normal file
689
docs/intent/noshow-case/bouwplan-cortex-casus-noshow.md
Normal file
@@ -0,0 +1,689 @@
|
||||
# Bouwplan — Cortex Casus: No Show Afhandeling
|
||||
|
||||
**Projectnaam:** Cortex — Casus 2 (No Show)
|
||||
**Versie:** v1.0
|
||||
**Datum:** 2026-03-29
|
||||
**Auteur:** Colin Lit / Antigravity
|
||||
|
||||
---
|
||||
|
||||
## 1. Doel en Context
|
||||
|
||||
**Doel:** Een werkende demonstratie bouwen van de no-show afhandelings-flow: van één getypte zin ("patiënt niet verschenen") tot een AI-herschreven, geaccordeerde huisartsbrief — zonder dat de regiebehandelaar een enkel scherm verlaat.
|
||||
|
||||
**Context:** De V2 Cortex architectuur (three-layer: Reflex → Orchestrator → Nudge) is volledig operationeel. De chat pipeline, artifact store en nudge infrastructure zijn live. Dit bouwplan bouwt de no-show casus **bovenop** die bestaande foundation.
|
||||
|
||||
**Kernprincipe:**
|
||||
> "Eén zin van de zorgverlener vervangt vijf administratieve handelingen."
|
||||
|
||||
**Beoogd resultaat:** Een demonstreerbare end-to-end flow die:
|
||||
- `register_no_show` intent herkent (Reflex + Orchestrator)
|
||||
- Declarabiliteitsregel proactief signaleert (Nudge stap 1)
|
||||
- Na 1-klik annulering de conceptbrief opspoort (Nudge stap 2)
|
||||
- De brief via LLM herschrijft en toont in Artifact (Document Block)
|
||||
- Accordering en `ready_for_dispatch` status afhandelt
|
||||
|
||||
**Referenties:**
|
||||
- PRD: `docs/intent/noshow-case/prd-cortex-casus-noshow.md`
|
||||
- FO: `docs/intent/noshow-case/fo-cortex-casus-noshow.md`
|
||||
- Bestaand bouwplan V2: `docs/intent/bouwplan-cortex-v2.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. Dev Quick Start
|
||||
|
||||
### Codebase oriëntatie
|
||||
|
||||
```
|
||||
lib/cortex/
|
||||
├── types.ts # ✅ UITBREIDEN — register_no_show intent + BlockType
|
||||
├── reflex-classifier.ts # ✅ UITBREIDEN — no-show patronen toevoegen
|
||||
├── nudge.ts # ✅ UITBREIDEN — 2 nieuwe rules toevoegen
|
||||
└── noshow-handler.ts # 🆕 NIEUW in E3 — multi-step state machine
|
||||
|
||||
stores/
|
||||
└── cortex-store.ts # ✅ UITBREIDEN — noShowFlow state in E3
|
||||
|
||||
components/cortex/
|
||||
├── blocks/
|
||||
│ └── noshow-document-block.tsx # 🆕 NIEUW in E4 — document artifact component
|
||||
└── chat/
|
||||
└── nudge-chat-message.tsx # ✅ HERGEBRUIK — werkt al met accept/dismiss
|
||||
|
||||
app/api/cortex/
|
||||
└── noshow/
|
||||
├── cancel/route.ts # 🆕 NIEUW in E3 — annuleer met cancelled_no_show
|
||||
├── context/route.ts # 🆕 NIEUW in E3 — concept brief ophalen
|
||||
└── rescript/route.ts # 🆕 NIEUW in E3 — LLM brief herschrijven
|
||||
```
|
||||
|
||||
### Werkwijze per story
|
||||
1. Lees story + done criteria
|
||||
2. Check bestaande code (zie "Bestaande code" sectie per epic)
|
||||
3. Implementeer — kleinste werkende stap
|
||||
4. Run `pnpm lint` en `pnpm build`
|
||||
5. Commit: `feat(cortex): NS.E{n}.S{n} — <beschrijving>`
|
||||
|
||||
### Conventies
|
||||
- TypeScript strict mode — geen `any`
|
||||
- Nederlandse gebruikersteksten, Engelse code/comments
|
||||
- Zod voor alle API validatie
|
||||
- Mock data voor prototype — geen echte Zorgmail/declaratie koppeling
|
||||
- Graceful degradation: als LLM faalt, toon originele brief + foutmelding
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecturele Keuzes
|
||||
|
||||
### 3.1 Multi-step flow: hoe?
|
||||
|
||||
De no-show flow heeft 5 sequentiële stappen waarbij elke stap afhangt van de vorige keuze. De huidige nudge engine is **single-shot** (trigger → 1 nudge → klaar).
|
||||
|
||||
**Gekozen aanpak: Lightweight state machine in de store**
|
||||
|
||||
```
|
||||
noShowFlowState: {
|
||||
step: 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done'
|
||||
appointmentId: string | null
|
||||
documentId: string | null
|
||||
originalBriefContent: string | null
|
||||
}
|
||||
```
|
||||
|
||||
Bij elke nudge-accept checkt de `chat-panel` welke `step` actief is en dispatcht de juiste vervolgactie. Dit hergebruikt de bestaande nudge-accept flow zonder een compleet nieuwe state machine framework nodig te hebben.
|
||||
|
||||
**Niet gekozen:** Een aparte state machine library (XState) — te zwaar voor prototype scope.
|
||||
|
||||
### 3.2 Document Block: Rich Text Editor?
|
||||
|
||||
De FO noemt een `rich-text-editor.tsx`, maar die bestaat niet in de codebase. Voor prototype scope is een gewone `<Textarea>` voldoende. De "kwaliteit" van de brief zit in de LLM output, niet in de editor.
|
||||
|
||||
**Besluit:** `<Textarea>` hergebruiken (zoals in `dagnotitie-block.tsx`), focus op Akkoord-knop en status-update.
|
||||
|
||||
### 3.3 Mock data strategie
|
||||
|
||||
Twee mock objecten toevoegen in `lib/cortex/mock-data/noshow.ts`:
|
||||
- Een declarabele afspraak voor de actieve patiënt
|
||||
- Een concept huisartsbrief gekoppeld aan die patiënt
|
||||
|
||||
De API routes checken eerst op mock data voordat ze Supabase bevragen — zodat de demo altijd werkt.
|
||||
|
||||
---
|
||||
|
||||
## 4. Epics & Stories Overzicht
|
||||
|
||||
| Epic ID | Titel | Doel | Stories | Geschatte tijd |
|
||||
|---------|-------|------|---------|----------------|
|
||||
| **NS.E1** | Intent Foundation | Type + classifier | 3 | ~1u |
|
||||
| **NS.E2** | Nudge Rules | 2 rules + chained flow | 3 | ~2u |
|
||||
| **NS.E3** | No-Show API | Cancel + brief-context + LLM rescript | 4 | ~3u |
|
||||
| **NS.E4** | Document Artifact | Block component + Akkoord-knop | 3 | ~2u |
|
||||
| **NS.E5** | Integration & Demo | End-to-end flow + polish | 3 | ~2u |
|
||||
|
||||
**Totaal:** 16 stories, ~10u bouwtijd
|
||||
|
||||
---
|
||||
|
||||
## 5. Epics & Stories (Gedetailleerd)
|
||||
|
||||
---
|
||||
|
||||
### NS.E1 — Intent Foundation
|
||||
|
||||
**Doel:** `register_no_show` herkenbaar maken voor het volledige classificatie systeem.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `lib/cortex/types.ts` — `CortexIntent` union type + `BLOCK_CONFIGS`
|
||||
- `lib/cortex/reflex-classifier.ts` — patroon hoe andere intents worden gematcht
|
||||
|
||||
---
|
||||
|
||||
#### NS.E1.S1 — CortexIntent type uitbreiden
|
||||
|
||||
**Taak:** `register_no_show` toevoegen aan de type definities.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/types.ts`:
|
||||
- Voeg `'register_no_show'` toe aan `CortexIntent` union
|
||||
- Voeg `'register_no_show'` toe aan `BLOCK_CONFIGS` met `{ type: 'register_no_show', title: 'No Show Registratie', size: 'md', icon: 'UserX' }`
|
||||
- `BlockType` wordt automatisch meegenomen (afgeleid van `CortexIntent`)
|
||||
|
||||
**Done criteria:**
|
||||
- `pnpm build` slaagt zonder type errors
|
||||
- `register_no_show` is een geldige `CortexIntent` en `BlockType`
|
||||
|
||||
---
|
||||
|
||||
#### NS.E1.S2 — Reflex Classifier patronen
|
||||
|
||||
**Taak:** Snelle (< 20ms) lokale patroonherkenning voor no-show input.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/reflex-classifier.ts` — voeg patronen toe aan de intent-map:
|
||||
```
|
||||
'register_no_show': [
|
||||
/no.?show/i,
|
||||
/niet verschenen/i,
|
||||
/niet gekomen/i,
|
||||
/afwezig.*afspraak/i,
|
||||
/no show/i,
|
||||
/komt niet op/i,
|
||||
]
|
||||
```
|
||||
- Confidence: `0.85` bij directe match (hoog — weinig ambiguïteit verwacht)
|
||||
|
||||
**Done criteria:**
|
||||
- `classifyWithReflex("patiënt is niet verschenen")` geeft `{ intent: 'register_no_show', confidence: 0.85 }`
|
||||
- `classifyWithReflex("no show vandaag")` geeft hetzelfde resultaat
|
||||
- Input zonder patroon escaleert correct naar AI
|
||||
|
||||
---
|
||||
|
||||
#### NS.E1.S3 — Orchestrator system prompt uitbreiden
|
||||
|
||||
**Taak:** Orchestrator (Layer 2) informeren over de nieuwe intent.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/orchestrator.ts` of het `buildSystemPrompt` in `app/api/cortex/chat/route.ts`:
|
||||
- Voeg `register_no_show` toe aan de intent-beschrijvingen:
|
||||
```
|
||||
register_no_show: Gebruik wanneer de zorgverlener aangeeft dat een patiënt
|
||||
niet op de afspraak is verschenen. Signaalwoorden: "no show", "niet verschenen",
|
||||
"niet gekomen", "afwezig bij afspraak".
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- System prompt bevat `register_no_show` met correcte beschrijving
|
||||
- AI classificeert "cliënt was er niet" als `register_no_show` (handmatig te testen via chat)
|
||||
|
||||
---
|
||||
|
||||
### NS.E2 — Nudge Rules & Chained Flow
|
||||
|
||||
**Doel:** Twee proactieve nudges aanmaken die sequentieel volgen op de no-show registratie.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `lib/cortex/nudge.ts` — `ProtocolRule` interface + `PROTOCOL_RULES` array + `evaluateNudge()`
|
||||
- `components/cortex/chat/nudge-chat-message.tsx` — hoe accept/dismiss werkt
|
||||
- `components/cortex/chat/chat-panel.tsx` — hoe `evaluateNudge` wordt aangeroepen na action completion
|
||||
- `stores/cortex-store.ts` — `noShowFlowState` is hier **nog niet** — toevoegen in S1
|
||||
|
||||
---
|
||||
|
||||
#### NS.E2.S1 — No-show flow state aan store toevoegen
|
||||
|
||||
**Taak:** Minimale state machine voor de 5-staps flow in de Zustand store.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `stores/cortex-store.ts`:
|
||||
```typescript
|
||||
// Voeg toe aan state interface:
|
||||
noShowFlow: {
|
||||
step: 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done';
|
||||
appointmentId: string | null;
|
||||
documentId: string | null;
|
||||
originalContent: string | null;
|
||||
};
|
||||
|
||||
// Actions:
|
||||
setNoShowStep: (step: NoShowStep) => void;
|
||||
setNoShowContext: (ctx: Partial<NoShowFlowContext>) => void;
|
||||
resetNoShowFlow: () => void;
|
||||
```
|
||||
- Initiële waarde: `{ step: 'idle', appointmentId: null, documentId: null, originalContent: null }`
|
||||
|
||||
**Done criteria:**
|
||||
- Store compileert zonder errors
|
||||
- `useCorteStore(s => s.noShowFlow)` geeft initiële state terug
|
||||
|
||||
---
|
||||
|
||||
#### NS.E2.S2 — Nudge Rule 1: Declarabiliteitscheck
|
||||
|
||||
**Taak:** Na `register_no_show` intent → nudge die waarschuwt over declarabele afspraak.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/nudge.ts` — voeg toe aan `PROTOCOL_RULES`:
|
||||
```typescript
|
||||
{
|
||||
id: 'noshow-declarabel-check',
|
||||
trigger: {
|
||||
intent: 'register_no_show',
|
||||
},
|
||||
priority: 'high',
|
||||
suggestion: {
|
||||
intent: 'cancel_appointment',
|
||||
message: 'Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als \'No Show\'?',
|
||||
prefillEntities: {},
|
||||
},
|
||||
expiresAfterMs: 5 * 60 * 1000,
|
||||
}
|
||||
```
|
||||
- **Let op:** `priority: 'high'` — deze nudge verschijnt altijd als eerste
|
||||
|
||||
**Done criteria:**
|
||||
- `evaluateNudge({ intent: 'register_no_show', ... })` geeft nudge met id `noshow-declarabel-check` terug
|
||||
- Nudge toont correct in chat UI met `[Ja, annuleer]` accept-knop
|
||||
|
||||
---
|
||||
|
||||
#### NS.E2.S3 — Nudge Rule 2: Concept Brief Alert + Chained Trigger
|
||||
|
||||
**Taak:** Na acceptatie van Nudge 1 → check op concept brief → Nudge 2 tonen.
|
||||
|
||||
**Aanpassingen:**
|
||||
|
||||
**Stap A — Nudge Rule 2 definiëren** in `lib/cortex/nudge.ts`:
|
||||
```typescript
|
||||
{
|
||||
id: 'noshow-brief-check',
|
||||
trigger: {
|
||||
intent: 'cancel_appointment',
|
||||
conditions: [{ field: 'noShowContext', operator: 'exists', value: true }],
|
||||
},
|
||||
priority: 'high',
|
||||
suggestion: {
|
||||
intent: 'register_no_show', // hergebruik als signaal voor brief-stap
|
||||
message: 'Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?',
|
||||
prefillEntities: {},
|
||||
},
|
||||
expiresAfterMs: 5 * 60 * 1000,
|
||||
}
|
||||
```
|
||||
|
||||
**Stap B — Chained accept logica** in `components/cortex/chat/chat-panel.tsx`:
|
||||
- Bij `acceptSuggestion` van `noshow-declarabel-check`:
|
||||
1. `setNoShowStep('waiting_cancel')`
|
||||
2. Roep `/api/cortex/noshow/cancel` aan
|
||||
3. Roep `/api/cortex/noshow/context` aan (check op conceptbrief)
|
||||
4. Als brief gevonden: `setNoShowContext({ documentId, originalContent })` + `evaluateNudge` voor rule 2
|
||||
5. `setNoShowStep('waiting_brief')`
|
||||
- Bij `acceptSuggestion` van `noshow-brief-check`:
|
||||
1. Roep `/api/cortex/noshow/rescript` aan
|
||||
2. `openArtifact({ type: 'register_no_show', ... })` met herschreven inhoud
|
||||
3. `setNoShowStep('brief_open')`
|
||||
|
||||
**Done criteria:**
|
||||
- Klikken op `[Ja, annuleer]` triggert de cancel API call + toont Nudge 2
|
||||
- Klikken op `[Ja, pas brief aan]` triggert rescript API + opent artifact
|
||||
- `noShowFlow.step` werkt correct door alle stappen
|
||||
|
||||
---
|
||||
|
||||
### NS.E3 — No-Show API Routes
|
||||
|
||||
**Doel:** Drie backend routes bouwen die de no-show flow ondersteunen.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `app/api/cortex/agenda/cancel/route.ts` — bestaand cancel patroon
|
||||
- `app/api/cortex/chat/route.ts` — Anthropic streaming patroon
|
||||
- `lib/auth/server.ts` — `createClient()` voor authenticated requests
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S1 — Mock data aanmaken
|
||||
|
||||
**Taak:** Statische mock data voor declarabele afspraak en concept huisartsbrief.
|
||||
|
||||
**Nieuw bestand:** `lib/cortex/mock-data/noshow.ts`
|
||||
```typescript
|
||||
export const MOCK_NO_SHOW_APPOINTMENT = {
|
||||
id: 'mock-appt-noshow-001',
|
||||
patientId: 'demo-patient-001', // matcht actieve demo patiënt
|
||||
date: new Date().toISOString(),
|
||||
type: 'intake_consult',
|
||||
is_billable: true,
|
||||
status: 'scheduled',
|
||||
title: 'Intake Consult',
|
||||
};
|
||||
|
||||
export const MOCK_CONCEPT_BRIEF = {
|
||||
id: 'mock-brief-001',
|
||||
patientId: 'demo-patient-001',
|
||||
type: 'huisartsbrief',
|
||||
status: 'concept',
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
content: `Geachte collega,
|
||||
|
||||
Hierbij informeer ik u over de intake van uw patiënt die bij ons in zorg is gekomen.
|
||||
Tijdens de intake is uitgebreid de hulpvraag besproken en zijn de volgende aandachtspunten
|
||||
naar voren gekomen: [...]
|
||||
|
||||
Het vervolgtraject bestaat uit wekelijkse gesprekken gericht op [...]
|
||||
|
||||
Met vriendelijke groet,
|
||||
[Behandelaar]`,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- Bestand bestaat en exporteert beide mock objecten
|
||||
- TypeScript compileert zonder errors
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S2 — Cancel Route (`/api/cortex/noshow/cancel`)
|
||||
|
||||
**Taak:** Afspraak annuleren met `cancelled_no_show` status.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/cancel/route.ts`
|
||||
|
||||
**Request body (Zod):**
|
||||
```typescript
|
||||
const CancelNoShowSchema = z.object({
|
||||
appointmentId: z.string(),
|
||||
patientId: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{ success: true, appointmentId: string, newStatus: 'cancelled_no_show' }
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
1. Auth check via `createClient()`
|
||||
2. Zod validatie
|
||||
3. Check op mock data (`MOCK_NO_SHOW_APPOINTMENT.id`) — als match, return mock success
|
||||
4. Anders: Supabase update op `encounters` tabel, `status = 'cancelled_no_show'`
|
||||
5. Return success response
|
||||
|
||||
**Done criteria:**
|
||||
- `POST /api/cortex/noshow/cancel` met mock appointment ID geeft `{ success: true }` terug
|
||||
- Route geeft correcte 400/401 bij ongeldige input/auth
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S3 — Context Route (`/api/cortex/noshow/context`)
|
||||
|
||||
**Taak:** Openstaande concept documenten ophalen voor actieve patiënt.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/context/route.ts`
|
||||
|
||||
**Query params:** `?patientId=<id>`
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
hasConceptBrief: boolean;
|
||||
document?: {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
1. Auth check
|
||||
2. `patientId` param ophalen
|
||||
3. Check op mock data — als `patientId` matcht `demo-patient-001`, return mock brief
|
||||
4. Anders: Supabase query op `reports` tabel, `type = 'huisartsbrief'` + `status = 'concept'` + `patient_id = patientId` + `deleted_at IS NULL`
|
||||
|
||||
**Done criteria:**
|
||||
- `GET /api/cortex/noshow/context?patientId=demo-patient-001` geeft mock brief terug
|
||||
- `hasConceptBrief: false` bij onbekende patiënt
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S4 — Rescript Route (`/api/cortex/noshow/rescript`)
|
||||
|
||||
**Taak:** LLM herschrijft de huisartsbrief met de no-show professioneel verwerkt.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/rescript/route.ts`
|
||||
|
||||
**Request body (Zod):**
|
||||
```typescript
|
||||
const RescriptSchema = z.object({
|
||||
documentId: z.string(),
|
||||
originalContent: z.string(),
|
||||
patientId: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{ rescriptedContent: string; documentId: string; }
|
||||
```
|
||||
|
||||
**System prompt voor LLM:**
|
||||
```
|
||||
Je bent een klinisch schrijver in een GGZ instelling. Je taak is een bestaande conceptbrief
|
||||
aanpassen zodat de no-show van de patiënt professioneel en contextbewust verwerkt is.
|
||||
|
||||
Regels:
|
||||
- Integreer de no-show in de lopende tekst, voeg het NIET achteraan toe
|
||||
- Gebruik formele GGZ-taal ("de patiënt is niet verschenen op het geplande consult")
|
||||
- Bewaar alle andere informatie in de brief
|
||||
- Voeg een zin toe over herschikken van het vervolggesprek
|
||||
- Reageer ALLEEN met de herschreven brieftekst, geen uitleg
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- Route roept Anthropic API aan met correct system prompt
|
||||
- Herschreven brief is contextbewust (no-show verwerkt, niet achteraan geplakt)
|
||||
- Route faalt graceful (geeft originele content terug + foutmelding bij API error)
|
||||
|
||||
---
|
||||
|
||||
### NS.E4 — Document Artifact Block
|
||||
|
||||
**Doel:** Een nieuw block component dat de herschreven brief weergeeft met een Akkoord-knop.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `components/cortex/blocks/dagnotitie-block.tsx` — patroon voor block met textarea
|
||||
- `components/cortex/shared/block-footer.tsx` — `primaryAction` / `secondaryAction` props
|
||||
- `components/cortex/artifacts/artifact-container.tsx` — `renderArtifactBlock()` switch
|
||||
|
||||
---
|
||||
|
||||
#### NS.E4.S1 — NoShowDocumentBlock component
|
||||
|
||||
**Taak:** Block component voor het weergeven en bewerken van de herschreven brief.
|
||||
|
||||
**Nieuw bestand:** `components/cortex/blocks/noshow-document-block.tsx`
|
||||
|
||||
**Props interface:**
|
||||
```typescript
|
||||
interface NoShowDocumentBlockProps {
|
||||
prefill: {
|
||||
documentId: string;
|
||||
content: string;
|
||||
title: string;
|
||||
originalContent?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**UI Structuur:**
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 📄 Huisartsbrief — aangepast door Cortex │
|
||||
│ ─────────────────────────────────────────── │
|
||||
│ [Textarea met herschreven inhoud] │
|
||||
│ (editeerbaar, minimaal 12 regels hoog) │
|
||||
│ │
|
||||
│ ─────────────────────────────────────────── │
|
||||
│ [Origineel bekijken ↕] [Akkoord & Verzendklaar →] │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
- State: `content` (editeerbaar), `isSubmitting`, `isDone`
|
||||
- "Origineel bekijken" toggle: toont `originalContent` in grijs eronder (collapsible)
|
||||
- "Akkoord & Verzendklaar" knop:
|
||||
1. `setIsSubmitting(true)`
|
||||
2. `PATCH /api/cortex/noshow/dispatch` (S2) met `{ documentId, finalContent: content }`
|
||||
3. Bij success: `setIsDone(true)`, `setNoShowStep('done')`, toast "Brief verzendklaar"
|
||||
4. Bij fout: toast foutmelding, `setIsSubmitting(false)`
|
||||
|
||||
**Done criteria:**
|
||||
- Component rendert herschreven inhoud in editeerbare textarea
|
||||
- "Akkoord" knop triggert dispatch API call
|
||||
- Na success toont component bevestiging (geen navigatie nodig)
|
||||
|
||||
---
|
||||
|
||||
#### NS.E4.S2 — Dispatch Route (`/api/cortex/noshow/dispatch`)
|
||||
|
||||
**Taak:** Document status naar `ready_for_dispatch` zetten na accordering.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/dispatch/route.ts`
|
||||
|
||||
**Request body (Zod):**
|
||||
```typescript
|
||||
const DispatchSchema = z.object({
|
||||
documentId: z.string(),
|
||||
finalContent: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
1. Auth check
|
||||
2. Als `documentId` matcht mock ID → return success (mock)
|
||||
3. Anders: Supabase update op `reports`, `status = 'ready_for_dispatch'`, `content = finalContent`
|
||||
|
||||
**Done criteria:**
|
||||
- `PATCH` met mock document ID geeft `{ success: true, status: 'ready_for_dispatch' }` terug
|
||||
|
||||
---
|
||||
|
||||
#### NS.E4.S3 — ArtifactContainer register_no_show toevoegen
|
||||
|
||||
**Taak:** `renderArtifactBlock()` switch uitbreiden met het nieuwe block.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `components/cortex/artifacts/artifact-container.tsx`:
|
||||
```typescript
|
||||
case 'register_no_show':
|
||||
return (
|
||||
<NoShowDocumentBlock
|
||||
prefill={artifact.prefill as NoShowDocumentPrefill}
|
||||
/>
|
||||
);
|
||||
```
|
||||
- Import toevoegen voor `NoShowDocumentBlock`
|
||||
|
||||
**Done criteria:**
|
||||
- `openArtifact({ type: 'register_no_show', prefill: { documentId, content, title } })` opent het juiste block
|
||||
- Geen TypeScript errors in artifact-container
|
||||
|
||||
---
|
||||
|
||||
### NS.E5 — Integration & Demo Polish
|
||||
|
||||
**Doel:** De volledige flow end-to-end werkend maken en demo-klaar polijsten.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `components/cortex/chat/chat-panel.tsx` — de centrale orchestrator voor de hele flow
|
||||
- `docs/intent/noshow-case/fo-cortex-casus-noshow.md` — de exacte stap-voor-stap flow
|
||||
|
||||
---
|
||||
|
||||
#### NS.E5.S1 — Chat Panel integratie
|
||||
|
||||
**Taak:** `chat-panel.tsx` uitbreiden met de no-show flow orchestratie.
|
||||
|
||||
**Aanpassingen in `chat-panel.tsx`:**
|
||||
|
||||
```typescript
|
||||
// Na handleConfirmAction / na evaluateNudge call:
|
||||
const handleNudgeAccept = async (suggestion: NudgeSuggestion) => {
|
||||
const { noShowFlow, setNoShowStep, setNoShowContext } = useCorteStore.getState();
|
||||
|
||||
if (suggestion.id === 'noshow-declarabel-check') {
|
||||
setNoShowStep('waiting_cancel');
|
||||
// Cancel afspraak
|
||||
await fetch('/api/cortex/noshow/cancel', { method: 'POST', body: ... });
|
||||
// Check concept brief
|
||||
const ctx = await fetch(`/api/cortex/noshow/context?patientId=${activePatient?.id}`).then(r => r.json());
|
||||
if (ctx.hasConceptBrief) {
|
||||
setNoShowContext({ documentId: ctx.document.id, originalContent: ctx.document.content });
|
||||
// Trigger nudge 2 als chat message
|
||||
addChatMessage({ type: 'nudge', content: BRIEF_NUDGE_MESSAGE, nudge: NOSHOW_BRIEF_NUDGE });
|
||||
setNoShowStep('waiting_brief');
|
||||
} else {
|
||||
setNoShowStep('done');
|
||||
addChatMessage({ type: 'assistant', content: 'Afspraak geannuleerd als No Show. Er zijn geen openstaande conceptbrieven gevonden.' });
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestion.id === 'noshow-brief-check') {
|
||||
setNoShowStep('brief_open');
|
||||
const { documentId, originalContent } = noShowFlow;
|
||||
const result = await fetch('/api/cortex/noshow/rescript', { method: 'POST', body: ... }).then(r => r.json());
|
||||
openArtifact({
|
||||
type: 'register_no_show',
|
||||
title: 'Huisartsbrief — aangepast',
|
||||
prefill: { documentId, content: result.rescriptedContent, title: 'Huisartsbrief', originalContent },
|
||||
});
|
||||
addChatMessage({ type: 'assistant', content: 'Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent.' });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- Volledige 5-staps flow is doorloopbaar in de browser
|
||||
- Elke stap produceert de juiste chat message en UI state
|
||||
|
||||
---
|
||||
|
||||
#### NS.E5.S2 — Loading states en foutafhandeling
|
||||
|
||||
**Taak:** Zorgdragen dat de flow nooit "vastloopt" voor de gebruiker.
|
||||
|
||||
**Aanpassingen:**
|
||||
- Voeg `isNoShowProcessing: boolean` toe aan store
|
||||
- `chat-panel.tsx`: toon `<ProcessingIndicator>` tijdens API calls tussen stappen
|
||||
- Bij API fout: voeg error chat message toe met Nederlandse foutmelding
|
||||
- `resetNoShowFlow()` aanroepen bij component unmount (cleanup)
|
||||
|
||||
**Done criteria:**
|
||||
- Loading indicator zichtbaar tijdens elke API call
|
||||
- Bij netwerk fout verschijnt: "Er is iets misgegaan. Probeer het opnieuw."
|
||||
- Flow kan opnieuw gestart worden na een fout
|
||||
|
||||
---
|
||||
|
||||
#### NS.E5.S3 — Demo script validatie
|
||||
|
||||
**Taak:** De flow handmatig doorlopen en valideren aan de succescriteria uit het PRD.
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Input "patiënt niet verschenen" → `register_no_show` herkend (Reflex, < 20ms)
|
||||
- [ ] Input "cliënt was er niet vandaag" → Orchestrator herkent correct
|
||||
- [ ] Nudge 1 verschijnt met juiste declarabiliteits-waarschuwing
|
||||
- [ ] Klik `[Ja, annuleer]` → Cancel API succesvol → Nudge 2 verschijnt
|
||||
- [ ] Klik `[Ja, pas brief aan]` → Rescript API → Artifact opent met aangepaste brief
|
||||
- [ ] Textarea is editeerbaar — aanpassing blijft behouden
|
||||
- [ ] Klik `[Akkoord & Verzendklaar]` → Dispatch API → bevestiging in chat
|
||||
- [ ] Volledige flow duurt < 30 seconden (excl. LLM latency)
|
||||
- [ ] Geen schermwissels nodig gedurende de hele flow
|
||||
- [ ] `pnpm build` slaagt zonder errors
|
||||
|
||||
**Done criteria:** Alle checkboxen afgevinkt = casus NS volledig gereed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Risico's en Mitigatie
|
||||
|
||||
| Risico | Kans | Impact | Mitigatie |
|
||||
|--------|------|--------|-----------|
|
||||
| LLM rescript kwaliteit onvoldoende | Middel | Hoog | System prompt itereren; originele brief altijd tonen als fallback |
|
||||
| Multi-step flow breekt bij snelle klikken | Laag | Middel | `isNoShowProcessing` boolean blokkeert dubbele submits |
|
||||
| Mock data matcht niet met actieve demo patiënt | Middel | Hoog | `patientId` hardcoderen in mock als `demo-patient-001` — zelfde als in bestaande demo setup |
|
||||
| `ArtifactContainer` switch mist nieuwe BlockType | Laag | Hoog | TypeScript exhaustive check dwingt dit af bij `pnpm build` |
|
||||
| Nudge 2 triggert niet als brief niet bestaat | Laag | Laag | Flow eindigt graceful met chat message "geen brief gevonden" |
|
||||
|
||||
---
|
||||
|
||||
## 7. Niet in Scope (Prototype)
|
||||
|
||||
- Echte declaratie-integratie (ZPM/HiX koppeling)
|
||||
- Zorgmail verzending van de brief
|
||||
- Automatisch inplannen van vervangende afspraak
|
||||
- Meerdere conceptbrieven per patiënt (multi-document select)
|
||||
- Audit log van de no-show registratie
|
||||
- Undo/rollback van de annulering
|
||||
Reference in New Issue
Block a user