| import { Mic, Square } from "lucide-react"; |
| import { useCallback, useEffect } from "react"; |
| import { useAudioRecorder } from "@/hooks/useAudioRecorder"; |
| import { useInferenceStore } from "@/store/inferenceStore"; |
|
|
| |
| |
| |
| |
| export default function MicRecorder() { |
| const audio = useInferenceStore((s) => s.audio); |
| const setAudio = useInferenceStore((s) => s.setAudio); |
| const { |
| state, |
| start: startInternal, |
| stop, |
| reset, |
| maxDurationMs, |
| } = useAudioRecorder(15_000); |
|
|
| const seconds = (state.durationMs / 1000).toFixed(1); |
|
|
| |
| useEffect(() => { |
| if (state.blob && state.objectUrl) { |
| setAudio({ |
| kind: "recording", |
| blob: state.blob, |
| objectUrl: state.objectUrl, |
| }); |
| } |
| }, [state.blob, state.objectUrl, setAudio]); |
|
|
| |
| useEffect(() => { |
| if (audio?.kind !== "recording" && state.blob) { |
| reset(); |
| } |
| }, [audio, state.blob, reset]); |
|
|
| |
| |
| |
| const start = useCallback(async () => { |
| if (audio?.kind === "recording") { |
| setAudio(null); |
| } |
| await startInternal(); |
| }, [audio, setAudio, startInternal]); |
|
|
| |
| |
| |
| |
| const hasRecording = state.blob != null || audio?.kind === "recording"; |
|
|
| return ( |
| <div className="panel p-4"> |
| <div className="flex items-center justify-between gap-3"> |
| <div className="min-w-0"> |
| <div className="label">microphone</div> |
| <div className="mt-1 truncate font-mono text-sm"> |
| {state.isRecording ? ( |
| <span className="text-danger">● rec {seconds} s</span> |
| ) : hasRecording ? ( |
| <span className="text-cyber"> |
| ● recorded {state.blob ? `${seconds} s · ` : ""} |
| play above |
| </span> |
| ) : ( |
| <span className="text-ink-dim"> |
| 15 s max · grants browser mic permission |
| </span> |
| )} |
| </div> |
| </div> |
| |
| {state.isRecording ? ( |
| <button onClick={stop} className="btn-danger"> |
| <Square className="h-3.5 w-3.5" /> Stop |
| </button> |
| ) : ( |
| <button onClick={start} className="btn-primary"> |
| <Mic className="h-3.5 w-3.5" /> |
| {hasRecording ? "Re-record" : "Record"} |
| </button> |
| )} |
| </div> |
| |
| {/* Recording progress bar (only meaningful while recording) */} |
| <div className="mt-3 h-1 overflow-hidden rounded-full bg-surface-alt"> |
| <div |
| className={`h-full transition-all ${ |
| state.isRecording ? "bg-danger/70" : "bg-cyber/40" |
| }`} |
| style={{ |
| width: state.isRecording |
| ? `${(state.durationMs / maxDurationMs) * 100}%` |
| : hasRecording |
| ? "100%" |
| : "0%", |
| }} |
| /> |
| </div> |
| |
| {state.error && ( |
| <div className="mt-2 font-mono text-xs text-danger">{state.error}</div> |
| )} |
| </div> |
| ); |
| } |
|
|