feat(swift): implementeer AI-filtering psychiater voor overdracht (E5.S1)

E5.S1 compleet: AI filtert op behandelrelevantie voor psychiaters.
Voortgang: 56 SP / 72 SP (78%) - Epic 5 in progress!

Implementatie:
- Twee-staps filtering: Server (include_in_handover=true) → AI (psychiater: behandelrelevant)
- Role toggle UI: Verpleegkundige/Psychiater knoppen in OverdrachtBlock
- buildSystemPrompt(role) functie met psychiater-specifieke filtering regels
- filterForRole parameter toegevoegd aan GenerateOverdrachtSchema
- API route verwerkt role parameter en stuurt door naar Claude AI

Psychiater filtering regels:
 WEL RELEVANT:
- Medicatie-issues: weigering, bijwerkingen, dosisaanpassingen
- Stemming/gedrag: veranderingen, afwijkend gedrag, agitatie
- Risico-signalen: suïcidaliteit, automutilatie, agressie
- Psychotische symptomen: wanen, hallucinaties
- Crisis/dwang: separatie, fixatie, dwangmedicatie
- Afwijkende vitals: HH, LL met behandelimpact

 FILTER UIT:
- Routine medicatie ("volgens schema", "zonder problemen")
- ADL activiteiten (douchen, eten) tenzij significant afwijkend
- Sociale activiteiten ("groepstherapie", "gesprek gehad")
- Standaard observaties ("rustige dag", "geen bijzonderheden")
- Routine vitals (normaal bereik, N interpretatie)
- Dagstructuur ("dagprogramma gevolgd")

UI/UX verbeteringen:
- Role toggle met 2 knoppen (verpleegkundige/psychiater)
- Visual hints: "Alle gemarkeerde items" vs "Behandelrelevante items"
- Dynamic description tekst onder toggle
- filterRole state + useCallback dependency update

Files Changed:
- lib/types/overdracht.ts: +1 line (filterForRole in schema)
- lib/ai/overdracht-prompt.ts: +52 lines (buildSystemPrompt functie)
- app/api/overdracht/generate/route.ts: +8/-8 lines (role parameter)
- components/swift/blocks/overdracht-block.tsx: +43 lines (UI + logic)

Technische details:
- filterForRole default: 'verpleegkundige'
- Psychiater max: 3 aandachtspunten, 2 actiepunten
- Verpleegkundige max: 5 aandachtspunten, 3 actiepunten
- Role parameter propageert van UI → API → Claude prompt

Testing:
- Build succesvol (pnpm build)
- Geen nieuwe type errors
- UI toggle functioneel

Voortgang: 56 SP / 72 SP (78%) - E5.S1 compleet (5 SP)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-12-27 17:44:09 +01:00
parent 03f252d162
commit c6616d85b2
4 changed files with 104 additions and 8 deletions

View File

@@ -9,7 +9,7 @@ import { createClient } from '@/lib/auth/server';
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import {
OVERDRACHT_SYSTEM_PROMPT,
buildSystemPrompt,
buildOverdrachtUserPrompt,
calculateAge,
formatPatientName,
@@ -154,7 +154,10 @@ async function loadOverdrachtContext(
/**
* Call Claude API
*/
async function callClaudeAPI(context: OverdrachtContext): Promise<{
async function callClaudeAPI(
context: OverdrachtContext,
role: 'psychiater' | 'verpleegkundige' = 'verpleegkundige'
): Promise<{
samenvatting: string;
aandachtspunten: Aandachtspunt[];
actiepunten: string[];
@@ -164,6 +167,7 @@ async function callClaudeAPI(context: OverdrachtContext): Promise<{
throw new Error('ANTHROPIC_API_KEY ontbreekt in environment');
}
const systemPrompt = buildSystemPrompt(role);
const userPrompt = buildOverdrachtUserPrompt(context);
const response = await fetch('https://api.anthropic.com/v1/messages', {
@@ -177,7 +181,7 @@ async function callClaudeAPI(context: OverdrachtContext): Promise<{
model: 'claude-sonnet-4-20250514',
max_tokens: 2048,
temperature: 0.3,
system: OVERDRACHT_SYSTEM_PROMPT,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
}),
});
@@ -277,7 +281,7 @@ export async function POST(request: NextRequest) {
);
}
const { patientId, period } = result.data;
const { patientId, period, filterForRole } = result.data;
const supabase = await createClient();
// Check auth
@@ -289,8 +293,8 @@ export async function POST(request: NextRequest) {
// Load context with period filter
const context = await loadOverdrachtContext(supabase, patientId, period);
// Call Claude API
const aiResult = await callClaudeAPI(context);
// Call Claude API with role-specific filtering
const aiResult = await callClaudeAPI(context, filterForRole);
const durationMs = Date.now() - startTime;

View File

@@ -56,6 +56,7 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
const { toast } = useToast();
const [period, setPeriod] = useState<PeriodValue>('1d');
const [filterRole, setFilterRole] = useState<'verpleegkundige' | 'psychiater'>('verpleegkundige');
const [patients, setPatients] = useState<PatientOverzicht[]>([]);
const [isLoadingPatients, setIsLoadingPatients] = useState(true);
const [patientSummaries, setPatientSummaries] = useState<Map<string, PatientSummary>>(new Map());
@@ -139,7 +140,7 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ patientId, period }),
body: JSON.stringify({ patientId, period, filterForRole: filterRole }),
},
{ operation: 'Overdracht genereren' }
),
@@ -182,7 +183,7 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
return updated;
});
}
}, [period]);
}, [period, filterRole]);
// Filter patients: if activePatient is set, only show that one
const displayPatients = activePatient
@@ -230,6 +231,44 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
</p>
</div>
{/* Role Filter Toggle */}
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Doelgroep</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setFilterRole('verpleegkundige')}
className={cn(
'px-3 py-2.5 rounded-lg text-sm font-medium transition-colors text-left',
filterRole === 'verpleegkundige'
? 'bg-slate-900 text-white border-2 border-slate-700'
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100 hover:text-slate-900'
)}
>
<div className="font-semibold">Verpleegkundige</div>
<div className="text-xs opacity-80 mt-0.5">Alle gemarkeerde items</div>
</button>
<button
type="button"
onClick={() => setFilterRole('psychiater')}
className={cn(
'px-3 py-2.5 rounded-lg text-sm font-medium transition-colors text-left',
filterRole === 'psychiater'
? 'bg-slate-900 text-white border-2 border-slate-700'
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100 hover:text-slate-900'
)}
>
<div className="font-semibold">Psychiater</div>
<div className="text-xs opacity-80 mt-0.5">Behandelrelevante items</div>
</button>
</div>
<p className="text-xs text-slate-500">
{filterRole === 'psychiater'
? 'AI filtert op behandelrelevantie voor psychiater'
: 'Toont alle door verpleegkundige gemarkeerde items'}
</p>
</div>
{/* Patients List */}
{isLoadingPatients ? (
<div className="flex items-center justify-center py-12">

View File

@@ -68,6 +68,58 @@ Je taak: Maak een beknopte, relevante overdracht voor de opvolgende dienst.
Geef je antwoord als PURE JSON, zonder markdown code blocks.`;
/**
* Build system prompt based on target role
* For psychiater: adds strict filtering on treatment relevance
* For verpleegkundige: uses base prompt (all marked items)
*/
export function buildSystemPrompt(role: 'psychiater' | 'verpleegkundige' = 'verpleegkundige'): string {
if (role === 'psychiater') {
return `${OVERDRACHT_SYSTEM_PROMPT}
## PSYCHIATER FILTERING
Je maakt overdracht voor een PSYCHIATER. Filter STRIKT op behandelrelevantie.
De verpleegkundige heeft al items geselecteerd (include_in_handover=true), maar jij moet VERDER FILTEREN.
✅ WEL RELEVANT (include in samenvatting):
- Medicatie-issues: weigering, bijwerkingen, dosisaanpassingen, therapietrouw
- Stemming/gedrag: veranderingen van baseline, afwijkend gedrag, agitatie
- Risico-signalen: suïcidale uitingen, automutilatie, agressie
- Psychotische symptomen: wanen, hallucinaties, desorganisatie, paranoia
- Crisis/dwang: separatie, fixatie, dwangmedicatie, vrijheidsbeperkende maatregelen
- Afwijkende vitals: HH (kritiek hoog), LL (kritiek laag) met behandelimpact
❌ FILTER UIT (niet in samenvatting):
- Routine medicatie: "medicatie volgens schema", "zonder problemen", "ingenomen conform afspraak"
- ADL activiteiten: douchen, aankleden, eten, drinken (tenzij significant afwijkend/weigering)
- Sociale activiteiten: "deelgenomen aan groepstherapie", "gesprek gehad", "koffie gedronken"
- Standaard observaties: "rustige dag", "geen bijzonderheden", "normaal functioneren"
- Routine vitals: bloeddruk/pols binnen normaalwaarden (N interpretatie)
- Dagstructuur: "dagprogramma gevolgd", "aanwezig bij activiteit"
VUISTREGEL: Include ALLEEN als een psychiater op basis van deze info een BEHANDELBESLISSING kan nemen.
Beperkingen:
- Maximum 3 aandachtspunten (ALLEEN behandelrelevant, geen routine items)
- Maximum 2 actiepunten (ALLEEN actionable voor psychiater)
- Bij twijfel of iets relevant is → FILTER UIT
Voorbeelden:
✅ INCLUDE: "Jan weigerde haloperidol, zegt dat medicatie hem controleert" → Medicatie-compliance issue
✅ INCLUDE: "Marie uitte suïcidale gedachten tijdens gesprek" → Risicosignaal, urgent
✅ INCLUDE: "Piet verbaal en fysiek agressief, separatie 30 min" → Crisis, gedragsverandering
❌ FILTER: "Jan heeft goed gegeten, ontbijt en lunch zonder problemen" → Routine ADL
❌ FILTER: "Marie deelgenomen aan groepstherapie" → Sociale activiteit, geen issues
❌ FILTER: "Piet heeft medicatie ingenomen volgens schema" → Routine medicatie
Geef je antwoord als PURE JSON, zonder markdown code blocks.`;
}
// Verpleegkundige uses base prompt (no filtering)
return OVERDRACHT_SYSTEM_PROMPT;
}
/**
* Format timestamp to Dutch date format
*/

View File

@@ -102,6 +102,7 @@ export interface Aandachtspunt {
export const GenerateOverdrachtSchema = z.object({
patientId: z.string().uuid('Patient ID moet een geldige UUID zijn'),
period: z.enum(['1d', '3d', '7d', '14d']).optional().default('1d'),
filterForRole: z.enum(['psychiater', 'verpleegkundige']).optional().default('verpleegkundige'),
});
export type GenerateOverdrachtInput = z.infer<typeof GenerateOverdrachtSchema>;