+ {/* Label */}
+
+
+ Recent
+
+
+ {/* Divider */}
+
+
+ {/* Actions */}
{recentActions.length === 0 ? (
Nog geen acties
) : (
-
- {recentActions.map((action) => (
-
- ))}
+
+ {recentActions.map((action) => {
+ const config = INTENT_CONFIG[action.intent];
+ const Icon = config.icon;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ {/* Quick actions hint (when empty) */}
+ {recentActions.length === 0 && (
+
+ Probeer:
+
+
)}
diff --git a/docs/swift/bouwplan-swift-v1.md b/docs/swift/bouwplan-swift-v1.md
index 1c3e8b8..4768ad7 100644
--- a/docs/swift/bouwplan-swift-v1.md
+++ b/docs/swift/bouwplan-swift-v1.md
@@ -129,14 +129,14 @@ lib/
| Epic ID | Titel | Doel | Status | Stories | Effort |
|---------|-------|------|--------|---------|--------|
-| E0 | Setup & Foundation | Zustand, routing, base layout | ⏳ To Do | 4 | 8 SP |
-| E1 | Command Center | Input, voice, context bar | ⏳ To Do | 5 | 13 SP |
+| E0 | Setup & Foundation | Zustand, routing, base layout | ✅ Done | 4 | 8 SP |
+| E1 | Command Center | Input, voice, context bar | ✅ Done | 5 | 13 SP |
| E2 | Intent Classification | Local + AI fallback | ⏳ To Do | 4 | 10 SP |
| E3 | P1 Blocks | Dagnotitie, Zoeken, Overdracht | ⏳ To Do | 6 | 21 SP |
| E4 | Navigation & Auth | Login keuze, routing, preferences | ⏳ To Do | 4 | 8 SP |
| E5 | Polish & Testing | Animaties, error handling, tests | ⏳ To Do | 4 | 8 SP |
-**Totaal: 27 stories, 68 story points**
+**Totaal: 27 stories, 68 story points (21 SP done, 47 SP remaining)**
**Belangrijk:**
- Bouw per epic en per story, niet alles tegelijk
@@ -152,10 +152,10 @@ lib/
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afh. | SP |
|----------|--------------|---------------------|--------|------|----|
-| E0.S1 | Zustand installeren | `pnpm add zustand` succesvol, import werkt | ⏳ | — | 1 |
-| E0.S2 | Swift store opzetten | `stores/swift-store.ts` met basis state | ⏳ | E0.S1 | 2 |
-| E0.S3 | Swift route aanmaken | `/epd/swift` route met eigen layout | ⏳ | E0.S2 | 2 |
-| E0.S4 | Swift folder structuur | `components/swift/`, `lib/swift/` aangemaakt | ⏳ | E0.S3 | 3 |
+| E0.S1 | Zustand installeren | `pnpm add zustand` succesvol, import werkt | ✅ | — | 1 |
+| E0.S2 | Swift store opzetten | `stores/swift-store.ts` met basis state | ✅ | E0.S1 | 2 |
+| E0.S3 | Swift route aanmaken | `/epd/swift` route met eigen layout | ✅ | E0.S2 | 2 |
+| E0.S4 | Swift folder structuur | `components/swift/`, `lib/swift/` aangemaakt | ✅ | E0.S3 | 3 |
**Technical Notes:**
```bash
@@ -192,11 +192,11 @@ interface SwiftStore {
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afh. | SP |
|----------|--------------|---------------------|--------|------|----|
-| E1.S1 | Command Center layout | 4-zone layout (context, canvas, recent, input) | ⏳ | E0.S4 | 3 |
-| E1.S2 | Context Bar | Dienst, patiënt dropdown, user info | ⏳ | E1.S1 | 2 |
-| E1.S3 | Command Input | Tekst input met placeholder, focus state | ⏳ | E1.S1 | 2 |
-| E1.S4 | Voice Input integratie | Deepgram streaming in command input | ⏳ | E1.S3 | 3 |
-| E1.S5 | Recent Strip | Laatste 5 acties als chips | ⏳ | E1.S1 | 3 |
+| E1.S1 | Command Center layout | 4-zone layout (context, canvas, recent, input) | ✅ | E0.S4 | 3 |
+| E1.S2 | Context Bar | Dienst, patiënt dropdown, user info | ✅ | E1.S1 | 2 |
+| E1.S3 | Command Input | Tekst input met placeholder, focus state | ✅ | E1.S1 | 2 |
+| E1.S4 | Voice Input integratie | Deepgram streaming in command input | ✅ | E1.S3 | 3 |
+| E1.S5 | Recent Strip | Laatste 5 acties als chips | ✅ | E1.S1 | 3 |
**Technical Notes:**
```
@@ -530,3 +530,4 @@ Een epic is **Done** wanneer:
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 23-12-2024 | Colin Lit | Initiële versie |
+| v1.1 | 23-12-2024 | Claude | E0 + E1 voltooid (21 SP) |
diff --git a/lib/swift/index.ts b/lib/swift/index.ts
index 73c4fe0..37db350 100644
--- a/lib/swift/index.ts
+++ b/lib/swift/index.ts
@@ -3,3 +3,4 @@
*/
export * from './types';
+export * from './use-swift-voice';
diff --git a/lib/swift/use-swift-voice.ts b/lib/swift/use-swift-voice.ts
new file mode 100644
index 0000000..484f70d
--- /dev/null
+++ b/lib/swift/use-swift-voice.ts
@@ -0,0 +1,115 @@
+'use client';
+
+/**
+ * Swift Voice Hook
+ *
+ * Wraps useDeepgramStreaming for Swift-specific voice input behavior.
+ * Streams transcript directly to the command input.
+ */
+
+import { useCallback, useEffect, useRef } from 'react';
+import { useSwiftStore } from '@/stores/swift-store';
+import {
+ useDeepgramStreaming,
+ type TranscriptResult,
+} from '@/hooks/use-deepgram-streaming';
+
+export interface UseSwiftVoiceReturn {
+ isRecording: boolean;
+ isConnecting: boolean;
+ isConnected: boolean;
+ error: string | null;
+ startRecording: () => Promise
;
+ stopRecording: () => void;
+ analyserNode: AnalyserNode | null;
+ isBrowserSupported: boolean;
+}
+
+export function useSwiftVoice(): UseSwiftVoiceReturn {
+ const { setInputValue, setVoiceActive, inputValue } = useSwiftStore();
+
+ // Track the base text (what was in input before recording started)
+ const baseTextRef = useRef('');
+ // Track interim transcript for replacement
+ const lastInterimRef = useRef('');
+
+ const handleTranscript = useCallback(
+ (result: TranscriptResult) => {
+ const { transcript, isFinal } = result;
+
+ if (isFinal) {
+ // Final transcript: append to base text and update base
+ const newText = baseTextRef.current
+ ? `${baseTextRef.current} ${transcript}`
+ : transcript;
+ baseTextRef.current = newText;
+ lastInterimRef.current = '';
+ setInputValue(newText);
+ } else {
+ // Interim transcript: show as preview (replace previous interim)
+ const previewText = baseTextRef.current
+ ? `${baseTextRef.current} ${transcript}`
+ : transcript;
+ setInputValue(previewText);
+ lastInterimRef.current = transcript;
+ }
+ },
+ [setInputValue]
+ );
+
+ const handleError = useCallback(
+ (error: Error) => {
+ console.error('[SwiftVoice] Error:', error.message);
+ },
+ []
+ );
+
+ const {
+ status,
+ isRecording,
+ startRecording: startDeepgram,
+ stopRecording: stopDeepgram,
+ analyserNode,
+ error,
+ isBrowserSupported,
+ } = useDeepgramStreaming({
+ onTranscript: handleTranscript,
+ onError: handleError,
+ language: 'nl',
+ model: 'nova-2',
+ endpointingMs: 2000, // Shorter for command-style input
+ });
+
+ const startRecording = useCallback(async () => {
+ // Store current input as base text
+ baseTextRef.current = inputValue;
+ lastInterimRef.current = '';
+ setVoiceActive(true);
+ await startDeepgram();
+ }, [inputValue, setVoiceActive, startDeepgram]);
+
+ const stopRecording = useCallback(() => {
+ stopDeepgram();
+ setVoiceActive(false);
+ // Keep whatever text is in the input
+ lastInterimRef.current = '';
+ }, [stopDeepgram, setVoiceActive]);
+
+ // Sync voice active state with recording state
+ useEffect(() => {
+ if (!isRecording) {
+ setVoiceActive(false);
+ }
+ }, [isRecording, setVoiceActive]);
+
+ return {
+ isRecording,
+ isConnecting: status === 'connecting' || status === 'reconnecting',
+ isConnected: status === 'connected',
+ error,
+ startRecording,
+ stopRecording,
+ analyserNode,
+ isBrowserSupported,
+ };
+}