import React, { useEffect, useRef, useState } from 'react'; import { Loader2, Mic, Square, Upload, X } from 'lucide-react'; import { UploadedMedia, uploadMedia } from '../../lib/media'; import { VoiceRecorder } from '../../lib/voice'; import { VoiceWave } from '../../components/VoiceWave'; import { triggerHaptic } from '../../lib/api'; interface AudioRecorderPickerProps { docId: string; onDone: (media: UploadedMedia) => void; onCancel: () => void; } const MAX_RECORDING_MS = 30 * 60 * 1000; function formatTime(ms: number): string { const s = Math.floor(ms / 1000); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; } // Inline bubble an empty bullet turns into after /audio. Green button = start, red button = stop and // save; the live wave runs left of the red button. An existing audio file can be uploaded instead. export const AudioRecorderPicker: React.FC = ({ docId, onDone, onCancel }) => { const [state, setState] = useState<'ready' | 'recording' | 'uploading'>('ready'); const [elapsed, setElapsed] = useState(0); const [error, setError] = useState(null); const recorderRef = useRef(null); const startedRef = useRef(0); const tickRef = useRef(null); const fileRef = useRef(null); const boxRef = useRef(null); const stopTicking = () => { if (tickRef.current !== null) window.clearInterval(tickRef.current); tickRef.current = null; }; useEffect(() => { boxRef.current?.focus(); return () => { stopTicking(); recorderRef.current?.cancel(); }; }, []); const upload = async (blob: Blob, name: string) => { setState('uploading'); setError(null); try { onDone(await uploadMedia(docId, blob, name)); } catch (err: any) { setError(err?.message || 'Hochladen fehlgeschlagen'); setState('ready'); } }; const stop = async () => { const rec = recorderRef.current; if (!rec) return; recorderRef.current = null; stopTicking(); triggerHaptic(20); const blob = await rec.stop(); if (!blob) { setError('Die Aufnahme ist leer'); setState('ready'); return; } const ext = blob.type.includes('mp4') ? 'm4a' : blob.type.includes('ogg') ? 'ogg' : 'webm'; await upload(blob, `sprachnotiz.${ext}`); }; const start = async () => { setError(null); const rec = new VoiceRecorder(); try { await rec.start(); } catch { setError('Kein Zugriff auf das Mikrofon (Berechtigung, oder die Seite läuft nicht über HTTPS)'); return; } recorderRef.current = rec; startedRef.current = Date.now(); setElapsed(0); setState('recording'); triggerHaptic([15, 60, 25]); tickRef.current = window.setInterval(() => { const ms = Date.now() - startedRef.current; setElapsed(ms); if (ms >= MAX_RECORDING_MS) void stop(); }, 250); }; const cancel = () => { stopTicking(); recorderRef.current?.cancel(); recorderRef.current = null; onCancel(); }; return (
{ if (e.key === 'Escape') { e.preventDefault(); cancel(); } }} className="my-1 inline-flex max-w-full flex-col gap-2 rounded-3xl border border-border bg-panel-elevated px-4 py-2 focus:outline-none" >
{state === 'recording' && ( <> {formatTime(elapsed)} recorderRef.current?.level() ?? null} bars={11} className="h-6 text-status-err" /> )} {state === 'ready' && Sprachnotiz aufnehmen} {state === 'uploading' && ( Wird gespeichert… )} {state === 'recording' ? ( ) : ( )} {state === 'ready' && ( )}
{error &&

{error}

} { const f = e.target.files?.[0]; if (f) void upload(f, f.name || 'audio'); }} />
); };