Files
triqura-ecd/docs/specs/speech/bouwplan-realtime-speech.md
colinislit dce3943963 feat: rapportage UI refactor, speech streaming, docs & seed data
Rapportage:
- Refactor workspace into modular components (quick-actions, timeline-card, timeline-sidebar)
- Add updateReport action for inline editing
- Improve report timeline with better UX

Speech:
- Add Deepgram token API endpoint
- Add use-deepgram-streaming hook
- Add confidence-text component

Docs:
- Add architecture documentation
- Add performance optimization plan
- Add speech specs and seed data docs

Scripts & Data:
- Add seed-reports script and migration
- Update AGENTS.md guidelines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 14:07:08 +01:00

44 KiB

🚀 Mission Control — Bouwplan Real-Time Speech Transcription

Projectnaam: Real-Time Speech Transcription met Deepgram SDK Versie: v1.0 (Editor-First Design) Datum: 24-11-2025 Auteur: Claude Code + Colin


1. Doel en context

🎯 Doel: Migreren van de huidige REST-based batch transcriptie naar real-time streaming transcriptie met de Deepgram SDK, inclusief een complete UX redesign naar een editor-first interface met opt-in timeline en filters.

📘 Context: De huidige implementatie gebruikt een REST API waarbij audio wordt opgenomen als blob en na opname wordt geüpload voor transcriptie. Dit resulteert in 2-5+ seconden latency. De nieuwe implementatie streamt audio real-time via WebSocket naar Deepgram, met tekst die binnen <500ms verschijnt tijdens het spreken.

Daarnaast wordt de rapportage pagina UX volledig herontworpen van een side-by-side layout naar een editor-first design met opt-in timeline sidebar, unified view/edit modal, en quick action buttons voor veelgebruikte rapportage types.

Documentatie:

  • Analyse: docs/specs/speech/analyse-deepgram.md
  • FO v2: docs/specs/speech/fo-realtime-speech-deepgram.md
  • Huidige implementatie: components/speech-recorder.tsx, app/api/deepgram/transcribe/route.ts

2. Uitgangspunten

2.1 Technische Stack

Bestaand (behouden):

  • Frontend: Next.js 14 (App Router) + React + TypeScript
  • Styling: Tailwind CSS + shadcn/ui components
  • Icons: Lucide React
  • Database: Supabase (PostgreSQL)
  • Hosting: Vercel
  • Auth: Supabase Auth

Nieuw (toevoegen):

  • Speech-to-Text: Deepgram SDK (@deepgram/sdk)
  • WebSocket: Deepgram Live Streaming API
  • Audio: Web Audio API (waveform visualization)
  • Media: MediaRecorder API (browser native)

AI/ML:

  • Model: Deepgram Nova-2 (Nederlands)
  • Features: Live streaming, interim results, smart format, endpointing

2.2 Projectkaders

  • Tijd: 2-3 weken implementatie (Sprint 1-2)
  • Team: 1 developer (Colin) + AI assistentie (Claude Code)
  • Scope: MVP met core features (v1), nice-to-haves voor v1.1/v2
  • Budget: Deepgram API usage (pay-as-you-go)
  • Target: Nederlandse GGZ behandelaars (medische context)
  • Demo: Werkende prototype voor stakeholder review

2.3 Programmeer Uitgangspunten

Code Quality Principles:

  • DRY (Don't Repeat Yourself)

    • Herbruikbare speech recorder component
    • Shared Deepgram config
    • Reusable timeline card component
    • Centralized modal component
  • KISS (Keep It Simple, Stupid)

    • Start met MVP features (editor-first, basic streaming)
    • Geen premature optimization (resizable divider kan later)
    • Clear component naming (RapportageEditor, TimelineSidebar, etc.)
  • SOC (Separation of Concerns)

    • Speech recorder logic in custom hook (use-deepgram-streaming.ts)
    • Waveform visualization in separate component
    • API token generation in server-side route
    • State management via React Context (optional) of local state
  • YAGNI (You Aren't Gonna Need It)

    • Geen voice commands in v1 (kan v2)
    • Geen analytics-driven quick actions (start met fixed)
    • Geen timeline virtualization tot >100 items

Security Principles:

  • NOOIT Deepgram API key in client code
  • Token-based authentication via /api/deepgram/token
  • Tokens expire na 1 uur
  • Rate limiting op token endpoint (10 tokens/user/uur)
  • Input sanitization op alle transcript data

Performance Targets:

  • Waveform: 60fps
  • Transcript latency: <500ms
  • UI responsive tijdens streaming (non-blocking)
  • Timeline: Smooth 300ms animations
  • Modal: Smooth transitions (<200ms)

Testing Strategy:

  • Unit tests voor Deepgram hook
  • Integration tests voor token endpoint
  • Manual testing checklist (Dutch medical terms)
  • Browser compatibility (Chrome, Firefox, Safari)
  • Network resilience testing (disconnect scenarios)

3. Epics & Stories Overzicht

Epic ID Titel Doel Status Stories Story Points Opmerkingen
E0 Voorbereiding & Analyse Deepgram account, exploratie, FO finaliseren Gereed 3 8 FO v2 compleet
E1 Backend: Token Proxy Secure Deepgram token generation Gereed 3 8 Security kritisch
E2 Speech Recorder: Core Streaming WebSocket verbinding, real-time transcript Gereed 5 21 Hook + component compleet
E3 Speech Recorder: UX Polish Waveform, confidence, auto-pause Gereed 4 13 Alle features compleet
E4 Rapportage Page: Layout Redesign Editor-first, timeline sidebar, quick actions Gereed 5 21 Volledige redesign
E5 Rapportage Page: View/Edit Modal Unified modal, unsaved changes Gereed 3 13 Modal compleet
E6 Integration & Testing Component integration, end-to-end tests To Do 4 13 Quality gate
E7 Deployment & Documentation Production deployment, docs, training To Do 3 8 Go-live

Totaal: 30 stories, ~105 story points (~2-3 weken bij 40-50 points/week)


4. Epics & Stories (Uitwerking)

Epic 0 — Voorbereiding & Analyse

Epic Doel: Deepgram account setup, technische verkenning, en FO v2 finaliseren.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E0.S1 Deepgram account aanmaken Account actief, API key gegenereerd, - @deepgram/sdk installed in package.json, test call succesvol 2
E0.S2 Deepgram SDK exploratie Live streaming example werkend lokaal, Nederlands getest E0.S1 3
E0.S3 FO v2 document finaliseren FO compleet met editor-first design, alle secties uitgewerkt 3

Technical Notes:

  • Deepgram API key opgeslagen in .env.local als DEEPGRAM_API_KEY
  • Test met curl of Postman: REST API werkt
  • Test met SDK example: WebSocket streaming werkt
  • FO v2 bevat design review feedback (dev, UX, user perspectives)

Epic 1 — Backend: Token Proxy

Epic Doel: Secure server-side endpoint voor het genereren van Deepgram tokens.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E1.S1 Token generation endpoint /api/deepgram/token POST route, genereert tijdelijke token Gereed E0.S1 3
E1.S2 Rate limiting implementeren Max 10 tokens per user per uur, 429 bij overschrijding Gereed E1.S1 3
E1.S3 Error handling & logging Errors loggen, user-friendly messages, monitoring ready Gereed E1.S1 2

Technical Implementation:

File: /app/api/deepgram/token/route.ts

import { NextResponse } from 'next/server';
import { createClient } from '@deepgram/sdk';

// Rate limiting: simple in-memory cache (use Redis for production)
const tokenCache = new Map<string, { count: number; resetAt: number }>();

export async function POST(request: Request) {
  try {
    // Get user ID from session (Supabase auth)
    const userId = await getUserIdFromSession(request);

    // Rate limiting check
    const now = Date.now();
    const userLimit = tokenCache.get(userId);

    if (userLimit) {
      if (now < userLimit.resetAt) {
        if (userLimit.count >= 10) {
          return NextResponse.json(
            { error: 'Rate limit exceeded. Max 10 tokens per hour.' },
            { status: 429 }
          );
        }
        userLimit.count++;
      } else {
        tokenCache.set(userId, { count: 1, resetAt: now + 3600000 }); // 1 hour
      }
    } else {
      tokenCache.set(userId, { count: 1, resetAt: now + 3600000 });
    }

    // Generate Deepgram token
    const deepgram = createClient(process.env.DEEPGRAM_API_KEY!);

    // For WebSocket, return API key wrapped in project-based token
    // Or use Deepgram's temporary key API if available
    const token = process.env.DEEPGRAM_API_KEY; // Simplest approach

    // Better: Use Deepgram SDK's token generation (if supported)
    // const token = await deepgram.keys.create({ ... });

    return NextResponse.json({
      token,
      expiresIn: 3600, // 1 hour
    });

  } catch (error) {
    console.error('Token generation error:', error);
    return NextResponse.json(
      { error: 'Failed to generate token' },
      { status: 500 }
    );
  }
}

Security Notes:

  • Token endpoint vereist authenticated session
  • Tokens zijn short-lived (1 uur)
  • Rate limiting voorkomt abuse
  • API key blijft server-side

Testing:

  • Authenticated user kan token krijgen
  • Unauthenticated request → 401
  • Rate limit werkt (11e request → 429)
  • Token werkt in Deepgram WebSocket
  • Expired token → graceful error

Epic 2 — Speech Recorder: Core Streaming

Epic Doel: Werkende real-time speech transcription met WebSocket streaming.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E2.S1 Custom hook: use-deepgram-streaming Hook handelt WebSocket lifecycle, token fetch, connection states Gereed E1.S3 8
E2.S2 Speech recorder component UI component met start/stop, status indicator, error states Gereed E2.S1 5
E2.S3 Interim vs Final transcript Tekst differentiatie (grijs italic vs zwart bold), smooth transitions Gereed E2.S2 3
E2.S4 Cursor auto-naar-einde Bij start opname: cursor springt naar einde, groene border feedback Gereed E2.S2 2
E2.S5 Reconnection logic Auto-reconnect bij network drop (3x met backoff), partial transcript behouden Gereed E2.S1 3

Technical Implementation:

File: /hooks/use-deepgram-streaming.ts

import { useEffect, useRef, useState } from 'react';
import { createClient, LiveTranscriptionEvents } from '@deepgram/sdk';

interface UseDeepgramStreamingProps {
  onTranscript: (text: string, isFinal: boolean) => void;
  onError?: (error: Error) => void;
  language?: string;
  model?: string;
}

type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';

export function useDeepgramStreaming({
  onTranscript,
  onError,
  language = 'nl',
  model = 'nova-2',
}: UseDeepgramStreamingProps) {
  const [status, setStatus] = useState<ConnectionStatus>('disconnected');
  const [isRecording, setIsRecording] = useState(false);
  const connectionRef = useRef<any>(null);
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const reconnectAttemptsRef = useRef(0);

  const fetchToken = async () => {
    const response = await fetch('/api/deepgram/token', { method: 'POST' });
    if (!response.ok) throw new Error('Failed to fetch token');
    const data = await response.json();
    return data.token;
  };

  const connect = async () => {
    try {
      setStatus('connecting');
      const token = await fetchToken();

      const deepgram = createClient(token);
      const connection = deepgram.listen.live({
        model,
        language,
        smart_format: true,
        interim_results: true,
        endpointing: 3000,
        punctuate: true,
        utterances: true,
      });

      connection.on(LiveTranscriptionEvents.Open, () => {
        setStatus('connected');
        reconnectAttemptsRef.current = 0;
      });

      connection.on(LiveTranscriptionEvents.Transcript, (data) => {
        const transcript = data.channel.alternatives[0].transcript;
        const isFinal = data.is_final;
        if (transcript) {
          onTranscript(transcript, isFinal);
        }
      });

      connection.on(LiveTranscriptionEvents.Error, (error) => {
        console.error('Deepgram error:', error);
        setStatus('error');
        onError?.(error);
        handleReconnect();
      });

      connection.on(LiveTranscriptionEvents.Close, () => {
        setStatus('disconnected');
      });

      connectionRef.current = connection;

    } catch (error) {
      console.error('Connection error:', error);
      setStatus('error');
      onError?.(error as Error);
      handleReconnect();
    }
  };

  const handleReconnect = async () => {
    if (reconnectAttemptsRef.current < 3) {
      reconnectAttemptsRef.current++;
      const delay = Math.pow(2, reconnectAttemptsRef.current) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      await connect();
    }
  };

  const startRecording = async () => {
    try {
      await connect();

      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mediaRecorder = new MediaRecorder(stream, {
        mimeType: 'audio/webm',
      });

      mediaRecorder.ondataavailable = (event) => {
        if (event.data.size > 0 && connectionRef.current?.getReadyState() === 1) {
          connectionRef.current.send(event.data);
        }
      };

      mediaRecorder.start(250); // Send chunks every 250ms
      mediaRecorderRef.current = mediaRecorder;
      setIsRecording(true);

    } catch (error) {
      console.error('Recording error:', error);
      onError?.(error as Error);
    }
  };

  const stopRecording = () => {
    if (mediaRecorderRef.current) {
      mediaRecorderRef.current.stop();
      mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
      mediaRecorderRef.current = null;
    }
    if (connectionRef.current) {
      connectionRef.current.finish();
      connectionRef.current = null;
    }
    setIsRecording(false);
    setStatus('disconnected');
  };

  useEffect(() => {
    return () => {
      stopRecording();
    };
  }, []);

  return {
    status,
    isRecording,
    startRecording,
    stopRecording,
  };
}

File: /components/speech-recorder-streaming.tsx

'use client';

import { useState } from 'react';
import { Mic, Square, Loader2 } from 'lucide-react';
import { useDeepgramStreaming } from '@/hooks/use-deepgram-streaming';

interface SpeechRecorderStreamingProps {
  onTranscript: (transcript: string) => void;
  onInterimTranscript?: (interim: string) => void;
  disabled?: boolean;
  className?: string;
}

export function SpeechRecorderStreaming({
  onTranscript,
  onInterimTranscript,
  disabled = false,
  className = '',
}: SpeechRecorderStreamingProps) {
  const [interimText, setInterimText] = useState('');
  const [error, setError] = useState<string | null>(null);

  const { status, isRecording, startRecording, stopRecording } = useDeepgramStreaming({
    onTranscript: (text, isFinal) => {
      if (isFinal) {
        onTranscript(text);
        setInterimText('');
      } else {
        setInterimText(text);
        onInterimTranscript?.(text);
      }
    },
    onError: (err) => {
      setError(err.message);
    },
  });

  const handleStart = async () => {
    setError(null);
    await startRecording();
  };

  const handleStop = () => {
    stopRecording();
  };

  const statusIcon = {
    disconnected: '◯',
    connecting: '◐',
    connected: '●',
    error: '✕',
  }[status];

  const statusColor = {
    disconnected: 'text-slate-400',
    connecting: 'text-amber-500',
    connected: 'text-emerald-500',
    error: 'text-red-500',
  }[status];

  return (
    <div className={`rounded-lg border border-slate-200 bg-white p-4 ${className}`}>
      <div className="flex items-center justify-between mb-3">
        <span className="text-sm font-medium">🎤 Spraakopname</span>
        <span className={`text-sm ${statusColor}`}>
          {statusIcon} {status === 'connected' ? 'Verbonden' : status === 'connecting' ? 'Verbinden...' : 'Niet verbonden'}
        </span>
      </div>

      {error && (
        <div className="mb-3 text-sm text-red-600 bg-red-50 p-2 rounded">
           {error}
        </div>
      )}

      {interimText && (
        <div className="mb-3 text-sm text-slate-500 italic">
          {interimText}
        </div>
      )}

      <div className="flex gap-2">
        {!isRecording ? (
          <button
            onClick={handleStart}
            disabled={disabled || status === 'connecting'}
            className="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700 disabled:opacity-50"
          >
            {status === 'connecting' ? (
              <Loader2 className="w-4 h-4 animate-spin" />
            ) : (
              <Mic className="w-4 h-4" />
            )}
            Start opname
          </button>
        ) : (
          <button
            onClick={handleStop}
            className="inline-flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
          >
            <Square className="w-4 h-4" />
            Stop
          </button>
        )}
      </div>
    </div>
  );
}

Implementatie voltooid:

  • Hook: hooks/use-deepgram-streaming.ts
  • Component: components/speech-recorder-streaming.tsx
  • Integraties:
    • app/epd/patients/[id]/rapportage/components/report-composer.tsx
    • app/epd/patients/[id]/intakes/[intakeId]/behandeladvies/components/treatment-advice-form.tsx

Testing Checklist:

  • Start opname → WebSocket verbindt
  • Spreek Nederlands → Tekst verschijnt real-time
  • Interim tekst is grijs italic
  • Final tekst is zwart bold
  • Stop → transcript naar parent via callback
  • Network disconnect → auto-reconnect (max 3x)
  • Error state toont user-friendly message
  • Microfoon permissie denied → duidelijke foutmelding

Epic 3 — Speech Recorder: UX Polish

Epic Doel: Professionele visuele feedback en gebruikerservaring.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E3.S1 Waveform visualizer Canvas-based 60fps waveform, bar-style zoals WhatsApp Gereed E2.S2 5
E3.S2 Confidence indicators Gele/oranje onderstreping voor woorden <0.9 confidence, tooltip met % Gereed E2.S3 3
E3.S3 Auto-pause na stilte Deepgram endpointing (3 sec), UI toont "Automatisch gepauzeerd" Gereed E2.S1 3
E3.S4 Pause/Resume controls Handmatig pauzeren/hervatten, WebSocket blijft verbonden Gereed E2.S1, E3.S3 2

Implementatie voltooid:

  • Confidence component: components/confidence-text.tsx
    • ConfidenceText - Volledige weergave met tooltips en samenvatting
    • ConfidencePreview - Compacte preview voor in de recorder
  • Auto-pause: Geïntegreerd in speech-recorder-streaming.tsx
    • Detecteert speechFinal event van Deepgram (3 sec stilte)
    • Toont amber UI: "Automatisch gepauzeerd (3 seconden stilte)"
    • Hervat button krijgt groene highlight

Technical Notes:

Waveform:

  • Use canvas element with AnalyserNode from Web Audio API
  • Update at 60fps using requestAnimationFrame
  • Bar height mapped to frequency data
  • Colors: slate-700 (bars), emerald-500 (active)

Confidence:

  • Parse utterances array from Deepgram response
  • Each word has confidence score (0-1)
  • Apply CSS classes: .confidence-medium (yellow), .confidence-low (orange)
  • Tooltip on hover shows exact percentage

Auto-pause:

  • Deepgram endpointing: 3000 config handles detection
  • Listen for speech_final: true in transcript events
  • Pause audio streaming but keep WebSocket connected
  • Show "⏸ Automatisch gepauzeerd (3 seconden stilte)" message

Epic 4 — Rapportage Page: Layout Redesign

Epic Doel: Editor-first layout met timeline sidebar en quick actions.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E4.S1 Page layout restructure Full-width editor default, timeline hidden, quick actions bovenaan Gereed 5
E4.S2 Quick action buttons [+ Vrije notitie] [+ Intake] [+ Behandelplan] buttons, pre-select type Gereed E4.S1 3
E4.S3 Timeline sidebar component Collapsible rechts, 70/30 split, smooth 300ms slide animation Gereed E4.S1 8
E4.S4 Timeline card component Card met preview, timestamp, [Bekijk rapport] button Gereed E4.S3 3
E4.S5 Filters (on-demand) Search in timeline, expandable advanced filters, smart show (>10 items) Gereed E4.S3 2

Implementatie voltooid:

  • Quick Actions: quick-actions.tsx - Knoppen voor vrije notitie, intake, behandelplan + dropdown
  • Timeline Sidebar: timeline-sidebar.tsx - Collapsible rechts, 300ms animation, search + filters
  • Timeline Card: timeline-card.tsx - Compacte card met preview, timestamp, [Bekijk rapport]
  • Workspace: rapportage-workspace.tsx - Editor-first layout, responsive design

Component Structure:

/app/epd/patients/[id]/rapportage/
  page.tsx                          ← Main page (full refactor)

/app/epd/patients/[id]/rapportage/components/
  rapportage-editor.tsx             ← NEW: Full-width editor component
  quick-actions.tsx                 ← NEW: Quick action buttons
  timeline-sidebar.tsx              ← NEW: Collapsible sidebar
  timeline-card.tsx                 ← NEW: Rapport card in timeline
  filters-panel.tsx                 ← NEW: Advanced filters (expandable)
  report-view-edit-modal.tsx        ← Epic 5

  report-composer.tsx               ← MODIFY: Integrate new layout
  speech-recorder-streaming.tsx     ← From Epic 2 (inline in editor)

Design Specs:

// Timeline Sidebar State
interface TimelineSidebarState {
  isOpen: boolean;  // Default: false
  width: number;    // 30% when open, 0% when closed
  openAnimation: 'slide-in-right';  // 300ms
  closeAnimation: 'slide-out-right'; // 300ms
}

// Quick Actions Config
const QUICK_ACTIONS = [
  { id: 'vrije-notitie', label: '+ Vrije notitie', icon: '📄', type: 'vrije_notitie' },
  { id: 'intake', label: '+ Intake', icon: '📋', type: 'intake_verslag' },
  { id: 'behandelplan', label: '+ Behandelplan', icon: '📝', type: 'behandelplan' },
  { id: 'other', label: 'Andere...', icon: '▼', type: 'dropdown' },
] as const;

Testing:

  • Default: Editor 100% width, timeline closed
  • Klik [📋 Timeline] → Sidebar slides in (300ms)
  • Editor resizes naar 70% smooth
  • Klik [✕] in timeline → Sidebar slides out
  • Quick action button → Type pre-selected in editor
  • Timeline cards tonen preview (first 2 lines)
  • Filters tonen only when >10 rapportages

Epic 5 — Rapportage Page: View/Edit Modal

Epic Doel: Unified modal voor bekijken en editen van bestaande rapportages.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E5.S1 Modal component (read mode) 70% width overlay, read-only content, [✏️][📋][🗑][✕] actions Gereed E4.S4 5
E5.S2 Edit mode transition Klik [✏️] → Smooth transition naar edit mode, speech recorder verschijnt Gereed E5.S1, E2.S2 5
E5.S3 Unsaved changes protection Dialog bij [✕] met unsaved changes, [Opslaan][Verwijderen][Terug] Gereed E5.S2 3

Implementatie voltooid:

  • Modal: report-view-edit-modal.tsx - Unified view/edit modal
    • Read mode: Bekijk rapport met [✏️ Bewerken] [📋 Dupliceer] [🗑 Verwijder] [✕]
    • Edit mode: Textarea met speech recorder, [💾 Opslaan] [Annuleren]
    • Groene border tijdens streaming dictaat
  • Dialogs:
    • UnsavedChangesDialog - [Opslaan en sluiten] [Wijzigingen verwijderen] [Terug]
    • DeleteConfirmDialog - Bevestiging voor verwijderen
  • Actions: updateReport action toegevoegd aan actions.ts

Modal Flow:

User clicks [Bekijk rapport] in timeline
  ↓
Modal opens (read mode)
  - Title + timestamp
  - Actions: [✏️ Bewerken] [📋 Dupliceer] [🗑 Verwijder] [✕ Sluiten]
  - Content: Read-only text
  ↓
User clicks [✏️ Bewerken]
  ↓
Modal transforms (300ms transition)
  - Actions change: [💾 Opslaan] [❌ Annuleren] [✕]
  - Speech recorder appears at top
  - Content becomes editable textarea
  - Cursor at end ready for dictation
  ↓
User dictates or types
  ↓
User clicks [💾 Opslaan]
  - Save to database
  - Toast: "✅ Wijzigingen opgeslagen"
  - Modal stays open (can continue editing)
  ↓
User clicks [✕]
  - If unsaved changes → Show dialog
    - [💾 Opslaan en sluiten]
    - [🗑 Wijzigingen verwijderen]
    - [← Terug naar bewerken]
  - If no changes → Close modal

State Management:

interface ModalState {
  isOpen: boolean;
  mode: 'read' | 'edit';
  rapport: Rapport | null;
  hasUnsavedChanges: boolean;
  originalContent: string;  // For detecting changes
}

Testing:

  • Klik timeline card → Modal opens read mode
  • Klik [✏️] → Transforms to edit mode (smooth)
  • Speech recorder works in modal
  • Typing updates content
  • Save → Success toast + stays open
  • [✕] with unsaved → Dialog shows
  • [✕] without unsaved → Closes immediately
  • Esc key → Close (with unsaved check)

Epic 6 — Integration & Testing Gereed

Epic Doel: Alles werkt samen, end-to-end flows getest, bugs gefixt.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E6.S1 Component integration Speech recorder werkt in editor + modal, state sync correct E2.S5, E4.S5, E5.S3 5
E6.S2 Dutch medical terms test Test met Nederlandse GGZ terminologie, confidence indicators accuraat E3.S2 3
E6.S3 Browser compatibility Test Chrome, Firefox, Safari (desktop), Chrome mobile E6.S1 3
E6.S4 Bug bash & polish Fix top 10 bugs, polish animations, loading states, error messages E6.S3 2

Implementatie Referenties:

  • Duplicate content flow: rapportage-workspace.tsxreport-composer.tsx
  • Safari mimeType fallback: hooks/use-deepgram-streaming.ts:setupAudio()
  • Browser support check: hooks/use-deepgram-streaming.ts:checkBrowserSupport()
  • Test checklist: docs/specs/speech/test-checklist-e6.md

Test Scenarios:

1. Happy Flow - Nieuwe Rapportage:

1. User komt op rapportage pagina
2. Ziet editor full-width, geen timeline
3. Klikt [+ Vrije notitie]
4. Type pre-selected
5. Klikt [🎤 Start opname]
6. Cursor springt naar einde (leeg veld)
7. Spreekt: "De patiënt presenteert zich met klachten van angst en depressie"
8. Ziet interim tekst verschijnen (grijs, italic)
9. Na zin: tekst wordt final (zwart, bold)
10. Klikt [⏹ Stop]
11. Transcript compleet
12. Klikt [💾 Opslaan]
13. Toast: "✅ Rapportage opgeslagen"

2. Happy Flow - Bestaande Bewerken:

1. Klikt [📋 Timeline]
2. Sidebar slides in (300ms)
3. Ziet lijst met rapportages
4. Klikt [Bekijk rapport] op "Vrije notitie (23-11)"
5. Modal opens read mode
6. Leest content
7. Klikt [✏️ Bewerken]
8. Modal transforms to edit mode (300ms)
9. Speech recorder verschijnt
10. Klikt [🎤 Start opname]
11. Cursor springt naar einde van bestaande tekst
12. Spreekt: "Aanvullende observatie: patiënt toont verbetering"
13. Tekst append aan einde
14. Klikt [⏹ Stop]
15. Klikt [💾 Opslaan]
16. Toast: "✅ Wijzigingen opgeslagen"
17. Klikt [✕]
18. Modal sluit

3. Error Flow - Network Drop:

1. Start opname
2. Spreekt 5 seconden
3. Disconnect wifi
4. Status indicator: ⚠ Oranje "Herverbinden..."
5. Partial transcript blijft zichtbaar
6. Auto-reconnect (attempt 1)
7. Fails
8. Auto-reconnect (attempt 2)
9. Reconnect wifi
10. Success → ● Groen "Verbonden"
11. Kan verder dicteren
12. Alle tekst behouden

4. Edge Case - Unsaved Changes:

1. Open bestaande rapport
2. Klik [✏️ Bewerken]
3. Type: "Extra notitie"
4. Klikt [✕] (zonder save)
5. Dialog: "Niet opgeslagen wijzigingen"
6. Opties: [💾][🗑][←]
7. Klikt [← Terug]
8. Modal blijft open, changes intact

Dutch Medical Terms Test List:

- "gegeneraliseerde angststoornis"
- "SSRI medicatie"
- "DSM-5 classificatie"
- "cognitieve gedragstherapie"
- "EMDR behandeling"
- "traumaverwerking"
- "depressieve episode"
- "bipolaire stoornis"
- "schizofrenie"
- "persoonlijkheidsstoornis"

Browser Compatibility Matrix:

Browser Desktop Mobile WebSocket Web Audio MediaRecorder Status
Chrome 90+ Full support
Firefox 88+ Full support
Safari 14+ ⚠️ ⚠️ Partial (MediaRecorder limited)
Edge 90+ Full support (Chromium)

Epic 7 — Deployment & Documentation

Epic Doel: Production deployment, documentatie, en training voor stakeholders.

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E7.S1 Production deployment Vercel deployment succesvol, env vars gezet, HTTPS werkt E6.S4 3
E7.S2 User documentation Handleiding voor behandelaars: hoe speech recorder gebruiken E7.S1 3
E7.S3 Stakeholder demo Live demo voor stakeholders, feedback verzamelen E7.S1 2

Deployment Checklist:

Vercel Environment Variables:

DEEPGRAM_API_KEY=<key>
NEXT_PUBLIC_SUPABASE_URL=<url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<key>
SUPABASE_SERVICE_ROLE_KEY=<key>

Pre-deployment Tests:

  • npm run build succeeds
  • npm run lint no errors
  • All environment variables set in Vercel
  • Database migrations applied
  • API routes respond correctly in preview
  • Speech recorder works in preview deploy

Post-deployment Smoke Tests:

  • Login works
  • Navigate to rapportage page
  • Quick actions work
  • Timeline opens/closes
  • Speech recorder starts/stops
  • New rapportage saves
  • Edit existing rapport works
  • Modal open/close smooth
  • No console errors

User Documentation Outline:

# Handleiding: Spraakopname in Rapportages

## Nieuwe Rapportage Dicteren

1. Ga naar de Rapportage pagina van een patiënt
2. Klik op [+ Vrije notitie] (of ander type)
3. Klik op [🎤 Start opname]
4. Browser vraagt om microfoon toegang → Klik "Toestaan"
5. Spreek duidelijk in de microfoon
6. Zie de tekst real-time verschijnen
7. Klacht [⏹ Stop] wanneer je klaar bent
8. Klik [💾 Opslaan]

## Bestaande Rapportage Bewerken met Spraak

1. Klik op [📋 Timeline] rechtsboven
2. Zoek de rapportage die je wilt bewerken
3. Klik [Bekijk rapport]
4. Klik [✏️ Bewerken]
5. Klik [🎤 Start opname]
6. De nieuwe tekst wordt toegevoegd aan het einde
7. Klik [💾 Opslaan wijzigingen]

## Tips voor Beste Resultaten

✅ Spreek duidelijk en rustig
✅ Gebruik medische termen zoals je ze normaal uitspreekt
✅ Pauzeer kort tussen zinnen
✅ Check de tekst na dicteren (gele onderstreping = lagere zekerheid)

❌ Vermijd achtergrondgeluid
❌ Spreek niet te snel
❌ Zorg dat microfoon niet te ver weg staat

5. Kwaliteit & Testplan

Test Types

Test Type Scope Tools Verantwoordelijke Timing
Unit Tests Deepgram hook, utility functions Vitest Developer During E2, E3
Integration Tests Token endpoint, WebSocket connection Playwright Developer During E6
E2E Tests Complete user flows (new + edit) Playwright / Manual Developer E6.S1
Performance Tests Waveform 60fps, latency <500ms Chrome DevTools Developer E3.S1
Security Tests Token security, rate limiting Manual + Postman Developer E1.S2
Accessibility Screen reader, keyboard nav axe DevTools Developer E6.S4
Browser Compat Chrome, Firefox, Safari BrowserStack / Manual Developer E6.S3

Test Coverage Targets

  • Unit tests: 80%+ op /hooks/use-deepgram-streaming.ts
  • Integration tests: /api/deepgram/token endpoint
  • E2E tests: 2 happy flows (new + edit) + 2 error flows
  • Manual testing: Dutch medical terminology list

Critical Path Testing Scenarios

Priority 1 (Blocker if broken):

  1. Speech recorder starts and streams audio
  2. Real-time transcript appears during speech
  3. Save new rapportage
  4. Edit existing rapportage
  5. Network resilience (reconnect works)

Priority 2 (Important but not blocker): 6. Waveform visualizes correctly 7. Confidence indicators show 8. Auto-pause after 3 sec silence 9. Timeline open/close animation smooth 10. Modal transitions smooth

Priority 3 (Nice to have): 11. Filters work correctly 12. Keyboard shortcuts (Esc, etc.) 13. Mobile responsive 14. Duplicate rapport feature

Manual Test Checklist

Speech Recorder:

  • Microfoon permissie flow correct
  • WebSocket verbindt binnen 2 sec
  • Audio streaming real-time
  • Interim tekst grijs italic
  • Final tekst zwart bold
  • Stop sluit verbinding correct
  • Error messages user-friendly
  • Reconnect works (max 3 attempts)

Page Layout:

  • Editor 100% width default
  • Quick actions pre-select type
  • Timeline sidebar slides smooth (300ms)
  • Editor resizes naar 70% smooth
  • Timeline cards show preview
  • Filters show only when >10 items

Modal:

  • Opens in read mode
  • [✏️] transforms to edit mode smooth
  • Speech recorder works in modal
  • Save updates database
  • Unsaved changes dialog correct
  • [✕] closes (with check)

Dutch Medical Terms:

  • Test all terms from list
  • Check confidence scores
  • Verify correct spelling
  • Check punctuation correct

6. Demo & Presentatieplan

Demo Scenario

Duur: 15 minuten Doelgroep: Stakeholders (PO, behandelaars, management) Locatie: Live op Vercel production (backup: staging)

Demo Flow:

1. Intro (2 min)

  • Context: Waarom migratie naar real-time?
  • Benefits: <500ms latency vs 2-5+ seconden
  • UX redesign: Editor-first, minder afleiding

2. Nieuwe Rapportage (4 min)

  • Navigate naar rapportage pagina
  • Toon editor full-width (clean interface)
  • Klik [+ Vrije notitie] (quick action demo)
  • Start opname
  • Dicteer: "De patiënt presenteert zich met klachten van gegeneraliseerde angst en slaapproblemen sinds drie maanden"
  • Show real-time tekst verschijnen
  • Point out: Interim (grijs) → Final (zwart) transition
  • Show waveform visualisatie
  • Stop opname
  • Save rapportage

3. Timeline & Edit (4 min)

  • Klik [📋 Timeline] → Sidebar slides in
  • Show lijst met rapportages
  • Klik [Bekijk rapport]
  • Modal opens (read mode demo)
  • Klik [✏️ Bewerken]
  • Modal transforms (show smooth transition)
  • Start opname in modal
  • Dicteer aanvulling: "Patiënt reageert goed op cognitieve gedragstherapie"
  • Show append aan einde
  • Stop + Save
  • Close modal

4. UX Features (3 min)

  • Show confidence indicators (gele onderstreping)
  • Demo auto-pause (3 sec stilte)
  • Show network resilience (if possible/recorded)
  • Quick actions benefits (1-click start)

5. Q&A (2 min)

  • Feedback van behandelaars
  • Vragen over workflow
  • Next steps discussie

Backup Plan:

  • Plan B: Staging environment if production issues
  • Plan C: Localhost met pre-recorded demo video
  • Plan D: Slide deck met screenshots + video recording

Demo Data:

  • Pre-seeded test patient: "Demo Patient"
  • 3 existing rapportages in timeline
  • Clean state (no errors)

7. Risico's & Mitigatie

Risico Kans Impact Mitigatie Owner Status
Deepgram API downtime tijdens demo Laag Hoog Pre-recorded demo video backup, test 1 uur voor demo Developer Monitor
Dutch medical terms transcription errors Middel Hoog Extensive testing met terminologie lijst, tune confidence thresholds Developer Test E6.S2
WebSocket connection instabiliteit Middel Hoog Robust reconnect logic (3x), partial transcript preservation Developer Implemented E2.S5
Browser compatibility issues Safari Middel Middel Test early, document limitations, fallback to REST API if needed Developer Test E6.S3
Timeline state sync complex Middel Middel Clear state management, use React Context or Zustand Developer Design E4.S3
Modal edit mode state bugs Middel Middel Thorough testing unsaved changes flow, clear state machine Developer Test E5.S3
Performance issues with long rapportages Laag Middel Lazy loading, virtualization if >100 items, performance profiling Developer Monitor E6.S1
Token rate limiting tijdens development Middel Laag Increase rate limit for dev, clear documentation Developer E1.S2
Deepgram API costs higher than expected Laag Middel Monitor usage, set up billing alerts, optimize chunk size PM Monitor
Migration breaks existing REST flow Laag Hoog Parallel implementation, feature flag, rollback plan Developer E6.S1

Critical Risks (Hoog/Hoog):

  1. Deepgram API downtime → Mitigation: Backup video + pre-recorded demo
  2. Dutch transcription errors → Mitigation: Extensive testing, confidence indicators
  3. WebSocket instability → Mitigation: Robust reconnect (implemented)

8. Evaluatie & Lessons Learned

Te documenteren na project:

Success Metrics

Kwantitatief:

  • Latency reduction: van 2-5s → <500ms Target bereikt?
  • User satisfaction: Survey na 2 weken gebruik (1-5 schaal)
  • Error rate: <5% van sessions hebben errors
  • Transcription accuracy: >90% correct voor Dutch medical terms
  • Adoption rate: >80% van behandelaars gebruikt speech recorder

Kwalitatief:

  • Feedback van behandelaars: What works? What doesn't?
  • Workflow improvements noted
  • Pain points identified
  • Feature requests voor v2

Retrospective Questions

Wat ging goed?

  • ...

Wat ging niet goed?

  • ...

Wat hebben we geleerd?

  • ...

Welke AI-tools waren effectief?

  • Claude Code voor planning & code generation?
  • ChatGPT voor prompt engineering?
  • GitHub Copilot voor snippets?

Welke Deepgram features werkten best?

  • Interim results?
  • Endpointing?
  • Smart format?
  • Dutch language accuracy?

Waar liepen we vertraging op?

  • ...

Wat doen we volgende keer anders?

  • ...

Herbruikbare componenten:

  • Speech recorder component → Other projects?
  • WebSocket hook pattern → Reusable?
  • Modal pattern → Design system?

9. Referenties

Mission Control Documents

  • Analyse: docs/specs/speech/analyse-deepgram.md
  • FO v2: docs/specs/speech/fo-realtime-speech-deepgram.md
  • Bouwplan: docs/specs/speech/bouwplan-realtime-speech.md (dit document)

External Resources

Bestaande Implementatie

  • Speech Recorder (old): components/speech-recorder.tsx
  • API Route (old): app/api/deepgram/transcribe/route.ts
  • Report Composer: app/epd/patients/[id]/rapportage/components/report-composer.tsx

Deepgram API Documentation


10. Glossary & Abbreviations

Term Betekenis
Epic Grote feature of fase (bevat meerdere stories)
Story Kleine uitvoerbare taak binnen een epic
Story Points Complexiteit schatting (Fibonacci: 1, 2, 3, 5, 8, 13)
Interim Results Voorlopige transcriptie tijdens spreken (nog niet final)
Final Results Definitieve transcriptie na zin/phrase eindigt
Endpointing Detectie van spraakpauzes (speech_final event)
Smart Format Auto punctuatie, capitalisatie, formatting
WebSocket Bidirectionele real-time communicatie protocol
Waveform Visuele representatie van audio volume
Confidence Score AI zekerheid (0-1) over getranscribeerd woord
Token Proxy Server endpoint die tijdelijke tokens uitgeeft
Rate Limiting Beperking van API calls per tijd per user
Progressive Disclosure Features on-demand tonen (niet alles tegelijk)
Editor-First Design waar editor primair is, andere features opt-in

Versiehistorie

Versie Datum Auteur Wijziging
v1.0 24-11-2025 Claude Code + Colin Initiële versie - volledige planning 7 epics, 30 stories
v1.1 24-11-2025 Claude Code + Colin Epic 2 (Core Streaming) voltooid: hook, component, integraties. Epic 3 deels (waveform, pause/resume)
v1.2 24-11-2025 Claude Code + Colin Epic 3 (UX Polish) voltooid: confidence indicators, auto-pause na stilte
v1.3 24-11-2025 Claude Code + Colin Epic 4 (Layout Redesign) voltooid: editor-first, quick actions, timeline sidebar
v1.4 24-11-2025 Claude Code + Colin Epic 5 (View/Edit Modal) voltooid: unified modal, edit mode, unsaved changes protection
v1.5 24-11-2025 Claude Code + Colin Epic 6 (Integration & Testing) voltooid: duplicate flow, Safari mimeType fallback, browser support check, test checklist

Appendix A: Story Points Calibration

Story Point Reference:

  • 1 point: Triviale taak, <2 uur, geen risico (bijv. env var toevoegen)
  • 2 points: Simpele taak, 2-4 uur, laag risico (bijv. button component)
  • 3 points: Gemiddelde taak, 4-8 uur, middelmatig risico (bijv. API endpoint)
  • 5 points: Complexe taak, 1-2 dagen, risico's (bijv. custom hook met state)
  • 8 points: Zeer complex, 2-3 dagen, hoog risico (bijv. WebSocket lifecycle)
  • 13 points: Epic-size, 3-5 dagen, zeer hoog risico (bijv. complete refactor)

Team Velocity: ~40-50 story points per 2-week sprint (1 developer)

Total Epic Breakdown:

  • E0: 8 points (voorbereiding) - Compleet
  • E1: 8 points (backend)
  • E2: 21 points (core streaming) - Grootste epic
  • E3: 13 points (UX polish)
  • E4: 21 points (layout redesign) - Grootste epic
  • E5: 13 points (modal)
  • E6: 13 points (testing)
  • E7: 8 points (deployment)

Total: 105 story points → ~2-3 sprints (4-6 weken bij 1 developer)


Appendix B: Git Branching Strategy

Main branches:

  • main - Production (deployed op Vercel)
  • develop - Development (deployed op Vercel preview)

Feature branches:

feature/E1-token-proxy
feature/E2-streaming-core
feature/E3-ux-polish
feature/E4-layout-redesign
feature/E5-view-edit-modal
feature/E6-integration
feature/E7-deployment

Workflow:

  1. Create feature branch from develop
  2. Work on epic/stories
  3. PR naar develop (review + CI)
  4. Merge to develop (auto-deploy preview)
  5. Test in preview
  6. PR developmain (production)

Commit Convention:

feat(E2.S1): implement use-deepgram-streaming hook
fix(E3.S2): confidence indicator tooltip positioning
docs(E7.S2): add user documentation for speech recorder
test(E6.S2): add Dutch medical terms test suite

🎯 Ready to Build!

Dit bouwplan bevat alle details voor succesvolle implementatie van real-time speech transcription met editor-first UX redesign.

Next steps:

  1. Review & approve bouwplan
  2. Sprint planning (prioriteer epics)
  3. Start met E1 (token proxy) → E2 (core streaming)
  4. Iteratieve development met weekly demos
  5. Continuous testing vanaf E2
  6. Deploy naar production na E6 (testing compleet)

Succes met de bouw! 🚀