chat suggestions, SEo integration, and more

This commit is contained in:
colinislit
2025-12-02 13:37:45 +01:00
parent 9dc2b4f216
commit aacada2197
17 changed files with 2276 additions and 739 deletions

4
.gitignore vendored
View File

@@ -45,4 +45,8 @@ next-env.d.ts
.claude
.mcp.json
# documentation
/archive/*
/docs/archive/*
/docs/specs/archive/*
/docs/reports/archive/*

View File

@@ -41,6 +41,72 @@ export async function generateMetadata({ params }: ReleasePageProps) {
}
}
function ArticleJsonLd({
title,
description,
releaseDate,
slug,
}: {
title: string
description: string
releaseDate: string
slug: string
}) {
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Article',
'@id': `${siteUrl}/documentatie/${slug}#article`,
headline: title,
description: description,
datePublished: releaseDate,
dateModified: releaseDate,
author: {
'@type': 'Person',
name: 'Colin van der Heijden',
url: 'https://ikbenlit.nl',
},
publisher: { '@id': `${siteUrl}/#organization` },
mainEntityOfPage: `${siteUrl}/documentatie/${slug}`,
inLanguage: 'nl-NL',
},
{
'@type': 'BreadcrumbList',
'@id': `${siteUrl}/documentatie/${slug}#breadcrumb`,
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: siteUrl,
},
{
'@type': 'ListItem',
position: 2,
name: 'Documentatie',
item: `${siteUrl}/documentatie`,
},
{
'@type': 'ListItem',
position: 3,
name: title,
},
],
},
],
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
export default async function ReleasePage({ params }: ReleasePageProps) {
const { category } = await params
const release = await getRelease(category)
@@ -53,6 +119,12 @@ export default async function ReleasePage({ params }: ReleasePageProps) {
return (
<div className="min-h-screen bg-white pb-16">
<ArticleJsonLd
title={frontmatter.title}
description={frontmatter.description}
releaseDate={frontmatter.releaseDate}
slug={category}
/>
<article className="max-w-4xl mx-auto px-4 md:px-8 pt-20 md:pt-20">
{/* Header */}
<header className="mb-8 pb-8 border-b border-slate-200">

View File

@@ -5,6 +5,9 @@ import { getSession } from '@/lib/auth/server'
import { detectCategories } from '@/lib/docs/category-detector'
import { loadKnowledgeSections } from '@/lib/docs/knowledge-loader'
import { buildSystemPrompt } from '@/lib/docs/prompt-builder'
import { detectQuestionType } from '@/lib/docs/question-type-detector'
import { loadClientContext } from '@/lib/docs/client-context-loader'
import { buildClientPrompt, buildClientErrorPrompt } from '@/lib/docs/client-prompt-builder'
const DOCS_ASSISTANT_MODEL = process.env.DOCS_ASSISTANT_MODEL ?? 'claude-sonnet-4-20250514'
const MAX_HISTORY_MESSAGES = 10
@@ -52,6 +55,7 @@ const ChatMessageSchema = z.object({
const RequestSchema = z.object({
messages: z.array(ChatMessageSchema).optional(),
userMessage: z.string().min(1).max(MAX_USER_MESSAGE_LENGTH),
clientId: z.string().uuid().optional(), // UUID van actieve patiënt
})
type ChatMessage = z.infer<typeof ChatMessageSchema>
@@ -114,9 +118,28 @@ export async function POST(request: NextRequest) {
const conversation: ChatMessage[] = [...history, { role: 'user', content: rawUserMessage }]
const categories = detectCategories(rawUserMessage)
const knowledgeSections = await loadKnowledgeSections(categories)
const systemPrompt = buildSystemPrompt(knowledgeSections)
// Detect question type and build appropriate prompt
const clientId = parsed.data.clientId
const questionType = detectQuestionType(rawUserMessage, !!clientId)
let systemPrompt: string
if (questionType === 'client' && clientId) {
// Client-specific question: load client context
const clientContext = await loadClientContext(clientId)
if (clientContext) {
systemPrompt = buildClientPrompt(clientContext)
} else {
// Client not found or error loading
systemPrompt = buildClientErrorPrompt()
}
} else {
// Documentation question: use existing knowledge base flow
const categories = detectCategories(rawUserMessage)
const knowledgeSections = await loadKnowledgeSections(categories)
systemPrompt = buildSystemPrompt(knowledgeSections)
}
const apiKey = process.env.ANTHROPIC_API_KEY
if (!apiKey) {

View File

@@ -119,6 +119,34 @@ export const metadata: Metadata = {
},
};
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Organization',
'@id': `${siteUrl}/#organization`,
name: 'AI Speedrun',
url: siteUrl,
description: 'AI-powered EPD development experiment - bouw een EPD in 4 weken voor €200',
founder: {
'@type': 'Person',
name: 'Colin van der Heijden',
url: 'https://ikbenlit.nl',
},
},
{
'@type': 'WebSite',
'@id': `${siteUrl}/#website`,
url: siteUrl,
name: 'AI Speedrun',
publisher: { '@id': `${siteUrl}/#organization` },
inLanguage: 'nl-NL',
},
],
}
export default function RootLayout({
children,
}: Readonly<{
@@ -126,6 +154,12 @@ export default function RootLayout({
}>) {
return (
<html lang="nl">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body
className={`${crimsonText.variable} ${inter.variable} ${jetBrainsMono.variable} antialiased`}
>

View File

@@ -6,13 +6,27 @@
*/
import type { MetadataRoute } from 'next'
import { getAllReleases } from '@/lib/mdx/documentatie'
export default function sitemap(): MetadataRoute.Sitemap {
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
// Get current date for lastModified
const currentDate = new Date()
// Fetch all documentation releases dynamically
const releases = await getAllReleases()
const releaseUrls: MetadataRoute.Sitemap = releases.map((release) => {
const releaseDate = new Date(release.frontmatter.releaseDate)
const isValidDate = !isNaN(releaseDate.getTime())
return {
url: `${baseUrl}/documentatie/${release.slug}`,
lastModified: isValidDate ? releaseDate : currentDate,
changeFrequency: 'monthly',
priority: 0.8,
}
})
return [
{
url: baseUrl,
@@ -20,19 +34,19 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'weekly',
priority: 1.0,
},
// Future routes can be added here:
// {
// url: `${baseUrl}/build-log`,
// lastModified: currentDate,
// changeFrequency: 'weekly',
// priority: 0.8,
// },
// {
// url: `${baseUrl}/demo`,
// lastModified: currentDate,
// changeFrequency: 'monthly',
// priority: 0.7,
// },
{
url: `${baseUrl}/documentatie`,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.9,
},
...releaseUrls,
{
url: `${baseUrl}/contact`,
lastModified: currentDate,
changeFrequency: 'monthly',
priority: 0.7,
},
]
}

View File

@@ -12,7 +12,10 @@ interface SuggestionCategory {
questions: string[]
}
const SUGGESTION_CATEGORIES: SuggestionCategory[] = [
/**
* Documentation suggestions - shown when not in a patient dossier
*/
const DOC_SUGGESTION_CATEGORIES: SuggestionCategory[] = [
{
id: 'clienten',
label: 'Cliënten & Dossiers',
@@ -45,19 +48,63 @@ const SUGGESTION_CATEGORIES: SuggestionCategory[] = [
},
]
/**
* Client suggestions - shown when in a patient dossier
*/
const CLIENT_SUGGESTION_CATEGORIES: SuggestionCategory[] = [
{
id: 'rapportages',
label: 'Rapportages',
icon: '📝',
questions: [
'Geef een samenvatting van de rapportages',
'Wat is er de laatste tijd genoteerd?',
'Zijn er behandeladviezen?',
],
},
{
id: 'intake',
label: 'Intake & Behandeling',
icon: '🏥',
questions: [
'Wat is het behandeladvies?',
'Op welke afdeling loopt de intake?',
'Is de intake afgerond?',
],
},
{
id: 'screening',
label: 'Screening',
icon: '📋',
questions: [
'Wat was de hulpvraag?',
'Wat is de screeningbeslissing?',
'Is de cliënt geschikt bevonden?',
],
},
]
interface ChatSuggestionsProps {
onSelect: (question: string) => void
disabled?: boolean
mode?: 'client' | 'documentation'
}
/**
* Two-step suggestion selector:
* 1. Show categories
* 2. After selecting category, show questions
*
* Supports two modes:
* - 'documentation': Questions about how to use the EPD system
* - 'client': Questions about the active patient (rapportages, intake, screening)
*/
export function ChatSuggestions({ onSelect, disabled = false }: ChatSuggestionsProps) {
export function ChatSuggestions({ onSelect, disabled = false, mode = 'documentation' }: ChatSuggestionsProps) {
const [selectedCategory, setSelectedCategory] = useState<SuggestionCategory | null>(null)
// Select the appropriate categories based on mode
const categories = mode === 'client' ? CLIENT_SUGGESTION_CATEGORIES : DOC_SUGGESTION_CATEGORIES
const handleQuestionSelect = (question: string) => {
onSelect(question)
setSelectedCategory(null)
@@ -106,9 +153,11 @@ export function ChatSuggestions({ onSelect, disabled = false }: ChatSuggestionsP
// Show category selection
return (
<div className="px-4 pb-3">
<p className="text-xs text-slate-500 mb-2">Kies een onderwerp:</p>
<p className="text-xs text-slate-500 mb-2">
{mode === 'client' ? 'Vragen over deze cliënt:' : 'Kies een onderwerp:'}
</p>
<div className="flex flex-col gap-1.5">
{SUGGESTION_CATEGORIES.map((category) => (
{categories.map((category) => (
<button
key={category.id}
type="button"

View File

@@ -1,9 +1,10 @@
'use client'
import { useState } from 'react'
import { Sparkles, X } from 'lucide-react'
import { Sparkles, X, FileText } from 'lucide-react'
import { cn } from '@/lib/utils'
import { usePatientContext } from '@/app/epd/components/patient-context'
import { ChatInput } from './chat-input'
import { ChatMessages } from './chat-messages'
@@ -11,6 +12,18 @@ import { ChatSuggestions } from './chat-suggestions'
import { RateLimitMessage } from './rate-limit-message'
import { useDocsChat } from './use-docs-chat'
/**
* Helper to format patient name from FHIR structure
*/
function formatPatientName(patient: { name?: Array<{ given?: string[]; family?: string; prefix?: string[] }> } | null): string | undefined {
if (!patient?.name?.[0]) return undefined
const name = patient.name[0]
const given = name.given?.join(' ') || ''
const prefix = name.prefix?.join(' ') || ''
const family = name.family || ''
return `${given} ${prefix ? prefix + ' ' : ''}${family}`.trim() || undefined
}
/**
* Floating chat widget for documentation assistant
*
@@ -20,7 +33,27 @@ import { useDocsChat } from './use-docs-chat'
*/
export function DocsChatWidget() {
const [isOpen, setIsOpen] = useState(false)
const { messages, isLoading, isStreaming, error, isRateLimited, rateLimitResetTime, sendMessage, clearError, clearRateLimit } = useDocsChat()
// Get patient context for client-aware chat
const { patient } = usePatientContext()
const patientName = formatPatientName(patient)
const {
messages,
isLoading,
isStreaming,
error,
isRateLimited,
rateLimitResetTime,
sendMessage,
clearError,
clearRateLimit,
hasClientContext,
clientName,
} = useDocsChat({
clientId: patient?.id,
clientName: patientName,
})
return (
<>
@@ -70,7 +103,7 @@ export function DocsChatWidget() {
EPD Assistent
</h2>
<p className="text-xs text-slate-500">
Stel vragen over het EPD
{hasClientContext ? 'Stel vragen over de client of het EPD' : 'Stel vragen over het EPD'}
</p>
</div>
</div>
@@ -90,6 +123,16 @@ export function DocsChatWidget() {
</button>
</div>
{/* Client indicator - shown when in patient dossier */}
{hasClientContext && clientName && (
<div className="px-4 py-2 bg-blue-50 border-b border-blue-100 flex items-center gap-2">
<FileText className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-700">
Dossier: <span className="font-medium">{clientName}</span>
</span>
</div>
)}
{/* Error banner */}
{error && (
<div className="px-4 py-2 bg-red-50 border-b border-red-100 flex items-center justify-between">
@@ -120,6 +163,7 @@ export function DocsChatWidget() {
<ChatSuggestions
onSelect={sendMessage}
disabled={isLoading || isStreaming}
mode={hasClientContext ? 'client' : 'documentation'}
/>
)}

View File

@@ -23,6 +23,14 @@ interface UseDocsChatState {
rateLimitResetTime: number | null // timestamp when rate limit resets
}
/**
* Hook options
*/
interface UseDocsChatOptions {
clientId?: string // UUID van actieve patiënt
clientName?: string // Naam van actieve patiënt (voor display)
}
/**
* Hook return type
*/
@@ -31,6 +39,8 @@ interface UseDocsChatReturn extends UseDocsChatState {
clearMessages: () => void
clearError: () => void
clearRateLimit: () => void
hasClientContext: boolean
clientName: string | null
}
/**
@@ -58,12 +68,20 @@ const WELCOME_MESSAGE: ChatMessage = {
* - Streaming responses from Claude API
* - Loading and error states
* - Welcome message on init
* - Client-aware: sends clientId for patient-specific questions
*
* @example
* // Documentation-only mode
* const { messages, isLoading, sendMessage } = useDocsChat()
* await sendMessage("Hoe maak ik een intake aan?")
*
* // Client-aware mode
* const { messages, sendMessage, hasClientContext } = useDocsChat({
* clientId: patient?.id,
* clientName: "Jan de Vries"
* })
*/
export function useDocsChat(): UseDocsChatReturn {
export function useDocsChat(options?: UseDocsChatOptions): UseDocsChatReturn {
const { clientId, clientName } = options ?? {}
const [state, setState] = useState<UseDocsChatState>({
messages: [WELCOME_MESSAGE],
isLoading: false,
@@ -111,6 +129,7 @@ export function useDocsChat(): UseDocsChatReturn {
body: JSON.stringify({
messages: history,
userMessage: trimmedContent,
clientId, // Include clientId if available for patient-specific questions
}),
})
@@ -203,7 +222,7 @@ export function useDocsChat(): UseDocsChatReturn {
),
}))
}
}, [state.messages])
}, [state.messages, clientId])
const clearMessages = useCallback(() => {
setState({
@@ -230,5 +249,7 @@ export function useDocsChat(): UseDocsChatReturn {
clearMessages,
clearError,
clearRateLimit,
hasClientContext: !!clientId,
clientName: clientName ?? null,
}
}

View File

@@ -0,0 +1,341 @@
# Bouwplan — AI Cliënt Assistent
**Projectnaam:** Mini-ECD AI Cliënt Assistent
**Versie:** v1.0
**Datum:** 01-12-2025
**Auteur:** Colin Lit
---
## 1. Doel en context
**Doel:** Uitbreiding van de bestaande AI Documentatie Assistent met cliënt-awareness. Wanneer een behandelaar in een cliëntdossier zit, kan de assistent vragen beantwoorden over díe specifieke cliënt.
**Aanleiding:** Behandelaren besteden veel tijd aan het navigeren door verschillende schermen om informatie over een cliënt te verzamelen. Bij een overdracht of voorbereiding op een consult moeten zij rapportages doorbladeren, risico-assessments opzoeken, behandeladviezen teruglezen en screeningresultaten checken.
**Referenties:**
- PRD: `docs/specs/ai-integratie/prd-ai-client-assistent-v1.md`
- FO: `docs/specs/ai-integratie/fo-ai-client-assistent-v1.md`
- TO: `docs/specs/ai-integratie/to-ai-client-assistent-v1.md`
---
## 2. Uitgangspunten
### 2.1 Technische Stack
- **Frontend:** Next.js 15 + React + Tailwind CSS
- **Backend:** Next.js API Routes
- **Database:** Supabase (PostgreSQL) met RLS
- **AI/ML:** Claude claude-sonnet-4-20250514 (Anthropic)
- **Hosting:** Vercel
- **Auth:** Supabase Auth
- **Streaming:** Server-Sent Events (SSE)
### 2.2 Projectkaders
- **Bouwtijd:** ~8-12 uur (MVP)
- **Team:** 1 developer
- **Data:** Bestaande demo-data (21 rapportages, 9 intakes, 5 screenings)
- **Doel:** Werkende cliënt-aware chat in bestaande docs-chat widget
### 2.3 Bestaande Infrastructuur (Hergebruik)
| Component | Status | Hergebruik |
|-----------|--------|------------|
| DocsChatWidget | ✅ Compleet | ~80% |
| Streaming (SSE) | ✅ Werkt | 100% |
| PatientContext | ✅ Werkt | 100% |
| Rate limiting | ✅ Werkt | 100% |
| Chat suggestions | ✅ Werkt | Uitbreiden |
| `/api/reports` | ✅ Bestaat | Direct bruikbaar |
| `/api/intakes` | ✅ Bestaat | Direct bruikbaar |
| `/api/screenings` | ✅ Bestaat | Direct bruikbaar |
### 2.4 Programmeer Uitgangspunten
- **DRY:** Hergebruik bestaande docs-chat componenten
- **KISS:** Minimale wijzigingen aan bestaande code
- **SOC:** Nieuwe modules in `lib/docs/` voor client-specifieke logica
- **YAGNI:** Alleen MVP features, geen toekomstige uitbreidingen
---
## 3. Epics & Stories Overzicht
| Epic ID | Titel | Doel | Status | Stories |
|---------|-------|------|--------|---------|
| E1 | Backend Modules | Context loader, detector, prompt builder | ✅ Done | 3 |
| E2 | API Uitbreiding | Chat endpoint uitbreiden met clientId | ✅ Done | 2 |
| E3 | Frontend Uitbreiding | Indicator, suggestions, hook aanpassing | ✅ Done | 3 |
| E4 | Testing & Refinement | Integratie testen, prompt tuning | ✅ Done | 2 |
---
## 4. Epics & Stories (Uitwerking)
### Epic 1 — Backend Modules
**Epic Doel:** Nieuwe modules voor cliënt-context laden, vraagtype detectie en prompt building.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E1.S1 | Client Context Loader | Laadt patient + reports + intakes + screenings uit Supabase | ✅ | — | 3 |
| E1.S2 | Question Type Detector | Detecteert 'client' vs 'documentation' vs 'ambiguous' | ✅ | — | 2 |
| E1.S3 | Client Prompt Builder | Bouwt system prompt met cliënt-context | ✅ | E1.S1 | 2 |
**Technical Notes:**
**E1.S1 - Client Context Loader** (`lib/docs/client-context-loader.ts`)
```typescript
interface ClientContext {
patient: { name: string; birthDate: string; status: string }
reports: Array<{ type: string; content: string; date: string }>
intakes: Array<{ title: string; treatmentAdvice: object; status: string }>
screening: { requestForHelp: string; decision: string } | null
riskAssessments: Array<{ type: string; level: string; rationale: string }>
}
// Directe Supabase queries (niet via HTTP voor performance)
// Parallel laden: Promise.all([reports, intakes, screening, risks])
// Laatste 5 rapportages, 3 intakes, 1 screening
```
**E1.S2 - Question Type Detector** (`lib/docs/question-type-detector.ts`)
```typescript
const CLIENT_KEYWORDS = [
'rapportage', 'risico', 'behandeladvies', 'screening',
'hulpvraag', 'samenvatting', 'dossier', 'deze cliënt'
]
const DOC_KEYWORDS = [
'hoe', 'waar', 'wat is', 'tutorial', 'handleiding',
'functie', 'knop', 'menu', 'systeem', 'epd'
]
// Return: 'client' | 'documentation' | 'ambiguous'
```
**E1.S3 - Client Prompt Builder** (`lib/docs/client-prompt-builder.ts`)
- Strikte regels: alleen beschikbare data, geen hallucinatie
- Geen medisch advies
- Beknopt en professioneel
- Max 4000 tokens context
---
### Epic 2 — API Uitbreiding
**Epic Doel:** Bestaande chat endpoint uitbreiden met cliënt-awareness.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E2.S1 | Request schema uitbreiden | Accepteert optioneel `clientId` parameter | ✅ | E1.S1-S3 | 2 |
| E2.S2 | Routing logica | Bij client-vraag: client prompt, bij doc-vraag: bestaande flow | ✅ | E2.S1 | 3 |
**Technical Notes:**
**E2.S1 - Request Schema** (`app/api/docs/chat/route.ts`)
```typescript
// Huidige schema uitbreiden:
{
messages: Array<{ role: 'user' | 'assistant', content: string }>,
userMessage: string,
clientId?: string // Nieuw: UUID van actieve patiënt
}
```
**E2.S2 - Routing Logica**
```typescript
// Pseudocode:
const questionType = detectQuestionType(userMessage, !!clientId)
if (questionType === 'client' && clientId) {
const context = await loadClientContext(clientId)
const systemPrompt = buildClientPrompt(context, userMessage)
// Skip ai_events logging (privacy)
} else {
// Bestaande documentatie flow
const categories = detectCategories(userMessage)
const knowledgeSections = await loadKnowledgeSections(categories)
const systemPrompt = buildSystemPrompt(knowledgeSections)
}
```
---
### Epic 3 — Frontend Uitbreiding
**Epic Doel:** UI aanpassingen voor cliënt-indicator en dynamische suggestions.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E3.S1 | useDocsChat hook uitbreiden | Stuurt clientId mee, exposed hasPatientContext | ✅ | E2.S2 | 2 |
| E3.S2 | Cliënt Indicator | Header toont "Dossier: [Naam]" wanneer in dossier | ✅ | E3.S1 | 1 |
| E3.S3 | Dynamische Suggestions | Cliënt-suggesties in dossier, doc-suggesties daarbuiten | ✅ | E3.S1 | 2 |
**Technical Notes:**
**E3.S1 - Hook Uitbreiding** (`components/docs-chat/use-docs-chat.ts`)
```typescript
import { usePatientContext } from '@/app/epd/components/patient-context'
// In hook:
const { patient } = usePatientContext()
// Bij sendMessage:
body: JSON.stringify({
messages: recentMessages,
userMessage,
clientId: patient?.id // Meesturen als patient actief
})
// Exposed voor UI:
return {
...state,
hasPatientContext: !!patient,
patientName: patient?.name?.[0]?.text || null
}
```
**E3.S2 - Cliënt Indicator** (`components/docs-chat/docs-chat-widget.tsx`)
```tsx
{hasPatientContext && patientName && (
<div className="px-4 py-1 text-xs text-amber-700 bg-amber-50 border-b">
Dossier: {patientName}
</div>
)}
```
**E3.S3 - Dynamische Suggestions** (`components/docs-chat/chat-suggestions.tsx`)
```typescript
const CLIENT_SUGGESTION_CATEGORIES = [
{
id: 'rapportages',
label: 'Rapportages',
icon: 'FileText',
questions: [
'Geef een samenvatting van de rapportages',
'Wat is er de laatste tijd genoteerd?',
'Zijn er behandeladviezen?',
],
},
{
id: 'intake',
label: 'Intake & Behandeling',
icon: 'Building2',
questions: [
'Wat is het behandeladvies?',
'Op welke afdeling loopt de intake?',
'Is de intake afgerond?',
],
},
{
id: 'screening',
label: 'Screening',
icon: 'ClipboardList',
questions: [
'Wat was de hulpvraag?',
'Wat is de screeningbeslissing?',
'Is de cliënt geschikt bevonden?',
],
},
]
// Props toevoegen:
interface ChatSuggestionsProps {
onSelect: (question: string) => void
disabled?: boolean
mode?: 'client' | 'documentation' // Nieuw
}
```
---
### Epic 4 — Testing & Refinement
**Epic Doel:** Integratie testen en prompt verfijning.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E4.S1 | Integratie tests | Happy flows werken voor alle 3 categorieën | ✅ | E3.S3 | 2 |
| E4.S2 | Prompt tuning | AI geeft accurate, beknopte antwoorden | ✅ | E4.S1 | 2 |
**Test Scenarios:**
1. Open dossier -> chat toont indicator + cliënt-suggesties
2. Vraag "Samenvatting rapportages" -> krijg rapportage overzicht
3. Vraag "Wat is het behandeladvies?" -> krijg intake info
4. Vraag "Hoe maak ik een intake?" -> krijg documentatie antwoord
5. Buiten dossier -> chat toont doc-suggesties
---
## 5. Kwaliteit & Testplan
### Acceptatiecriteria (uit PRD)
| Criterium | Target |
|-----------|--------|
| Cliënt correct herkend | 100% (via URL/PatientContext) |
| Vraagtype correct | >90% correcte classificatie |
| Eerste token | < 3 seconden |
| Context laden | < 200ms |
| Data-integriteit | Alleen data van actieve cliënt |
### Test Checklist
- [ ] Cliënt-indicator toont correcte naam in dossier
- [ ] Cliënt-suggesties verschijnen in dossier
- [ ] Doc-suggesties verschijnen buiten dossier
- [ ] Vraag over rapportages geeft correcte samenvatting
- [ ] Vraag over risico's toont "geen data" (0 rows)
- [ ] Doc-vraag vanuit dossier werkt normaal
- [ ] Rate limiting werkt nog steeds
- [ ] Streaming werkt nog steeds
---
## 6. Bestanden Overzicht
### Te wijzigen
| Bestand | Wijziging |
|---------|-----------|
| `app/api/docs/chat/route.ts` | clientId parameter, routing logica |
| `components/docs-chat/use-docs-chat.ts` | PatientContext integratie |
| `components/docs-chat/chat-suggestions.tsx` | mode prop, client categories |
| `components/docs-chat/docs-chat-widget.tsx` | Cliënt indicator |
### Nieuw aan te maken
| Bestand | Doel |
|---------|------|
| `lib/docs/client-context-loader.ts` | Laadt cliëntdata uit Supabase |
| `lib/docs/question-type-detector.ts` | Detecteert vraagtype |
| `lib/docs/client-prompt-builder.ts` | Bouwt AI prompt met context |
### Referentie (te lezen)
| Bestand | Waarom |
|---------|--------|
| `app/epd/components/patient-context.tsx` | PatientContext API |
| `lib/docs/prompt-builder.ts` | Bestaande prompt structuur |
| `lib/docs/knowledge-loader.ts` | Bestaande knowledge loading |
---
## 7. Risico's & Mitigatie
| Risico | Kans | Impact | Mitigatie |
|--------|------|--------|-----------|
| AI hallucineert informatie | Middel | Hoog | Strikte prompt: "alleen beschikbare data" |
| Geen risk_assessments data | Zeker | Laag | "Geen data" response (feature, niet bug) |
| Token overflow | Laag | Middel | Truncatie met limit (4000 tokens) |
| Verkeerde cliëntdata | Laag | Kritiek | clientId uit PatientContext (betrouwbaar) |
| Performance degradatie | Laag | Middel | Parallel queries, geen HTTP overhead |
---
## 8. Geschatte Doorlooptijd
| Epic | Schatting |
|------|-----------|
| E1 - Backend Modules | 3-4 uur |
| E2 - API Uitbreiding | 2-3 uur |
| E3 - Frontend Uitbreiding | 2-3 uur |
| E4 - Testing & Refinement | 1-2 uur |
| **Totaal** | **8-12 uur** |
---
## Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-12-2025 | Colin Lit | Initiële versie op basis van PRD/FO/TO |
| v1.1 | 02-12-2025 | Colin Lit | E1 (Backend Modules) afgerond, E2 gestart |

View File

@@ -1,321 +0,0 @@
# Mission Control — Bouwplan AI Documentatie Assistent
**Projectnaam:** Mini-ECD AI Documentatie Assistent
**Versie:** v1.0
**Datum:** 01-12-2025
**Auteur:** Colin van der Heijden
---
## 1. Doel en context
**Doel:** Een floating chat widget bouwen die eindgebruikers van het EPD helpt door vragen te beantwoorden op basis van de systeemdocumentatie.
**Context:** Dit is de eerste AI-integratie in het Mini-ECD prototype. Het dient als fundament voor toekomstige AI features (zoals AI Pre-fill Behandelplan). De widget maakt documentatie direct toegankelijk via een conversatie-interface.
**Relatie met andere documenten:**
- PRD: `prd-ai-docs-assistent-v1.md` — Wat en waarom
- FO: `fo-ai-docs-assistent-v1.md` — Hoe het werkt voor gebruikers
---
## 2. Uitgangspunten
### 2.1 Technische Stack
| Laag | Technologie |
|------|-------------|
| **Frontend** | Next.js 14.2 + React + Tailwind CSS |
| **Backend** | Next.js API Routes (App Router) |
| **Database** | Supabase PostgreSQL (alleen voor auth check) |
| **AI** | Claude API (claude-sonnet-4-20250514) met streaming |
| **Hosting** | Vercel |
| **Icons** | Lucide React (Sparkles, X, Send) |
### 2.2 Projectkaders
| Aspect | Waarde |
|--------|--------|
| **Bouwtijd** | 1-2 dagen |
| **Team** | 1 developer |
| **Data** | Alleen bestaande MDX documentatie |
| **Scope** | MVP — chat widget met streaming responses |
| **Persistentie** | Sessie-only (geen database opslag) |
### 2.3 Programmeer Uitgangspunten
**Code Quality Principles:**
- **DRY** — Herbruikbare hook voor chat state, centrale prompt configuratie
- **KISS** — Eenvoudige fetch naar Claude API, geen SDK overhead
- **SOC** — UI componenten gescheiden van API logic en knowledge base
- **YAGNI** — Geen RAG, geen database opslag, geen multi-provider support
**Security:**
- API key alleen server-side (Next.js API route)
- Widget alleen voor ingelogde gebruikers
- Geen logging van conversaties
---
## 3. Epics & Stories Overzicht
| Epic ID | Titel | Doel | Status | Stories |
|---------|-------|------|--------|---------|
| E0 | Knowledge Base Content | FAQ's en guidelines in markdown | ✅ Done | 2 |
| E1 | Knowledge Services | CategoryDetector, KnowledgeLoader, PromptBuilder | ✅ Done | 3 |
| E2 | API Endpoint | Streaming Claude integratie | ✅ Done | 1 |
| E3 | Chat UI Components | Widget, messages, input | ✅ Done | 4 |
| E4 | Integratie & Testing | Widget in EPD, testen | ✅ Done | 2 |
**Totaal:** 12 stories, ~18 story points
---
## 4. Epics & Stories (Uitwerking)
### Epic 0 — Knowledge Base Content
**Epic Doel:** Gestructureerde FAQ's en guidelines in markdown bestanden.
| Story ID | Beschrijving | Acceptatiecriteria | Status | SP |
|----------|--------------|---------------------|--------|-----|
| E0.S1 | FAQ markdown bestanden | 6 FAQ bestanden met Q&A per categorie | ✅ | 2 |
| E0.S2 | Guidelines bestanden | 2 guideline bestanden (interface, technisch) | ✅ | 1 |
**Deliverables:**
```
lib/docs/knowledge/
├── faq_clientbeheer.md # Cliënt aanmaken, zoeken, verwijderen
├── faq_intake.md # Intake starten, notities, spraak
├── faq_screening.md # Screening resultaten, vragenlijsten
├── faq_behandelplan.md # Plan maken, doelen, interventies
├── faq_spraak.md # Microfoon, dicteren, transcriptie
├── faq_inloggen.md # Login, wachtwoord, rechten
├── guidelines_interface.md # UI uitleg, navigatie, menu's
└── guidelines_technisch.md # FHIR API, data model (devs)
```
---
### Epic 1 — Knowledge Services
**Epic Doel:** Intelligente services voor dynamische knowledge loading.
| Story ID | Beschrijving | Acceptatiecriteria | Status | SP |
|----------|--------------|---------------------|--------|-----|
| E1.S1 | CategoryDetector | Keyword matching om relevante categorieën te bepalen | ✅ | 2 |
| E1.S2 | KnowledgeLoader | Laadt alleen relevante markdown bestanden | ✅ | 2 |
| E1.S3 | PromptBuilder | Combineert base prompt + relevante knowledge | ✅ | 2 |
**Deliverables:**
```
lib/docs/
├── category-detector.ts # Analyseert vraag → categorieën
├── knowledge-loader.ts # Laadt relevante knowledge
└── prompt-builder.ts # Bouwt geoptimaliseerde prompt
```
**Category Mapping:**
```typescript
const CATEGORY_KEYWORDS: Record<Category, string[]> = {
clientbeheer: ['cliënt', 'patient', 'aanmaken', 'zoeken', 'dossier'],
intake: ['intake', 'gesprek', 'notitie', 'verslag'],
screening: ['screening', 'vragenlijst', 'score', 'resultaat'],
behandelplan: ['behandelplan', 'doel', 'interventie', 'plan'],
spraak: ['spraak', 'microfoon', 'dicteren', 'stem', 'transcriptie'],
inloggen: ['inloggen', 'wachtwoord', 'login', 'account'],
interface: ['menu', 'knop', 'scherm', 'navigatie', 'waar vind'],
technisch: ['api', 'fhir', 'endpoint', 'database', 'developer']
};
```
**Flow:**
```
Vraag: "Hoe maak ik een intake aan?"
CategoryDetector → ['intake']
KnowledgeLoader → laadt faq_intake.md
PromptBuilder → base prompt + intake FAQ
Claude API → streaming response
```
---
### Epic 2 — API Endpoint
**Epic Doel:** Streaming API endpoint met dynamische knowledge.
| Story ID | Beschrijving | Acceptatiecriteria | Status | SP |
|----------|--------------|---------------------|--------|-----|
| E2.S1 | Streaming endpoint | `POST /api/docs/chat` met dynamic knowledge, SSE stream | ✅ | 3 |
**Deliverables:**
```
app/api/docs/chat/
route.ts # Streaming API endpoint
```
**API Contract:**
```typescript
// Request
POST /api/docs/chat
{
messages: Array<{ role: 'user' | 'assistant', content: string }>,
userMessage: string
}
// Response: Server-Sent Events stream
event: content_block_delta
data: {"delta":{"text":"..."}}
```
---
### Epic 3 — Chat UI Components
**Epic Doel:** Complete chat widget UI volgens FO specificaties.
| Story ID | Beschrijving | Acceptatiecriteria | Status | SP |
|----------|--------------|---------------------|--------|-----|
| E3.S1 | Chat state hook | `use-docs-chat.ts` met messages, loading, sendMessage, streaming | ✅ | 2 |
| E3.S2 | Message list component | `chat-messages.tsx` met styling, auto-scroll, streaming cursor | ✅ | 1 |
| E3.S3 | Input component | `chat-input.tsx` met textarea, send, Enter/Shift+Enter | ✅ | 1 |
| E3.S4 | Widget container | `docs-chat-widget.tsx` met trigger, panel, header, animaties | ✅ | 2 |
**Deliverables:**
```
components/docs-chat/
use-docs-chat.ts # Custom hook
chat-messages.tsx # Message list
chat-input.tsx # Input area
docs-chat-widget.tsx # Main container
```
**UI Specs (uit FO):**
| Element | Specificatie |
|---------|--------------|
| Trigger button | 56x56px, amber gradient, Sparkles icon, `fixed bottom-6 right-6` |
| Panel | 384px breed, max 80vh, slide-in animatie |
| User messages | Rechts, `bg-amber-100`, rounded |
| Assistant messages | Links, `bg-slate-100`, rounded |
| Streaming | Pulserende cursor `▊` |
---
### Epic 4 — Integratie & Testing
**Epic Doel:** Widget geïntegreerd in EPD en getest.
| Story ID | Beschrijving | Acceptatiecriteria | Status | SP |
|----------|--------------|---------------------|--------|-----|
| E4.S1 | Widget integratie | `DocsChatWidget` in EPD layout, alleen ingelogde users | ✅ | 1 |
| E4.S2 | Smoke tests | Happy flow, error states, category detection werkt | ✅ | 1 |
**Te wijzigen bestand:**
```
app/epd/components/epd-layout-client.tsx
```
---
## 5. Kwaliteit & Testplan
### Test Types
| Test Type | Scope | Methode |
|-----------|-------|---------|
| Unit | Knowledge base loader | Console test |
| Integration | API endpoint | curl/Postman |
| Smoke | Volledige flow | Manual in browser |
### Manual Test Checklist
- [ ] Widget trigger button zichtbaar in EPD
- [ ] Klik opent chat panel met animatie
- [ ] Welkomstbericht wordt getoond
- [ ] Vraag versturen werkt (Enter + button)
- [ ] Streaming response verschijnt woord-voor-woord
- [ ] Vervolgvraag behoudt context
- [ ] X-knop sluit panel
- [ ] Conversatie blijft behouden na sluiten/openen
- [ ] Error state toont bij API failure
- [ ] Widget verdwijnt bij uitloggen
### Acceptatiecriteria (uit PRD)
| Criterium | Target |
|-----------|--------|
| First token | < 2 seconden |
| Volledige response | < 30 seconden |
| Error rate | < 5% |
---
## 6. Demo & Presentatieplan
**Duur:** 3 minuten
**Scenario:**
1. **Intro** (30s): "Dit is de documentatie assistent"
2. **Vraag 1** (45s): "Hoe maak ik een intake aan?" → streaming antwoord
3. **Vraag 2** (45s): "En hoe gebruik ik spraakherkenning?" → context behouden
4. **Edge case** (30s): "Wat is de beste behandeling?" → "weet ik niet" response
5. **Afsluiting** (30s): Widget sluiten, conversatie behouden
---
## 7. Risico's & Mitigatie
| Risico | Kans | Impact | Mitigatie |
|--------|------|--------|-----------|
| AI hallucineert | Middel | Hoog | Strikte system prompt, FAQ's eerst, "alleen uit docs" regel |
| Trage response | Laag | Middel | Streaming UX, timeout na 30s |
| Context te groot | Laag | Middel | ~119KB past in context window, monitoring |
| API rate limits | Laag | Middel | Sessie-based (geen caching nodig) |
---
## 8. Referenties
### Mission Control Documents
- **PRD:** `prd-ai-docs-assistent-v1.md`
- **FO:** `fo-ai-docs-assistent-v1.md`
- **Gerelateerd:** `prd-ai-prefill-behandelplan-v1.md`
### Bestaande Code Patterns
| Bestand | Pattern |
|---------|---------|
| `app/api/reports/classify/route.ts` | Claude API fetch pattern |
| `lib/mdx/documentatie.ts` | MDX loading met gray-matter |
| `components/ui/ai-button.tsx` | Amber AI styling |
### Externe Referenties
- [Claude API Docs](https://docs.anthropic.com)
- [Anthropic Streaming](https://docs.anthropic.com/en/api/streaming)
---
## 9. Glossary
| Term | Betekenis |
|------|-----------|
| SSE | Server-Sent Events (streaming protocol) |
| Knowledge Base | Verzameling documentatie voor AI context |
| FAQ | Frequently Asked Questions |
| Streaming | Real-time response, woord-voor-woord |
---
**Versiehistorie:**
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-12-2025 | Colin van der Heijden | Initiële versie |

View File

@@ -0,0 +1,367 @@
# 🧩 Functioneel Ontwerp (FO) AI Cliënt Assistent
**Projectnaam:** Mini-ECD AI Cliënt Assistent
**Versie:** v1.0
**Datum:** 01-12-2025
**Auteur:** Colin Lit
---
## 1. Doel en relatie met het PRD
**Doel van dit document:**
Dit FO beschrijft hoe de AI Cliënt Assistent functioneel werkt vanuit gebruikersperspectief. Het PRD beschrijft *wat* we bouwen (cliënt-aware chat), dit FO laat zien *hoe* de gebruiker dit ervaart.
**Scope (prototype):**
Gebaseerd op de huidige data in het EPD:
- **21 rapportages** (16 vrije notities, 5 behandeladviezen)
- **9 intakes** (7 Volwassenen, 1 Jeugd)
- **5 screenings** (1 geschikt, 1 niet geschikt, 3 open)
- **0 risico-assessments** (tabel bestaat, geen data)
---
## 2. Overzicht van de onderdelen
| Onderdeel | Beschrijving | Status |
|-----------|--------------|--------|
| **Chat Widget** | Bestaande floating widget rechtsonder | Uitbreiden |
| **Cliënt Indicator** | Header die toont welke cliënt actief is | Nieuw |
| **Cliënt Suggesties** | Voorbeeldvragen over de actieve cliënt | Nieuw |
| **Vraagtype Detectie** | Herkent of vraag over cliënt of systeem gaat | Nieuw |
---
## 3. User Stories
| ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit |
|----|-----|--------------|------------------|------------|
| US-01 | Behandelaar | Samenvatting van rapportages opvragen | Snel overzicht voor consult | Hoog |
| US-02 | Behandelaar | Behandeladvies opvragen | Inzicht in geadviseerde zorg | Hoog |
| US-03 | Verpleegkundige | Recente notities bekijken | Overdracht voorbereiding | Hoog |
| US-04 | Intaker | Hulpvraag en screening status opvragen | Intake afronden | Middel |
| US-05 | Behandelaar | Documentatie-vraag stellen vanuit dossier | Hulp bij EPD gebruik | Middel |
---
## 4. Functionele werking per onderdeel
### 4.1 Chat Widget (uitgebreid)
**Huidige situatie:**
- Floating button rechtsonder (amber, sparkles icon)
- Beantwoordt alleen documentatie-vragen
- Toont 3 categorieën met voorbeeldvragen
**Nieuwe situatie:**
- Detecteert automatisch of gebruiker in cliëntdossier zit
- Toont cliënt-indicator in header wanneer in dossier
- Schakelt tussen cliënt- en documentatie-suggesties
- Beantwoordt vragen over de actieve cliënt
### 4.2 Cliënt Indicator
**Locatie:** Header van chat widget, onder "EPD Assistent"
**Weergave:**
```
┌─────────────────────────────┐
│ ✨ EPD Assistent │
│ 📋 Dossier: Jan de Vries │ ← Nieuw: cliënt indicator
└─────────────────────────────┘
```
**Gedrag:**
| Context | Indicator |
|---------|-----------|
| In cliëntdossier | `📋 Dossier: [Cliëntnaam]` |
| Buiten dossier | Geen indicator (alleen "EPD Assistent") |
### 4.3 Cliënt Suggesties
**Wanneer tonen:** Bij eerste opening chat in cliëntdossier, alleen welkomstbericht zichtbaar
**Categorieën en vragen (gebaseerd op beschikbare data):**
| Categorie | Icon | Voorbeeldvragen |
|-----------|------|-----------------|
| **Rapportages** | 📝 | "Geef een samenvatting van de rapportages", "Wat is er de laatste tijd genoteerd?", "Zijn er behandeladviezen?" |
| **Intake & Behandeling** | 🏥 | "Wat is het behandeladvies?", "Op welke afdeling loopt de intake?", "Is de intake afgerond?" |
| **Screening** | 📋 | "Wat was de hulpvraag?", "Wat is de screeningbeslissing?", "Is de cliënt geschikt bevonden?" |
**Interactie:**
1. Gebruiker ziet 3 categorieën (knoppen)
2. Klik op categorie → toont 3 voorbeeldvragen
3. Klik op vraag → vraag wordt direct verstuurd
4. "Terug" knop om naar categorieën te gaan
### 4.4 Vraagtype Detectie
**Doel:** Bepalen of een vraag over de cliënt of over het systeem gaat
**Gedrag:**
| Vraag | Detectie | Actie |
|-------|----------|-------|
| "Geef een samenvatting van de rapportages" | Cliënt | Beantwoord met cliëntdata |
| "Hoe maak ik een intake aan?" | Documentatie | Beantwoord met systeemdocumentatie |
| "Wat zijn de risico's?" | Cliënt | Beantwoord met cliëntdata (of "geen data") |
| "Hoe werkt de spraakherkenning?" | Documentatie | Beantwoord met systeemdocumentatie |
**Edge cases:**
| Situatie | Gedrag |
|----------|--------|
| Ambigue vraag in dossier | Default naar documentatie, toon hint |
| Cliënt-vraag buiten dossier | "Open eerst een cliëntdossier om vragen te stellen" |
| Data ontbreekt | "Er zijn nog geen [rapportages/risico's] voor deze cliënt" |
---
## 5. UI-overzicht
### 5.1 Chat Widget Layout
```
┌─────────────────────────────────────┐
│ ✨ EPD Assistent [X] │ ← Header
│ 📋 Dossier: Jan de Vries │ ← Cliënt indicator (nieuw)
├─────────────────────────────────────┤
│ │
│ [Welkomstbericht] │ ← Messages area
│ │
│ [Gebruiker vraag] → │
│ [Assistent antwoord] ← │
│ │
├─────────────────────────────────────┤
│ Kies een onderwerp: │ ← Suggesties (context-aware)
│ [📝 Rapportages] │
│ [🏥 Intake & Behandeling] │
│ [📋 Screening] │
├─────────────────────────────────────┤
│ [Typ een vraag... ] [Send] │ ← Input
└─────────────────────────────────────┘
```
### 5.2 Suggestie Flow (twee stappen)
**Stap 1: Categorieën**
```
Kies een onderwerp:
┌──────────────────────┐
│ 📝 Rapportages │
├──────────────────────┤
│ 🏥 Intake & Behandeling │
├──────────────────────┤
│ 📋 Screening │
└──────────────────────┘
```
**Stap 2: Vragen (na klik op categorie)**
```
← Terug
📝 Rapportages
┌────────────────────────────────────┐
│ Geef een samenvatting van de │
│ rapportages │
├────────────────────────────────────┤
│ Wat is er de laatste tijd genoteerd?│
├────────────────────────────────────┤
│ Zijn er behandeladviezen? │
└────────────────────────────────────┘
```
### 5.3 Context Switching
| Locatie in EPD | Chat Gedrag |
|----------------|-------------|
| Dashboard (geen cliënt) | Documentatie-modus, standaard suggesties |
| Cliëntdossier | Cliënt-modus, cliënt-indicator, cliënt-suggesties |
| Cliënt → Dashboard | Terug naar documentatie-modus |
---
## 6. Interacties met AI
### 6.1 Cliënt-vragen
| Vraag | AI Context | Verwacht Antwoord |
|-------|------------|-------------------|
| "Samenvatting rapportages" | Laatste 5 reports | Bullets per rapportage met datum en type |
| "Wat is het behandeladvies?" | intakes.treatment_advice | Advies, programma, afdeling |
| "Wat was de hulpvraag?" | screenings.request_for_help | Hulpvraag tekst of "niet ingevuld" |
| "Zijn er risico's?" | risk_assessments (leeg) | "Er zijn nog geen risico-assessments" |
### 6.2 Voorbeeld Antwoorden
**Vraag: "Geef een samenvatting van de rapportages"**
```
De laatste rapportages voor Jan de Vries:
📝 28-11-2025 - Vrije notitie
Cliënt ervaart stemming als overwegend somber, maar benoemt ook
positieve momenten tijdens sport. Slaapproblemen blijven aandachtspunt.
📝 25-11-2025 - Vrije notitie
Gesprek over terugvalpreventie. Cliënt toont inzet en motivatie.
📋 22-11-2025 - Behandeladvies
Doorzetten naar behandeling, FACT programma, afdeling Volwassenen.
```
**Vraag: "Wat is het behandeladvies?"**
```
Het behandeladvies voor Jan de Vries:
• Advies: Doorzetten naar behandeling
• Programma: FACT
• Afdeling: Volwassenen
• Uitkomst: In zorg
Dit advies is vastgelegd bij intake "Intake Depressie" (afgerond).
```
**Vraag: "Wat zijn de risico's?"**
```
Er zijn nog geen risico-assessments vastgelegd voor Jan de Vries.
Risico-assessments kunnen worden toegevoegd via de intake of
het tabblad 'Risico's' in het cliëntdossier.
```
### 6.3 Foutafhandeling
| Situatie | Gebruiker ziet |
|----------|----------------|
| Geen rapportages | "Er zijn nog geen rapportages voor [naam]" |
| API error | "Er ging iets mis. Probeer het opnieuw." |
| Rate limit bereikt | Countdown timer + uitleg (bestaand) |
---
## 7. Gebruikersrollen en rechten
**Prototype scope:** Alle ingelogde gebruikers hebben dezelfde rechten.
| Rol | Toegang Chat | Cliënt Data |
|-----|--------------|-------------|
| Behandelaar | ✅ | Eigen cliënten (via RLS) |
| Demo-user | ✅ | Fictieve demo-cliënten |
**Security:**
- Cliënt-ID komt uit URL/PatientContext (betrouwbaar)
- RLS policies op database niveau
- Geen cliëntdata in logs
---
## 8. Configuratie Suggesties
### 8.1 Cliënt Suggesties (nieuw)
```typescript
const CLIENT_SUGGESTION_CATEGORIES = [
{
id: 'rapportages',
label: 'Rapportages',
icon: '📝',
questions: [
'Geef een samenvatting van de rapportages',
'Wat is er de laatste tijd genoteerd?',
'Zijn er behandeladviezen?',
],
},
{
id: 'intake',
label: 'Intake & Behandeling',
icon: '🏥',
questions: [
'Wat is het behandeladvies?',
'Op welke afdeling loopt de intake?',
'Is de intake afgerond?',
],
},
{
id: 'screening',
label: 'Screening',
icon: '📋',
questions: [
'Wat was de hulpvraag?',
'Wat is de screeningbeslissing?',
'Is de cliënt geschikt bevonden?',
],
},
]
```
### 8.2 Documentatie Suggesties (bestaand, behouden)
```typescript
const DOC_SUGGESTION_CATEGORIES = [
{
id: 'clienten',
label: 'Cliënten & Dossiers',
icon: '👤',
questions: [
'Hoe maak ik een nieuwe cliënt aan?',
'Hoe zoek ik een bestaande cliënt?',
'Hoe open ik een cliëntdossier?',
],
},
// ... bestaande categorieën
]
```
---
## 9. Acceptatiecriteria
### 9.1 Functioneel
| Criterium | Test |
|-----------|------|
| Cliënt-indicator toont correcte naam | Open dossier → check header |
| Cliënt-suggesties verschijnen in dossier | Open chat in dossier → zie 3 categorieën |
| Documentatie-suggesties buiten dossier | Open chat op dashboard → zie bestaande categorieën |
| Vraag over rapportages werkt | Stel vraag → ontvang samenvatting |
| Ontbrekende data wordt gemeld | Vraag naar risico's → "geen data" bericht |
### 9.2 Niet-functioneel
| Criterium | Target |
|-----------|--------|
| Eerste antwoord | < 3 seconden |
| Context laden | < 200ms |
| Correcte cliënt | 100% (via URL) |
---
## 10. Bijlagen & Referenties
### Projectdocumenten
| Document | Locatie |
|----------|---------|
| PRD | `docs/specs/ai-integratie/prd-ai-client-assistent-v1.md` |
| TO | `docs/specs/ai-integratie/to-ai-client-assistent-v1.md` |
| Bestaande chat widget | `components/docs-chat/docs-chat-widget.tsx` |
| Bestaande suggesties | `components/docs-chat/chat-suggestions.tsx` |
### Data beschikbaarheid (prototype)
| Tabel | Rows | Bruikbaar voor vragen |
|-------|------|----------------------|
| reports | 21 | ✅ Samenvatting rapportages |
| intakes | 9 | ✅ Behandeladvies, status |
| screenings | 5 | ✅ Hulpvraag, beslissing |
| risk_assessments | 0 | ⚠️ "Geen data" response |
---
## Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-12-2025 | Colin Lit | Initiële versie, prototype scope |

View File

@@ -1,386 +0,0 @@
# Functioneel Ontwerp (FO) AI Documentatie Assistent
**Projectnaam:** Mini-ECD AI Documentatie Assistent
**Versie:** v1.0
**Datum:** 01-12-2025
**Auteur:** Colin van der Heijden
---
## 1. Doel en relatie met het PRD
**Doel van dit document:**
Dit Functioneel Ontwerp beschrijft **hoe** de AI Documentatie Assistent functioneel werkt — wat de gebruiker ziet, doet en ervaart. Waar het PRD (`prd-ai-docs-assistent-v1.md`) uitlegt *wat en waarom*, laat dit FO zien *hoe dit in de praktijk werkt*.
**Toelichting aan de lezer:**
De AI Documentatie Assistent is een floating chat widget die eindgebruikers van het EPD helpt door vragen te beantwoorden op basis van de systeemdocumentatie. Dit is de eerste AI-integratie in het Mini-ECD prototype en dient als fundament voor toekomstige AI features.
---
## 2. Overzicht van de belangrijkste onderdelen
1. **Floating Trigger Button** — Amber knop rechtsonder om widget te openen
2. **Chat Panel** — Uitklapbaar gesprekspaneel
3. **Message List** — Weergave van conversatie (gebruiker + assistent)
4. **Input Area** — Tekstveld voor vragen stellen
5. **Streaming Response** — Real-time weergave van AI antwoorden
---
## 3. Userstories
| ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit |
|----|------|---------------|------------------|-------------|
| US-01 | Behandelaar | Vraag stellen over EPD functie | Direct antwoord zonder zoeken | Hoog |
| US-02 | Verpleegkundige | Uitleg krijgen over onbekende functie | Zelfstandig werken zonder collega's te storen | Hoog |
| US-03 | Nieuwe medewerker | Systeem leren kennen via vragen | Interactieve onboarding | Hoog |
| US-04 | Behandelaar | Vervolgvraag stellen | Context behouden in gesprek | Middel |
| US-05 | Developer | Technische vraag over API | Snelle referentie zonder docs te openen | Middel |
| US-06 | Alle gebruikers | Widget sluiten | Terug naar werk zonder afleiding | Hoog |
**User Story Details:**
> **US-01:** Als behandelaar wil ik een vraag kunnen stellen over het EPD zodat ik direct antwoord krijg zonder door documentatie te hoeven zoeken.
> **US-02:** Als verpleegkundige wil ik uitleg kunnen vragen over een functie die ik niet ken zodat ik zelfstandig verder kan werken.
> **US-03:** Als nieuwe medewerker wil ik via vragen het systeem leren kennen zodat ik sneller productief ben.
---
## 4. Functionele werking per onderdeel
### 4.1 Floating Trigger Button
**Locatie:** Rechtsonder in het scherm, altijd zichtbaar binnen EPD (`/epd/*` routes)
**Gedrag:**
- Amber gradient knop (56x56px) met Sparkles icon
- Hover: lichte kleurverandering
- Klik: opent chat panel, knop verdwijnt
- Altijd bovenop andere content (z-index: 50)
**States:**
| State | Weergave |
|-------|----------|
| Default | Amber gradient met wit icon |
| Hover | Donkerder amber |
| Widget open | Knop verborgen |
---
### 4.2 Chat Panel
**Afmetingen:** 384px breed × max 80vh hoog
**Structuur:**
```
┌────────────────────────────────────┐
│ Header: titel + sluit-knop │
├────────────────────────────────────┤
│ │
│ Message List (scrollbaar) │
│ │
│ │
├────────────────────────────────────┤
│ Input Area: tekstveld + verzenden │
└────────────────────────────────────┘
```
**Header:**
- Sparkles icon + "Documentatie Assistent" tekst
- X-knop rechts om te sluiten
- Amber/amber-100 achtergrond gradient
**Gedrag bij openen:**
1. Panel verschijnt met slide-in animatie (van onder)
2. Welkomstbericht wordt getoond (indien eerste keer)
3. Focus gaat naar input veld
**Gedrag bij sluiten:**
- Klik op X-knop → panel verdwijnt
- Trigger button verschijnt weer
- Conversatie blijft behouden (sessie)
---
### 4.3 Message List
**Weergave van berichten:**
| Type | Positie | Styling |
|------|---------|---------|
| Gebruiker | Rechts uitgelijnd | `bg-amber-100`, rounded |
| Assistent | Links uitgelijnd | `bg-slate-100`, rounded |
**Welkomstbericht (eerste bericht):**
```
Hallo! Ik ben de documentatie assistent voor het Mini-ECD.
Stel gerust vragen over hoe het systeem werkt, bijvoorbeeld:
• Hoe maak ik een nieuwe intake aan?
• Hoe werkt de spraakherkenning?
• Waar vind ik de screening resultaten?
```
**Scroll gedrag:**
- Automatisch scrollen naar nieuwste bericht
- Gebruiker kan omhoog scrollen door historie
- Bij nieuw bericht: scroll naar beneden
**Streaming weergave:**
- Tekst verschijnt woord-voor-woord
- Pulserende cursor aan einde tijdens streaming
- Cursor verdwijnt wanneer response compleet is
---
### 4.4 Input Area
**Componenten:**
- Textarea (auto-resize, max 4 regels)
- Verzend-knop (amber, pijl icon)
**Interacties:**
| Actie | Resultaat |
|-------|-----------|
| Enter | Verstuur bericht |
| Shift + Enter | Nieuwe regel |
| Klik verzend-knop | Verstuur bericht |
| Leeg bericht versturen | Geen actie |
**States:**
| State | Textarea | Verzend-knop |
|-------|----------|--------------|
| Idle | Enabled, placeholder | Enabled (amber) |
| Typing | Enabled, tekst zichtbaar | Enabled |
| Loading | Disabled | Disabled (grijs) |
| Error | Enabled | Enabled |
**Placeholder tekst:** "Stel een vraag..."
---
### 4.5 Streaming Response
**Proces:**
1. Gebruiker verstuurt vraag
2. Input wordt disabled
3. Nieuw assistent-bericht verschijnt (leeg)
4. Tekst streamt woord-voor-woord in
5. Bij completion: input wordt enabled
**Visuele feedback tijdens streaming:**
- Pulserende cursor (`▊`) aan einde van tekst
- Tekst verschijnt met ~50ms interval per chunk
**Timeout:**
- Na 30 seconden zonder response: toon foutmelding
- Gebruiker kan opnieuw proberen
---
## 5. UI-overzicht (visuele structuur)
### Widget Gesloten
```
┌─────────────────────────────────────────────────┐
│ │
│ EPD Interface │
│ │
│ │
│ │
│ ┌─────┐ │
│ │ ✨ │ │
│ └─────┘ │
└─────────────────────────────────────────────────┘
Trigger Button
```
### Widget Open
```
┌─────────────────────────────────────────────────┐
│ │
│ EPD Interface │
│ │
│ ┌────────────────────────┤
│ │ ✨ Docs Assistent ✕ │
│ ├────────────────────────┤
│ │ Welkomstbericht... │
│ │ │
│ │ ┌──────────────────┐ │
│ │ │ Hoe maak ik... │←──│── User
│ │ └──────────────────┘ │
│ │ │
│ │ ┌──────────────────┐ │
│ │ │ Om een intake... │←──│── Assistant
│ │ │ ... │ │
│ │ └──────────────────┘ │
│ ├────────────────────────┤
│ │ [Stel een vraag...] ➤ │
│ └────────────────────────┘
└─────────────────────────────────────────────────┘
```
---
## 6. Interacties met AI (functionele beschrijving)
| Locatie | AI-actie | Trigger | Output |
|---------|----------|---------|--------|
| Chat widget | Vraag beantwoorden | Gebruiker verstuurt bericht | Streaming tekst-antwoord |
| Chat widget | Vervolgvraag beantwoorden | Gebruiker stuurt vervolgvraag | Context-aware antwoord |
| Chat widget | Buiten scope afhandelen | Vraag niet in documentatie | Eerlijk "weet ik niet" + suggesties |
### AI Gedragsregels
**Wel doen:**
- Antwoorden baseren op de 14 MDX documentatiebestanden
- Nederlands schrijven
- Bullet points gebruiken voor stappen
- Verwijzen naar specifieke menu's en knoppen
- Eerlijk zeggen als informatie ontbreekt
**Niet doen:**
- Informatie verzinnen die niet in de documentatie staat
- Medisch advies geven
- Behandelsuggesties doen
- Engels antwoorden (tenzij gevraagd)
### Beschikbare Knowledge Base
De assistent heeft toegang tot deze documentatie:
| Bestand | Onderwerp |
|---------|-----------|
| `authentication.mdx` | Inloggen en authenticatie |
| `client-management.mdx` | Cliëntbeheer |
| `intake-system.mdx` | Intake proces |
| `screening-system.mdx` | Screening functionaliteit |
| `treatment-planning.mdx` | Behandelplannen |
| `interface-design.mdx` | UI uitleg |
| `spraakgestuurde-verslaglegging.mdx` | Spraakfuncties (NL) |
| `voice-controlled-reporting.mdx` | Spraakfuncties (EN) |
| `verpleegkundige-overdracht.mdx` | Overdracht workflow |
| `fhir-datamodel.mdx` | Data model |
| `fhir-api.mdx` | API documentatie |
| `release-notes-system.mdx` | Release notes |
| `build-errors-fix.mdx` | Troubleshooting |
| `webpack-module-resolution.mdx` | Technische docs |
---
## 7. Gebruikersrollen en rechten
| Rol | Toegang tot widget | Beperkingen |
|-----|-------------------|-------------|
| Behandelaar | Ja, binnen EPD | Geen |
| Verpleegkundige | Ja, binnen EPD | Geen |
| Admin | Ja, binnen EPD | Geen |
| Niet-ingelogd | Nee | Widget niet zichtbaar |
**Authenticatie:** Widget is alleen zichtbaar voor ingelogde gebruikers binnen `/epd/*` routes.
---
## 8. Error States en Edge Cases
### Error States
| Situatie | Weergave | Gebruikersactie |
|----------|----------|-----------------|
| API timeout (>30s) | "Het duurt langer dan verwacht. Probeer het opnieuw." | Retry knop |
| API error | "Er ging iets mis. Probeer het later opnieuw." | Retry knop |
| Netwerk offline | "Geen internetverbinding." | - |
| Rate limit | "Te veel verzoeken. Wacht even." | Automatisch retry |
### Edge Cases
| Situatie | Gedrag |
|----------|--------|
| Zeer lange vraag (>2000 chars) | Vraag wordt afgekapt met waarschuwing |
| Lege vraag | Verzenden geblokkeerd |
| Alleen spaties | Behandeld als lege vraag |
| Snelle opeenvolgende vragen | Vorige request wordt geannuleerd |
| Browser tab wisselen | Streaming gaat door |
| Page refresh | Conversatie verloren (sessie-based) |
---
## 9. Bijlagen & Referenties
### Project Documentatie
- PRD AI Documentatie Assistent (`prd-ai-docs-assistent-v1.md`)
- PRD AI Pre-fill Behandelplan (`prd-ai-prefill-behandelplan-v1.md`)
- UX Stylesheet (`ux-stylesheet.md`)
- Technisch Ontwerp Mini-ECD (`to-mini-ecd-v1_2.md`)
### Externe Referenties
- [Claude API Documentation](https://docs.anthropic.com)
- [Anthropic Streaming Guide](https://docs.anthropic.com/en/api/streaming)
---
## Appendix A: Conversatie Voorbeelden
### Voorbeeld 1: Intake Vraag
**Gebruiker:** "Hoe maak ik een nieuwe intake aan?"
**Assistent:**
```
Om een nieuwe intake aan te maken:
1. Ga naar het cliëntdossier via **Cliënten** in het hoofdmenu
2. Selecteer de gewenste cliënt
3. Klik op het tabblad **Intake**
4. Klik op de knop **Nieuwe Intake**
5. Vul de intake-informatie in het formulier
6. Klik op **Opslaan** om de intake te bewaren
Je kunt ook spraakherkenning gebruiken om de intake
in te spreken via de microfoon-knop.
```
### Voorbeeld 2: Buiten Scope
**Gebruiker:** "Wat is de beste behandeling voor depressie?"
**Assistent:**
```
Die informatie heb ik niet. Ik kan alleen helpen met
vragen over hoe het Mini-ECD systeem werkt.
Misschien kan ik je helpen met:
• Hoe maak ik een behandelplan aan?
• Hoe voeg ik een diagnose toe?
• Hoe werkt de screening functie?
```
### Voorbeeld 3: Technische Vraag
**Gebruiker:** "Welke FHIR resources worden ondersteund?"
**Assistent:**
```
Het Mini-ECD ondersteunt de volgende FHIR R4 resources:
**Kern resources:**
• Patient — Cliëntgegevens
• Practitioner — Behandelaren
• Encounter — Contactmomenten
• Condition — Diagnoses/problemen
• Observation — Metingen en observaties
• CarePlan — Behandelplannen
Zie de API documentatie voor endpoints en voorbeelden.
```
---
*Document gegenereerd als onderdeel van AI Speedrun — Week 3*

View File

@@ -0,0 +1,205 @@
# 📄 Product Requirements Document (PRD) AI Cliënt Assistent
**Projectnaam:** Mini-ECD AI Cliënt Assistent
**Versie:** v1.0
**Datum:** 01-12-2025
**Auteur:** Colin Lit
---
## 1. Doelstelling
**Probleem:** Behandelaren besteden veel tijd aan het navigeren door verschillende schermen om informatie over een cliënt te verzamelen. Bij een overdracht of voorbereiding op een consult moeten zij:
- Rapportages doorbladeren
- Risico-assessments opzoeken
- Behandeladviezen teruglezen
- Screeningresultaten checken
**Oplossing:** De bestaande AI Documentatie Assistent uitbreiden met cliënt-awareness. Wanneer een behandelaar in een cliëntdossier zit, kan de assistent vragen beantwoorden over díe specifieke cliënt.
**Voorbeeld interacties:**
> "Geef een samenvatting van de laatste rapportages"
> "Wat zijn de risico's van deze cliënt?"
> "Wat staat er in het behandeladvies?"
**Type:** MVP-uitbreiding op bestaande feature (AI Documentatie Assistent)
---
## 2. Doelgroep
| Rol | Situatie | Behoefte |
|-----|----------|----------|
| **Behandelaar** | Voorbereiding op consult | Snel overzicht van recente rapportages en behandeladvies |
| **Verpleegkundige** | Overdracht dienst | Risico's en actuele status checken |
| **Intaker** | Afsluiten intake | Samenvatting van screeningresultaat en hulpvraag |
| **Regiebehandelaar** | Caseload review | Per cliënt snel de status kunnen opvragen |
**Kernbehoefte:** Informatie opvragen via natuurlijke taal, zonder te navigeren door meerdere schermen.
---
## 3. Kernfunctionaliteiten (MVP-scope)
### 3.1 Automatische cliënt-herkenning
De assistent weet automatisch over welke cliënt je praat op basis van het dossier waarin je zit. Geen handmatige selectie nodig.
**Gedrag:**
- In dossier van Jan de Vries → assistent beantwoordt vragen over Jan de Vries
- Buiten cliëntdossier → assistent beantwoordt alleen documentatie-vragen
### 3.2 Cliënt-indicator in chat
De gebruiker ziet duidelijk dat de assistent in "cliënt-modus" staat:
```
┌─────────────────────────────┐
│ ✨ EPD Assistent │
│ 📋 Dossier: Jan de Vries │ ← Zichtbaar wanneer in dossier
└─────────────────────────────┘
```
### 3.3 Ondersteunde vragen
| Categorie | Voorbeeldvragen |
|-----------|-----------------|
| **Rapportages** | "Samenvatting van de rapportages", "Wat is er de laatste tijd genoteerd?" |
| **Risico's** | "Wat zijn de risico's?", "Is er suïciderisico?" |
| **Behandeladvies** | "Wat is het behandeladvies?", "Welke zorg is geadviseerd?" |
| **Screening** | "Wat was de hulpvraag?", "Is de screening afgerond?" |
| **Overzicht** | "Geef een samenvatting van dit dossier" |
### 3.4 Context-aware suggesties
Wanneer je in een cliëntdossier zit, toont de assistent relevante voorbeeldvragen:
- "Geef een samenvatting van de rapportages"
- "Wat zijn de risico's?"
- "Wat staat in het behandeladvies?"
### 3.5 Gescheiden vraagtypen
De assistent beantwoordt óf vragen over de cliënt óf vragen over het systeem, niet gemengd. Dit voorkomt verwarring.
| Vraag | Type | Antwoord gebaseerd op |
|-------|------|----------------------|
| "Wat zijn de risico's?" | Cliënt | Dossiergegevens |
| "Hoe maak ik een intake aan?" | Systeem | Documentatie |
---
## 4. Gebruikersflows
### Flow 1: Snelle cliënt-check voor consult
```
Behandelaar opent dossier van cliënt
Ziet chat-widget rechtsonder, header toont "Dossier: Jan de Vries"
Klikt op suggestie "Wat zijn de risico's?"
Assistent toont overzicht: "Jan heeft 2 risico-assessments:
• Suïciderisico: laag (beoordeeld 15-11-2025)
• Agressierisico: middel (beoordeeld 10-11-2025)"
```
### Flow 2: Overdracht voorbereiding
```
Verpleegkundige opent dossier
Vraagt: "Geef een samenvatting van de laatste rapportages"
Assistent toont: "De laatste 3 rapportages:
• 28-11: Stabiele stemming, medicatie ongewijzigd
• 25-11: Gesprek over terugvalpreventie
• 22-11: Contactmoment familie, zorgen over isolatie"
```
### Flow 3: Documentatie-vraag vanuit dossier
```
Gebruiker is in dossier maar vraagt: "Hoe werkt de spraakherkenning?"
Systeem herkent: dit is een documentatie-vraag
Bestaande documentatie-flow wordt gevolgd
Antwoord komt uit systeemdocumentatie, niet uit cliëntdossier
```
---
## 5. Niet in Scope
| Uitgesloten | Reden |
|-------------|-------|
| **Schrijven naar dossier** | Privacy, audit trail vereisten |
| **Medisch advies geven** | Liability, AI mag niet adviseren |
| **Multi-cliënt vergelijkingen** | Complexiteit, privacy |
| **Historische trends** | "Hoe ging het vorige maand?" - te complex voor MVP |
| **Bijlagen/PDF's lezen** | Technische complexiteit |
| **Gemengde vragen** | "Hoe maak ik een intake voor deze cliënt?" - te ambigu |
---
## 6. Succescriteria
| Criterium | Meetbaar doel |
|-----------|---------------|
| **Cliënt correct herkend** | 100% - als je in dossier zit, moet juiste cliënt actief zijn |
| **Vraagtype correct** | >90% correcte classificatie (cliënt vs. documentatie) |
| **Responstijd** | Eerste woord binnen 3 seconden |
| **Data-integriteit** | Alleen data van actieve cliënt wordt getoond |
| **Gebruikersacceptatie** | Positieve feedback in demo |
---
## 7. Risico's & Mitigatie
| Risico | Impact | Kans | Mitigatie |
|--------|--------|------|-----------|
| **Verkeerde cliëntdata tonen** | Kritiek | Laag | Cliënt-ID uit betrouwbare context (URL), niet uit vraag |
| **AI hallucineert informatie** | Hoog | Middel | Strikte prompt: "alleen beschikbare data, zeg eerlijk als info ontbreekt" |
| **Privacy-schending** | Kritiek | Laag | Bestaande autorisatie, RLS, geen logging van cliëntdata |
| **Ambigue vragen** | Middel | Middel | Duidelijke vraagtype-detectie, bij twijfel → documentatie-modus |
| **Te veel data in context** | Middel | Laag | Maximum 5 items per categorie laden |
---
## 8. Roadmap / Vervolg (Post-MVP)
### Fase 2: Uitgebreidere context
- Diagnoses en condities
- Contactmomenten/encounters
- Behandelplan doelen en voortgang
- Medicatie-overzicht
### Fase 3: Slimme acties
- "Start een rapportage op basis van dit gesprek"
- Suggesties voor behandelplan-updates
- Pre-fill formulieren met AI
### Fase 4: Caseload-niveau
- "Welke cliënten hebben hoog risico?"
- Overzicht van openstaande acties
- Prioritering suggesties
---
## 9. Bijlagen & Referenties
### Gerelateerde documenten
| Document | Beschrijving |
|----------|--------------|
| `prd-ai-docs-assistent-v1.md` | PRD van basis documentatie assistent |
| `fo-ai-docs-assistent-v1.md` | Functioneel ontwerp chat widget |
| `bouwplan-ai-docs-assistent-v1.md` | Technisch implementatieplan v1 |
### Beschikbare cliëntdata (voor context)
- **Rapportages** - Vrije notities en behandeladviezen
- **Intakes** - Behandeladviezen, notities, status
- **Risico-assessments** - Type, niveau, onderbouwing
- **Screening** - Hulpvraag, beslissing
---
## Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-12-2025 | Colin Lit | Initiële versie |

View File

@@ -0,0 +1,483 @@
# ⚙️ Technisch Ontwerp (TO) AI Cliënt Assistent
**Projectnaam:** Mini-ECD AI Cliënt Assistent
**Versie:** v1.0
**Datum:** 01-12-2025
**Auteur:** Colin Lit
---
## 1. Doel en relatie met PRD en FO
**Doel van dit document:**
Dit TO beschrijft de technische implementatie van de AI Cliënt Assistent: een uitbreiding op de bestaande AI Documentatie Assistent die vragen over specifieke cliënten kan beantwoorden.
**Relatie met PRD:**
- PRD beschrijft *wat* we bouwen: cliënt-aware chat die rapportages, risico's en behandeladvies kan samenvatten
- TO beschrijft *hoe* we dit technisch realiseren binnen de bestaande architectuur
**Scope:**
- Uitbreiding van bestaande `docs-chat` component
- Nieuwe context loader voor cliëntdata
- Vraagtype-detectie (cliënt vs. documentatie)
- Cliënt-specifieke prompt templates
---
## 2. Technische Architectuur Overzicht
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ ┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ DocsChatWidget │ │ PatientContext │ │ ChatSuggestions│ │
│ │ (uitgebreid) │──│ (bestaand) │ │ (dynamisch) │ │
│ └────────┬────────┘ └────────┬─────────┘ └────────────────┘ │
│ │ │ │
└───────────┼────────────────────┼─────────────────────────────────┘
│ │
▼ ▼
┌───────────────────────────────────────────────────────────────────┐
│ API Route: /api/docs/chat │
│ ┌──────────────────┐ ┌───────────────────┐ ┌────────────────┐ │
│ │ QuestionDetector │ │ ClientContextLoader│ │ PromptBuilder │ │
│ │ (nieuw) │ │ (nieuw) │ │ (uitgebreid) │ │
│ └────────┬─────────┘ └─────────┬──────────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────────────┼─────────────────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Claude API │ │
│ │ (streaming) │ │
│ └───────────────┘ │
└───────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ Supabase (PostgreSQL) │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌─────────────────┐ │
│ │ patients │ │ reports │ │ intakes │ │ risk_assessments│ │
│ │ (6 rows) │ │ (21 rows)│ │ (9 rows) │ │ (via intake) │ │
│ └──────────┘ └──────────┘ └────────────┘ └─────────────────┘ │
│ ┌──────────────┐ ┌────────────┐ │
│ │ screenings │ │ care_plans │ │
│ │ (5 rows) │ │ (0 rows) │ │
│ └──────────────┘ └────────────┘ │
└───────────────────────────────────────────────────────────────────┘
```
---
## 3. Techstack Selectie
| Component | Technologie | Argumentatie |
|-----------|-------------|--------------|
| Frontend | Next.js 15 + React | Bestaande stack, geen wijziging |
| State | PatientContext | Bestaande context, hergebruiken |
| API | Next.js API Routes | Bestaande `/api/docs/chat` uitbreiden |
| AI | Claude claude-sonnet-4-20250514 | Huidige model, goed voor Nederlands |
| Database | Supabase (PostgreSQL) | Bestaand, RLS enabled |
| Streaming | Server-Sent Events | Bestaande implementatie |
**Geen nieuwe dependencies nodig** - alles bouwt voort op bestaande technologie.
---
## 4. Datamodel Analyse
### 4.1 Beschikbare data per cliënt
Op basis van database-analyse is de volgende data beschikbaar:
| Tabel | Veld | Beschikbaar | Bruikbaar voor AI |
|-------|------|-------------|-------------------|
| **patients** | name, birth_date, status | ✅ 6 patiënten | Context header |
| **reports** | content, type, created_at | ✅ 21 rapportages | Samenvatting rapportages |
| **intakes** | treatment_advice (JSONB), notes | ✅ 9 intakes | Behandeladvies vragen |
| **screenings** | request_for_help, decision | ✅ 5 screenings | Hulpvraag/beslissing |
| **risk_assessments** | risk_type, risk_level, rationale | ⚠️ 0 rows (via intake) | Risico-overzicht |
| **care_plans** | goals, activities (JSONB) | ⚠️ 0 rows | Behandelplan doelen |
### 4.2 Datastructuur voorbeelden
**Reports (content):**
```
S Subjectief: Cliënt geeft aan dat piekergedachten over werk...
O Objectief: Cliënt verschijnt op tijd en verzorgd...
E Evaluatie: Er is sprake van lichte verbetering...
P Plan: Cliënt gaat komende week dagelijks...
```
**Intakes (treatment_advice JSONB):**
```json
{
"advice": "<p>Doorzetten naar behandeling</p>",
"outcome": "in_zorg",
"program": "FACT",
"department": "Volwassenen",
"psychologist": "Colin"
}
```
### 4.3 Context Loading Query
```sql
-- Rapportages (laatste 5)
SELECT type, content, created_at
FROM reports
WHERE patient_id = $1 AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 5;
-- Intakes met behandeladvies
SELECT title, department, status, treatment_advice, notes
FROM intakes
WHERE patient_id = $1
ORDER BY created_at DESC
LIMIT 3;
-- Screening hulpvraag
SELECT request_for_help, decision, decision_notes
FROM screenings
WHERE patient_id = $1
ORDER BY created_at DESC
LIMIT 1;
-- Risico-assessments (via intake)
SELECT ra.risk_type, ra.risk_level, ra.rationale, ra.assessment_date
FROM risk_assessments ra
JOIN intakes i ON ra.intake_id = i.id
WHERE i.patient_id = $1
ORDER BY ra.assessment_date DESC
LIMIT 5;
```
---
## 5. API Ontwerp
### 5.1 Bestaande API's Analyse
**FHIR API's (bestaand):**
| Endpoint | Methode | Bruikbaar voor AI Chat |
|----------|---------|------------------------|
| `/api/fhir/Patient/[id]` | GET | ⚠️ Beperkt - alleen demographics |
| `/api/fhir/Patient` | GET/POST | ❌ Niet nodig |
| `/api/fhir/Practitioner/[id]` | GET | ❌ Niet relevant |
**REST API's (bestaand):**
| Endpoint | Methode | Data | Bruikbaar |
|----------|---------|------|-----------|
| `/api/reports?patientId=` | GET | Rapportages met content | ✅ **Zeer bruikbaar** |
| `/api/intakes/[id]` | GET | Intake + treatment_advice | ✅ **Zeer bruikbaar** |
| `/api/screenings/[id]` | GET | Hulpvraag + beslissing + activities | ✅ **Zeer bruikbaar** |
### 5.2 Data Access Strategie
**Overwogen opties:**
| Optie | Beschrijving | Voordelen | Nadelen |
|-------|--------------|-----------|---------|
| **A: Bestaande API's** | Fetch naar `/api/reports`, `/api/intakes`, etc. | Hergebruik, consistentie | Extra HTTP overhead, intakes/screenings list endpoints ontbreken |
| **B: Directe Supabase** | Server-side queries in API route | Sneller, 1 DB roundtrip, RLS automatisch | Duplicatie van query logic |
| **C: FHIR $summary** | Nieuw endpoint `GET /api/fhir/Patient/[id]/$summary` | FHIR-compliant, extern bruikbaar | Meeste werk, overkill voor MVP |
**Gekozen: Optie B - Directe Supabase queries**
Argumentatie:
1. **Performance**: 1 database roundtrip vs. 3-4 HTTP calls
2. **Simpliciteit**: Geen nieuwe endpoints nodig voor MVP
3. **Security**: RLS policies werken automatisch op server-side queries
4. **Latency**: ~50ms vs. ~200ms+ bij HTTP calls
**Post-MVP overweging:** Een FHIR `$summary` operation kan waardevol zijn voor externe systeem-integraties.
### 5.3 Chat endpoint uitbreiden
**Endpoint:** `POST /api/docs/chat`
**Huidige input:**
```typescript
{
messages: Array<{ role: 'user' | 'assistant', content: string }>
}
```
**Uitgebreide input:**
```typescript
{
messages: Array<{ role: 'user' | 'assistant', content: string }>,
clientId?: string // UUID van actieve patiënt (optioneel)
}
```
**Response:** Ongewijzigd (SSE streaming)
### 5.4 Nieuwe interne modules
```typescript
// lib/docs/question-type-detector.ts
export type QuestionType = 'client' | 'documentation' | 'ambiguous'
export function detectQuestionType(
question: string,
hasClientContext: boolean
): QuestionType
// lib/docs/client-context-loader.ts
export interface ClientContext {
patient: { name: string; birthDate: string; status: string }
reports: Array<{ type: string; content: string; date: string }>
intakes: Array<{ title: string; treatmentAdvice: object }>
screening: { requestForHelp: string; decision: string } | null
riskAssessments: Array<{ type: string; level: string; rationale: string }>
}
export async function loadClientContext(
clientId: string
): Promise<ClientContext>
// lib/docs/client-prompt-builder.ts
export function buildClientPrompt(
context: ClientContext,
question: string
): string
```
---
## 6. Security & Compliance
### 6.1 Bestaande beveiliging (behouden)
| Maatregel | Status | Implementatie |
|-----------|--------|---------------|
| **Authentication** | ✅ | Supabase Auth, sessie vereist |
| **RLS Policies** | ✅ | Alle tabellen hebben RLS enabled |
| **Rate Limiting** | ✅ | 10 req/min per user (in-memory) |
| **HTTPS** | ✅ | Vercel enforced |
### 6.2 Aanvullende maatregelen
| Maatregel | Implementatie |
|-----------|---------------|
| **Client ID validatie** | UUID format check + bestaat in database |
| **Context isolatie** | Alleen data van opgegeven clientId laden |
| **Geen logging cliëntdata** | AI responses niet loggen naar ai_events |
| **Token limit** | Max 4000 tokens context om data-lekkage te beperken |
### 6.3 Privacy overwegingen
```typescript
// NIET loggen naar ai_events bij cliënt-vragen
if (questionType === 'client') {
// Skip ai_events insert - geen cliëntdata in logs
}
// Wel loggen bij documentatie-vragen (bestaand gedrag)
if (questionType === 'documentation') {
await logAiEvent({ kind: 'chat', request, response })
}
```
---
## 7. AI/LLM Integratie
### 7.1 Vraagtype Detectie
**Heuristiek voor detectie:**
```typescript
const CLIENT_KEYWORDS = [
'rapportage', 'risico', 'behandeladvies', 'screening',
'hulpvraag', 'samenvatting', 'dossier', 'deze cliënt',
'zijn/haar', 'behandeling', 'medicatie', 'diagnose'
]
const DOC_KEYWORDS = [
'hoe', 'waar', 'wat is', 'tutorial', 'handleiding',
'functie', 'knop', 'menu', 'systeem', 'epd'
]
function detectQuestionType(question: string, hasClientContext: boolean): QuestionType {
if (!hasClientContext) return 'documentation'
const q = question.toLowerCase()
const clientScore = CLIENT_KEYWORDS.filter(k => q.includes(k)).length
const docScore = DOC_KEYWORDS.filter(k => q.includes(k)).length
if (clientScore > docScore) return 'client'
if (docScore > clientScore) return 'documentation'
return 'ambiguous' // Fallback naar documentation
}
```
### 7.2 Client Prompt Template
```typescript
const CLIENT_SYSTEM_PROMPT = `Je bent een EPD-assistent die vragen beantwoordt over een specifieke cliënt.
BELANGRIJKE REGELS:
1. Beantwoord ALLEEN op basis van de gegeven context
2. Als informatie ontbreekt, zeg dit eerlijk
3. Geef NOOIT medisch advies of diagnoses
4. Verzin NOOIT informatie die niet in de context staat
5. Antwoord beknopt en professioneel
CLIËNT: {patientName}
GEBOORTEDATUM: {birthDate}
STATUS: {status}
RAPPORTAGES (laatste {reportCount}):
{reportsFormatted}
BEHANDELADVIES:
{treatmentAdviceFormatted}
SCREENING/HULPVRAAG:
{screeningFormatted}
RISICO-ASSESSMENTS:
{riskAssessmentsFormatted}
`
```
### 7.3 Fallback bij ambigue vragen
Bij `questionType === 'ambiguous'`:
- Default naar documentatie-modus
- Toon hint: "Bedoelde je een vraag over de documentatie of over deze cliënt?"
---
## 8. Performance & Scalability
### 8.1 Performance Targets
| Metric | Target | Huidige baseline |
|--------|--------|------------------|
| Context laden | < 200ms | N.v.t. (nieuw) |
| Vraagtype detectie | < 10ms | N.v.t. (nieuw) |
| Eerste token | < 3 sec | ~2 sec (docs) |
| Totale response | < 10 sec | ~5-8 sec (docs) |
### 8.2 Optimalisaties
```typescript
// Parallel laden van context
const [reports, intakes, screening, risks] = await Promise.all([
loadReports(clientId),
loadIntakes(clientId),
loadScreening(clientId),
loadRiskAssessments(clientId)
])
// Token budget management
const MAX_CONTEXT_TOKENS = 4000
const contextText = truncateToTokenLimit(
formatContext(reports, intakes, screening, risks),
MAX_CONTEXT_TOKENS
)
```
### 8.3 Caching strategie
| Data | Cache | TTL |
|------|-------|-----|
| Cliënt context | Geen | - |
| Documentatie chunks | In-memory | Session |
| Rate limit state | In-memory | 60 sec |
**Geen caching van cliëntdata** - altijd verse data uit database voor medische nauwkeurigheid.
---
## 9. Haalbaarheidsanalyse
### 9.1 Technische haalbaarheid: ✅ HOOG
| Aspect | Beoordeling | Toelichting |
|--------|-------------|-------------|
| **Datamodel** | ✅ Compleet | Alle benodigde tabellen bestaan en bevatten data |
| **API structuur** | ✅ Eenvoudig | Kleine uitbreiding op bestaande endpoint |
| **Frontend** | ✅ Minimaal | PatientContext bestaat al |
| **AI integratie** | ✅ Bewezen | Zelfde Claude API als documentatie-chat |
### 9.2 Data beschikbaarheid
| Categorie | PRD Requirement | Database Status |
|-----------|-----------------|-----------------|
| Rapportages | ✅ | 21 rows, SOAP-format content |
| Behandeladvies | ✅ | JSONB in intakes.treatment_advice |
| Risico's | ⚠️ | Tabel bestaat, 0 rows (seed data nodig) |
| Screening | ✅ | 5 rows, hulpvraag veld beschikbaar |
### 9.3 Geschatte implementatietijd
| Component | Schatting |
|-----------|-----------|
| `question-type-detector.ts` | 2 uur |
| `client-context-loader.ts` | 3 uur |
| `client-prompt-builder.ts` | 2 uur |
| API route uitbreiding | 2 uur |
| Frontend (indicator + suggestions) | 3 uur |
| Testing & refinement | 4 uur |
| **Totaal** | **~16 uur** |
### 9.4 Risico's en mitigatie
| Risico | Impact | Mitigatie |
|--------|--------|-----------|
| **Geen risk_assessments data** | Middel | Seed data toevoegen of feature uitstellen |
| **Token overflow** | Laag | Truncatie met prioriteit (nieuwste eerst) |
| **Hallucinatie** | Hoog | Strikte prompt + "ik weet het niet" response |
| **Performance** | Laag | Parallel queries, geen joins |
---
## 10. Conclusie & Aanbeveling
### Haalbaarheid: ✅ JA
De AI Cliënt Assistent is technisch haalbaar binnen de huidige architectuur:
1. **Datamodel is compleet** - Alle benodigde tabellen bestaan met RLS
2. **Geen nieuwe dependencies** - Bouwt voort op bestaande stack
3. **Minimale frontend wijzigingen** - PatientContext hergebruiken
4. **Bewezen AI integratie** - Zelfde Claude API als docs-chat
### Aanbevolen aanpak
1. **Fase 1:** Seed data voor risk_assessments (test coverage)
2. **Fase 2:** Backend modules (detector, loader, prompt builder)
3. **Fase 3:** API route uitbreiding
4. **Fase 4:** Frontend indicator en dynamische suggestions
5. **Fase 5:** Integratie testing met echte cliëntdata
---
## 11. Bijlagen & Referenties
### Projectdocumenten
| Document | Locatie |
|----------|---------|
| PRD | `docs/specs/ai-integratie/prd-ai-client-assistent-v1.md` |
| Bestaande docs-chat | `components/docs-chat/` |
| API route | `app/api/docs/chat/route.ts` |
| PatientContext | `contexts/patient-context.tsx` |
### Database schema
- Volledige schema via `mcp__supabase__list_tables`
- RLS policies actief op alle tabellen
---
## Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-12-2025 | Colin Lit | Initiële versie met haalbaarheidsanalyse |
| v1.1 | 01-12-2025 | Colin Lit | FHIR/REST API analyse toegevoegd, data access strategie onderbouwd |

View File

@@ -0,0 +1,263 @@
/**
* Client Context Loader
*
* Loads client-specific data from Supabase for the AI Client Assistant.
* Uses direct database queries for performance (not HTTP APIs).
*/
import { supabaseAdmin } from '@/lib/supabase/server'
import type { Database } from '@/lib/supabase/database.types'
type Patient = Database['public']['Tables']['patients']['Row']
type Report = Database['public']['Tables']['reports']['Row']
type Intake = Database['public']['Tables']['intakes']['Row']
type Screening = Database['public']['Tables']['screenings']['Row']
type RiskAssessment = Database['public']['Tables']['risk_assessments']['Row']
/**
* Simplified patient info for AI context
*/
export interface ClientPatient {
name: string
birthDate: string
status: string | null
}
/**
* Simplified report for AI context
*/
export interface ClientReport {
type: string
content: string
date: string
}
/**
* Simplified intake for AI context
*/
export interface ClientIntake {
title: string
department: string
status: string
treatmentAdvice: {
advice?: string
outcome?: string
program?: string
department?: string
} | null
notes: string | null
}
/**
* Simplified screening for AI context
*/
export interface ClientScreening {
requestForHelp: string | null
decision: string | null
decisionNotes: string | null
}
/**
* Simplified risk assessment for AI context
*/
export interface ClientRiskAssessment {
type: string
level: string
rationale: string
date: string
}
/**
* Complete client context for AI prompt
*/
export interface ClientContext {
patient: ClientPatient
reports: ClientReport[]
intakes: ClientIntake[]
screening: ClientScreening | null
riskAssessments: ClientRiskAssessment[]
}
/**
* Format patient name from database fields
*/
function formatPatientName(patient: Patient): string {
const givenNames = patient.name_given?.join(' ') || ''
const prefix = patient.name_prefix ? `${patient.name_prefix} ` : ''
return `${givenNames} ${prefix}${patient.name_family}`.trim()
}
/**
* Format date for display (Dutch format)
*/
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('nl-NL', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
/**
* Load patient basic info
*/
async function loadPatient(clientId: string): Promise<ClientPatient | null> {
const { data, error } = await supabaseAdmin
.from('patients')
.select('name_given, name_family, name_prefix, birth_date, status')
.eq('id', clientId)
.single()
if (error || !data) {
console.error('Error loading patient:', error)
return null
}
return {
name: formatPatientName(data as Patient),
birthDate: formatDate(data.birth_date),
status: data.status,
}
}
/**
* Load recent reports (max 5, newest first)
*/
async function loadReports(clientId: string): Promise<ClientReport[]> {
const { data, error } = await supabaseAdmin
.from('reports')
.select('type, content, created_at')
.eq('patient_id', clientId)
.is('deleted_at', null)
.order('created_at', { ascending: false })
.limit(5)
if (error || !data) {
console.error('Error loading reports:', error)
return []
}
return data.map((report) => ({
type: report.type === 'behandeladvies' ? 'Behandeladvies' : 'Vrije notitie',
content: report.content,
date: formatDate(report.created_at!),
}))
}
/**
* Load recent intakes with treatment advice (max 3, newest first)
*/
async function loadIntakes(clientId: string): Promise<ClientIntake[]> {
const { data, error } = await supabaseAdmin
.from('intakes')
.select('title, department, status, treatment_advice, notes')
.eq('patient_id', clientId)
.order('created_at', { ascending: false })
.limit(3)
if (error || !data) {
console.error('Error loading intakes:', error)
return []
}
return data.map((intake) => ({
title: intake.title,
department: intake.department,
status: intake.status === 'afgerond' ? 'Afgerond' : 'Bezig',
treatmentAdvice: intake.treatment_advice as ClientIntake['treatmentAdvice'],
notes: intake.notes,
}))
}
/**
* Load most recent screening
*/
async function loadScreening(clientId: string): Promise<ClientScreening | null> {
const { data, error } = await supabaseAdmin
.from('screenings')
.select('request_for_help, decision, decision_notes')
.eq('patient_id', clientId)
.order('created_at', { ascending: false })
.limit(1)
.single()
if (error || !data) {
// No screening is a valid state, not an error
return null
}
return {
requestForHelp: data.request_for_help,
decision: data.decision === 'geschikt' ? 'Geschikt' : data.decision === 'niet_geschikt' ? 'Niet geschikt' : null,
decisionNotes: data.decision_notes,
}
}
/**
* Load risk assessments via intakes (max 5, newest first)
*/
async function loadRiskAssessments(clientId: string): Promise<ClientRiskAssessment[]> {
// First get intake IDs for this patient
const { data: intakes, error: intakesError } = await supabaseAdmin
.from('intakes')
.select('id')
.eq('patient_id', clientId)
if (intakesError || !intakes || intakes.length === 0) {
return []
}
const intakeIds = intakes.map((i) => i.id)
// Then get risk assessments for those intakes
const { data, error } = await supabaseAdmin
.from('risk_assessments')
.select('risk_type, risk_level, rationale, assessment_date')
.in('intake_id', intakeIds)
.order('assessment_date', { ascending: false })
.limit(5)
if (error || !data) {
console.error('Error loading risk assessments:', error)
return []
}
return data.map((ra) => ({
type: ra.risk_type,
level: ra.risk_level,
rationale: ra.rationale,
date: formatDate(ra.assessment_date),
}))
}
/**
* Load complete client context for AI assistant
* Loads all data in parallel for performance
*
* @param clientId - UUID of the patient
* @returns ClientContext or null if patient not found
*/
export async function loadClientContext(clientId: string): Promise<ClientContext | null> {
// Load all data in parallel
const [patient, reports, intakes, screening, riskAssessments] = await Promise.all([
loadPatient(clientId),
loadReports(clientId),
loadIntakes(clientId),
loadScreening(clientId),
loadRiskAssessments(clientId),
])
// Patient must exist
if (!patient) {
return null
}
return {
patient,
reports,
intakes,
screening,
riskAssessments,
}
}

View File

@@ -0,0 +1,170 @@
/**
* Client Prompt Builder
*
* Builds the system prompt for client-specific questions.
* Includes patient context, reports, intakes, screenings, and risk assessments.
*/
import type { ClientContext } from './client-context-loader'
/**
* Base system prompt for client questions
*/
const CLIENT_BASE_PROMPT = `Je bent een EPD-assistent die vragen beantwoordt over een specifieke cliënt in het Mini-ECD systeem.
## Belangrijke regels
1. Beantwoord ALLEEN op basis van de gegeven cliëntgegevens hieronder
2. Als informatie ontbreekt, zeg dit eerlijk (bijv. "Er zijn nog geen rapportages voor deze cliënt")
3. Geef NOOIT medisch advies, diagnoses of behandelsuggesties
4. Verzin NOOIT informatie die niet in de context staat
5. Antwoord beknopt en professioneel
## Jouw publiek
Zorgprofessionals (behandelaars, verpleegkundigen) die het EPD gebruiken.
## Stijl
- Schrijf in het Nederlands
- Wees beknopt maar volledig
- Gebruik bullet points voor overzichten
- Vermeld datums waar relevant`
/**
* Format reports for prompt context
*/
function formatReports(reports: ClientContext['reports']): string {
if (reports.length === 0) {
return 'Geen rapportages beschikbaar.'
}
return reports
.map((report, index) => {
const truncatedContent =
report.content.length > 500 ? report.content.substring(0, 500) + '...' : report.content
return `${index + 1}. [${report.date}] ${report.type}\n${truncatedContent}`
})
.join('\n\n')
}
/**
* Format intakes for prompt context
*/
function formatIntakes(intakes: ClientContext['intakes']): string {
if (intakes.length === 0) {
return 'Geen intakes beschikbaar.'
}
return intakes
.map((intake, index) => {
let text = `${index + 1}. ${intake.title}\n`
text += ` - Afdeling: ${intake.department}\n`
text += ` - Status: ${intake.status}`
if (intake.treatmentAdvice) {
const ta = intake.treatmentAdvice
if (ta.advice) text += `\n - Advies: ${ta.advice.replace(/<[^>]*>/g, '')}`
if (ta.program) text += `\n - Programma: ${ta.program}`
if (ta.outcome) text += `\n - Uitkomst: ${ta.outcome}`
}
if (intake.notes) {
const truncatedNotes =
intake.notes.length > 200 ? intake.notes.substring(0, 200) + '...' : intake.notes
text += `\n - Notities: ${truncatedNotes}`
}
return text
})
.join('\n\n')
}
/**
* Format screening for prompt context
*/
function formatScreening(screening: ClientContext['screening']): string {
if (!screening) {
return 'Geen screening beschikbaar.'
}
let text = ''
if (screening.requestForHelp) {
text += `Hulpvraag: ${screening.requestForHelp}\n`
} else {
text += 'Hulpvraag: Niet ingevuld\n'
}
if (screening.decision) {
text += `Beslissing: ${screening.decision}`
if (screening.decisionNotes) {
text += ` - ${screening.decisionNotes}`
}
} else {
text += 'Beslissing: Nog niet genomen'
}
return text
}
/**
* Format risk assessments for prompt context
*/
function formatRiskAssessments(riskAssessments: ClientContext['riskAssessments']): string {
if (riskAssessments.length === 0) {
return 'Geen risico-assessments beschikbaar.'
}
return riskAssessments
.map((ra, index) => {
return `${index + 1}. ${ra.type} - Niveau: ${ra.level} (${ra.date})\n Onderbouwing: ${ra.rationale}`
})
.join('\n\n')
}
/**
* Build the complete system prompt with client context
*
* @param context - The loaded client context
* @returns The complete system prompt string
*/
export function buildClientPrompt(context: ClientContext): string {
const sections = [
CLIENT_BASE_PROMPT,
'---',
`## Cliënt: ${context.patient.name}`,
`Geboortedatum: ${context.patient.birthDate}`,
context.patient.status ? `Status: ${context.patient.status}` : '',
'',
'---',
'## Rapportages (laatste 5)',
formatReports(context.reports),
'',
'---',
'## Intakes & Behandeladvies',
formatIntakes(context.intakes),
'',
'---',
'## Screening / Hulpvraag',
formatScreening(context.screening),
'',
'---',
"## Risico-assessments",
formatRiskAssessments(context.riskAssessments),
'---',
]
return sections.filter(Boolean).join('\n')
}
/**
* Build a fallback prompt when client context fails to load
*/
export function buildClientErrorPrompt(): string {
return `${CLIENT_BASE_PROMPT}
---
Er is een fout opgetreden bij het laden van de cliëntgegevens.
Vraag de gebruiker om de pagina te verversen of later opnieuw te proberen.
---`
}

View File

@@ -0,0 +1,154 @@
/**
* Question Type Detector
*
* Detects whether a user question is about:
* - 'client': Questions about the active patient/client
* - 'documentation': Questions about how to use the EPD system
* - 'ambiguous': Unclear, defaults to documentation
*/
export type QuestionType = 'client' | 'documentation' | 'ambiguous'
/**
* Keywords that indicate a client-related question
*/
const CLIENT_KEYWORDS = [
// Direct client references
'rapportage',
'rapportages',
'rapportage',
'notitie',
'notities',
'risico',
"risico's",
'risicoassessment',
'behandeladvies',
'behandeladviezen',
'screening',
'hulpvraag',
'samenvatting',
'dossier',
'deze cliënt',
'deze client',
'deze patiënt',
'deze patient',
// Client data questions
'wat staat er',
'wat is er genoteerd',
'laatste',
'recente',
'actuele',
'huidige status',
'zijn risico',
'haar risico',
'zijn behandeling',
'haar behandeling',
// Actions on client data
'geef een overzicht',
'vat samen',
'samenvatten',
'wat weten we',
]
/**
* Keywords that indicate a documentation/system question
*/
const DOC_KEYWORDS = [
// How-to questions
'hoe',
'hoe maak ik',
'hoe kan ik',
'hoe werkt',
'hoe doe ik',
// System references
'waar',
'waar vind ik',
'waar kan ik',
'wat is',
'wat betekent',
'wat doet',
// UI elements
'knop',
'menu',
'scherm',
'tab',
'tabblad',
'pagina',
'formulier',
// Feature references
'functie',
'functionaliteit',
'feature',
'optie',
'instelling',
// Documentation terms
'tutorial',
'handleiding',
'uitleg',
'instructie',
'help',
// System references
'systeem',
'epd',
'applicatie',
'software',
'spraakherkenning',
'spraak',
]
/**
* Count keyword matches in a question
*/
function countKeywordMatches(question: string, keywords: string[]): number {
const lowerQuestion = question.toLowerCase()
return keywords.filter((keyword) => lowerQuestion.includes(keyword.toLowerCase())).length
}
/**
* Detect the type of question based on keywords and context
*
* @param question - The user's question
* @param hasClientContext - Whether a client is currently active
* @returns The detected question type
*/
export function detectQuestionType(question: string, hasClientContext: boolean): QuestionType {
// If no client context, always treat as documentation question
if (!hasClientContext) {
return 'documentation'
}
const clientScore = countKeywordMatches(question, CLIENT_KEYWORDS)
const docScore = countKeywordMatches(question, DOC_KEYWORDS)
// Clear winner
if (clientScore > docScore) {
return 'client'
}
if (docScore > clientScore) {
return 'documentation'
}
// Tie or no matches - check for implicit client references
const lowerQuestion = question.toLowerCase()
// Short questions in client context are often about the client
if (hasClientContext && question.length < 50) {
// Check for implicit client questions
const implicitClientPatterns = [
/^wat zijn/i,
/^wat is de/i,
/^geef/i,
/^toon/i,
/^overzicht/i,
/\?$/,
]
if (implicitClientPatterns.some((pattern) => pattern.test(lowerQuestion))) {
return 'ambiguous' // Let the system handle ambiguity gracefully
}
}
// Default to documentation for safety
return 'ambiguous'
}