nexus/ui/src/components/VoiceMicButton.tsx
Nexus Dev 21ecf23d9a feat(37-02): VoiceWaveform canvas component and VoiceMicButton
- 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
2026-04-04 03:55:50 +00:00

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>
);
}