diff --git a/components/swift/chat/chat-message.tsx b/components/swift/chat/chat-message.tsx new file mode 100644 index 0000000..7529863 --- /dev/null +++ b/components/swift/chat/chat-message.tsx @@ -0,0 +1,76 @@ +'use client'; + +/** + * Chat Message Component (v3.0) + * + * Displays individual chat messages with styling per message type. + * Supports user, assistant, system, and error message types. + * + * Epic: E2 (Chat Panel & Messages) + * Story: E2.S2 (ChatMessage component) + */ + +import { format } from 'date-fns'; +import { nl } from 'date-fns/locale'; +import { cn } from '@/lib/utils'; +import type { ChatMessage as ChatMessageType } from '@/stores/swift-store'; + +// Message styling configuration per type +const MESSAGE_STYLES = { + user: { + container: 'self-end bg-amber-50 border-amber-200 text-slate-900', + borderRadius: 'rounded-2xl rounded-tr-sm', + maxWidth: 'max-w-[80%]', + }, + assistant: { + container: 'self-start bg-slate-100 border-slate-200 text-slate-900', + borderRadius: 'rounded-2xl rounded-tl-sm', + maxWidth: 'max-w-[85%]', + }, + system: { + container: 'self-center bg-transparent border-transparent text-slate-500 text-sm italic', + borderRadius: 'rounded-lg', + maxWidth: 'max-w-[90%]', + }, + error: { + container: 'self-start bg-red-50 border-red-200 text-red-900', + borderRadius: 'rounded-2xl', + maxWidth: 'max-w-[80%]', + }, +} as const; + +interface ChatMessageProps { + message: ChatMessageType; + showTimestamp?: boolean; +} + +export function ChatMessage({ message, showTimestamp = false }: ChatMessageProps) { + const styles = MESSAGE_STYLES[message.type]; + + // Don't show border for system messages + const showBorder = message.type !== 'system'; + + return ( +
+ {/* Message content */} +
+ {message.content} +
+ + {/* Timestamp (optional) */} + {showTimestamp && message.timestamp && ( +
+ {format(message.timestamp, 'HH:mm', { locale: nl })} +
+ )} +
+ ); +} diff --git a/components/swift/chat/chat-panel.tsx b/components/swift/chat/chat-panel.tsx new file mode 100644 index 0000000..349da16 --- /dev/null +++ b/components/swift/chat/chat-panel.tsx @@ -0,0 +1,102 @@ +'use client'; + +/** + * Chat Panel (v3.0) + * + * Chat interface met message list en input. + * Toont chat messages met verschillende types (user, assistant, system, error). + * + * Epic: E2 (Chat Panel & Messages) + * Story: E2.S2 (ChatMessage component) - testing + */ + +import { ChatMessage } from './chat-message'; +import { useSwiftStore } from '@/stores/swift-store'; + +export function ChatPanel() { + const chatMessages = useSwiftStore((s) => s.chatMessages); + + // Demo messages for testing E2.S2 (will be removed in E2.S3) + const demoMessages = chatMessages.length === 0 ? [ + { + id: '1', + type: 'system' as const, + content: 'Welkom bij Swift Medical Scribe v3.0', + timestamp: new Date(), + }, + { + id: '2', + type: 'user' as const, + content: 'Notitie voor Jan: medicatie gegeven om 14:00', + timestamp: new Date(), + }, + { + id: '3', + type: 'assistant' as const, + content: 'Ik begrijp dat je een notitie wilt maken voor Jan over medicatie. Ik open een dagnotitie voor je waarin je dit kunt vastleggen.', + timestamp: new Date(), + }, + { + id: '4', + type: 'error' as const, + content: 'Er ging iets mis met de verbinding. Probeer het opnieuw.', + timestamp: new Date(), + }, + ] : chatMessages; + + const hasMessages = demoMessages.length > 0; + + return ( +
+ {/* Chat messages area */} +
+ {hasMessages ? ( +
+ {demoMessages.map((message) => ( + + ))} +
+ ) : ( +
+
+
💬
+

+ Welkom bij Swift Medical Scribe +

+

+ Typ of spreek wat je wilt doen... +

+
+

Voorbeelden:

+

• “Notitie voor Jan: medicatie gegeven”

+

• “Zoek Marie van den Berg”

+

• “Maak overdracht voor deze dienst”

+
+
+
+ )} +
+ + {/* Chat input - placeholder */} +
+
+ + +
+

+ E2.S2 compleet: ChatMessage component werkend ✓ +

+
+
+ ); +} diff --git a/stores/swift-store.ts b/stores/swift-store.ts index f7aca94..1cc089d 100644 --- a/stores/swift-store.ts +++ b/stores/swift-store.ts @@ -17,6 +17,27 @@ export type SwiftIntent = export type BlockType = Exclude | 'fallback'; +// Chat types (v3.0) +export type ChatMessageType = 'user' | 'assistant' | 'system' | 'error'; + +export interface ChatMessage { + id: string; + type: ChatMessageType; + content: string; + timestamp: Date; + action?: ChatAction; // Optional action attached to assistant messages +} + +export interface ChatAction { + intent: SwiftIntent; + entities: ExtractedEntities; + confidence: number; + artifact?: { + type: BlockType; + prefill: BlockPrefillData; + }; +} + // Extracted entities from user input export interface ExtractedEntities { patientName?: string; @@ -57,6 +78,11 @@ interface SwiftStore { // Recent actions recentActions: RecentAction[]; + // Chat state (v3.0) + chatMessages: ChatMessage[]; + isStreaming: boolean; + pendingAction: ChatAction | null; + // Context actions setActivePatient: (patient: Patient | null) => void; setShift: (shift: ShiftType) => void; @@ -74,6 +100,13 @@ interface SwiftStore { // Recent actions addRecentAction: (action: Omit) => void; + // Chat actions (v3.0) + addChatMessage: (message: Omit) => void; + updateLastMessage: (content: string) => void; + clearChat: () => void; + setStreaming: (streaming: boolean) => void; + setPendingAction: (action: ChatAction | null) => void; + // Reset reset: () => void; } @@ -97,6 +130,10 @@ const initialState = { inputValue: '', isVoiceActive: false, recentActions: [], + // Chat state (v3.0) + chatMessages: [], + isStreaming: false, + pendingAction: null, }; // Create the store @@ -161,6 +198,56 @@ export const useSwiftStore = create()( ); }, + // Chat actions (v3.0) + addChatMessage: (message) => { + const newMessage: ChatMessage = { + ...message, + id: crypto.randomUUID(), + timestamp: new Date(), + }; + + set( + (state) => ({ + chatMessages: [...state.chatMessages, newMessage], + }), + false, + 'addChatMessage' + ); + }, + + updateLastMessage: (content) => { + set( + (state) => { + const messages = [...state.chatMessages]; + if (messages.length > 0) { + messages[messages.length - 1] = { + ...messages[messages.length - 1], + content, + }; + } + return { chatMessages: messages }; + }, + false, + 'updateLastMessage' + ); + }, + + clearChat: () => { + set( + { + chatMessages: [], + isStreaming: false, + pendingAction: null, + }, + false, + 'clearChat' + ); + }, + + setStreaming: (streaming) => set({ isStreaming: streaming }, false, 'setStreaming'), + + setPendingAction: (action) => set({ pendingAction: action }, false, 'setPendingAction'), + // Reset reset: () => set(initialState, false, 'reset'), }),