diff --git a/app-config.ts b/app-config.ts index 80fb6a902..cfb5ef9cb 100644 --- a/app-config.ts +++ b/app-config.ts @@ -14,6 +14,14 @@ export interface AppConfig { logoDark?: string; accentDark?: string; + audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura'; + audioVisualizerRadius?: number; + audioVisualizerBarCount?: number; + audioVisualizerRowCount?: number; + audioVisualizerColumnCount?: number; + audioVisualizerColor?: `#${string}`; + audioVisualizerColorShift?: number; + // agent dispatch configuration agentName?: string; @@ -37,6 +45,13 @@ export const APP_CONFIG_DEFAULTS: AppConfig = { accentDark: '#1fd5f9', startButtonText: 'Start call', + audioVisualizerType: 'wave', + audioVisualizerBarCount: 25, + audioVisualizerRowCount: 11, + audioVisualizerColumnCount: 11, + audioVisualizerColor: '#1fd5f9', + audioVisualizerColorShift: 0.3, + // agent dispatch configuration agentName: process.env.AGENT_NAME ?? undefined, diff --git a/components/agents-ui/agent-audio-visualizer-aura.tsx b/components/agents-ui/agent-audio-visualizer-aura.tsx new file mode 100644 index 000000000..28f5474dc --- /dev/null +++ b/components/agents-ui/agent-audio-visualizer-aura.tsx @@ -0,0 +1,438 @@ +'use client'; + +import React, { type ComponentProps, useMemo } from 'react'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client'; +import { type AgentState, type TrackReferenceOrPlaceholder } from '@livekit/components-react'; +import { ReactShaderToy } from '@/components/agents-ui/react-shader-toy'; +import { useAgentAudioVisualizerAura } from '@/hooks/agents-ui/use-agent-audio-visualizer-aura'; +import { cn } from '@/lib/shadcn/utils'; + +const DEFAULT_COLOR = '#1FD5F9'; + +function hexToRgb(hexColor: string) { + try { + const rgbColor = hexColor.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); + + if (rgbColor) { + const [, r, g, b] = rgbColor; + const color = [r, g, b].map((c = '00') => parseInt(c, 16) / 255); + + return color; + } + } catch (error) { + console.error( + `Invalid hex color '${hexColor}'.\nFalling back to default color '${DEFAULT_COLOR}'.` + ); + } + + return hexToRgb(DEFAULT_COLOR); +} + +const shaderSource = ` +const float TAU = 6.283185; + +// Noise for dithering +vec2 randFibo(vec2 p) { + p = fract(p * vec2(443.897, 441.423)); + p += dot(p, p.yx + 19.19); + return fract((p.xx + p.yx) * p.xy); +} + +// Tonemap +vec3 Tonemap(vec3 x) { + x *= 4.0; + return x / (1.0 + x); +} + +// Luma for alpha +float luma(vec3 color) { + return dot(color, vec3(0.299, 0.587, 0.114)); +} + +// RGB to HSV +vec3 rgb2hsv(vec3 c) { + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +// HSV to RGB +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +// SDF shapes +float sdCircle(vec2 st, float r) { + return length(st) - r; +} + +float sdLine(vec2 p, float r) { + float halfLen = r * 2.0; + vec2 a = vec2(-halfLen, 0.0); + vec2 b = vec2(halfLen, 0.0); + vec2 pa = p - a; + vec2 ba = b - a; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + return length(pa - ba * h); +} + +float getSdf(vec2 st) { + if(uShape == 1.0) return sdCircle(st, uScale); + else if(uShape == 2.0) return sdLine(st, uScale); + return sdCircle(st, uScale); // Default +} + +vec2 turb(vec2 pos, float t, float it) { + // Initial rotation matrix for swirl direction + mat2 rotation = mat2(0.6, -0.25, 0.25, 0.9); + // Secondary rotation applied each iteration (approx 53 degree rotation) + mat2 layerRotation = mat2(0.6, -0.8, 0.8, 0.6); + + float frequency = mix(2.0, 15.0, uFrequency); + float amplitude = uAmplitude; + float frequencyGrowth = 1.4; + float animTime = t * 0.1 * uSpeed; + + const int LAYERS = 4; + for(int i = 0; i < LAYERS; i++) { + // Calculate wave displacement for this layer + vec2 rotatedPos = pos * rotation; + vec2 wave = sin(frequency * rotatedPos + float(i) * animTime + it); + + // Apply displacement along rotation direction + pos += (amplitude / frequency) * rotation[0] * wave; + + // Evolve parameters for next layer + rotation *= layerRotation; + amplitude *= mix(1.0, max(wave.x, wave.y), uVariance); + frequency *= frequencyGrowth; + } + + return pos; +} + +const float ITERATIONS = 36.0; + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + + vec3 pp = vec3(0.0); + vec3 bloom = vec3(0.0); + float t = iTime * 0.5; + vec2 pos = uv - 0.5; + + vec2 prevPos = turb(pos, t, 0.0 - 1.0 / ITERATIONS); + float spacing = mix(1.0, TAU, uSpacing); + + for(float i = 1.0; i < ITERATIONS + 1.0; i++) { + float iter = i / ITERATIONS; + vec2 st = turb(pos, t, iter * spacing); + float d = abs(getSdf(st)); + float pd = distance(st, prevPos); + prevPos = st; + float dynamicBlur = exp2(pd * 2.0 * 1.4426950408889634) - 1.0; + float ds = smoothstep(0.0, uBlur * 0.05 + max(dynamicBlur * uSmoothing, 0.001), d); + + // Shift color based on iteration using uColorScale + vec3 color = uColor; + if(uColorShift > 0.01) { + vec3 hsv = rgb2hsv(color); + // Shift hue by iteration + hsv.x = fract(hsv.x + (1.0 - iter) * uColorShift * 0.3); + color = hsv2rgb(hsv); + } + + float invd = 1.0 / max(d + dynamicBlur, 0.001); + pp += (ds - 1.0) * color; + bloom += clamp(invd, 0.0, 250.0) * color; + } + + pp *= 1.0 / ITERATIONS; + + vec3 color; + + // Dark mode (default) + if(uMode < 0.5) { + // use bloom effect + bloom = bloom / (bloom + 2e4); + color = (-pp + bloom * 3.0 * uBloom) * 1.2; + color += (randFibo(fragCoord).x - 0.5) / 255.0; + color = Tonemap(color); + float alpha = luma(color) * uMix; + fragColor = vec4(color * uMix, alpha); + } + + // Light mode + else { + // no bloom effect + color = -pp; + color += (randFibo(fragCoord).x - 0.5) / 255.0; + + // Preserve hue by tone mapping brightness only + float brightness = length(color); + vec3 direction = brightness > 0.0 ? color / brightness : color; + + // Reinhard on brightness + float factor = 2.0; + float mappedBrightness = (brightness * factor) / (1.0 + brightness * factor); + color = direction * mappedBrightness; + + // Boost saturation to compensate for white background bleed-through + // When alpha < 1.0, white bleeds through making colors look desaturated + // So we increase saturation to maintain vibrant appearance + float gray = dot(color, vec3(0.2, 0.5, 0.1)); + float saturationBoost = 3.0; + color = mix(vec3(gray), color, saturationBoost); + + // Clamp between 0-1 + color = clamp(color, 0.0, 1.0); + + float alpha = mappedBrightness * clamp(uMix, 1.0, 2.0); + fragColor = vec4(color, alpha); + } +}`; + +interface AuraShaderProps { + /** + * Aurora wave speed + * @default 1.0 + */ + speed?: number; + + /** + * Turbulence amplitude + * @default 0.5 + */ + amplitude?: number; + + /** + * Wave frequency and complexity + * @default 0.5 + */ + frequency?: number; + + /** + * Shape scale + * @default 0.3 + */ + scale?: number; + + /** + * Shape type: 1=circle, 2=line + * @default 1 + */ + shape?: number; + + /** + * Edge blur/softness + * @default 1.0 + */ + blur?: number; + + /** + * Color of the aura + * @default '#1FD5F9' + */ + color?: string; + + /** + * Color variation across layers (0-1) + * Controls how much colors change between iterations + * @default 0.5 + * @example 0.0 - minimal color variation (more uniform) + * @example 0.5 - moderate variation (default) + * @example 1.0 - maximum variation (rainbow effect) + */ + colorShift?: number; + + /** + * Brightness of the aurora (0-1) + * @default 1.0 + */ + brightness?: number; + + /** + * Display mode for different backgrounds + * - 'dark': Optimized for dark backgrounds (default) + * - 'light': Optimized for light/white backgrounds (inverts colors) + * @default 'dark' + */ + themeMode?: 'dark' | 'light'; +} + +function AuraShader({ + shape = 1.0, + speed = 1.0, + amplitude = 0.5, + frequency = 0.5, + scale = 0.2, + blur = 1.0, + color = DEFAULT_COLOR, + colorShift = 1.0, + brightness = 1.0, + themeMode = typeof window !== 'undefined' && document.documentElement.classList.contains('dark') + ? 'dark' + : 'light', + ref, + className, + ...props +}: AuraShaderProps & ComponentProps<'div'>) { + const rgbColor = useMemo(() => hexToRgb(color), [color]); + + return ( +
+ { + console.error('Shader error:', error); + }} + onWarning={(warning) => { + console.warn('Shader warning:', warning); + }} + style={{ width: '100%', height: '100%' }} + /> +
+ ); +} + +AuraShader.displayName = 'AuraShader'; + +export const AgentAudioVisualizerAuraVariants = cva(['aspect-square'], { + variants: { + size: { + icon: 'h-[24px] gap-[2px]', + sm: 'h-[56px] gap-[4px]', + md: 'h-[112px] gap-[8px]', + lg: 'h-[224px] gap-[16px]', + xl: 'h-[448px] gap-[32px]', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +export interface AgentAudioVisualizerAuraProps { + /** + * The size of the visualizer. + * @defaultValue 'lg' + */ + size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl'; + /** + * Agent state + * @default 'connecting' + */ + state?: AgentState; + /** + * The color of the aura in hex format. + * @defaultValue '#1FD5F9' + */ + color?: string; + /** + * The color shift of the aura. + * @defaultValue 0.05 + */ + colorShift?: number; + /** + * The theme mode of the aura. + * @defaultValue 'dark' + */ + themeMode?: 'dark' | 'light'; + /** + * The audio track to visualize. Can be a local/remote audio track or a track reference. + */ + audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder; +} + +/** + * An shader-based audio visualizer that responds to agent state and audio levels. + * Displays an animated elliptical aura that reacts to the current agent state (connecting, thinking, speaking, etc.) + * and audio volume when speaking. + * + * @extends ComponentProps<'div'> + * + * @example + * ```tsx + * + * ``` + */ +export function AgentAudioVisualizerAura({ + size = 'lg', + state = 'connecting', + color = DEFAULT_COLOR, + colorShift = 0.05, + audioTrack, + themeMode, + className, + ref, + ...props +}: AgentAudioVisualizerAuraProps & + ComponentProps<'div'> & + VariantProps) { + const { speed, scale, amplitude, frequency, brightness } = useAgentAudioVisualizerAura( + state, + audioTrack + ); + + return ( + + ); +} diff --git a/components/agents-ui/agent-audio-visualizer-grid.tsx b/components/agents-ui/agent-audio-visualizer-grid.tsx index 182aaf669..0ed09b4e1 100644 --- a/components/agents-ui/agent-audio-visualizer-grid.tsx +++ b/components/agents-ui/agent-audio-visualizer-grid.tsx @@ -51,16 +51,16 @@ export const AgentAudioVisualizerGridVariants = cva( [ 'grid', '*:size-1 *:rounded-full', - '*:bg-foreground/10 [&_>_[data-lk-highlighted=true]]:bg-foreground [&_>_[data-lk-highlighted=true]]:scale-125 [&_>_[data-lk-highlighted=true]]:shadow-[0px_0px_10px_2px_rgba(255,255,255,0.4)]', + '*:bg-foreground/10 *:data-[lk-highlighted=true]:bg-foreground *:data-[lk-highlighted=true]:scale-125 *:data-[lk-highlighted=true]:shadow-[0px_0px_10px_2px_rgba(255,255,255,0.4)]', ], { variants: { size: { - icon: ['gap-[2px] *:size-[4px]'], + icon: ['gap-[2px] *:size-[2px]'], sm: ['gap-[4px] *:size-[4px]'], md: ['gap-[8px] *:size-[8px]'], - lg: ['gap-[8px] *:size-[8px]'], - xl: ['gap-[8px] *:size-[8px]'], + lg: ['gap-[12px] *:size-[12px]'], + xl: ['gap-[16px] *:size-[16px]'], }, }, defaultVariants: { diff --git a/components/agents-ui/agent-audio-visualizer-radial.tsx b/components/agents-ui/agent-audio-visualizer-radial.tsx index 211e276d3..7da6c118f 100644 --- a/components/agents-ui/agent-audio-visualizer-radial.tsx +++ b/components/agents-ui/agent-audio-visualizer-radial.tsx @@ -14,12 +14,13 @@ import { cn } from '@/lib/shadcn/utils'; export const AgentAudioVisualizerRadialVariants = cva( [ 'relative flex items-center justify-center', - '[&_[data-lk-index]]:absolute [&_[data-lk-index]]:top-1/2 [&_[data-lk-index]]:left-1/2 [&_[data-lk-index]]:origin-bottom [&_[data-lk-index]]:-translate-x-1/2', - '[&_[data-lk-index]]:rounded-full [&_[data-lk-index]]:transition-colors [&_[data-lk-index]]:duration-150 [&_[data-lk-index]]:ease-linear [&_[data-lk-index]]:bg-transparent [&_[data-lk-index]]:data-[lk-highlighted=true]:bg-current', - 'has-data-[lk-state=connecting]:[&_[data-lk-index]]:duration-300 has-data-[lk-state=connecting]:[&_[data-lk-index]]:bg-current/10', - 'has-data-[lk-state=initializing]:[&_[data-lk-index]]:duration-300 has-data-[lk-state=initializing]:[&_[data-lk-index]]:bg-current/10', - 'has-data-[lk-state=listening]:[&_[data-lk-index]]:duration-300 has-data-[lk-state=listening]:[&_[data-lk-index]]:bg-current/10 has-data-[lk-state=listening]:[&_[data-lk-index]]:duration-300', - 'has-data-[lk-state=thinking]:animate-spin has-data-[lk-state=thinking]:[animation-duration:5s] has-data-[lk-state=thinking]:[&_[data-lk-index]]:bg-current', + '**:data-lk-index:bg-current/10', + '**:data-lk-index:absolute **:data-lk-index:top-1/2 **:data-lk-index:left-1/2 **:data-lk-index:origin-bottom **:data-lk-index:-translate-x-1/2', + '**:data-lk-index:rounded-full **:data-lk-index:transition-colors **:data-lk-index:duration-150 **:data-lk-index:ease-linear **:data-lk-index:data-[lk-highlighted=true]:bg-current', + 'has-data-[lk-state=connecting]:**:data-lk-index:duration-300', + 'has-data-[lk-state=initializing]:**:data-lk-index:duration-300', + 'has-data-[lk-state=listening]:**:data-lk-index:duration-300 has-data-[lk-state=listening]:**:data-lk-index:duration-300', + 'has-data-[lk-state=thinking]:animate-spin has-data-[lk-state=thinking]:[animation-duration:5s] has-data-[lk-state=thinking]:**:data-lk-index:bg-current', ], { variants: { diff --git a/components/agents-ui/agent-audio-visualizer-wave.tsx b/components/agents-ui/agent-audio-visualizer-wave.tsx new file mode 100644 index 000000000..4b89088e0 --- /dev/null +++ b/components/agents-ui/agent-audio-visualizer-wave.tsx @@ -0,0 +1,328 @@ +'use client'; + +import { type ComponentProps, useMemo } from 'react'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client'; +import { type AgentState, type TrackReferenceOrPlaceholder } from '@livekit/components-react'; +import { ReactShaderToy } from '@/components/agents-ui/react-shader-toy'; +import { useAgentAudioVisualizerWave } from '@/hooks/agents-ui/use-agent-audio-visualizer-wave'; +import { cn } from '@/lib/shadcn/utils'; + +const DEFAULT_COLOR = '#1FD5F9'; + +function hexToRgb(hexColor: string) { + try { + const rgbColor = hexColor.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); + + if (rgbColor) { + const [, r, g, b] = rgbColor; + const color = [r, g, b].map((c = '00') => parseInt(c, 16) / 255); + + return color; + } + } catch (error) { + console.error( + `Invalid hex color '${hexColor}'.\nFalling back to default color '${DEFAULT_COLOR}'.` + ); + } + + return hexToRgb(DEFAULT_COLOR); +} + +const shaderSource = ` +const float TAU = 6.28318530718; + +// Noise for dithering +vec2 randFibo(vec2 p) { + p = fract(p * vec2(443.897, 441.423)); + p += dot(p, p.yx + 19.19); + return fract((p.xx + p.yx) * p.xy); +} + +// Luma for alpha +float luma(vec3 color) { + return dot(color, vec3(0.299, 0.587, 0.114)); +} + +// Bell curve function for attenuation from center with rounded top +float bellCurve(float distanceFromCenter, float maxDistance) { + float normalizedDistance = distanceFromCenter / maxDistance; + // Use cosine with high power for smooth rounded top + return pow(cos(normalizedDistance * (3.14159265359 / 4.0)), 16.0); +} + +// Calculate the sine wave +float oscilloscopeWave(float x, float centerX, float time) { + float relativeX = x - centerX; + float maxDistance = centerX; + float distanceFromCenter = abs(relativeX); + + // Apply bell curve for amplitude attenuation + float bell = bellCurve(distanceFromCenter, maxDistance); + + // Calculate wave with uniforms and bell curve attenuation + float wave = sin(relativeX * uFrequency + time * uSpeed) * uAmplitude * bell; + + return wave; +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 pos = uv - 0.5; + + // Calculate center and positions + float centerX = 0.5; + float centerY = 0.5; + float x = uv.x; + float y = uv.y; + + // Convert line width from pixels to UV space + // Use the average of width and height to handle aspect ratio + float pixelSize = 2.0 / (iResolution.x + iResolution.y); + float lineWidthUV = uLineWidth * pixelSize; + float smoothingUV = uSmoothing * pixelSize; + + // Find minimum distance to the wave by sampling nearby points + // This gives us consistent line width without high-frequency artifacts + const int NUM_SAMPLES = 50; // Must be const for GLSL loop + float minDist = 1000.0; + float sampleRange = 0.02; // Range to search for closest point + + for(int i = 0; i < NUM_SAMPLES; i++) { + float offset = (float(i) / float(NUM_SAMPLES - 1) - 0.5) * sampleRange; + float sampleX = x + offset; + float waveY = centerY + oscilloscopeWave(sampleX, centerX, iTime); + + // Calculate distance from current pixel to this point on the wave + vec2 wavePoint = vec2(sampleX, waveY); + vec2 currentPoint = vec2(x, y); + float dist = distance(currentPoint, wavePoint); + + minDist = min(minDist, dist); + } + + // Solid line with smooth edges using minimum distance + float line = smoothstep(lineWidthUV + smoothingUV, lineWidthUV - smoothingUV, minDist); + + // Calculate color position based on x position for gradient effect + float colorPos = x; + vec3 color = uColor; + + // Apply line intensity + color *= line; + + // Add dithering for smoother gradients + // color += (randFibo(fragCoord).x - 0.5) / 255.0; + + // Calculate alpha based on line intensity + float alpha = line * uMix; + + fragColor = vec4(color * uMix, alpha); +}`; + +interface WaveShaderProps { + /** + * Class name + * @default '' + */ + className?: string; + /** + * Speed of the oscilloscope + * @default 10 + */ + speed?: number; + /** + * Amplitude of the oscilloscope + * @default 0.02 + */ + amplitude?: number; + /** + * Frequency of the oscilloscope + * @default 20.0 + */ + frequency?: number; + /** + * Color of the oscilloscope + * @default '#1FD5F9' + */ + color?: string; + /** + * Mix of the oscilloscope + * @default 1.0 + */ + mix?: number; + /** + * Line width of the oscilloscope in pixels + * @default 2.0 + */ + lineWidth?: number; + /** + * Blur of the oscilloscope in pixels + * @default 0.5 + */ + blur?: number; +} + +function WaveShader({ + speed = 10, + color = '#1FD5F9', + mix = 1.0, + amplitude = 0.02, + frequency = 20.0, + lineWidth = 2.0, + blur = 0.5, + ref, + className, + ...props +}: WaveShaderProps & ComponentProps<'div'>) { + const rgbColor = useMemo(() => hexToRgb(color), [color]); + + return ( +
+ { + console.error('Shader error:', error); + }} + onWarning={(warning) => { + console.warn('Shader warning:', warning); + }} + style={{ width: '100%', height: '100%' }} + /> +
+ ); +} + +WaveShader.displayName = 'WaveShader'; + +export const AgentAudioVisualizerWaveVariants = cva(['aspect-square'], { + variants: { + size: { + icon: 'h-[24px]', + sm: 'h-[56px]', + md: 'h-[112px]', + lg: 'h-[224px]', + xl: 'h-[448px]', + }, + }, + defaultVariants: { + size: 'lg', + }, +}); + +export interface AgentAudioVisualizerWaveProps { + /** + * The size of the visualizer. + * @defaultValue 'lg' + */ + size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl'; + /** + * The agent state. + * @defaultValue 'speaking' + */ + state?: AgentState; + /** + * The color of the wave in hex format. + * @defaultValue '#1FD5F9' + */ + color?: string; + /** + * The line width of the wave in pixels. + * @defaultValue 2.0 + */ + lineWidth?: number; + /** + * The blur of the wave in pixels. + * @defaultValue 0.5 + */ + blur?: number; + /** + * The audio track to visualize. Can be a local/remote audio track or a track reference. + */ + audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder; + /** + * Additional CSS class names to apply to the container. + */ + className?: string; +} + +/** + * A wave-style audio visualizer that responds to agent state and audio levels. + * Displays an animated wave that reacts to the current agent state (connecting, thinking, speaking, etc.) + * and audio volume when speaking. + * + * @extends ComponentProps<'div'> + * + * @example ```tsx + * + * ``` + */ +export function AgentAudioVisualizerWave({ + size = 'lg', + state = 'speaking', + color, + lineWidth, + blur, + audioTrack, + className, + style, + ref, + ...props +}: AgentAudioVisualizerWaveProps & + ComponentProps<'div'> & + VariantProps) { + const _lineWidth = useMemo(() => { + if (lineWidth !== undefined) { + return lineWidth; + } + switch (size) { + case 'icon': + case 'sm': + return 2; + default: + return 1; + } + }, [lineWidth, size]); + + const { speed, amplitude, frequency, opacity } = useAgentAudioVisualizerWave({ + state, + audioTrack, + }); + + return ( + + ); +} diff --git a/components/agents-ui/agent-control-bar.tsx b/components/agents-ui/agent-control-bar.tsx index 85dd2bde4..fc1702127 100644 --- a/components/agents-ui/agent-control-bar.tsx +++ b/components/agents-ui/agent-control-bar.tsx @@ -3,7 +3,7 @@ import { type ComponentProps, useEffect, useRef, useState } from 'react'; import { Track } from 'livekit-client'; import { Loader, MessageSquareTextIcon, SendHorizontal } from 'lucide-react'; -import { motion } from 'motion/react'; +import { type MotionProps, motion } from 'motion/react'; import { useChat } from '@livekit/components-react'; import { AgentDisconnectButton } from '@/components/agents-ui/agent-disconnect-button'; import { AgentTrackControl } from '@/components/agents-ui/agent-track-control'; @@ -20,17 +20,17 @@ import { } from '@/hooks/agents-ui/use-agent-control-bar'; import { cn } from '@/lib/shadcn/utils'; -const TOGGLE_VARIANT_1 = [ - '[&_[data-state=off]]:bg-accent [&_[data-state=off]]:hover:bg-foreground/10', - '[&_[data-state=off]_~_button]:bg-accent [&_[data-state=off]_~_button]:hover:bg-foreground/10', - '[&_[data-state=off]]:border-border [&_[data-state=off]]:hover:border-foreground/12', - '[&_[data-state=off]_~_button]:border-border [&_[data-state=off]_~_button]:hover:border-foreground/12', - '[&_[data-state=off]]:text-destructive [&_[data-state=off]]:hover:text-destructive [&_[data-state=off]]:focus:text-destructive', - '[&_[data-state=off]]:focus-visible:ring-foreground/12 [&_[data-state=off]]:focus-visible:border-ring', - 'dark:[&_[data-state=off]_~_button]:bg-accent dark:[&_[data-state=off]_~_button:hover]:bg-foreground/10', +const LK_TOGGLE_VARIANT_1 = [ + 'data-[state=off]:bg-accent data-[state=off]:hover:bg-foreground/10', + 'data-[state=off]:[&_~_button]:bg-accent data-[state=off]:[&_~_button]:hover:bg-foreground/10', + 'data-[state=off]:border-border data-[state=off]:hover:border-foreground/12', + 'data-[state=off]:[&_~_button]:border-border data-[state=off]:[&_~_button]:hover:border-foreground/12', + 'data-[state=off]:text-destructive data-[state=off]:hover:text-destructive data-[state=off]:focus:text-destructive', + 'data-[state=off]:focus-visible:ring-foreground/12 data-[state=off]:focus-visible:border-ring', + 'dark:data-[state=off]:[&_~_button]:bg-accent dark:data-[state=off]:[&_~_button]:hover:bg-foreground/10', ]; -const TOGGLE_VARIANT_2 = [ +const LK_TOGGLE_VARIANT_2 = [ 'data-[state=off]:bg-accent data-[state=off]:hover:bg-foreground/10', 'data-[state=off]:border-border data-[state=off]:hover:border-foreground/12', 'data-[state=off]:focus-visible:border-ring data-[state=off]:focus-visible:ring-foreground/12', @@ -41,7 +41,7 @@ const TOGGLE_VARIANT_2 = [ 'dark:data-[state=on]:bg-blue-500/20 dark:data-[state=on]:text-blue-300', ]; -const MOTION_PROPS = { +const MOTION_PROPS: MotionProps = { variants: { hidden: { height: 0, @@ -71,13 +71,16 @@ function AgentChatInput({ chatOpen, onSend = async () => {}, className }: AgentC const inputRef = useRef(null); const [isSending, setIsSending] = useState(false); const [message, setMessage] = useState(''); + const isDisabled = isSending || message.trim().length === 0; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSend = async () => { + if (isDisabled) { + return; + } try { setIsSending(true); - await onSend(message); + await onSend(message.trim()); setMessage(''); } catch (error) { console.error(error); @@ -86,7 +89,17 @@ function AgentChatInput({ chatOpen, onSend = async () => {}, className }: AgentC } }; - const isDisabled = isSending || message.trim().length === 0; + const handleKeyDown = async (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const handleButtonClick = async () => { + if (isDisabled) return; + await handleSend(); + }; useEffect(() => { if (chatOpen) return; @@ -95,59 +108,61 @@ function AgentChatInput({ chatOpen, onSend = async () => {}, className }: AgentC }, [chatOpen]); return ( -
+