Voice AI Orb
AI & AgentsFluid WebGL neural voice visualizer with waveform frequencies.
Interactive Live Preview
components/ui/voice-agent-orb.tsxTypeScript · React 19
1"use client";23import * as React from "react";4import { Button } from "@/components/atoms/Button";56interface VoiceAgentOrbProps {7 variant?: string;8}910export 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 );1617 React.useEffect(() => {18 if (variant === "Reasoning" || variant === "Speaking" || variant === "Listening") {19 setState(variant.toLowerCase() as any);20 }21 }, [variant]);2223 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 };4647 const current = stateConfigs[state];4849 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 <div55 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 />6162 {/* Ambient Ring 1 */}63 <div64 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 />6970 {/* Ambient Ring 2 */}71 <div72 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 />7980 {/* Center Dynamic Core Orb */}81 <div82 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 <span93 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>103104 {/* Status Badge */}105 <div106 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>113114 {/* 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 “{transcript}”121 </p>122 </div>123124 {/* Control Bar */}125 <div className="flex items-center gap-2 rounded-full bg-surface p-1 shadow-card border border-line">126 <button127 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 Listen133 </button>134 <button135 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 Reason141 </button>142 <button143 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 Speak149 </button>150 <div className="h-4 w-px bg-line mx-0.5" />151 <button152 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
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | Listening | Reasoning | Speaking | Listening | Visual state and layout presentation mode. |
| className | string | "" | Optional Tailwind CSS wrapper class overrides. |