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'),
}),