realtime-voice-ai
Version:
A powerful, fully configurable React component for real-time voice chat powered by OpenAI's Realtime API. Create natural conversations with AI using advanced voice recognition and synthesis.
424 lines (381 loc) • 17.6 kB
JSX
import { useState, useRef, useEffect } from 'react';
const AudioWaveUI = ({ isListening, isSpeaking, isUserSpeaking, isBotSpeaking, isConnected }) => {
const [animationTime, setAnimationTime] = useState(0);
const [isTransformed, setIsTransformed] = useState(false);
const [particles, setParticles] = useState([]);
const animationRef = useRef();
// Generate particles for enhanced visual effects
useEffect(() => {
const generateParticles = () => {
const newParticles = [];
for (let i = 0; i < 25; i++) {
newParticles.push({
id: i,
x: Math.random() * 600,
y: Math.random() * 200,
size: Math.random() * 3 + 1,
speed: Math.random() * 0.5 + 0.2,
opacity: Math.random() * 0.5 + 0.2,
phase: Math.random() * Math.PI * 2
});
}
setParticles(newParticles);
};
generateParticles();
}, []);
// High-performance animation loop
useEffect(() => {
const animate = () => {
setAnimationTime(prev => prev + 0.08);
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
// Transform trigger
useEffect(() => {
if (isConnected && !isTransformed) {
setTimeout(() => {
setIsTransformed(true);
}, 800);
}
}, [isConnected, isTransformed]);
// Determine current state
const getWaveState = () => {
if (isBotSpeaking) return 'bot-speaking';
if (isUserSpeaking) return 'user-speaking';
if (isListening) return 'listening';
return 'idle';
};
const waveState = getWaveState();
// Generate multiple wave layers for depth
const generateWaveLayers = () => {
const layers = [];
const numLayers = 4;
const width = 600;
const height = 200;
const centerY = height / 2;
for (let layer = 0; layer < numLayers; layer++) {
let path = `M 0 ${centerY}`;
for (let x = 0; x <= width; x += 4) {
let y = centerY;
const progress = x / width;
// Different wave patterns based on state and layer
switch (waveState) {
case 'bot-speaking':
const botIntensity = 1.2 + Math.sin(animationTime * 3) * 0.4;
const botWave1 = Math.sin(animationTime * 4 + progress * 8 + layer) * (25 - layer * 4) * botIntensity;
const botWave2 = Math.sin(animationTime * 6 + progress * 12 + layer * 1.5) * (15 - layer * 2) * botIntensity;
const botWave3 = Math.sin(animationTime * 8 + progress * 16 + layer * 2) * (10 - layer) * botIntensity;
y = centerY + botWave1 + botWave2 * 0.7 + botWave3 * 0.4;
break;
case 'user-speaking':
const userIntensity = 1.0 + Math.sin(animationTime * 2.5) * 0.3;
const userWave1 = Math.sin(animationTime * 3 + progress * 6 + layer * 0.8) * (20 - layer * 3) * userIntensity;
const userWave2 = Math.cos(animationTime * 4.5 + progress * 10 + layer * 1.2) * (12 - layer * 2) * userIntensity;
y = centerY + userWave1 + userWave2 * 0.6;
break;
case 'listening':
const breatheIntensity = 0.6 + Math.sin(animationTime * 0.8) * 0.2;
const listenWave = Math.sin(animationTime * 1.5 + progress * 4 + layer * 0.5) * (8 - layer) * breatheIntensity;
const pulseWave = Math.sin(animationTime * 0.5 + layer) * (4 - layer) * breatheIntensity;
y = centerY + listenWave + pulseWave;
break;
default:
const idleWave = Math.sin(animationTime * 0.5 + progress * 2 + layer * 0.3) * (3 - layer * 0.5);
y = centerY + idleWave;
}
path += ` L ${x} ${y}`;
}
layers.push({
path,
layer,
opacity: 0.9 - layer * 0.15,
strokeWidth: 3 - layer * 0.4
});
}
return layers;
};
const waveLayers = generateWaveLayers();
// Professional and modern color scheme
const getStateTheme = () => {
switch (waveState) {
case 'bot-speaking':
return {
primary: '#0ea5e9',
secondary: '#0284c7',
accent: '#38bdf8',
gradient: ['#0c4a6e', '#075985', '#0369a1', '#0284c7', '#0ea5e9', '#38bdf8'],
glow: '#0ea5e9',
particles: '#7dd3fc'
};
case 'user-speaking':
return {
primary: '#10b981',
secondary: '#059669',
accent: '#34d399',
gradient: ['#064e3b', '#065f46', '#047857', '#059669', '#10b981', '#34d399'],
glow: '#10b981',
particles: '#6ee7b7'
};
case 'listening':
return {
primary: '#f59e0b',
secondary: '#d97706',
accent: '#fbbf24',
gradient: ['#78350f', '#92400e', '#b45309', '#d97706', '#f59e0b', '#fbbf24'],
glow: '#f59e0b',
particles: '#fde68a'
};
default:
return {
primary: '#6366f1',
secondary: '#4f46e5',
accent: '#818cf8',
gradient: ['#312e81', '#3730a3', '#4338ca', '#4f46e5', '#6366f1', '#818cf8'],
glow: '#6366f1',
particles: '#a5b4fc'
};
}
};
const theme = getStateTheme();
// Render initial pulsing orb (keeping your original design)
if (!isTransformed) {
return (
<div className="relative flex items-center justify-center w-full h-64">
{/* Background gradient */}
<div
className="absolute inset-0 rounded-full blur-3xl opacity-30"
style={{
background: `radial-gradient(circle, ${theme.gradient[3]}40 0%, ${theme.gradient[2]}20 50%, transparent 100%)`
}}
/>
{/* Main orb */}
<div
className="relative w-32 h-32 rounded-full flex items-center justify-center"
style={{
background: `radial-gradient(circle at 30% 30%, ${theme.gradient[3]}80, ${theme.gradient[2]}60, ${theme.gradient[1]}40)`,
boxShadow: `0 0 50px ${theme.glow}60, inset 0 0 30px ${theme.gradient[3]}40`,
animation: 'pulse-orb 2s ease-in-out infinite'
}}
>
{/* Inner glow */}
<div
className="w-20 h-20 rounded-full opacity-70"
style={{
background: `radial-gradient(circle, ${theme.gradient[3]}60, transparent 70%)`,
animation: 'pulse-inner 1.5s ease-in-out infinite alternate'
}}
/>
</div>
{/* Status text */}
<div className="absolute -bottom-8 text-center">
<div
className="text-sm font-medium"
style={{ color: theme.primary }}
>
Initializing Neural Link...
</div>
</div>
<style jsx>{`
@keyframes pulse-orb {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes pulse-inner {
0% { opacity: 0.4; transform: scale(1); }
100% { opacity: 0.8; transform: scale(1.1); }
}
`}</style>
</div>
);
}
// Main wave visualization
return (
<div className="relative flex items-center justify-center w-full">
<svg
viewBox="0 0 600 200"
className="drop-shadow-2xl"
style={{
width: '600px',
height: '200px',
transition: 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)'
}}
>
<defs>
{/* Modern professional gradient for waves */}
<linearGradient id="wave-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: theme.gradient[1], stopOpacity: 0.6 }} />
<stop offset="25%" style={{ stopColor: theme.gradient[2], stopOpacity: 0.8 }} />
<stop offset="50%" style={{ stopColor: theme.primary, stopOpacity: 1 }} />
<stop offset="75%" style={{ stopColor: theme.gradient[2], stopOpacity: 0.8 }} />
<stop offset="100%" style={{ stopColor: theme.gradient[1], stopOpacity: 0.6 }} />
</linearGradient>
{/* Clean glow effects */}
<filter id="glow-effect" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="strong-glow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="8" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
{/* Subtle background pattern */}
<pattern id="audio-grid" width="40" height="20" patternUnits="userSpaceOnUse">
<path
d="M 0 10 L 40 10 M 20 0 L 20 20"
fill="none"
stroke={theme.gradient[1]}
strokeWidth="0.5"
opacity="0.3"
/>
</pattern>
</defs>
{/* Background */}
<rect width="100%" height="100%" fill="url(#audio-grid)" />
{/* Background glow */}
<rect
width="100%"
height="100%"
fill={`url(#wave-gradient)`}
opacity="0.05"
/>
{/* Multiple wave layers for depth */}
{waveLayers.map((layer, index) => (
<path
key={index}
d={layer.path}
fill="none"
stroke="url(#wave-gradient)"
strokeWidth={layer.strokeWidth}
opacity={layer.opacity}
filter={index === 0 ? "url(#strong-glow)" : "url(#glow-effect)"}
style={{
transition: 'all 0.3s ease-out'
}}
/>
))}
{/* Animated particles */}
{particles.map((particle, index) => {
const x = particle.x + Math.sin(animationTime * particle.speed + particle.phase) * 20;
const y = particle.y + Math.cos(animationTime * particle.speed * 0.7 + particle.phase) * 10;
const opacity = particle.opacity * (waveState !== 'idle' ? 1 : 0.3);
return (
<circle
key={particle.id}
cx={x}
cy={y}
r={particle.size}
fill={theme.particles}
opacity={opacity}
filter="url(#glow-effect)"
/>
);
})}
{/* Center frequency indicator */}
<circle
cx="300"
cy="100"
r={waveState !== 'idle' ? (4 + Math.sin(animationTime * 4) * 2) : 2}
fill={theme.primary}
opacity="0.8"
filter="url(#strong-glow)"
/>
{/* Scanner line */}
{(waveState === 'bot-speaking' || waveState === 'user-speaking') && (
<line
x1={((animationTime * 60) % 600)}
y1="0"
x2={((animationTime * 60) % 600)}
y2="200"
stroke={theme.primary}
strokeWidth="1"
opacity="0.4"
filter="url(#glow-effect)"
/>
)}
{/* Peak indicators */}
{waveState !== 'idle' && (
<>
<circle
cx="150"
cy={100 + Math.sin(animationTime * 3) * 15}
r="3"
fill={theme.accent}
opacity="0.7"
/>
<circle
cx="450"
cy={100 + Math.sin(animationTime * 2.5 + Math.PI) * 15}
r="3"
fill={theme.accent}
opacity="0.7"
/>
</>
)}
</svg>
{/* Status display */}
<div className="absolute -bottom-10 left-1/2 transform -translate-x-1/2">
<div className="flex items-center space-x-3">
{/* Status indicator */}
<div
className="w-2 h-2 rounded-full animate-pulse"
style={{ backgroundColor: theme.primary }}
/>
{/* Status text */}
<div
className="text-sm font-medium tracking-wide"
style={{ color: theme.primary }}
>
{(() => {
switch (waveState) {
case 'bot-speaking':
return 'AI RESPONDING';
case 'user-speaking':
return 'PROCESSING INPUT';
case 'listening':
return 'LISTENING';
default:
return 'STANDBY';
}
})()}
</div>
{/* Activity meter */}
{waveState !== 'idle' && (
<div className="flex space-x-1">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="w-1 bg-current animate-pulse"
style={{
height: `${8 + Math.sin(animationTime * 4 + i) * 4}px`,
color: theme.primary,
animationDelay: `${i * 0.1}s`
}}
/>
))}
</div>
)}
</div>
</div>
{/* Ambient glow */}
<div
className="absolute inset-0 -z-10 blur-3xl opacity-20"
style={{
background: `radial-gradient(ellipse at center, ${theme.primary}40 0%, transparent 70%)`
}}
/>
</div>
);
};
export default AudioWaveUI;