Opslaan faalde altijd door label/code-mismatch met de database-constraints (reviewbevinding). Tabs komen terug bij de rebuild op het nieuwe datamodel. - tab-mappen anamnese/, examination/, rom/ verwijderd - tabs uit intake-tabs.tsx, server actions uit actions.ts - intake-status API telt de secties niet meer mee (voortgangsring klopt) - Cortex-navigatiedoelen (regex, entity-mapping, prompt) bijgewerkt zodat 'ga naar anamnese' geen 404 meer geeft Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
741 lines
20 KiB
TypeScript
741 lines
20 KiB
TypeScript
/**
|
|
* Entity Extractor
|
|
*
|
|
* Extracts entities (patient name, category, content, date/time) from user input.
|
|
*
|
|
* IMPORTANT: All functions accept an optional `referenceDate` parameter.
|
|
* Server-side: pass `getServerNow()` from `lib/utils/server-date.ts`
|
|
* This ensures consistent date handling between Cortex and regular EPD.
|
|
*/
|
|
|
|
import type { VerpleegkundigCategory } from '@/lib/types/report';
|
|
import type { ExtractedEntities, CortexIntent } from './types';
|
|
import {
|
|
parseRelativeDate,
|
|
parseTime,
|
|
isDateRange,
|
|
dateToRange,
|
|
type DateRange,
|
|
} from './date-time-parser';
|
|
|
|
// Category aliases mapping to canonical values
|
|
const CATEGORY_ALIASES: Record<string, VerpleegkundigCategory> = {
|
|
// Medicatie
|
|
medicatie: 'medicatie',
|
|
medicijn: 'medicatie',
|
|
medicijnen: 'medicatie',
|
|
med: 'medicatie',
|
|
meds: 'medicatie',
|
|
|
|
// ADL
|
|
adl: 'adl',
|
|
verzorging: 'adl',
|
|
zorg: 'adl',
|
|
wassen: 'adl',
|
|
eten: 'adl',
|
|
douchen: 'adl',
|
|
|
|
// Gedrag
|
|
gedrag: 'gedrag',
|
|
gedrags: 'gedrag',
|
|
stemming: 'gedrag',
|
|
mood: 'gedrag',
|
|
emotie: 'gedrag',
|
|
|
|
// Incident
|
|
incident: 'incident',
|
|
val: 'incident',
|
|
gevallen: 'incident',
|
|
ongeluk: 'incident',
|
|
agressie: 'incident',
|
|
|
|
// Observatie
|
|
observatie: 'observatie',
|
|
obs: 'observatie',
|
|
waarneming: 'observatie',
|
|
opmerking: 'observatie',
|
|
};
|
|
|
|
// Command words to strip from input
|
|
const COMMAND_WORDS = [
|
|
'notitie',
|
|
'dagnotitie',
|
|
'nieuwe',
|
|
'schrijf',
|
|
'rapporteer',
|
|
'registreer',
|
|
'zoek',
|
|
'zoeken',
|
|
'vind',
|
|
'wie',
|
|
'is',
|
|
'waar',
|
|
'info',
|
|
'gegevens',
|
|
'dossier',
|
|
'overdracht',
|
|
'dienst',
|
|
'klaar',
|
|
'afronden',
|
|
'einde',
|
|
'start',
|
|
'begin',
|
|
];
|
|
|
|
// Common Dutch first names for better name detection
|
|
const COMMON_NAMES = new Set([
|
|
'jan', 'piet', 'klaas', 'marie', 'anna', 'lisa', 'eva', 'emma', 'sophie',
|
|
'thomas', 'lucas', 'daan', 'sem', 'liam', 'noah', 'julia', 'sara', 'lotte',
|
|
'willem', 'johannes', 'cornelis', 'hendrik', 'maria', 'johanna', 'elisabeth',
|
|
'peter', 'hans', 'henk', 'johan', 'bert', 'dick', 'kees', 'jaap', 'wim',
|
|
'annie', 'bep', 'corrie', 'dinie', 'els', 'gerda', 'hanneke', 'ineke', 'joke',
|
|
]);
|
|
|
|
/**
|
|
* Extract entities from user input based on the detected intent.
|
|
*
|
|
* @param input - User input text
|
|
* @param intent - Detected intent
|
|
* @param referenceDate - Reference date for relative date calculations (default: new Date())
|
|
* Server-side: pass getServerNow() for consistency
|
|
*/
|
|
export function extractEntities(
|
|
input: string,
|
|
intent: CortexIntent,
|
|
referenceDate: Date = new Date()
|
|
): ExtractedEntities {
|
|
const trimmedInput = input.trim().toLowerCase();
|
|
const entities: ExtractedEntities = {};
|
|
|
|
switch (intent) {
|
|
case 'dagnotitie':
|
|
return extractDagnotatieEntities(trimmedInput, input);
|
|
case 'zoeken':
|
|
return extractZoekenEntities(trimmedInput, input);
|
|
case 'overdracht':
|
|
// Overdracht doesn't need entity extraction
|
|
return entities;
|
|
case 'agenda_query':
|
|
return extractAgendaQueryEntities(trimmedInput, input, referenceDate);
|
|
case 'create_appointment':
|
|
return extractCreateAppointmentEntities(trimmedInput, input, referenceDate);
|
|
case 'cancel_appointment':
|
|
return extractCancelAppointmentEntities(trimmedInput, input, referenceDate);
|
|
case 'reschedule_appointment':
|
|
return extractRescheduleAppointmentEntities(trimmedInput, input, referenceDate);
|
|
// Intake intents (MVP)
|
|
case 'intake_navigeer':
|
|
return extractIntakeNavigeerEntities(trimmedInput);
|
|
case 'intake_status':
|
|
case 'risico_query':
|
|
case 'diagnose_query':
|
|
// These intents use patient context from store, no entity extraction needed
|
|
return entities;
|
|
default:
|
|
return entities;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract entities for dagnotitie intent.
|
|
* Patterns:
|
|
* - "notitie jan medicatie" → name: jan, category: medicatie
|
|
* - "jan medicatie gegeven" → name: jan, category: medicatie, content: gegeven
|
|
* - "notitie medicatie jan" → name: jan, category: medicatie
|
|
*/
|
|
function extractDagnotatieEntities(lowerInput: string, originalInput: string): ExtractedEntities {
|
|
const entities: ExtractedEntities = {};
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Remove command words
|
|
const filteredWords = words.filter(w => !COMMAND_WORDS.includes(w));
|
|
|
|
// Find category
|
|
let categoryIndex = -1;
|
|
for (let i = 0; i < filteredWords.length; i++) {
|
|
const category = CATEGORY_ALIASES[filteredWords[i]];
|
|
if (category) {
|
|
entities.category = category;
|
|
categoryIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find name (word that's not a category and looks like a name)
|
|
for (let i = 0; i < filteredWords.length; i++) {
|
|
if (i === categoryIndex) continue;
|
|
|
|
const word = filteredWords[i];
|
|
// Check if it's a known name or starts with uppercase in original
|
|
if (isLikelyName(word, originalInput)) {
|
|
entities.patientName = capitalizeFirst(word);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Extract remaining content
|
|
const contentWords = filteredWords.filter((w, i) => {
|
|
if (i === categoryIndex) return false;
|
|
if (entities.patientName && w === entities.patientName.toLowerCase()) return false;
|
|
return true;
|
|
});
|
|
|
|
if (contentWords.length > 0) {
|
|
entities.content = contentWords.join(' ');
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* Extract entities for zoeken intent.
|
|
* Patterns:
|
|
* - "zoek jan" → name: jan
|
|
* - "wie is marie" → name: marie
|
|
* - "dossier piet" → name: piet
|
|
*/
|
|
function extractZoekenEntities(lowerInput: string, originalInput: string): ExtractedEntities {
|
|
const entities: ExtractedEntities = {};
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Remove command words
|
|
const filteredWords = words.filter(w => !COMMAND_WORDS.includes(w));
|
|
|
|
// The remaining word(s) should be the name
|
|
for (const word of filteredWords) {
|
|
if (isLikelyName(word, originalInput)) {
|
|
entities.patientName = capitalizeFirst(word);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no name found but there are remaining words, use the first one
|
|
if (!entities.patientName && filteredWords.length > 0) {
|
|
entities.patientName = capitalizeFirst(filteredWords[0]);
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* Check if a word is likely a patient name.
|
|
*/
|
|
function isLikelyName(word: string, originalInput: string): boolean {
|
|
// Check if it's a common name
|
|
if (COMMON_NAMES.has(word.toLowerCase())) {
|
|
return true;
|
|
}
|
|
|
|
// Check if the word starts with uppercase in the original input
|
|
const regex = new RegExp(`\\b${escapeRegex(word)}\\b`, 'i');
|
|
const match = originalInput.match(regex);
|
|
if (match && match[0][0] === match[0][0].toUpperCase()) {
|
|
return true;
|
|
}
|
|
|
|
// Single word that's not a category or command
|
|
if (
|
|
word.length >= 2 &&
|
|
!CATEGORY_ALIASES[word] &&
|
|
!COMMAND_WORDS.includes(word) &&
|
|
/^[a-z]+$/i.test(word)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Capitalize the first letter of a string.
|
|
*/
|
|
function capitalizeFirst(str: string): string {
|
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Escape special regex characters.
|
|
*/
|
|
function escapeRegex(str: string): string {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
/**
|
|
* Parse category from a string (with alias support).
|
|
*/
|
|
export function parseCategory(input: string): VerpleegkundigCategory | undefined {
|
|
const lower = input.toLowerCase().trim();
|
|
return CATEGORY_ALIASES[lower];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Agenda Entity Extraction
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Extract entities for agenda_query intent.
|
|
* Patterns:
|
|
* - "afspraken vandaag" → dateRange: today
|
|
* - "agenda morgen" → dateRange: tomorrow
|
|
* - "wat is volgende afspraak" → dateRange: from now (no explicit range)
|
|
* - "afspraken deze week" → dateRange: this week
|
|
*/
|
|
function extractAgendaQueryEntities(
|
|
lowerInput: string,
|
|
originalInput: string,
|
|
referenceDate: Date
|
|
): ExtractedEntities {
|
|
const entities: ExtractedEntities = {};
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Try to find date expression
|
|
// Check multi-word patterns first (e.g., "deze week", "volgende week")
|
|
for (let i = 0; i < words.length - 1; i++) {
|
|
const twoWords = `${words[i]} ${words[i + 1]}`;
|
|
const parsed = parseRelativeDate(twoWords, referenceDate);
|
|
if (parsed) {
|
|
if (isDateRange(parsed)) {
|
|
entities.dateRange = parsed;
|
|
} else {
|
|
entities.dateRange = dateToRange(parsed, extractDateLabel(twoWords));
|
|
}
|
|
return entities;
|
|
}
|
|
}
|
|
|
|
// Check single word patterns
|
|
for (const word of words) {
|
|
const parsed = parseRelativeDate(word, referenceDate);
|
|
if (parsed) {
|
|
if (isDateRange(parsed)) {
|
|
entities.dateRange = parsed;
|
|
} else {
|
|
entities.dateRange = dateToRange(parsed, extractDateLabel(word));
|
|
}
|
|
return entities;
|
|
}
|
|
}
|
|
|
|
// Default to today if no date specified
|
|
entities.dateRange = dateToRange(referenceDate, 'vandaag');
|
|
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* Extract entities for create_appointment intent.
|
|
* Patterns:
|
|
* - "maak afspraak jan morgen 14:00" → patient: Jan, date: tomorrow, time: 14:00
|
|
* - "plan intake marie vrijdag 10:00" → patient: Marie, type: intake, date: friday, time: 10:00
|
|
* - "afspraak met piet 14:00" → patient: Piet, time: 14:00, date: today (implied)
|
|
*/
|
|
function extractCreateAppointmentEntities(
|
|
lowerInput: string,
|
|
originalInput: string,
|
|
referenceDate: Date
|
|
): ExtractedEntities {
|
|
const entities: ExtractedEntities = {};
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Remove command words
|
|
const appointmentCommands = ['maak', 'plan', 'afspraak', 'met', 'nieuwe', 'voor'];
|
|
const filteredWords = words.filter(w => !appointmentCommands.includes(w));
|
|
|
|
// Extract appointment type
|
|
const typeKeywords: Record<string, ExtractedEntities['appointmentType']> = {
|
|
'intake': 'intake',
|
|
'behandeling': 'behandeling',
|
|
'vervolg': 'follow-up',
|
|
'vervolgafspraak': 'follow-up',
|
|
'telefonisch': 'telefonisch',
|
|
'bellen': 'telefonisch',
|
|
'huisbezoek': 'huisbezoek',
|
|
'thuis': 'huisbezoek',
|
|
'online': 'online',
|
|
'video': 'online',
|
|
'crisis': 'crisis',
|
|
'spoed': 'crisis',
|
|
};
|
|
|
|
for (const [keyword, type] of Object.entries(typeKeywords)) {
|
|
if (lowerInput.includes(keyword)) {
|
|
entities.appointmentType = type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Extract location
|
|
const locationKeywords: Record<string, ExtractedEntities['location']> = {
|
|
'praktijk': 'praktijk',
|
|
'online': 'online',
|
|
'video': 'online',
|
|
'thuis': 'thuis',
|
|
'huisbezoek': 'thuis',
|
|
};
|
|
|
|
for (const [keyword, location] of Object.entries(locationKeywords)) {
|
|
if (lowerInput.includes(keyword)) {
|
|
entities.location = location;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Extract patient name
|
|
for (const word of filteredWords) {
|
|
if (isLikelyName(word, originalInput)) {
|
|
entities.patientName = capitalizeFirst(word);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Extract date and time
|
|
let foundDate: Date | null = null;
|
|
let foundTime: string | null = null;
|
|
|
|
// Try multi-word date patterns
|
|
for (let i = 0; i < filteredWords.length - 1; i++) {
|
|
const twoWords = `${filteredWords[i]} ${filteredWords[i + 1]}`;
|
|
const parsed = parseRelativeDate(twoWords, referenceDate);
|
|
if (parsed && !isDateRange(parsed)) {
|
|
foundDate = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try single word date patterns
|
|
if (!foundDate) {
|
|
for (const word of filteredWords) {
|
|
const parsed = parseRelativeDate(word, referenceDate);
|
|
if (parsed && !isDateRange(parsed)) {
|
|
foundDate = parsed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to find time
|
|
for (const word of filteredWords) {
|
|
const parsed = parseTime(word);
|
|
if (parsed) {
|
|
foundTime = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try multi-word time patterns (e.g., "half drie")
|
|
if (!foundTime) {
|
|
for (let i = 0; i < filteredWords.length - 1; i++) {
|
|
const twoWords = `${filteredWords[i]} ${filteredWords[i + 1]}`;
|
|
const parsed = parseTime(twoWords);
|
|
if (parsed) {
|
|
foundTime = parsed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Combine date and time if both found
|
|
if (foundDate && foundTime) {
|
|
entities.datetime = {
|
|
date: foundDate,
|
|
time: foundTime,
|
|
};
|
|
} else if (foundDate) {
|
|
// Date without time
|
|
entities.datetime = {
|
|
date: foundDate,
|
|
time: '', // Will be filled by UI or AI
|
|
};
|
|
} else if (foundTime) {
|
|
// Time without date (assume today based on referenceDate)
|
|
entities.datetime = {
|
|
date: referenceDate,
|
|
time: foundTime,
|
|
};
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* Extract entities for cancel_appointment intent.
|
|
* Patterns:
|
|
* - "annuleer afspraak jan" → identifier: { type: patient, patientName: Jan }
|
|
* - "cancel de 14:00 afspraak" → identifier: { type: time, time: 14:00 }
|
|
* - "annuleer jan morgen" → identifier: { type: both, patientName: Jan, date: tomorrow }
|
|
*/
|
|
function extractCancelAppointmentEntities(
|
|
lowerInput: string,
|
|
originalInput: string,
|
|
referenceDate: Date
|
|
): ExtractedEntities {
|
|
const entities: ExtractedEntities = {};
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Remove command words
|
|
const cancelCommands = ['annuleer', 'cancel', 'verwijder', 'afspraak', 'de', 'van'];
|
|
const filteredWords = words.filter(w => !cancelCommands.includes(w));
|
|
|
|
// Extract patient name
|
|
let patientName: string | undefined;
|
|
for (const word of filteredWords) {
|
|
if (isLikelyName(word, originalInput)) {
|
|
patientName = capitalizeFirst(word);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Extract time
|
|
let time: string | null = null;
|
|
for (const word of filteredWords) {
|
|
const parsed = parseTime(word);
|
|
if (parsed) {
|
|
time = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Extract date
|
|
let date: Date | null = null;
|
|
for (const word of filteredWords) {
|
|
const parsed = parseRelativeDate(word, referenceDate);
|
|
if (parsed && !isDateRange(parsed)) {
|
|
date = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Build identifier
|
|
if (patientName && time) {
|
|
entities.identifier = {
|
|
type: 'both',
|
|
patientName,
|
|
time,
|
|
date: date || undefined,
|
|
};
|
|
} else if (patientName) {
|
|
entities.identifier = {
|
|
type: 'patient',
|
|
patientName,
|
|
date: date || undefined,
|
|
};
|
|
} else if (time) {
|
|
entities.identifier = {
|
|
type: 'time',
|
|
time,
|
|
date: date || undefined,
|
|
};
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* Extract entities for reschedule_appointment intent.
|
|
* Patterns:
|
|
* - "verzet 14:00 naar 15:00" → identifier: { time: 14:00 }, newDatetime: { time: 15:00 }
|
|
* - "verzet jan naar dinsdag" → identifier: { patientName: Jan }, newDatetime: { date: tuesday }
|
|
* - "verzet de afspraak naar morgen 10:00" → newDatetime: { date: tomorrow, time: 10:00 }
|
|
*/
|
|
function extractRescheduleAppointmentEntities(
|
|
lowerInput: string,
|
|
originalInput: string,
|
|
referenceDate: Date
|
|
): ExtractedEntities {
|
|
const entities: ExtractedEntities = {};
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Split input on "naar" to separate old and new parts
|
|
const naarIndex = words.indexOf('naar');
|
|
const oldPart = naarIndex > 0 ? words.slice(0, naarIndex).join(' ') : lowerInput;
|
|
const newPart = naarIndex > 0 ? words.slice(naarIndex + 1).join(' ') : '';
|
|
|
|
// Remove command words
|
|
const rescheduleCommands = ['verzet', 'verplaats', 'verschuif', 'afspraak', 'de', 'van'];
|
|
const oldWords = oldPart.split(/\s+/).filter(w => !rescheduleCommands.includes(w));
|
|
const newWords = newPart.split(/\s+/);
|
|
|
|
// Extract old appointment identifier
|
|
let patientName: string | undefined;
|
|
for (const word of oldWords) {
|
|
if (isLikelyName(word, originalInput)) {
|
|
patientName = capitalizeFirst(word);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let oldTime: string | null = null;
|
|
for (const word of oldWords) {
|
|
const parsed = parseTime(word);
|
|
if (parsed) {
|
|
oldTime = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let oldDate: Date | null = null;
|
|
for (const word of oldWords) {
|
|
const parsed = parseRelativeDate(word, referenceDate);
|
|
if (parsed && !isDateRange(parsed)) {
|
|
oldDate = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Build identifier
|
|
if (patientName && oldTime) {
|
|
entities.identifier = {
|
|
type: 'both',
|
|
patientName,
|
|
time: oldTime,
|
|
date: oldDate || undefined,
|
|
};
|
|
} else if (patientName) {
|
|
entities.identifier = {
|
|
type: 'patient',
|
|
patientName,
|
|
date: oldDate || undefined,
|
|
};
|
|
} else if (oldTime) {
|
|
entities.identifier = {
|
|
type: 'time',
|
|
time: oldTime,
|
|
date: oldDate || undefined,
|
|
};
|
|
}
|
|
|
|
// Extract new datetime
|
|
if (newWords.length > 0) {
|
|
let newDate: Date | null = null;
|
|
let newTime: string | null = null;
|
|
|
|
// Try multi-word date patterns
|
|
for (let i = 0; i < newWords.length - 1; i++) {
|
|
const twoWords = `${newWords[i]} ${newWords[i + 1]}`;
|
|
const parsed = parseRelativeDate(twoWords, referenceDate);
|
|
if (parsed && !isDateRange(parsed)) {
|
|
newDate = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try single word date patterns
|
|
if (!newDate) {
|
|
for (const word of newWords) {
|
|
const parsed = parseRelativeDate(word, referenceDate);
|
|
if (parsed && !isDateRange(parsed)) {
|
|
newDate = parsed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to find new time
|
|
for (const word of newWords) {
|
|
const parsed = parseTime(word);
|
|
if (parsed) {
|
|
newTime = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try multi-word time patterns
|
|
if (!newTime) {
|
|
for (let i = 0; i < newWords.length - 1; i++) {
|
|
const twoWords = `${newWords[i]} ${newWords[i + 1]}`;
|
|
const parsed = parseTime(twoWords);
|
|
if (parsed) {
|
|
newTime = parsed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newDate || newTime) {
|
|
entities.newDatetime = {
|
|
date: newDate || referenceDate, // Default to referenceDate if only time specified
|
|
time: newTime || '',
|
|
};
|
|
}
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* Extract date label from input string
|
|
*/
|
|
function extractDateLabel(input: string): DateRange['label'] {
|
|
const normalized = input.toLowerCase();
|
|
if (normalized.includes('vandaag')) return 'vandaag';
|
|
if (normalized.includes('morgen')) return 'morgen';
|
|
if (normalized.includes('deze week')) return 'deze week';
|
|
if (normalized.includes('volgende week')) return 'volgende week';
|
|
return 'custom';
|
|
}
|
|
|
|
// ============================================================================
|
|
// Intake Entity Extraction (MVP)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Intake tab targets for navigation.
|
|
*/
|
|
export type IntakeTab =
|
|
| 'contacts'
|
|
| 'kindcheck'
|
|
| 'risk'
|
|
| 'diagnosis'
|
|
| 'behandeladvies';
|
|
|
|
/**
|
|
* Mapping of keywords to intake tabs.
|
|
*/
|
|
const INTAKE_TAB_KEYWORDS: Record<string, IntakeTab> = {
|
|
// Risk
|
|
'risico': 'risk',
|
|
'risicotaxatie': 'risk',
|
|
'risicos': 'risk',
|
|
// Diagnosis
|
|
'diagnose': 'diagnosis',
|
|
'diagnoses': 'diagnosis',
|
|
'dsm': 'diagnosis',
|
|
// Contacts
|
|
'contact': 'contacts',
|
|
'contacten': 'contacts',
|
|
'contactmomenten': 'contacts',
|
|
// Kindcheck
|
|
'kindcheck': 'kindcheck',
|
|
'kinderen': 'kindcheck',
|
|
// Behandeladvies
|
|
'behandeladvies': 'behandeladvies',
|
|
'advies': 'behandeladvies',
|
|
'behandeling': 'behandeladvies',
|
|
'samenvatting': 'behandeladvies',
|
|
// Doelen (maps to behandeladvies as it's part of that tab in MVP)
|
|
'doelen': 'behandeladvies',
|
|
'doelstellingen': 'behandeladvies',
|
|
// Netwerk (maps to contacts as it's closest in MVP)
|
|
'netwerk': 'contacts',
|
|
'sociaal': 'contacts',
|
|
};
|
|
|
|
/**
|
|
* Extract entities for intake_navigeer intent.
|
|
* Determines which intake tab to navigate to.
|
|
*/
|
|
function extractIntakeNavigeerEntities(lowerInput: string): ExtractedEntities & { navigationTarget?: IntakeTab } {
|
|
const words = lowerInput.split(/\s+/);
|
|
|
|
// Find the navigation target
|
|
for (const word of words) {
|
|
const target = INTAKE_TAB_KEYWORDS[word];
|
|
if (target) {
|
|
return { navigationTarget: target } as ExtractedEntities & { navigationTarget?: IntakeTab };
|
|
}
|
|
}
|
|
|
|
// Default to risk (most common navigation target in intake context)
|
|
return { navigationTarget: 'risk' } as ExtractedEntities & { navigationTarget?: IntakeTab };
|
|
}
|