Spaces:
Running
Running
| import { motion } from "motion/react"; | |
| import { Loader2, Mic, MicOff, Volume2 } from "lucide-react"; | |
| import type { VoiceState } from "../../../hooks/useVoiceSession"; | |
| interface VoiceMicButtonProps { | |
| voiceState: VoiceState; | |
| onToggle: () => void; | |
| disabled?: boolean; | |
| } | |
| export default function VoiceMicButton({ voiceState, onToggle, disabled }: VoiceMicButtonProps) { | |
| const isDisabled = disabled ?? false; | |
| const stateConfig: Record< | |
| VoiceState, | |
| { icon: React.ReactNode; className: string; title: string; pulse: boolean; scalePulse: boolean } | |
| > = { | |
| IDLE: { | |
| icon: <Mic className="h-4 w-4" />, | |
| className: "bg-neutral-100 text-neutral-400 hover:bg-brand-green/10 hover:text-brand-green", | |
| title: "Start voice session", | |
| pulse: false, | |
| scalePulse: false, | |
| }, | |
| CONNECTING: { | |
| icon: <Loader2 className="h-4 w-4 animate-spin" />, | |
| className: "bg-brand-green/10 text-brand-green/60", | |
| title: "Connecting...", | |
| pulse: false, | |
| scalePulse: false, | |
| }, | |
| LISTENING: { | |
| icon: <Mic className="h-4 w-4" />, | |
| className: "bg-brand-green text-white shadow-md shadow-brand-green/25", | |
| title: "Listening β click to stop", | |
| pulse: true, | |
| scalePulse: false, | |
| }, | |
| PROCESSING: { | |
| icon: <Loader2 className="h-4 w-4 animate-spin" />, | |
| className: "bg-brand-amber text-white", | |
| title: "Processing...", | |
| pulse: false, | |
| scalePulse: false, | |
| }, | |
| SPEAKING: { | |
| icon: <Volume2 className="h-4 w-4" />, | |
| className: "bg-brand-cyan text-white", | |
| title: "Agent is speaking β click to stop", | |
| pulse: false, | |
| scalePulse: true, | |
| }, | |
| ERROR: { | |
| icon: <MicOff className="h-4 w-4" />, | |
| className: "bg-red-100 text-red-400 hover:bg-red-200", | |
| title: "Connection failed β click to retry", | |
| pulse: false, | |
| scalePulse: false, | |
| }, | |
| }; | |
| const cfg = stateConfig[voiceState]; | |
| return ( | |
| <div className="relative flex-shrink-0"> | |
| {cfg.pulse && ( | |
| <motion.span | |
| className="absolute inset-0 rounded-xl bg-brand-green/30" | |
| animate={{ scale: [1, 1.6], opacity: [0.6, 0] }} | |
| transition={{ duration: 1.2, repeat: Infinity, ease: "easeOut" }} | |
| /> | |
| )} | |
| <motion.button | |
| onClick={onToggle} | |
| disabled={isDisabled} | |
| title={cfg.title} | |
| animate={cfg.scalePulse ? { scale: [1, 1.05, 1] } : { scale: 1 }} | |
| transition={cfg.scalePulse ? { duration: 1.4, repeat: Infinity, ease: "easeInOut" } : {}} | |
| className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-all duration-200 ${cfg.className} ${isDisabled ? "pointer-events-none" : ""}`} | |
| aria-label={cfg.title} | |
| > | |
| {cfg.icon} | |
| </motion.button> | |
| </div> | |
| ); | |
| } | |