Spaces:
Running
Running
File size: 2,805 Bytes
c0ddd13 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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>
);
}
|