feat(cortex): update chat/nudge flow, deepgram streaming and agenda form
Refines chat panel, nudge messages and artifact rendering, reworks deepgram token/streaming handling, and adds test tooling deps (playwright, cypress) to package.json. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,8 @@ import { ChatEmptyState } from './chat-empty-state';
|
||||
import { ActionChainCard } from './action-chain-card';
|
||||
import { ClarificationCard } from './clarification-card';
|
||||
import { ProcessingIndicator } from './processing-indicator';
|
||||
import { useCortexStore, type ChatMessage as ChatMessageType } from '@/stores/cortex-store';
|
||||
import { useCortexStore, type ChatMessage as ChatMessageType, type NoShowFlowState } from '@/stores/cortex-store';
|
||||
import type { NudgeSuggestion } from '@/lib/cortex/types';
|
||||
import { sendChatMessage } from '@/lib/cortex/chat-api';
|
||||
import { parseActionFromResponse, shouldOpenArtifact, routeIntentToArtifact, getDefaultConfirmationMessage } from '@/lib/cortex/action-parser';
|
||||
import { evaluateNudge } from '@/lib/cortex/nudge';
|
||||
@@ -56,6 +57,12 @@ export function ChatPanel() {
|
||||
const acceptSuggestion = useCortexStore((s) => s.acceptSuggestion);
|
||||
const dismissSuggestion = useCortexStore((s) => s.dismissSuggestion);
|
||||
|
||||
// No-show flow state
|
||||
const setNoShowStep = useCortexStore((s) => s.setNoShowStep);
|
||||
const setNoShowContext = useCortexStore((s) => s.setNoShowContext);
|
||||
const setNoShowProcessing = useCortexStore((s) => s.setNoShowProcessing);
|
||||
const resetNoShowFlow = useCortexStore((s) => s.resetNoShowFlow);
|
||||
|
||||
// Refs for scrolling
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -118,6 +125,16 @@ export function ChatPanel() {
|
||||
}
|
||||
}, [hasMessages]);
|
||||
|
||||
// Cleanup no-show flow bij unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const { noShowFlow } = useCortexStore.getState();
|
||||
if (noShowFlow.step !== 'idle' && noShowFlow.step !== 'done') {
|
||||
resetNoShowFlow();
|
||||
}
|
||||
};
|
||||
}, [resetNoShowFlow]);
|
||||
|
||||
// Handle suggestion selection - fill input with selected text
|
||||
const handleSelectSuggestion = useCallback((text: string) => {
|
||||
chatInputRef.current?.setValue(text);
|
||||
@@ -137,6 +154,111 @@ export function ChatPanel() {
|
||||
return () => window.removeEventListener('keydown', handleGlobalKeyDown);
|
||||
}, []);
|
||||
|
||||
// No-show flow: stap 2→3 — annuleer afspraak + check concept brief
|
||||
const handleNoShowCancelStep = useCallback(async (_suggestion: NudgeSuggestion) => {
|
||||
const { isNoShowProcessing } = useCortexStore.getState();
|
||||
if (isNoShowProcessing) return;
|
||||
|
||||
setNoShowProcessing(true);
|
||||
setNoShowStep('waiting_cancel');
|
||||
addChatMessage({ type: 'assistant', content: 'Bezig met annuleren...' });
|
||||
|
||||
try {
|
||||
const patientId = activePatient?.id ?? 'demo-patient-001';
|
||||
|
||||
// Stap 1: annuleer de afspraak
|
||||
const cancelRes = await fetch('/api/cortex/noshow/cancel', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ appointmentId: 'mock-appt-noshow-001', patientId }),
|
||||
});
|
||||
if (!cancelRes.ok) throw new Error('Annuleren mislukt');
|
||||
|
||||
// Stap 2: check op concept brief
|
||||
const ctxRes = await fetch(`/api/cortex/noshow/context?patientId=${patientId}`);
|
||||
const ctx = await ctxRes.json();
|
||||
|
||||
if (ctx.hasConceptBrief) {
|
||||
setNoShowContext({ documentId: ctx.document.id, originalContent: ctx.document.content });
|
||||
setNoShowStep('waiting_brief');
|
||||
|
||||
// Construeer nudge 2 handmatig
|
||||
const briefNudge: NudgeSuggestion = {
|
||||
id: `nudge-noshow-brief-${Date.now()}`,
|
||||
trigger: { actionId: 'noshow-cancel-done', intent: 'cancel_appointment', entities: {} },
|
||||
suggestion: {
|
||||
intent: 'register_no_show',
|
||||
entities: {},
|
||||
message: 'Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?',
|
||||
rationale: 'noshow-brief-check',
|
||||
},
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
addChatMessage({ type: 'nudge', content: briefNudge.suggestion.message, nudge: briefNudge });
|
||||
} else {
|
||||
setNoShowStep('done');
|
||||
addChatMessage({
|
||||
type: 'assistant',
|
||||
content: 'Afspraak geannuleerd als No Show. Er zijn geen openstaande conceptbrieven gevonden.',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
setNoShowStep('idle');
|
||||
addChatMessage({ type: 'error', content: 'Er is iets misgegaan bij het annuleren. Probeer het opnieuw.' });
|
||||
} finally {
|
||||
setNoShowProcessing(false);
|
||||
}
|
||||
}, [activePatient, setNoShowStep, setNoShowContext, setNoShowProcessing, addChatMessage]);
|
||||
|
||||
// No-show flow: stap 4→5 — rescript brief + open artifact
|
||||
const handleNoShowRescriptStep = useCallback(async () => {
|
||||
const { isNoShowProcessing, noShowFlow } = useCortexStore.getState();
|
||||
if (isNoShowProcessing) return;
|
||||
|
||||
setNoShowProcessing(true);
|
||||
setNoShowStep('brief_open');
|
||||
addChatMessage({ type: 'assistant', content: 'Huisartsbrief aanpassen...' });
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/cortex/noshow/rescript', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentId: noShowFlow.documentId ?? 'mock-brief-noshow-001',
|
||||
originalContent: noShowFlow.originalContent ?? '',
|
||||
patientId: activePatient?.id ?? 'demo-patient-001',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
openArtifact({
|
||||
type: 'register_no_show',
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
prefill: {
|
||||
documentId: result.documentId,
|
||||
content: result.rescriptedContent,
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
originalContent: result.originalContent,
|
||||
rescriptWarning: result.warning,
|
||||
},
|
||||
});
|
||||
|
||||
addChatMessage({
|
||||
type: 'assistant',
|
||||
content: 'Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent.',
|
||||
});
|
||||
} catch {
|
||||
setNoShowStep('waiting_brief');
|
||||
addChatMessage({ type: 'error', content: 'Herschrijven mislukt. Probeer het opnieuw.' });
|
||||
} finally {
|
||||
setNoShowProcessing(false);
|
||||
}
|
||||
}, [activePatient, setNoShowStep, setNoShowProcessing, addChatMessage, openArtifact]);
|
||||
|
||||
// V2 Chain action handlers (E5.S2)
|
||||
const handleConfirmAction = useCallback((actionId: string) => {
|
||||
// Find the action in the active chain
|
||||
@@ -223,28 +345,40 @@ export function ChatPanel() {
|
||||
}, [setPendingClarification]);
|
||||
|
||||
// Nudge handlers (chat-based nudges)
|
||||
const handleAcceptNudge = useCallback((suggestionId: string, suggestion: ChatMessageType['nudge']) => {
|
||||
const handleAcceptNudge = useCallback(async (suggestionId: string, suggestion: ChatMessageType['nudge']) => {
|
||||
console.log('[ChatPanel] Nudge accepted:', suggestionId);
|
||||
acceptSuggestion(suggestionId);
|
||||
|
||||
if (suggestion) {
|
||||
// Route to artifact with prefilled entities
|
||||
const artifact = routeIntentToArtifact(
|
||||
suggestion.suggestion.intent,
|
||||
suggestion.suggestion.entities,
|
||||
0.9 // High confidence for nudge-initiated actions
|
||||
);
|
||||
if (!suggestion) return;
|
||||
|
||||
if (artifact) {
|
||||
console.log('[ChatPanel] Opening artifact from nudge:', artifact.type);
|
||||
openArtifact({
|
||||
type: artifact.type,
|
||||
prefill: artifact.prefill,
|
||||
title: artifact.title,
|
||||
});
|
||||
}
|
||||
// No-show flow stap 2→3: declarabiliteitscheck geaccepteerd
|
||||
if (suggestion.trigger.intent === 'register_no_show') {
|
||||
await handleNoShowCancelStep(suggestion);
|
||||
return;
|
||||
}
|
||||
}, [acceptSuggestion, openArtifact]);
|
||||
|
||||
// No-show flow stap 4→5: brief-check geaccepteerd
|
||||
if (suggestion.suggestion.rationale === 'noshow-brief-check') {
|
||||
await handleNoShowRescriptStep();
|
||||
return;
|
||||
}
|
||||
|
||||
// Generieke flow voor alle andere nudges
|
||||
const artifact = routeIntentToArtifact(
|
||||
suggestion.suggestion.intent,
|
||||
suggestion.suggestion.entities,
|
||||
0.9
|
||||
);
|
||||
|
||||
if (artifact) {
|
||||
console.log('[ChatPanel] Opening artifact from nudge:', artifact.type);
|
||||
openArtifact({
|
||||
type: artifact.type,
|
||||
prefill: artifact.prefill,
|
||||
title: artifact.title,
|
||||
});
|
||||
}
|
||||
}, [acceptSuggestion, openArtifact, handleNoShowCancelStep, handleNoShowRescriptStep]);
|
||||
|
||||
const handleDismissNudge = useCallback((suggestionId: string) => {
|
||||
console.log('[ChatPanel] Nudge dismissed:', suggestionId);
|
||||
@@ -453,6 +587,19 @@ export function ChatPanel() {
|
||||
} else {
|
||||
console.log('[ChatPanel] Action confidence too low:', parsed.action.confidence);
|
||||
}
|
||||
|
||||
// No-show nudge trigger na register_no_show classificatie
|
||||
if (parsed.action.intent === 'register_no_show' && isFeatureEnabled('CORTEX_NUDGE')) {
|
||||
const suggestions = evaluateNudge({
|
||||
intent: 'register_no_show',
|
||||
actionId: crypto.randomUUID(),
|
||||
entities: {}, // register_no_show heeft geen entiteiten nodig
|
||||
content: message,
|
||||
});
|
||||
suggestions.forEach((suggestion) => {
|
||||
addChatMessage({ type: 'nudge', content: suggestion.suggestion.message, nudge: suggestion });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('[ChatPanel] No action detected in response');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user