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