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
|
.claude
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
|
||||||
|
# documentation
|
||||||
/archive/*
|
/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) {
|
export default async function ReleasePage({ params }: ReleasePageProps) {
|
||||||
const { category } = await params
|
const { category } = await params
|
||||||
const release = await getRelease(category)
|
const release = await getRelease(category)
|
||||||
@@ -53,6 +119,12 @@ export default async function ReleasePage({ params }: ReleasePageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white pb-16">
|
<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">
|
<article className="max-w-4xl mx-auto px-4 md:px-8 pt-20 md:pt-20">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="mb-8 pb-8 border-b border-slate-200">
|
<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 { detectCategories } from '@/lib/docs/category-detector'
|
||||||
import { loadKnowledgeSections } from '@/lib/docs/knowledge-loader'
|
import { loadKnowledgeSections } from '@/lib/docs/knowledge-loader'
|
||||||
import { buildSystemPrompt } from '@/lib/docs/prompt-builder'
|
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 DOCS_ASSISTANT_MODEL = process.env.DOCS_ASSISTANT_MODEL ?? 'claude-sonnet-4-20250514'
|
||||||
const MAX_HISTORY_MESSAGES = 10
|
const MAX_HISTORY_MESSAGES = 10
|
||||||
@@ -52,6 +55,7 @@ const ChatMessageSchema = z.object({
|
|||||||
const RequestSchema = z.object({
|
const RequestSchema = z.object({
|
||||||
messages: z.array(ChatMessageSchema).optional(),
|
messages: z.array(ChatMessageSchema).optional(),
|
||||||
userMessage: z.string().min(1).max(MAX_USER_MESSAGE_LENGTH),
|
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>
|
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 conversation: ChatMessage[] = [...history, { role: 'user', content: rawUserMessage }]
|
||||||
|
|
||||||
|
// 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 categories = detectCategories(rawUserMessage)
|
||||||
const knowledgeSections = await loadKnowledgeSections(categories)
|
const knowledgeSections = await loadKnowledgeSections(categories)
|
||||||
const systemPrompt = buildSystemPrompt(knowledgeSections)
|
systemPrompt = buildSystemPrompt(knowledgeSections)
|
||||||
|
}
|
||||||
|
|
||||||
const apiKey = process.env.ANTHROPIC_API_KEY
|
const apiKey = process.env.ANTHROPIC_API_KEY
|
||||||
if (!apiKey) {
|
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({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
@@ -126,6 +154,12 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="nl">
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${crimsonText.variable} ${inter.variable} ${jetBrainsMono.variable} antialiased`}
|
className={`${crimsonText.variable} ${inter.variable} ${jetBrainsMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,13 +6,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MetadataRoute } from 'next'
|
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'
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
|
||||||
|
|
||||||
// Get current date for lastModified
|
|
||||||
const currentDate = new Date()
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
@@ -20,19 +34,19 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
changeFrequency: 'weekly',
|
changeFrequency: 'weekly',
|
||||||
priority: 1.0,
|
priority: 1.0,
|
||||||
},
|
},
|
||||||
// Future routes can be added here:
|
{
|
||||||
// {
|
url: `${baseUrl}/documentatie`,
|
||||||
// url: `${baseUrl}/build-log`,
|
lastModified: currentDate,
|
||||||
// lastModified: currentDate,
|
changeFrequency: 'weekly',
|
||||||
// changeFrequency: 'weekly',
|
priority: 0.9,
|
||||||
// priority: 0.8,
|
},
|
||||||
// },
|
...releaseUrls,
|
||||||
// {
|
{
|
||||||
// url: `${baseUrl}/demo`,
|
url: `${baseUrl}/contact`,
|
||||||
// lastModified: currentDate,
|
lastModified: currentDate,
|
||||||
// changeFrequency: 'monthly',
|
changeFrequency: 'monthly',
|
||||||
// priority: 0.7,
|
priority: 0.7,
|
||||||
// },
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ interface SuggestionCategory {
|
|||||||
questions: string[]
|
questions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUGGESTION_CATEGORIES: SuggestionCategory[] = [
|
/**
|
||||||
|
* Documentation suggestions - shown when not in a patient dossier
|
||||||
|
*/
|
||||||
|
const DOC_SUGGESTION_CATEGORIES: SuggestionCategory[] = [
|
||||||
{
|
{
|
||||||
id: 'clienten',
|
id: 'clienten',
|
||||||
label: 'Cliënten & Dossiers',
|
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 {
|
interface ChatSuggestionsProps {
|
||||||
onSelect: (question: string) => void
|
onSelect: (question: string) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
mode?: 'client' | 'documentation'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Two-step suggestion selector:
|
* Two-step suggestion selector:
|
||||||
* 1. Show categories
|
* 1. Show categories
|
||||||
* 2. After selecting category, show questions
|
* 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)
|
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) => {
|
const handleQuestionSelect = (question: string) => {
|
||||||
onSelect(question)
|
onSelect(question)
|
||||||
setSelectedCategory(null)
|
setSelectedCategory(null)
|
||||||
@@ -106,9 +153,11 @@ export function ChatSuggestions({ onSelect, disabled = false }: ChatSuggestionsP
|
|||||||
// Show category selection
|
// Show category selection
|
||||||
return (
|
return (
|
||||||
<div className="px-4 pb-3">
|
<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">
|
<div className="flex flex-col gap-1.5">
|
||||||
{SUGGESTION_CATEGORIES.map((category) => (
|
{categories.map((category) => (
|
||||||
<button
|
<button
|
||||||
key={category.id}
|
key={category.id}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Sparkles, X } from 'lucide-react'
|
import { Sparkles, X, FileText } from 'lucide-react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { usePatientContext } from '@/app/epd/components/patient-context'
|
||||||
|
|
||||||
import { ChatInput } from './chat-input'
|
import { ChatInput } from './chat-input'
|
||||||
import { ChatMessages } from './chat-messages'
|
import { ChatMessages } from './chat-messages'
|
||||||
@@ -11,6 +12,18 @@ import { ChatSuggestions } from './chat-suggestions'
|
|||||||
import { RateLimitMessage } from './rate-limit-message'
|
import { RateLimitMessage } from './rate-limit-message'
|
||||||
import { useDocsChat } from './use-docs-chat'
|
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
|
* Floating chat widget for documentation assistant
|
||||||
*
|
*
|
||||||
@@ -20,7 +33,27 @@ import { useDocsChat } from './use-docs-chat'
|
|||||||
*/
|
*/
|
||||||
export function DocsChatWidget() {
|
export function DocsChatWidget() {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -70,7 +103,7 @@ export function DocsChatWidget() {
|
|||||||
EPD Assistent
|
EPD Assistent
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-slate-500">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +123,16 @@ export function DocsChatWidget() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 banner */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-4 py-2 bg-red-50 border-b border-red-100 flex items-center justify-between">
|
<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
|
<ChatSuggestions
|
||||||
onSelect={sendMessage}
|
onSelect={sendMessage}
|
||||||
disabled={isLoading || isStreaming}
|
disabled={isLoading || isStreaming}
|
||||||
|
mode={hasClientContext ? 'client' : 'documentation'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ interface UseDocsChatState {
|
|||||||
rateLimitResetTime: number | null // timestamp when rate limit resets
|
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
|
* Hook return type
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +39,8 @@ interface UseDocsChatReturn extends UseDocsChatState {
|
|||||||
clearMessages: () => void
|
clearMessages: () => void
|
||||||
clearError: () => void
|
clearError: () => void
|
||||||
clearRateLimit: () => void
|
clearRateLimit: () => void
|
||||||
|
hasClientContext: boolean
|
||||||
|
clientName: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,12 +68,20 @@ const WELCOME_MESSAGE: ChatMessage = {
|
|||||||
* - Streaming responses from Claude API
|
* - Streaming responses from Claude API
|
||||||
* - Loading and error states
|
* - Loading and error states
|
||||||
* - Welcome message on init
|
* - Welcome message on init
|
||||||
|
* - Client-aware: sends clientId for patient-specific questions
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
|
* // Documentation-only mode
|
||||||
* const { messages, isLoading, sendMessage } = useDocsChat()
|
* 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>({
|
const [state, setState] = useState<UseDocsChatState>({
|
||||||
messages: [WELCOME_MESSAGE],
|
messages: [WELCOME_MESSAGE],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -111,6 +129,7 @@ export function useDocsChat(): UseDocsChatReturn {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: history,
|
messages: history,
|
||||||
userMessage: trimmedContent,
|
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(() => {
|
const clearMessages = useCallback(() => {
|
||||||
setState({
|
setState({
|
||||||
@@ -230,5 +249,7 @@ export function useDocsChat(): UseDocsChatReturn {
|
|||||||
clearMessages,
|
clearMessages,
|
||||||
clearError,
|
clearError,
|
||||||
clearRateLimit,
|
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