- VoiceWaveform: 80x32 canvas with Web Audio AnalyserNode (fftSize=64), 20 animated bars drawn from frequency data using --primary color - VoiceMicButton: three visual states — idle (Mic icon), recording (VoiceWaveform + ring-2 ring-primary), processing (Loader2 animate-spin) - All three states have correct aria-labels per UI spec copywriting contract
60 lines
1.5 KiB
TypeScript
60 lines
1.5 KiB
TypeScript
import { Mic, Loader2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { VoiceWaveform } from "./VoiceWaveform";
|
|
import { useVadRecorder } from "../hooks/useVadRecorder";
|
|
|
|
interface VoiceMicButtonProps {
|
|
onTranscript: (text: string) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function VoiceMicButton({ onTranscript, disabled }: VoiceMicButtonProps) {
|
|
const { state, start, stop, mediaStream } = useVadRecorder({ onTranscript });
|
|
|
|
// Idle state (also used when disabled)
|
|
if (state === "idle") {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={start}
|
|
disabled={disabled}
|
|
aria-label="Start voice input"
|
|
title="Start voice input"
|
|
>
|
|
<Mic className="h-4 w-4" />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
// Recording state — show waveform with primary ring, click to stop
|
|
if (state === "recording") {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 ring-2 ring-primary"
|
|
onClick={stop}
|
|
aria-label="Recording — speak now"
|
|
title="Recording — speak now"
|
|
>
|
|
<VoiceWaveform stream={mediaStream} active={true} />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
// Processing state — transcribing, disabled
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
disabled
|
|
aria-label="Transcribing..."
|
|
title="Transcribing..."
|
|
>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
</Button>
|
|
);
|
|
}
|