Voice AI Orb

AI & Agents

Fluid WebGL neural voice visualizer with waveform frequencies.

Interactive Live Preview
components/ui/voice-agent-orb.tsxTypeScript · React 19
1"use client";
2
3import * as React from "react";
4import { Button } from "@/components/atoms/Button";
5
6interface VoiceAgentOrbProps {
7 variant?: string;
8}
9
10export default function VoiceAgentOrb({ variant = "Listening" }: VoiceAgentOrbProps) {
11 const [state, setState] = React.useState<"listening" | "reasoning" | "speaking">("listening");
12 const [isMuted, setIsMuted] = React.useState(false);
13 const [transcript, setTranscript] = React.useState(
14 "Can you audit our PostgreSQL query latency and recommend indexing optimizations?"
15 );
16
17 React.useEffect(() => {
18 if (variant === "Reasoning" || variant === "Speaking" || variant === "Listening") {
19 setState(variant.toLowerCase() as any);
20 }
21 }, [variant]);
22
23 const stateConfigs = {
24 listening: {
25 color: "#7DFFB3",
26 glow: "rgba(125, 255, 179, 0.4)",
27 label: "Listening to user…",
28 badge: "Mic Active",
29 ringSpeed: "duration-700",
30 },
31 reasoning: {
32 color: "#FB923C",
33 glow: "rgba(251, 146, 60, 0.4)",
34 label: "Reasoning over schemas & index profiles…",
35 badge: "DeepSeek R1",
36 ringSpeed: "duration-300",
37 },
38 speaking: {
39 color: "#38BDF8",
40 glow: "rgba(56, 189, 248, 0.4)",
41 label: "Streaming neural voice response…",
42 badge: "TTS 24kHz",
43 ringSpeed: "duration-500",
44 },
45 };
46
47 const current = stateConfigs[state];
48
49 return (
50 <div className="flex w-full max-w-105 flex-col items-center gap-4 p-2">
51 {/* Orb Frame */}
52 <div className="relative flex size-44 items-center justify-center">
53 {/* Pulsing Back Glow */}
54 <div
55 className="absolute inset-0 rounded-full blur-2xl transition-all duration-700"
56 style={{
57 background: current.glow,
58 transform: state === "speaking" ? "scale(1.25)" : "scale(1)",
59 }}
60 />
61
62 {/* Ambient Ring 1 */}
63 <div
64 className={`absolute inset-2 rounded-full border border-dashed transition-all duration-1000 ${
65 state === "reasoning" ? "animate-spin" : "animate-pulse"
66 }`}
67 style={{ borderColor: current.color, opacity: 0.35 }}
68 />
69
70 {/* Ambient Ring 2 */}
71 <div
72 className="absolute inset-5 rounded-full border transition-all duration-700 animate-ping"
73 style={{
74 borderColor: current.color,
75 opacity: state === "listening" ? 0.2 : 0.05,
76 animationDuration: "3s",
77 }}
78 />
79
80 {/* Center Dynamic Core Orb */}
81 <div
82 className="relative flex size-24 items-center justify-center rounded-full shadow-2xl transition-all duration-500"
83 style={{
84 background: `radial-gradient(circle at 35% 30%, #ffffff 0%, ${current.color} 50%, var(--surface) 100%)`,
85 boxShadow: `0 0 35px ${current.glow}, inset 0 0 15px rgba(255,255,255,0.6)`,
86 transform: state === "speaking" ? "scale(1.1)" : "scale(1)",
87 }}
88 >
89 {/* Animated Wave Bars Inside Core */}
90 <div className="flex items-center gap-1">
91 {[40, 75, 100, 60, 85, 45].map((height, i) => (
92 <span
93 key={i}
94 className="w-1 rounded-full bg-surface"
95 style={{
96 height: isMuted ? "4px" : `${Math.max(6, height * (state === "speaking" ? 0.35 : 0.2))}px`,
97 animation: !isMuted ? `eq-bounce 800ms ease-in-out ${i * 120}ms infinite` : "none",
98 }}
99 />
100 ))}
101 </div>
102 </div>
103
104 {/* Status Badge */}
105 <div
106 className="absolute -bottom-1 flex items-center gap-1.5 rounded-full bg-surface px-2.5 py-0.5 font-mono text-[10.5px] font-medium shadow-card border border-line"
107 style={{ color: current.color }}
108 >
109 <span className="size-1.5 rounded-full animate-ping" style={{ background: current.color }} />
110 {current.badge}
111 </div>
112 </div>
113
114 {/* Transcript & Status Label */}
115 <div className="flex w-full flex-col items-center text-center">
116 <p className="font-mono text-[11px] text-ink-3 transition-colors duration-300">
117 {current.label}
118 </p>
119 <p className="mt-1.5 text-[13px] leading-relaxed text-ink font-body max-w-sm">
120 &ldquo;{transcript}&rdquo;
121 </p>
122 </div>
123
124 {/* Control Bar */}
125 <div className="flex items-center gap-2 rounded-full bg-surface p-1 shadow-card border border-line">
126 <button
127 onClick={() => setState("listening")}
128 className={`rounded-full px-3 py-1 text-[11.5px] font-medium transition-all ${
129 state === "listening" ? "bg-accent text-accent-ink shadow-btn font-semibold" : "text-ink-3 hover:text-ink"
130 }`}
131 >
132 Listen
133 </button>
134 <button
135 onClick={() => setState("reasoning")}
136 className={`rounded-full px-3 py-1 text-[11.5px] font-medium transition-all ${
137 state === "reasoning" ? "bg-orange text-white shadow-btn font-semibold" : "text-ink-3 hover:text-ink"
138 }`}
139 >
140 Reason
141 </button>
142 <button
143 onClick={() => setState("speaking")}
144 className={`rounded-full px-3 py-1 text-[11.5px] font-medium transition-all ${
145 state === "speaking" ? "bg-sky-500 text-white shadow-btn font-semibold" : "text-ink-3 hover:text-ink"
146 }`}
147 >
148 Speak
149 </button>
150 <div className="h-4 w-px bg-line mx-0.5" />
151 <button
152 onClick={() => setIsMuted(!isMuted)}
153 className={`flex size-7 items-center justify-center rounded-full transition-colors ${
154 isMuted ? "bg-red text-white" : "text-ink-3 hover:bg-hover hover:text-ink"
155 }`}
156 title={isMuted ? "Unmute Mic" : "Mute Mic"}
157 >
158 {isMuted ? (
159 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
160 <line x1="1" y1="1" x2="23" y2="23" />
161 <path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6" />
162 <path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23" />
163 <line x1="12" y1="19" x2="12" y2="23" />
164 <line x1="8" y1="23" x2="16" y2="23" />
165 </svg>
166 ) : (
167 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
168 <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z" />
169 <path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8" />
170 </svg>
171 )}
172 </button>
173 </div>
174 </div>
175 );
176}
177

Props & Specification

PropTypeDefaultDescription
variantListening | Reasoning | SpeakingListeningVisual state and layout presentation mode.
classNamestring""Optional Tailwind CSS wrapper class overrides.

Related Components in AI & Agents