'use client' import { useState, useRef } from 'react' import { transcribeWithVoxtral } from '@/lib/voxtral' import { loadVoxtralLocal, transcribeWithVoxtralLocal, isModelLoaded, type LoadProgress, } from '@/lib/voxtral-local' type MicMode = 'api' | 'local' function lsGet(key: string) { return typeof window !== 'undefined' ? (localStorage.getItem(key) ?? '') : '' } function lsSet(key: string, val: string) { if (typeof window !== 'undefined') localStorage.setItem(key, val) } interface ChatPanelProps { onSend: (message: string) => void onToNotes: () => void voxtralApiKey: string } export function ChatPanel({ onSend, onToNotes, voxtralApiKey }: ChatPanelProps) { const [message, setMessage] = useState('') const [isRecording, setIsRecording] = useState(false) const [isTranscribing, setIsTranscribing] = useState(false) const [recordError, setRecordError] = useState(null) const [micMode, setMicMode] = useState( () => (lsGet('mic_mode') as MicMode) || 'api', ) const [loadProgress, setLoadProgress] = useState(null) const mediaRecorderRef = useRef(null) const chunksRef = useRef([]) const handleSend = () => { if (message.trim()) { onSend(message.trim()) setMessage('') } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() } } const toggleMicMode = () => { const next: MicMode = micMode === 'api' ? 'local' : 'api' setMicMode(next) lsSet('mic_mode', next) setRecordError(null) setLoadProgress(null) } const startRecording = async () => { setRecordError(null) if (micMode === 'api' && !voxtralApiKey) { setRecordError('No Mistral API key — set it in the player settings below.') return } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const recorder = new MediaRecorder(stream) chunksRef.current = [] recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data) } recorder.onstop = async () => { stream.getTracks().forEach(t => t.stop()) const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' }) setIsTranscribing(true) try { let text: string if (micMode === 'local') { // Load model on first use (shows download progress) if (!isModelLoaded()) { await loadVoxtralLocal(setLoadProgress) } setLoadProgress(null) text = await transcribeWithVoxtralLocal(audioBlob) } else { text = await transcribeWithVoxtral(audioBlob, voxtralApiKey) } if (text) setMessage(prev => (prev ? `${prev} ${text}` : text)) } catch (err) { setRecordError(err instanceof Error ? err.message : 'Transcription failed') } finally { setIsTranscribing(false) } } recorder.start() mediaRecorderRef.current = recorder setIsRecording(true) } catch { setRecordError('Microphone access denied.') } } const stopRecording = () => { mediaRecorderRef.current?.stop() mediaRecorderRef.current = null setIsRecording(false) } const toggleRecording = () => { if (isRecording) stopRecording() else startRecording() } const isLocalReady = micMode === 'local' const canRecord = isLocalReady || !!voxtralApiKey return (

Chat: