chat suggestions, SEo integration, and more
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -45,4 +45,8 @@ next-env.d.ts
|
||||
.claude
|
||||
.mcp.json
|
||||
|
||||
# documentation
|
||||
/archive/*
|
||||
/docs/archive/*
|
||||
/docs/specs/archive/*
|
||||
/docs/reports/archive/*
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
341
docs/specs/ai-integratie/bouwplan-ai-client-assistent-v1.md
Normal file
341
docs/specs/ai-integratie/bouwplan-ai-client-assistent-v1.md
Normal 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 |
|
||||
@@ -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 |
|
||||
367
docs/specs/ai-integratie/fo-ai-client-assistent-v1.md
Normal file
367
docs/specs/ai-integratie/fo-ai-client-assistent-v1.md
Normal 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 |
|
||||
@@ -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*
|
||||
205
docs/specs/ai-integratie/prd-ai-client-assistent-v1.md
Normal file
205
docs/specs/ai-integratie/prd-ai-client-assistent-v1.md
Normal 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 |
|
||||
483
docs/specs/ai-integratie/to-ai-client-assistent-v1.md
Normal file
483
docs/specs/ai-integratie/to-ai-client-assistent-v1.md
Normal 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 |
|
||||
263
lib/docs/client-context-loader.ts
Normal file
263
lib/docs/client-context-loader.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
170
lib/docs/client-prompt-builder.ts
Normal file
170
lib/docs/client-prompt-builder.ts
Normal 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.
|
||||
|
||||
---`
|
||||
}
|
||||
154
lib/docs/question-type-detector.ts
Normal file
154
lib/docs/question-type-detector.ts
Normal 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'
|
||||
}
|
||||
Reference in New Issue
Block a user