diff --git a/app/api/deepgram/token/route.ts b/app/api/deepgram/token/route.ts index befacf4..a55c26b 100644 --- a/app/api/deepgram/token/route.ts +++ b/app/api/deepgram/token/route.ts @@ -2,9 +2,13 @@ import { NextResponse } from 'next/server' import { createClient as createSupabaseClient } from '@/lib/auth/server' import { createClient as createDeepgramClient } from '@deepgram/sdk' -const MAX_TOKENS_PER_HOUR = 10 -const TOKEN_TTL_SECONDS = 3600 +// Rate limiting configuratie +const MAX_TOKENS_PER_USER_PER_HOUR = 5 // Per gebruiker +const MAX_TOKENS_GLOBAL_PER_HOUR = 50 // Totaal voor hele app +const MAX_RECORDING_MINUTES_PER_DAY = 30 // Geschatte minuten per dag (globaal) +const TOKEN_TTL_SECONDS = 600 // Token geldig voor 10 min (korter = veiliger) const HOUR_IN_MS = 60 * 60 * 1000 +const DAY_IN_MS = 24 * 60 * 60 * 1000 type RateLimitEntry = { count: number @@ -15,61 +19,109 @@ type RateLimitResult = { allowed: boolean remaining: number resetAt: number + reason?: string } -const rateLimitStore = new Map() +// Per-user rate limiting +const userRateLimitStore = new Map() + +// Global rate limiting (beschermt tegen misbruik door meerdere users) +let globalHourlyCount = 0 +let globalHourlyResetAt = Date.now() + HOUR_IN_MS +let globalDailyMinutes = 0 +let globalDailyResetAt = Date.now() + DAY_IN_MS function consumeRateLimit(userId: string): RateLimitResult { const now = Date.now() - const entry = rateLimitStore.get(userId) + + // Reset global counters if needed + if (now >= globalHourlyResetAt) { + globalHourlyCount = 0 + globalHourlyResetAt = now + HOUR_IN_MS + } + if (now >= globalDailyResetAt) { + globalDailyMinutes = 0 + globalDailyResetAt = now + DAY_IN_MS + } + + // Check global hourly limit + if (globalHourlyCount >= MAX_TOKENS_GLOBAL_PER_HOUR) { + console.log('[RateLimit] Global hourly limit reached:', globalHourlyCount) + return { + allowed: false, + remaining: 0, + resetAt: globalHourlyResetAt, + reason: 'Globale limiet bereikt. Probeer later opnieuw.', + } + } + + // Check global daily minutes (rough estimate: 1 token ≈ 10 min recording) + const estimatedMinutes = globalHourlyCount * 10 + if (estimatedMinutes >= MAX_RECORDING_MINUTES_PER_DAY) { + console.log('[RateLimit] Daily recording limit reached:', estimatedMinutes, 'min') + return { + allowed: false, + remaining: 0, + resetAt: globalDailyResetAt, + reason: 'Dagelijkse opnamelimiet bereikt.', + } + } + + // Check per-user limit + const entry = userRateLimitStore.get(userId) if (!entry || now >= entry.resetAt) { const resetAt = now + HOUR_IN_MS - rateLimitStore.set(userId, { count: 1, resetAt }) + userRateLimitStore.set(userId, { count: 1, resetAt }) + globalHourlyCount++ return { allowed: true, - remaining: MAX_TOKENS_PER_HOUR - 1, + remaining: MAX_TOKENS_PER_USER_PER_HOUR - 1, resetAt, } } - if (entry.count >= MAX_TOKENS_PER_HOUR) { + if (entry.count >= MAX_TOKENS_PER_USER_PER_HOUR) { return { allowed: false, remaining: 0, resetAt: entry.resetAt, + reason: 'Je hebt de limiet van 5 opnames per uur bereikt.', } } entry.count += 1 + globalHourlyCount++ return { allowed: true, - remaining: MAX_TOKENS_PER_HOUR - entry.count, + remaining: MAX_TOKENS_PER_USER_PER_HOUR - entry.count, resetAt: entry.resetAt, } } function buildRateLimitHeaders(result: RateLimitResult) { return { - 'X-RateLimit-Limit': `${MAX_TOKENS_PER_HOUR}`, + 'X-RateLimit-Limit': `${MAX_TOKENS_PER_USER_PER_HOUR}`, 'X-RateLimit-Remaining': `${Math.max(result.remaining, 0)}`, 'X-RateLimit-Reset': `${Math.floor(result.resetAt / 1000)}`, } } export async function POST() { + console.log('[API /deepgram/token] POST request received') try { const skipAuthCheck = process.env.SKIP_AUTH_CHECK === 'true' let userId = 'demo-user' // Auth check (skip in development/demo mode) if (!skipAuthCheck) { + console.log('[API /deepgram/token] Checking auth...') const supabase = await createSupabaseClient() const { data: authData, error: authError } = await supabase.auth.getUser() if (authError) { - console.error('Deepgram token: auth error', authError) + console.error('[API /deepgram/token] Auth error:', authError) return NextResponse.json( { error: 'Kon sessie niet ophalen' }, { status: 500 } @@ -77,17 +129,23 @@ export async function POST() { } if (!authData?.user) { + console.log('[API /deepgram/token] No user found - returning 401') return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 }) } userId = authData.user.id + console.log('[API /deepgram/token] Auth OK, userId:', userId.slice(0, 8) + '...') } else { - console.log('🔓 Deepgram token: Auth check overgeslagen (demo mode)') + console.log('[API /deepgram/token] Auth check skipped (SKIP_AUTH_CHECK=true)') } - const apiKey = process.env.DEEPGRAM_API_KEY + // Prefer admin key (can generate tokens), fallback to regular key + const apiKey = process.env.DEEPGRAM_ADMIN_API_KEY || process.env.DEEPGRAM_API_KEY + const keyType = process.env.DEEPGRAM_ADMIN_API_KEY ? 'ADMIN' : 'REGULAR' + console.log('[API /deepgram/token] Using', keyType, 'API key, present:', !!apiKey) if (!apiKey) { + console.error('[API /deepgram/token] No Deepgram API key configured!') return NextResponse.json( { error: 'Deepgram API key ontbreekt in server configuratie' }, { status: 500 } @@ -96,14 +154,16 @@ export async function POST() { const rateLimit = consumeRateLimit(userId) const headers = buildRateLimitHeaders(rateLimit) + console.log('[API /deepgram/token] Rate limit check:', { allowed: rateLimit.allowed, remaining: rateLimit.remaining }) if (!rateLimit.allowed) { const retryAfterSeconds = Math.max( 0, Math.ceil((rateLimit.resetAt - Date.now()) / 1000) ) + console.log('[API /deepgram/token] Rate limit exceeded:', rateLimit.reason, 'retry after:', retryAfterSeconds) return NextResponse.json( - { error: 'Rate limit overschreden. Max 10 tokens per uur.' }, + { error: rateLimit.reason || 'Rate limit bereikt.' }, { status: 429, headers: { @@ -114,19 +174,48 @@ export async function POST() { ) } - const deepgram = createDeepgramClient(apiKey) - const tokenResponse = await deepgram.auth.grantToken({ - ttl_seconds: TOKEN_TTL_SECONDS, - }) + // Check if we should use direct API key (for development when grantToken doesn't work) + const useDirectKey = process.env.DEEPGRAM_USE_DIRECT_KEY === 'true' - if (!tokenResponse.result) { - console.error('Deepgram token: API error', tokenResponse.error) + if (useDirectKey) { + console.log('[API /deepgram/token] Using direct API key (DEEPGRAM_USE_DIRECT_KEY=true)') return NextResponse.json( - { error: 'Genereren van tijdelijk token mislukt' }, + { + token: apiKey, + expiresIn: TOKEN_TTL_SECONDS, + }, + { headers } + ) + } + + console.log('[API /deepgram/token] Requesting token from Deepgram...') + console.log('[API /deepgram/token] API key length:', apiKey.length, 'starts with:', apiKey.slice(0, 8)) + const deepgram = createDeepgramClient(apiKey) + + let tokenResponse + try { + tokenResponse = await deepgram.auth.grantToken({ + ttl_seconds: TOKEN_TTL_SECONDS, + }) + console.log('[API /deepgram/token] Deepgram response:', JSON.stringify(tokenResponse, null, 2)) + } catch (deepgramError) { + console.error('[API /deepgram/token] Deepgram SDK threw error:', deepgramError) + return NextResponse.json( + { error: 'Deepgram SDK error: ' + (deepgramError instanceof Error ? deepgramError.message : String(deepgramError)) }, { status: 502, headers } ) } + if (!tokenResponse.result) { + console.error('[API /deepgram/token] Deepgram API error - no result. Full response:', tokenResponse) + console.log('[API /deepgram/token] TIP: Set DEEPGRAM_USE_DIRECT_KEY=true in .env.local to use API key directly') + return NextResponse.json( + { error: 'Genereren van tijdelijk token mislukt', details: tokenResponse.error }, + { status: 502, headers } + ) + } + + console.log('[API /deepgram/token] Token generated successfully, expiresIn:', tokenResponse.result.expires_in) return NextResponse.json( { token: tokenResponse.result.access_token, @@ -135,7 +224,7 @@ export async function POST() { { headers } ) } catch (error) { - console.error('Deepgram token: onverwachte fout', error) + console.error('[API /deepgram/token] Unexpected error:', error) return NextResponse.json( { error: 'Onverwachte fout bij genereren token' }, { status: 500 } diff --git a/app/epd/components/epd-sidebar.tsx b/app/epd/components/epd-sidebar.tsx index c60e2d6..fa2d279 100644 --- a/app/epd/components/epd-sidebar.tsx +++ b/app/epd/components/epd-sidebar.tsx @@ -127,8 +127,9 @@ const SidebarItem = memo(function SidebarItem({ item, isActive, isCollapsed, onC export function EPDSidebar({ className = "", userEmail, userName }: EPDSidebarProps) { const pathname = usePathname(); - const [isOpen, setIsOpen] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [isOpen, setIsOpen] = useState(false); // Context detection: Level 2 if URL contains /patients/[id] const isPatientContext = pathname?.match(/\/epd\/patients\/[^\/]+/); @@ -145,20 +146,19 @@ export function EPDSidebar({ className = "", userEmail, userName }: EPDSidebarPr return level1NavigationItems; }, [isPatientContext, patientId]); - // Stabilize event handlers with useCallback - const toggleSidebar = useCallback(() => { - setIsOpen(prev => !prev); - }, []); - const toggleCollapse = useCallback(() => { setIsCollapsed(prev => !prev); }, []); + const toggleSidebar = useCallback(() => { + setIsOpen(prev => !prev); + }, []); + const handleItemClick = useCallback(() => { - if (window.innerWidth < 768) { + if (isMobile) { setIsOpen(false); } - }, []); + }, [isMobile]); // Memoize isActive check function const getIsActive = useCallback((item: NavigationItem): boolean => { @@ -168,13 +168,14 @@ export function EPDSidebar({ className = "", userEmail, userName }: EPDSidebarPr return pathname === item.href || Boolean(item.href && pathname?.startsWith(item.href + '/')); }, [pathname]); - // Auto-open sidebar on desktop + // Track mobile state and auto-collapse on mobile useEffect(() => { const handleResize = () => { - if (window.innerWidth >= 768) { - setIsOpen(true); - } else { - setIsOpen(false); + const mobile = window.innerWidth < 768; + setIsMobile(mobile); + // Auto-collapse on mobile, remember preference on desktop + if (mobile) { + setIsCollapsed(true); } }; @@ -183,6 +184,9 @@ export function EPDSidebar({ className = "", userEmail, userName }: EPDSidebarPr return () => window.removeEventListener('resize', handleResize); }, []); + // Effective collapsed state: always collapsed on mobile + const effectiveCollapsed = isMobile || isCollapsed; + // Get user initials const userInitials = userName ? userName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) diff --git a/app/epd/patients/[id]/rapportage/components/report-composer.tsx b/app/epd/patients/[id]/rapportage/components/report-composer.tsx index a60b971..3c36467 100644 --- a/app/epd/patients/[id]/rapportage/components/report-composer.tsx +++ b/app/epd/patients/[id]/rapportage/components/report-composer.tsx @@ -3,13 +3,13 @@ import { useMemo, useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import dynamic from 'next/dynamic'; -import { Mic, Sparkles } from 'lucide-react'; +import { Sparkles } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { toast } from '@/hooks/use-toast'; import type { ClassificationResult, Report, ReportType } from '@/lib/types/report'; import { createReport } from '../actions'; -import { cn } from '@/lib/utils'; import type { Editor } from '@/components/rich-text-editor'; +import { ToolbarMicButton } from './toolbar-mic-button'; // Dynamic imports const RichTextEditor = dynamic( @@ -17,11 +17,6 @@ const RichTextEditor = dynamic( { ssr: false, loading: () => } ); -const SpeechRecorderStreaming = dynamic( - () => import('@/components/speech-recorder-streaming').then((m) => m.SpeechRecorderStreaming), - { ssr: false, loading: () => } -); - function EditorSkeleton() { return (
@@ -31,15 +26,6 @@ function EditorSkeleton() { ); } -function RecorderSkeleton() { - return ( -
-
-
-
- ); -} - // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── @@ -81,8 +67,8 @@ export function ReportComposer({ const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [lastAutosave, setLastAutosave] = useState(null); - const [isStreaming, setIsStreaming] = useState(false); - const [showRecorder, setShowRecorder] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [interimText, setInterimText] = useState(''); const draftStorageKey = useMemo(() => `rapportage-draft-${patientId}`, [patientId]); // Calculate plain text length from HTML content @@ -155,14 +141,16 @@ export function ReportComposer({ // Recording handlers const handleRecordingStart = useCallback(() => { - setIsStreaming(true); + setIsRecording(true); + setInterimText(''); if (editorRef) { editorRef.chain().focus().run(); } }, [editorRef]); const handleRecordingStop = useCallback(() => { - setIsStreaming(false); + setIsRecording(false); + setInterimText(''); }, []); const handleTranscript = useCallback((text: string) => { @@ -176,6 +164,10 @@ export function ReportComposer({ }); }, []); + const handleInterimTranscript = useCallback((text: string) => { + setInterimText(text); + }, []); + // Reference snippet for context const referenceSnippet = useMemo(() => { if (!selectedReport) return null; @@ -257,22 +249,16 @@ export function ReportComposer({ setContent((prev) => (prev ? `${prev}${block}` : block)); }; - // Mic button for toolbar + // Mic button for toolbar - directly starts/stops recording const MicToolbarButton = ( - + ); return ( @@ -301,29 +287,23 @@ export function ReportComposer({ placeholder="Begin met typen of gebruik spraakopname..." toolbarExtra={MicToolbarButton} onEditorReady={setEditorRef} - isStreaming={isStreaming} + isStreaming={isRecording} minHeight="180px" /> + {/* Interim transcript preview during recording */} + {interimText && ( +
+ {interimText} +
+ )} + {/* Character count */}
{characterCount} / 5000 karakters {characterCount > 0 && characterCount < 20 && Minimaal 20 karakters}
- {/* Speech recorder (collapsible) */} - {showRecorder && ( -
- -
- )} - {/* AI Classification result (inline) */} {classification && (
diff --git a/app/epd/patients/[id]/rapportage/components/toolbar-mic-button.tsx b/app/epd/patients/[id]/rapportage/components/toolbar-mic-button.tsx new file mode 100644 index 0000000..aac7f40 --- /dev/null +++ b/app/epd/patients/[id]/rapportage/components/toolbar-mic-button.tsx @@ -0,0 +1,241 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Mic, Square, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { + useDeepgramStreaming, + type TranscriptResult, +} from '@/hooks/use-deepgram-streaming'; + +// ───────────────────────────────────────────────────────────────────────────── +// Types +// ───────────────────────────────────────────────────────────────────────────── + +interface ToolbarMicButtonProps { + /** Callback voor final transcripts */ + onTranscript: (text: string) => void; + /** Callback voor interim transcripts (live preview) */ + onInterimTranscript?: (text: string) => void; + /** Callback wanneer recording start */ + onRecordingStart?: () => void; + /** Callback wanneer recording stopt */ + onRecordingStop?: () => void; + /** Disabled state */ + disabled?: boolean; + /** Patient ID voor telemetry */ + patientId?: string; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Mini Waveform Component +// ───────────────────────────────────────────────────────────────────────────── + +function MiniWaveform({ analyserNode }: { analyserNode: AnalyserNode | null }) { + const canvasRef = useRef(null); + const animationRef = useRef(null); + + useEffect(() => { + if (!analyserNode || !canvasRef.current) { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + animationRef.current = null; + } + return; + } + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const bufferLength = analyserNode.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + + const draw = () => { + animationRef.current = requestAnimationFrame(draw); + analyserNode.getByteFrequencyData(dataArray); + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw bars + const barCount = 5; + const barWidth = 3; + const gap = 2; + const maxHeight = canvas.height - 4; + const startX = (canvas.width - (barCount * barWidth + (barCount - 1) * gap)) / 2; + + for (let i = 0; i < barCount; i++) { + // Sample from different parts of frequency spectrum + const dataIndex = Math.floor((i / barCount) * bufferLength * 0.5); + const value = dataArray[dataIndex] / 255; + const barHeight = Math.max(4, value * maxHeight); + + const x = startX + i * (barWidth + gap); + const y = (canvas.height - barHeight) / 2; + + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.roundRect(x, y, barWidth, barHeight, 1); + ctx.fill(); + } + }; + + draw(); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [analyserNode]); + + return ( + + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Main Component +// ───────────────────────────────────────────────────────────────────────────── + +export function ToolbarMicButton({ + onTranscript, + onInterimTranscript, + onRecordingStart, + onRecordingStop, + disabled = false, + patientId, +}: ToolbarMicButtonProps) { + const [interimText, setInterimText] = useState(''); + // Completed utterances (after speechFinal) + const completedUtterancesRef = useRef([]); + // Current utterance being built (before speechFinal) + const currentUtteranceRef = useRef(''); + + const handleTranscript = useCallback( + (result: TranscriptResult) => { + if (result.isFinal) { + // Update current utterance with latest final text + currentUtteranceRef.current = result.transcript; + setInterimText(''); + + if (result.speechFinal) { + // Utterance complete - commit to completed list + if (currentUtteranceRef.current.trim()) { + completedUtterancesRef.current.push(currentUtteranceRef.current); + } + currentUtteranceRef.current = ''; + } + + // Build full text: completed utterances + current utterance + const parts = [...completedUtterancesRef.current]; + if (currentUtteranceRef.current.trim()) { + parts.push(currentUtteranceRef.current); + } + onTranscript(parts.join(' ')); + } else { + // Interim result - show as preview + setInterimText(result.transcript); + onInterimTranscript?.(result.transcript); + } + }, + [onTranscript, onInterimTranscript] + ); + + const { + status, + isRecording, + startRecording, + stopRecording, + analyserNode, + isBrowserSupported, + } = useDeepgramStreaming({ + onTranscript: handleTranscript, + }); + + const handleClick = async () => { + console.log('[ToolbarMicButton] Click - isRecording:', isRecording, 'status:', status); + + if (isRecording) { + console.log('[ToolbarMicButton] Stopping recording...'); + stopRecording(); + onRecordingStop?.(); + completedUtterancesRef.current = []; + currentUtteranceRef.current = ''; + setInterimText(''); + } else { + console.log('[ToolbarMicButton] Starting recording...'); + completedUtterancesRef.current = []; + currentUtteranceRef.current = ''; + setInterimText(''); + onRecordingStart?.(); + try { + await startRecording(); + console.log('[ToolbarMicButton] startRecording() completed'); + } catch (err) { + console.error('[ToolbarMicButton] startRecording() failed:', err); + } + } + }; + + const isConnecting = status === 'connecting'; + const isActive = isRecording && status === 'connected'; + + // Determine button state and colors + const getButtonClasses = () => { + if (isActive) { + // Recording - orange background + return 'bg-orange-500 text-white hover:bg-orange-600 shadow-sm'; + } + if (isConnecting) { + // Connecting - subtle loading state + return 'bg-amber-100 text-amber-700'; + } + // Default - green background + return 'bg-emerald-500 text-white hover:bg-emerald-600 shadow-sm'; + }; + + if (!isBrowserSupported) { + return ( + + ); + } + + return ( + + ); +} diff --git a/components/rich-text-editor.tsx b/components/rich-text-editor.tsx index 2faaa55..ff15dfd 100644 --- a/components/rich-text-editor.tsx +++ b/components/rich-text-editor.tsx @@ -80,7 +80,7 @@ export function RichTextEditor({
diff --git a/docs/troubleshooting/screenprint-nieuwe-rapportage-ux-01.png b/docs/troubleshooting/screenprint-nieuwe-rapportage-ux-01.png new file mode 100644 index 0000000..19e1073 Binary files /dev/null and b/docs/troubleshooting/screenprint-nieuwe-rapportage-ux-01.png differ diff --git a/hooks/use-deepgram-streaming.ts b/hooks/use-deepgram-streaming.ts index cf0d4e9..04e8eab 100644 --- a/hooks/use-deepgram-streaming.ts +++ b/hooks/use-deepgram-streaming.ts @@ -78,12 +78,12 @@ export interface UseDeepgramStreamingReturn { function checkBrowserSupport(): boolean { if (typeof window === 'undefined') return false - + const hasMediaDevices = !!(navigator.mediaDevices?.getUserMedia) const hasMediaRecorder = typeof MediaRecorder !== 'undefined' const hasAudioContext = typeof AudioContext !== 'undefined' || typeof (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext !== 'undefined' const hasWebSocket = typeof WebSocket !== 'undefined' - + return hasMediaDevices && hasMediaRecorder && hasAudioContext && hasWebSocket } @@ -152,10 +152,13 @@ export function useDeepgramStreaming({ // Fetch token van onze proxy endpoint const fetchToken = useCallback(async (): Promise => { + console.log('[Deepgram] Fetching token from /api/deepgram/token...') const response = await fetch('/api/deepgram/token', { method: 'POST' }) + console.log('[Deepgram] Token response status:', response.status) if (!response.ok) { const data = await response.json().catch(() => ({})) + console.error('[Deepgram] Token fetch failed:', response.status, data) if (response.status === 429) { throw new Error('Rate limit bereikt. Probeer over een uur opnieuw.') } @@ -166,18 +169,26 @@ export function useDeepgramStreaming({ } const data = await response.json() + console.log('[Deepgram] Token received, expiresIn:', data.expiresIn) return data.token }, []) // Setup WebSocket verbinding met Deepgram const connect = useCallback(async () => { + console.log('[Deepgram] connect() called') try { updateStatus('connecting') setError(null) const token = await fetchToken() - const deepgram = createClient(token) + // IMPORTANT: Use accessToken option for JWT tokens from grantToken() + // This uses Bearer scheme authentication instead of Token scheme + // See: https://github.com/deepgram/deepgram-js-sdk + console.log('[Deepgram] Creating Deepgram client with accessToken...') + const deepgram = createClient({ accessToken: token }) + + console.log('[Deepgram] Creating live connection with options:', { model, language, endpointingMs }) const connection = deepgram.listen.live({ model, language, @@ -187,9 +198,11 @@ export function useDeepgramStreaming({ punctuate: true, utterances: true, }) + console.log('[Deepgram] Live connection created, setting up event handlers...') // Event handlers connection.on(LiveTranscriptionEvents.Open, () => { + console.log('[Deepgram] WebSocket OPEN') updateStatus('connected') reconnectAttemptsRef.current = 0 setError(null) @@ -226,7 +239,7 @@ export function useDeepgramStreaming({ ) connection.on(LiveTranscriptionEvents.Error, (err: Error) => { - console.error('Deepgram WebSocket error:', err) + console.error('[Deepgram] WebSocket ERROR:', err) setError(err.message || 'WebSocket fout') updateStatus('error') onErrorRef.current?.(err) @@ -238,6 +251,7 @@ export function useDeepgramStreaming({ }) connection.on(LiveTranscriptionEvents.Close, () => { + console.log('[Deepgram] WebSocket CLOSE') if (shouldReconnectRef.current && reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS) { handleReconnect() } else { @@ -246,8 +260,9 @@ export function useDeepgramStreaming({ }) liveClientRef.current = connection + console.log('[Deepgram] Connection setup complete') } catch (err) { - console.error('Connection error:', err) + console.error('[Deepgram] Connection error:', err) const errorMessage = err instanceof Error ? err.message : 'Verbindingsfout' setError(errorMessage) updateStatus('error') @@ -281,10 +296,14 @@ export function useDeepgramStreaming({ // Setup audio stream en analyser const setupAudio = useCallback(async () => { + console.log('[Deepgram] setupAudio() called') + console.log('[Deepgram] Requesting microphone access...') const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + console.log('[Deepgram] Microphone access granted') mediaStreamRef.current = stream // Setup Web Audio API voor waveform + console.log('[Deepgram] Setting up AudioContext...') const audioContext = new AudioContext() const source = audioContext.createMediaStreamSource(stream) const analyser = audioContext.createAnalyser() @@ -300,6 +319,7 @@ export function useDeepgramStreaming({ : MediaRecorder.isTypeSupported('audio/mp4') ? 'audio/mp4' : undefined // Browser default + console.log('[Deepgram] Using mimeType:', mimeType || 'browser default') // Setup MediaRecorder voor streaming naar Deepgram const mediaRecorder = mimeType @@ -312,26 +332,32 @@ export function useDeepgramStreaming({ } } + console.log('[Deepgram] Starting MediaRecorder with interval:', AUDIO_CHUNK_INTERVAL_MS) mediaRecorder.start(AUDIO_CHUNK_INTERVAL_MS) mediaRecorderRef.current = mediaRecorder + console.log('[Deepgram] Audio setup complete') }, []) // Start opname const startRecording = useCallback(async () => { + console.log('[Deepgram] startRecording() called') try { setError(null) shouldReconnectRef.current = true // Eerst WebSocket verbinding opzetten + console.log('[Deepgram] Step 1: Connecting to Deepgram...') await connect() // Dan audio stream starten + console.log('[Deepgram] Step 2: Setting up audio...') await setupAudio() + console.log('[Deepgram] Recording started successfully') setIsRecording(true) setIsPaused(false) } catch (err) { - console.error('Start recording error:', err) + console.error('[Deepgram] startRecording error:', err) const errorMessage = err instanceof Error ? err.message : 'Kon opname niet starten'