diff --git a/app-config.ts b/app-config.ts index 892e99f97..2e950af5a 100644 --- a/app-config.ts +++ b/app-config.ts @@ -17,12 +17,12 @@ export interface AppConfig { audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura'; audioVisualizerColor?: `#${string}`; audioVisualizerColorDark?: `#${string}`; + audioVisualizerColorShift?: number; audioVisualizerBarCount?: number; audioVisualizerGridRowCount?: number; audioVisualizerGridColumnCount?: number; audioVisualizerRadialBarCount?: number; audioVisualizerRadialRadius?: number; - audioVisualizerAuraColorShift?: number; audioVisualizerWaveLineWidth?: number; // agent dispatch configuration @@ -49,9 +49,10 @@ export const APP_CONFIG_DEFAULTS: AppConfig = { startButtonText: 'Start call', // optional: audio visualization configuration + // audioVisualizerType: 'bar', // audioVisualizerColor: '#002cf2', // audioVisualizerColorDark: '#1fd5f9', - // audioVisualizerType: 'bar', + // audioVisualizerColorShift: 0.3, // audioVisualizerBarCount: 5, // audioVisualizerType: 'radial', // audioVisualizerRadialBarCount: 24, @@ -62,7 +63,6 @@ export const APP_CONFIG_DEFAULTS: AppConfig = { // audioVisualizerType: 'wave', // audioVisualizerWaveLineWidth: 3, // audioVisualizerType: 'aura', - // audioVisualizerAuraColorShift: 0.3, // agent dispatch configuration agentName: process.env.AGENT_NAME ?? undefined, diff --git a/app/api/connection-details/route.ts b/app/api/token/route.ts similarity index 80% rename from app/api/connection-details/route.ts rename to app/api/token/route.ts index 8f2c48418..c920ed387 100644 --- a/app/api/connection-details/route.ts +++ b/app/api/token/route.ts @@ -18,6 +18,12 @@ const LIVEKIT_URL = process.env.LIVEKIT_URL; export const revalidate = 0; export async function POST(req: Request) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'THIS API ROUTE IS INSECURE. DO NOT USE THIS ROUTE IN PRODUCTION WITHOUT AN AUTHENTICATION LAYER.' + ); + } + try { if (LIVEKIT_URL === undefined) { throw new Error('LIVEKIT_URL is not defined'); @@ -29,9 +35,10 @@ export async function POST(req: Request) { throw new Error('LIVEKIT_API_SECRET is not defined'); } - // Parse agent configuration from request body + // Parse room config from request body. const body = await req.json(); - const agentName: string = body?.room_config?.agents?.[0]?.agent_name; + // Recreate the RoomConfiguration object from JSON object. + const roomConfig = RoomConfiguration.fromJson(body?.room_config, { ignoreUnknownFields: true }); // Generate participant token const participantName = 'user'; @@ -41,15 +48,15 @@ export async function POST(req: Request) { const participantToken = await createParticipantToken( { identity: participantIdentity, name: participantName }, roomName, - agentName + roomConfig ); // Return connection details const data: ConnectionDetails = { serverUrl: LIVEKIT_URL, roomName, - participantToken: participantToken, participantName, + participantToken, }; const headers = new Headers({ 'Cache-Control': 'no-store', @@ -66,7 +73,7 @@ export async function POST(req: Request) { function createParticipantToken( userInfo: AccessTokenOptions, roomName: string, - agentName?: string + roomConfig: RoomConfiguration ): Promise { const at = new AccessToken(API_KEY, API_SECRET, { ...userInfo, @@ -81,10 +88,8 @@ function createParticipantToken( }; at.addGrant(grant); - if (agentName) { - at.roomConfig = new RoomConfiguration({ - agents: [{ agentName }], - }); + if (roomConfig) { + at.roomConfig = roomConfig; } return at.toJwt(); diff --git a/components/agents-ui/agent-audio-visualizer-aura.tsx b/components/agents-ui/agent-audio-visualizer-aura.tsx index 09cf89ded..832883eb4 100644 --- a/components/agents-ui/agent-audio-visualizer-aura.tsx +++ b/components/agents-ui/agent-audio-visualizer-aura.tsx @@ -8,6 +8,14 @@ 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'; +// Originally developed for Unicorn Studio +// https://unicorn.studio +// +// Licensed under the Polyform Non-Resale License 1.0.0 +// https://polyformproject.org/licenses/non-resale/1.0.0/ +// +// © 2026 UNCRN LLC + const DEFAULT_COLOR = '#1FD5F9'; function hexToRgb(hexColor: string) { @@ -236,10 +244,10 @@ interface AuraShaderProps { blur?: number; /** - * Color of the aura + * Color of the aura in hexidecimal format. * @default '#1FD5F9' */ - color?: string; + color?: `#${string}`; /** * Color variation across layers (0-1) @@ -361,10 +369,10 @@ export interface AgentAudioVisualizerAuraProps { */ state?: AgentState; /** - * The color of the aura in hex format. + * The color of the aura in hexidecimal format. * @defaultValue '#1FD5F9' */ - color?: string; + color?: `#${string}`; /** * The color shift of the aura. * @defaultValue 0.05 @@ -428,11 +436,7 @@ export function AgentAudioVisualizerAura({ amplitude={amplitude} frequency={frequency} brightness={brightness} - className={cn( - AgentAudioVisualizerAuraVariants({ size }), - 'overflow-hidden rounded-full', - className - )} + className={cn(AgentAudioVisualizerAuraVariants({ size }), className)} {...props} /> ); diff --git a/components/agents-ui/agent-audio-visualizer-bar.tsx b/components/agents-ui/agent-audio-visualizer-bar.tsx index a1444705d..621c9893c 100644 --- a/components/agents-ui/agent-audio-visualizer-bar.tsx +++ b/components/agents-ui/agent-audio-visualizer-bar.tsx @@ -46,7 +46,7 @@ function cloneSingleChild( export const AgentAudioVisualizerBarElementVariants = cva( [ 'rounded-full transition-colors duration-250 ease-linear', - 'bg-transparent data-[lk-highlighted=true]:bg-current', + 'bg-current/10 data-[lk-highlighted=true]:bg-current', ], { variants: { @@ -93,6 +93,10 @@ export interface AgentAudioVisualizerBarProps { * @defaultValue 'connecting' */ state?: AgentState; + /** + * The color of the bars in hexidecimal format. + */ + color?: `#${string}`; /** * The number of bars to display in the visualizer. * If not provided, defaults based on size: 3 for 'icon'/'sm', 5 for others. @@ -132,10 +136,12 @@ export interface AgentAudioVisualizerBarProps { export function AgentAudioVisualizerBar({ size = 'md', state = 'connecting', + color, barCount, audioTrack, className, children, + style, ...props }: AgentAudioVisualizerBarProps & VariantProps & @@ -192,6 +198,7 @@ export function AgentAudioVisualizerBar({ return (
diff --git a/components/agents-ui/agent-audio-visualizer-grid.tsx b/components/agents-ui/agent-audio-visualizer-grid.tsx index 8a4eb90a5..e15d20e00 100644 --- a/components/agents-ui/agent-audio-visualizer-grid.tsx +++ b/components/agents-ui/agent-audio-visualizer-grid.tsx @@ -198,6 +198,10 @@ export type AgentAudioVisualizerGridProps = GridOptions & { * @defaultValue 'connecting' */ state?: AgentState; + /** + * The color of the grid cells in hexidecimal format. + */ + color?: `#${string}`; /** * The audio track to visualize. Can be a local/remote audio track or a track reference. */ @@ -235,6 +239,7 @@ export function AgentAudioVisualizerGrid({ size = 'md', state = 'connecting', radius, + color, rowCount: _rowCount = 5, columnCount: _columnCount = 5, interval = 100, @@ -266,7 +271,9 @@ export function AgentAudioVisualizerGrid({
{items.map((idx) => ( diff --git a/components/agents-ui/agent-audio-visualizer-radial.tsx b/components/agents-ui/agent-audio-visualizer-radial.tsx index 8bdeb9d20..cdaf127c8 100644 --- a/components/agents-ui/agent-audio-visualizer-radial.tsx +++ b/components/agents-ui/agent-audio-visualizer-radial.tsx @@ -1,6 +1,6 @@ 'use client'; -import { type ComponentProps, useMemo } from 'react'; +import { type CSSProperties, type ComponentProps, useMemo } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client'; import { @@ -19,7 +19,7 @@ export const AgentAudioVisualizerRadialVariants = cva( '**: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=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', ], { @@ -52,6 +52,10 @@ export interface AgentAudioVisualizerRadialProps { * @defaultValue 'connecting' */ state?: AgentState; + /** + * The color of the radial bars in hexidecimal format. + */ + color?: `#${string}`; /** * The radius (distance from center) for the radial bars. * If not provided, defaults based on size. @@ -93,10 +97,12 @@ export interface AgentAudioVisualizerRadialProps { export function AgentAudioVisualizerRadial({ size = 'md', state = 'connecting', + color, radius, barCount, audioTrack, className, + style, ...props }: AgentAudioVisualizerRadialProps & ComponentProps<'div'> & @@ -175,6 +181,7 @@ export function AgentAudioVisualizerRadial({
{bands.map((band, idx) => { diff --git a/components/agents-ui/agent-audio-visualizer-wave.tsx b/components/agents-ui/agent-audio-visualizer-wave.tsx index 33744b479..eec4562fd 100644 --- a/components/agents-ui/agent-audio-visualizer-wave.tsx +++ b/components/agents-ui/agent-audio-visualizer-wave.tsx @@ -20,7 +20,7 @@ function hexToRgb(hexColor: string) { return color; } - } catch (error) { + } catch { console.error( `Invalid hex color '${hexColor}'.\nFalling back to default color '${DEFAULT_COLOR}'.` ); @@ -44,6 +44,23 @@ 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); +} + // Bell curve function for attenuation from center with rounded top float bellCurve(float distanceFromCenter, float maxDistance) { float normalizedDistance = distanceFromCenter / maxDistance; @@ -104,9 +121,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { // 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; + if(abs(uColorShift) > 0.01) { + // Keep the center 50% at base color, then ramp shift across outer 25% on each side. + float centerBandHalfWidth = 0.2; + float edgeBandWidth = 0.5; + float distanceFromCenter = abs(x - centerX); + float edgeFactor = clamp((distanceFromCenter - centerBandHalfWidth) / edgeBandWidth, 0.0, 1.0); + vec3 hsv = rgb2hsv(color); + // Hue shift is zero in the center band and strongest at far edges. + hsv.x = fract(hsv.x + edgeFactor * uColorShift * 0.3); + color = hsv2rgb(hsv); + } // Apply line intensity color *= line; @@ -142,10 +168,15 @@ interface WaveShaderProps { */ frequency?: number; /** - * Color of the oscilloscope + * Color of the oscilloscope in hexidecimal format. * @default '#1FD5F9' */ - color?: string; + color?: `#${string}`; + /** + * Hue shift amount applied toward the outside of the wave. Center remains at the base color. + * @default 0.05 + */ + colorShift?: number; /** * Mix of the oscilloscope * @default 1.0 @@ -166,6 +197,7 @@ interface WaveShaderProps { function WaveShader({ speed = 10, color = '#1FD5F9', + colorShift = 0.05, mix = 1.0, amplitude = 0.02, frequency = 20.0, @@ -190,6 +222,7 @@ function WaveShader({ uLineWidth: { type: '1f', value: lineWidth }, uSmoothing: { type: '1f', value: blur }, uColor: { type: '3fv', value: rgbColor }, + uColorShift: { type: '1f', value: colorShift }, }} onError={(error) => { console.error('Shader error:', error); @@ -232,10 +265,15 @@ export interface AgentAudioVisualizerWaveProps { */ state?: AgentState; /** - * The color of the wave in hex format. + * The color of the wave in hexidecimal format. * @defaultValue '#1FD5F9' */ - color?: string; + color?: `#${string}`; + /** + * The color shift of the wave. Higher values increase hue variation toward the edges. + * @defaultValue 0.05 + */ + colorShift?: number; /** * The line width of the wave in pixels. * @defaultValue 2.0 @@ -268,6 +306,7 @@ export interface AgentAudioVisualizerWaveProps { * size="lg" * state="speaking" * color="#1FD5F9" + * colorShift={0.3} * lineWidth={2} * blur={0.5} * audioTrack={audioTrack} @@ -278,6 +317,7 @@ export function AgentAudioVisualizerWave({ size = 'lg', state = 'speaking', color, + colorShift = 0.05, lineWidth, blur, audioTrack, @@ -311,6 +351,8 @@ export function AgentAudioVisualizerWave({ data-lk-state={state} speed={speed} color={color} + colorShift={colorShift} + mix={opacity} amplitude={amplitude} frequency={frequency} lineWidth={_lineWidth} @@ -318,7 +360,6 @@ export function AgentAudioVisualizerWave({ className={cn( AgentAudioVisualizerWaveVariants({ size }), 'mask-[linear-gradient(90deg,transparent_0%,black_20%,black_80%,transparent_100%)]', - 'overflow-hidden rounded-full', className )} {...props} diff --git a/components/agents-ui/agent-disconnect-button.tsx b/components/agents-ui/agent-disconnect-button.tsx index c222fed46..8b2cf45c9 100644 --- a/components/agents-ui/agent-disconnect-button.tsx +++ b/components/agents-ui/agent-disconnect-button.tsx @@ -1,21 +1,57 @@ 'use client'; +import { type ComponentProps } from 'react'; import { type VariantProps } from 'class-variance-authority'; import { PhoneOffIcon } from 'lucide-react'; import { useSessionContext } from '@livekit/components-react'; import { Button, buttonVariants } from '@/components/ui/button'; import { cn } from '@/lib/shadcn/utils'; +/** + * Props for the AgentDisconnectButton component. + */ export interface AgentDisconnectButtonProps - extends React.ComponentProps<'button'>, + extends ComponentProps<'button'>, VariantProps { + /** + * Custom icon to display. Defaults to PhoneOffIcon. + */ icon?: React.ReactNode; + /** + * The size of the button. + * @default 'default' + */ + size?: 'default' | 'sm' | 'lg' | 'icon'; + /** + * The variant of the button. + * @default 'destructive' + */ + variant?: 'default' | 'outline' | 'destructive' | 'ghost' | 'link'; + /** + * The children to render. + */ children?: React.ReactNode; + /** + * The callback for when the button is clicked. + */ + onClick?: (event: React.MouseEvent) => void; } +/** + * A button to disconnect from the current agent session. + * Calls the session's end() method when clicked. + * + * @extends ComponentProps<'button'> + * + * @example + * ```tsx + * console.log('Disconnecting...')} /> + * ``` + */ export function AgentDisconnectButton({ icon, size = 'default', + variant = 'destructive', children, onClick, ...props @@ -23,11 +59,13 @@ export function AgentDisconnectButton({ const { end } = useSessionContext(); const handleClick = (event: React.MouseEvent) => { onClick?.(event); - end(); + if (typeof end === 'function') { + end(); + } }; return ( - diff --git a/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx b/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx new file mode 100644 index 000000000..1c24a5ba1 --- /dev/null +++ b/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx @@ -0,0 +1,274 @@ +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; +import { AnimatePresence, type MotionProps, motion } from 'motion/react'; +import { useAgent, useSessionContext, useSessionMessages } from '@livekit/components-react'; +import { AgentChatTranscript } from '@/components/agents-ui/agent-chat-transcript'; +import { + AgentControlBar, + type AgentControlBarControls, +} from '@/components/agents-ui/agent-control-bar'; +import { Shimmer } from '@/components/ai-elements/shimmer'; +import { cn } from '@/lib/shadcn/utils'; +import { TileLayout } from './tile-view'; + +const MotionMessage = motion.create(Shimmer); + +const BOTTOM_VIEW_MOTION_PROPS: MotionProps = { + variants: { + visible: { + opacity: 1, + translateY: '0%', + }, + hidden: { + opacity: 0, + translateY: '100%', + }, + }, + initial: 'hidden', + animate: 'visible', + exit: 'hidden', + transition: { + duration: 0.3, + delay: 0.5, + ease: 'easeOut', + }, +}; + +const CHAT_MOTION_PROPS: MotionProps = { + variants: { + hidden: { + opacity: 0, + transition: { + ease: 'easeOut', + duration: 0.3, + }, + }, + visible: { + opacity: 1, + transition: { + delay: 0.2, + ease: 'easeOut', + duration: 0.3, + }, + }, + }, + initial: 'hidden', + animate: 'visible', + exit: 'hidden', +}; + +const SHIMMER_MOTION_PROPS: MotionProps = { + variants: { + visible: { + opacity: 1, + transition: { + ease: 'easeIn', + duration: 0.5, + delay: 0.8, + }, + }, + hidden: { + opacity: 0, + transition: { + ease: 'easeIn', + duration: 0.5, + delay: 0, + }, + }, + }, + initial: 'hidden', + animate: 'visible', + exit: 'hidden', +}; + +interface FadeProps { + top?: boolean; + bottom?: boolean; + className?: string; +} + +export function Fade({ top = false, bottom = false, className }: FadeProps) { + return ( +
+ ); +} + +export interface AgentSessionView_01Props { + /** + * Message shown above the controls before the first chat message is sent. + * + * @default 'Agent is listening, ask it a question' + */ + preConnectMessage?: string; + /** + * Enables or disables the chat toggle and transcript input controls. + * + * @default true + */ + supportsChatInput?: boolean; + /** + * Enables or disables camera controls in the bottom control bar. + * + * @default true + */ + supportsVideoInput?: boolean; + /** + * Enables or disables screen sharing controls in the bottom control bar. + * + * @default true + */ + supportsScreenShare?: boolean; + /** + * Shows a pre-connect buffer state with a shimmer message before messages appear. + * + * @default true + */ + isPreConnectBufferEnabled?: boolean; + + /** Selects the visualizer style rendered in the main tile area. */ + audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura'; + /** Primary hex color used by supported audio visualizer variants. */ + audioVisualizerColor?: `#${string}`; + /** Hue shift intensity used by certain visualizers. */ + audioVisualizerColorShift?: number; + /** Number of bars to render when `audioVisualizerType` is `bar`. */ + audioVisualizerBarCount?: number; + /** Number of rows in the visualizer when `audioVisualizerType` is `grid`. */ + audioVisualizerGridRowCount?: number; + /** Number of columns in the visualizer when `audioVisualizerType` is `grid`. */ + audioVisualizerGridColumnCount?: number; + /** Number of radial bars when `audioVisualizerType` is `radial`. */ + audioVisualizerRadialBarCount?: number; + /** Base radius of the radial visualizer when `audioVisualizerType` is `radial`. */ + audioVisualizerRadialRadius?: number; + /** Stroke width of the wave path when `audioVisualizerType` is `wave`. */ + audioVisualizerWaveLineWidth?: number; + /** Optional class name merged onto the outer `
` container. */ + className?: string; +} + +export function AgentSessionView_01({ + preConnectMessage = 'Agent is listening, ask it a question', + supportsChatInput = true, + supportsVideoInput = true, + supportsScreenShare = true, + isPreConnectBufferEnabled = true, + + audioVisualizerType, + audioVisualizerColor, + audioVisualizerColorShift, + audioVisualizerBarCount, + audioVisualizerGridRowCount, + audioVisualizerGridColumnCount, + audioVisualizerRadialBarCount, + audioVisualizerRadialRadius, + audioVisualizerWaveLineWidth, + ref, + className, + ...props +}: React.ComponentProps<'section'> & AgentSessionView_01Props) { + const session = useSessionContext(); + const { messages } = useSessionMessages(session); + const [chatOpen, setChatOpen] = useState(false); + const scrollAreaRef = useRef(null); + const { state: agentState } = useAgent(); + + const controls: AgentControlBarControls = { + leave: true, + microphone: true, + chat: supportsChatInput, + camera: supportsVideoInput, + screenShare: supportsScreenShare, + }; + + useEffect(() => { + const lastMessage = messages.at(-1); + const lastMessageIsLocal = lastMessage?.from?.isLocal === true; + + if (scrollAreaRef.current && lastMessageIsLocal) { + scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; + } + }, [messages]); + + return ( +
+ + {/* transcript */} + +
+ + {chatOpen && ( + + + + )} + +
+ {/* Tile layout */} + + {/* Bottom */} + + {/* Pre-connect message */} + {isPreConnectBufferEnabled && ( + + {messages.length === 0 && ( + 0} + {...SHIMMER_MOTION_PROPS} + className="pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold" + > + {preConnectMessage} + + )} + + )} +
+ + +
+
+
+ ); +} diff --git a/components/app/audio-visualizer.tsx b/components/agents-ui/blocks/agent-session-view-01/components/audio-visualizer.tsx similarity index 61% rename from components/app/audio-visualizer.tsx rename to components/agents-ui/blocks/agent-session-view-01/components/audio-visualizer.tsx index 931999ddf..d6d35d838 100644 --- a/components/app/audio-visualizer.tsx +++ b/components/agents-ui/blocks/agent-session-view-01/components/audio-visualizer.tsx @@ -1,11 +1,10 @@ +'use client'; + +import React from 'react'; import { type MotionProps, motion } from 'motion/react'; -import { useAgent } from '@livekit/components-react'; -import { AppConfig } from '@/app-config'; +import { useVoiceAssistant } from '@livekit/components-react'; import { AgentAudioVisualizerAura } from '@/components/agents-ui/agent-audio-visualizer-aura'; -import { - AgentAudioVisualizerBar, - AgentAudioVisualizerBarElementVariants, -} from '@/components/agents-ui/agent-audio-visualizer-bar'; +import { AgentAudioVisualizerBar } from '@/components/agents-ui/agent-audio-visualizer-bar'; import { AgentAudioVisualizerGrid } from '@/components/agents-ui/agent-audio-visualizer-grid'; import { AgentAudioVisualizerRadial } from '@/components/agents-ui/agent-audio-visualizer-radial'; import { AgentAudioVisualizerWave } from '@/components/agents-ui/agent-audio-visualizer-wave'; @@ -18,42 +17,56 @@ const MotionAgentAudioVisualizerRadial = motion.create(AgentAudioVisualizerRadia const MotionAgentAudioVisualizerWave = motion.create(AgentAudioVisualizerWave); interface AudioVisualizerProps extends MotionProps { - appConfig: AppConfig; isChatOpen: boolean; + audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura'; + audioVisualizerColor?: `#${string}`; + audioVisualizerColorShift?: number; + audioVisualizerWaveLineWidth?: number; + audioVisualizerGridRowCount?: number; + audioVisualizerGridColumnCount?: number; + audioVisualizerRadialBarCount?: number; + audioVisualizerRadialRadius?: number; + audioVisualizerBarCount?: number; className?: string; } export function AudioVisualizer({ - appConfig, + audioVisualizerType = 'bar', + audioVisualizerColor, + audioVisualizerColorShift = 0.3, + audioVisualizerBarCount = 5, + audioVisualizerRadialRadius = 100, + audioVisualizerRadialBarCount = 25, + audioVisualizerGridRowCount = 15, + audioVisualizerGridColumnCount = 15, + audioVisualizerWaveLineWidth = 3, isChatOpen, className, ...props }: AudioVisualizerProps) { - const { audioVisualizerType } = appConfig; - const { state, microphoneTrack } = useAgent(); + const { state, audioTrack } = useVoiceAssistant(); switch (audioVisualizerType) { case 'aura': { - const { audioVisualizerColor, audioVisualizerAuraColorShift } = appConfig; return ( ); } case 'wave': { - const { audioVisualizerColor, audioVisualizerWaveLineWidth = 3 } = appConfig; return ( @@ -61,15 +74,14 @@ export function AudioVisualizer({ ); } case 'grid': { - const { audioVisualizerGridRowCount = 9, audioVisualizerGridColumnCount = 9 } = appConfig; const totalCount = audioVisualizerGridRowCount * audioVisualizerGridColumnCount; let size: 'icon' | 'sm' | 'md' | 'lg' | 'xl' = 'sm'; - if (totalCount <= 100) { + if (totalCount < 100) { size = 'xl'; - } else if (totalCount <= 200) { + } else if (totalCount < 200) { size = 'lg'; - } else if (totalCount <= 300) { + } else if (totalCount < 300) { size = 'md'; } @@ -77,25 +89,26 @@ export function AudioVisualizer({ ); } case 'radial': { - const { audioVisualizerRadialBarCount = 25, audioVisualizerRadialRadius = 12 } = appConfig; return ( - + ); } diff --git a/components/app/tile-layout.tsx b/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx similarity index 71% rename from components/app/tile-layout.tsx rename to components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx index c27eb9fc2..2fbe2f841 100644 --- a/components/app/tile-layout.tsx +++ b/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx @@ -1,28 +1,24 @@ import React, { useMemo } from 'react'; -import { useTheme } from 'next-themes'; import { Track } from 'livekit-client'; -import { AnimatePresence, motion } from 'motion/react'; +import { AnimatePresence, type MotionProps, motion } from 'motion/react'; import { type TrackReference, VideoTrack, - useAgent, useLocalParticipant, useTracks, + useVoiceAssistant, } from '@livekit/components-react'; -import { AppConfig } from '@/app-config'; -import { AudioVisualizer } from '@/components/app/audio-visualizer'; import { cn } from '@/lib/shadcn/utils'; +import { AudioVisualizer } from './audio-visualizer'; -const MotionContainer = motion.create('div'); - -const ANIMATION_TRANSITION = { +const ANIMATION_TRANSITION: MotionProps['transition'] = { type: 'spring', stiffness: 675, damping: 75, mass: 1, }; -const classNames = { +const tileViewClassNames = { // GRID // 2 Columns x 3 Rows grid: [ @@ -73,12 +69,30 @@ export function useLocalTrackRef(source: Track.Source) { interface TileLayoutProps { chatOpen: boolean; - appConfig: AppConfig; + audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura'; + audioVisualizerColor?: `#${string}`; + audioVisualizerColorShift?: number; + audioVisualizerWaveLineWidth?: number; + audioVisualizerGridRowCount?: number; + audioVisualizerGridColumnCount?: number; + audioVisualizerRadialBarCount?: number; + audioVisualizerRadialRadius?: number; + audioVisualizerBarCount?: number; } -export function TileLayout({ chatOpen, appConfig }: TileLayoutProps) { - const { resolvedTheme } = useTheme(); - const { cameraTrack: agentVideoTrack } = useAgent(); +export function TileLayout({ + chatOpen, + audioVisualizerType, + audioVisualizerColor, + audioVisualizerColorShift, + audioVisualizerBarCount, + audioVisualizerRadialBarCount, + audioVisualizerRadialRadius, + audioVisualizerGridRowCount, + audioVisualizerGridColumnCount, + audioVisualizerWaveLineWidth, +}: TileLayoutProps) { + const { videoTrack: agentVideoTrack } = useVoiceAssistant(); const [screenShareTrack] = useTracks([Track.Source.ScreenShare]); const cameraTrack: TrackReference | undefined = useLocalTrackRef(Track.Source.Camera); @@ -92,22 +106,22 @@ export function TileLayout({ chatOpen, appConfig }: TileLayoutProps) { const videoHeight = agentVideoTrack?.publication.dimensions?.height ?? 0; return ( -
+
-
+
{/* Agent */}
{!isAvatar && ( // Audio Agent - - + )} {isAvatar && ( // Avatar Agent - - + )}
@@ -190,14 +207,14 @@ export function TileLayout({ chatOpen, appConfig }: TileLayoutProps) {
{/* Camera & Screen Share */} {((cameraTrack && isCameraEnabled) || (screenShareTrack && isScreenShareEnabled)) && ( - - + )}
diff --git a/components/agents-ui/blocks/agent-session-view-01/index.ts b/components/agents-ui/blocks/agent-session-view-01/index.ts new file mode 100644 index 000000000..417dff66d --- /dev/null +++ b/components/agents-ui/blocks/agent-session-view-01/index.ts @@ -0,0 +1 @@ +export * from './components/agent-session-block'; diff --git a/components/agents-ui/react-shader-toy.tsx b/components/agents-ui/react-shader-toy.tsx index 7a2af62df..a8bf860ec 100644 --- a/components/agents-ui/react-shader-toy.tsx +++ b/components/agents-ui/react-shader-toy.tsx @@ -1,3 +1,26 @@ +// MIT License +// +// Copyright (c) 2018 Morgan Villedieu +// Copyright (c) 2023 Rysana, Inc. (forked from the above) +// Copyright (c) 2026 LiveKit, Inc. (forked from the above) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import React, { type ComponentPropsWithoutRef, useEffect, useRef } from 'react'; const PRECISIONS = ['lowp', 'mediump', 'highp']; @@ -434,6 +457,13 @@ export interface ReactShaderToyProps { /** Custom callback to handle warnings. Defaults to `console.warn`. */ onWarning?: (warning: string) => void; + + /** + * When true, the animation loop runs even when the canvas is not visible in the viewport. + * When false (default), animation runs only while visible (uses Intersection Observer), + * reducing CPU/GPU usage when the shader is off-screen. + */ + animateWhenNotVisible?: boolean; } export function ReactShaderToy({ @@ -450,6 +480,7 @@ export function ReactShaderToy({ onDoneLoadingTextures, onError = console.error, onWarning = console.warn, + animateWhenNotVisible = false, ...canvasProps }: ReactShaderToyProps & ComponentPropsWithoutRef<'canvas'>) { // Refs for WebGL state @@ -459,6 +490,9 @@ export function ReactShaderToy({ const shaderProgramRef = useRef(null); const vertexPositionAttributeRef = useRef(undefined); const animFrameIdRef = useRef(undefined); + const initFrameIdRef = useRef(undefined); + const isVisibleRef = useRef(true); + const animateWhenNotVisibleRef = useRef(animateWhenNotVisible); const mousedownRef = useRef(false); const canvasPositionRef = useRef(undefined); const timerRef = useRef(0); @@ -818,7 +852,9 @@ export function ReactShaderToy({ mouseValue[0] = lerpVal(currentX, lastMouseArrRef.current[0] ?? 0, lerp); mouseValue[1] = lerpVal(currentY, lastMouseArrRef.current[1] ?? 0, lerp); } - animFrameIdRef.current = requestAnimationFrame(drawScene); + if (animateWhenNotVisibleRef.current || isVisibleRef.current) { + animFrameIdRef.current = requestAnimationFrame(drawScene); + } }; const addEventListeners = () => { @@ -866,6 +902,33 @@ export function ReactShaderToy({ propsUniformsRef.current = propUniforms; }, [propUniforms]); + useEffect(() => { + animateWhenNotVisibleRef.current = animateWhenNotVisible; + if (animateWhenNotVisible) { + isVisibleRef.current = true; + } + }, [animateWhenNotVisible]); + + // Intersection Observer: pause animation when off-screen when animateWhenNotVisible is false + useEffect(() => { + if (animateWhenNotVisible || !canvasRef.current) return; + const canvas = canvasRef.current; + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + isVisibleRef.current = entry.isIntersecting; + if (entry.isIntersecting) { + requestAnimationFrame(drawScene); + } + } + }, + { threshold: 0 } + ); + observer.observe(canvas); + return () => observer.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [animateWhenNotVisible]); + // Main effect for initialization and cleanup useEffect(() => { const textures = texturesArrRef.current; @@ -891,7 +954,7 @@ export function ReactShaderToy({ } } - requestAnimationFrame(init); + initFrameIdRef.current = requestAnimationFrame(init); // Cleanup function return () => { @@ -908,6 +971,7 @@ export function ReactShaderToy({ shaderProgramRef.current = null; } removeEventListeners(); + cancelAnimationFrame(initFrameIdRef.current ?? 0); cancelAnimationFrame(animFrameIdRef.current ?? 0); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/components/app/app.tsx b/components/app/app.tsx index bd0500800..f604cfb5a 100644 --- a/components/app/app.tsx +++ b/components/app/app.tsx @@ -30,7 +30,7 @@ export function App({ appConfig }: AppProps) { const tokenSource = useMemo(() => { return typeof process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT === 'string' ? getSandboxTokenSource(appConfig) - : TokenSource.endpoint('/api/connection-details'); + : TokenSource.endpoint('/api/token'); }, [appConfig]); const session = useSession( diff --git a/components/app/chat-transcript.tsx b/components/app/chat-transcript.tsx deleted file mode 100644 index bd33a68e1..000000000 --- a/components/app/chat-transcript.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react'; -import { type ReceivedMessage, useAgent } from '@livekit/components-react'; -import { AgentChatTranscript } from '@/components/agents-ui/agent-chat-transcript'; -import { cn } from '@/lib/shadcn/utils'; - -const MotionContainer = motion.create('div'); - -const CONTAINER_MOTION_PROPS = { - variants: { - hidden: { - opacity: 0, - transition: { - ease: 'easeOut', - duration: 0.3, - }, - }, - visible: { - opacity: 1, - transition: { - delay: 0.2, - ease: 'easeOut', - duration: 0.3, - }, - }, - }, - initial: 'hidden', - animate: 'visible', - exit: 'hidden', -}; - -interface ChatTranscriptProps { - hidden?: boolean; - messages?: ReceivedMessage[]; -} - -export function ChatTranscript({ - hidden = false, - messages = [], - className, - ...props -}: ChatTranscriptProps & Omit, 'ref'>) { - const { state: agentState } = useAgent(); - - return ( -
- - {!hidden && ( - - - - )} - -
- ); -} diff --git a/components/app/session-view.tsx b/components/app/session-view.tsx deleted file mode 100644 index 597a3e82c..000000000 --- a/components/app/session-view.tsx +++ /dev/null @@ -1,160 +0,0 @@ -'use client'; - -import React, { useEffect, useRef, useState } from 'react'; -import { AnimatePresence, motion } from 'motion/react'; -import { useSessionContext, useSessionMessages } from '@livekit/components-react'; -import type { AppConfig } from '@/app-config'; -import { - AgentControlBar, - type AgentControlBarControls, -} from '@/components/agents-ui/agent-control-bar'; -import { ChatTranscript } from '@/components/app/chat-transcript'; -import { TileLayout } from '@/components/app/tile-layout'; -import { cn } from '@/lib/shadcn/utils'; -import { Shimmer } from '../ai-elements/shimmer'; - -const MotionBottom = motion.create('div'); - -const MotionMessage = motion.create(Shimmer); - -const BOTTOM_VIEW_MOTION_PROPS = { - variants: { - visible: { - opacity: 1, - translateY: '0%', - }, - hidden: { - opacity: 0, - translateY: '100%', - }, - }, - initial: 'hidden', - animate: 'visible', - exit: 'hidden', - transition: { - duration: 0.3, - delay: 0.5, - ease: 'easeOut', - }, -}; - -const SHIMMER_MOTION_PROPS = { - variants: { - visible: { - opacity: 1, - transition: { - ease: 'easeIn', - duration: 0.5, - delay: 0.8, - }, - }, - hidden: { - opacity: 0, - transition: { - ease: 'easeIn', - duration: 0.5, - delay: 0, - }, - }, - }, - initial: 'hidden', - animate: 'visible', - exit: 'hidden', -}; - -interface FadeProps { - top?: boolean; - bottom?: boolean; - className?: string; -} - -export function Fade({ top = false, bottom = false, className }: FadeProps) { - return ( -
- ); -} - -interface SessionViewProps { - appConfig: AppConfig; -} - -export const SessionView = ({ - appConfig, - ...props -}: React.ComponentProps<'section'> & SessionViewProps) => { - const session = useSessionContext(); - const { messages } = useSessionMessages(session); - const [chatOpen, setChatOpen] = useState(false); - const scrollAreaRef = useRef(null); - - const controls: AgentControlBarControls = { - leave: true, - microphone: true, - chat: appConfig.supportsChatInput, - camera: appConfig.supportsVideoInput, - screenShare: appConfig.supportsScreenShare, - }; - - useEffect(() => { - const lastMessage = messages.at(-1); - const lastMessageIsLocal = lastMessage?.from?.isLocal === true; - - if (scrollAreaRef.current && lastMessageIsLocal) { - scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; - } - }, [messages]); - - return ( -
- - {/* transcript */} -
- ); -}; diff --git a/components/app/view-controller.tsx b/components/app/view-controller.tsx index d64e00afa..dc0732232 100644 --- a/components/app/view-controller.tsx +++ b/components/app/view-controller.tsx @@ -1,13 +1,14 @@ 'use client'; +import { useTheme } from 'next-themes'; import { AnimatePresence, motion } from 'motion/react'; import { useSessionContext } from '@livekit/components-react'; import type { AppConfig } from '@/app-config'; -import { SessionView } from '@/components/app/session-view'; +import { AgentSessionView_01 } from '@/components/agents-ui/blocks/agent-session-view-01'; import { WelcomeView } from '@/components/app/welcome-view'; const MotionWelcomeView = motion.create(WelcomeView); -const MotionSessionView = motion.create(SessionView); +const MotionSessionView = motion.create(AgentSessionView_01); const VIEW_MOTION_PROPS = { variants: { @@ -33,6 +34,7 @@ interface ViewControllerProps { export function ViewController({ appConfig }: ViewControllerProps) { const { isConnected, start } = useSessionContext(); + const { resolvedTheme } = useTheme(); return ( @@ -47,7 +49,28 @@ export function ViewController({ appConfig }: ViewControllerProps) { )} {/* Session view */} {isConnected && ( - + )} ); diff --git a/package.json b/package.json index 12a2d99ed..346e6c5cb 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,11 @@ "lint": "next lint", "format": "prettier --write .", "format:check": "prettier --check .", - "shadcn:install": "pnpm dlx shadcn@latest add -y @agents-ui/agent-audio-visualizer-bar @agents-ui/agent-audio-visualizer-grid @agents-ui/agent-audio-visualizer-radial @agents-ui/agent-control-bar @agents-ui/agent-session-provider @agents-ui/agent-track-control @agents-ui/agent-track-toggle @agents-ui/agent-chat-transcript @agents-ui/agent-chat-indicator @agents-ui/start-audio-button @agents-ui/agent-audio-visualizer-aura @agents-ui/agent-audio-visualizer-wave && pnpm format" + "shadcn:install": "pnpm dlx shadcn@latest add --yes --overwrite @agents-ui/agent-audio-visualizer-bar @agents-ui/agent-audio-visualizer-grid @agents-ui/agent-audio-visualizer-radial @agents-ui/agent-control-bar @agents-ui/agent-session-provider @agents-ui/agent-track-control @agents-ui/agent-track-toggle @agents-ui/agent-chat-transcript @agents-ui/agent-chat-indicator @agents-ui/start-audio-button @agents-ui/agent-audio-visualizer-aura @agents-ui/agent-audio-visualizer-wave @agents-ui/nextjs-api-token-route @agents-ui/agent-session-view-01 && pnpm format" }, "dependencies": { - "@livekit/components-react": "^2.9.18", - "@livekit/protocol": "^1.40.0", + "@livekit/components-react": "^2.9.20", + "@livekit/protocol": "^1.41.0", "@phosphor-icons/react": "^2.1.8", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", @@ -37,8 +37,8 @@ "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", "jose": "^6.0.12", - "livekit-client": "^2.15.15", - "livekit-server-sdk": "^2.13.2", + "livekit-client": "^2.17.2", + "livekit-server-sdk": "^2.13.3", "lucide-react": "^0.555.0", "media-chrome": "^4.17.2", "mime": "^4.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67d807f21..043ee61d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,10 +9,10 @@ importers: .: dependencies: '@livekit/components-react': - specifier: ^2.9.18 - version: 2.9.18(@livekit/krisp-noise-filter@0.2.16(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22)))(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tslib@2.8.1) + specifier: ^2.9.20 + version: 2.9.20(@livekit/krisp-noise-filter@0.2.16(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22)))(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tslib@2.8.1) '@livekit/protocol': - specifier: ^1.40.0 + specifier: ^1.41.0 version: 1.41.0 '@phosphor-icons/react': specifier: ^2.1.8 @@ -84,10 +84,10 @@ importers: specifier: ^6.0.12 version: 6.1.0 livekit-client: - specifier: ^2.15.15 - version: 2.15.15(@types/dom-mediacapture-record@1.0.22) + specifier: ^2.17.2 + version: 2.17.2(@types/dom-mediacapture-record@1.0.22) livekit-server-sdk: - specifier: ^2.13.2 + specifier: ^2.13.3 version: 2.13.3 lucide-react: specifier: ^0.555.0 @@ -506,19 +506,19 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@livekit/components-core@0.12.12': - resolution: {integrity: sha512-DQ+lOAMPvum37Ta4lQLETxQe7ZxhivI78ZfE4nnWP0AcnwNByNR2vVLp9VGvw577HmvgHEkbjBbGBJBSZEBEZA==} + '@livekit/components-core@0.12.13': + resolution: {integrity: sha512-DQmi84afHoHjZ62wm8y+XPNIDHTwFHAltjd3lmyXj8UZHOY7wcza4vFt1xnghJOD5wLRY58L1dkAgAw59MgWvw==} engines: {node: '>=18'} peerDependencies: - livekit-client: ^2.15.14 + livekit-client: ^2.17.2 tslib: ^2.6.2 - '@livekit/components-react@2.9.18': - resolution: {integrity: sha512-apnT0tEL1lQdTWO+m5qIfnxfOJAjFeX8Vx7g1qU73V7L+upM+xZBClAgD3eoOMhLyqgW1Aea2HUE5JdEdPKFhA==} + '@livekit/components-react@2.9.20': + resolution: {integrity: sha512-hjkYOsJj9Jbghb7wM5cI8HoVisKeL6Zcy1VnRWTLm0sqVbto8GJp/17T4Udx85mCPY6Jgh8I1Cv0yVzgz7CQtg==} engines: {node: '>=18'} peerDependencies: '@livekit/krisp-noise-filter': ^0.2.12 || ^0.3.0 - livekit-client: ^2.16.0 + livekit-client: ^2.17.2 react: '>=18' react-dom: '>=18' tslib: ^2.6.2 @@ -537,8 +537,8 @@ packages: '@livekit/protocol@1.41.0': resolution: {integrity: sha512-bozBB39VSbd0IjRBwShMlLskqkd9weWJNskaB1CVpcEO9UUI1gMwAtBJOKYblzZJT9kE1SJa3L4oWWwsZMzSXw==} - '@livekit/protocol@1.42.2': - resolution: {integrity: sha512-0jeCwoMJKcwsZICg5S6RZM4xhJoF78qMvQELjACJQn6/VB+jmiySQKOSELTXvPBVafHfEbMlqxUw2UR1jTXs2g==} + '@livekit/protocol@1.44.0': + resolution: {integrity: sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg==} '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} @@ -3035,8 +3035,8 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} - livekit-client@2.15.15: - resolution: {integrity: sha512-z1mrGIyn6BZhCf4VDFcIuvHkbWftihwO6Y9+Kicr4VWQ2TH8pb1PIF97OsS1TiLD2QlhIFp2HNYfuvv6Kz2fVA==} + livekit-client@2.17.2: + resolution: {integrity: sha512-+67y2EtAWZabARlY7kANl/VT1Uu1EJYR5a8qwpT2ub/uBCltsEgEDOxCIMwE9HFR5w+z41HR6GL9hyEvW/y6CQ==} peerDependencies: '@types/dom-mediacapture-record': ^1 @@ -4488,31 +4488,31 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@livekit/components-core@0.12.12(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': + '@livekit/components-core@0.12.13(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': dependencies: '@floating-ui/dom': 1.7.4 - livekit-client: 2.15.15(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.17.2(@types/dom-mediacapture-record@1.0.22) loglevel: 1.9.1 rxjs: 7.8.2 tslib: 2.8.1 - '@livekit/components-react@2.9.18(@livekit/krisp-noise-filter@0.2.16(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22)))(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tslib@2.8.1)': + '@livekit/components-react@2.9.20(@livekit/krisp-noise-filter@0.2.16(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22)))(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tslib@2.8.1)': dependencies: - '@livekit/components-core': 0.12.12(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + '@livekit/components-core': 0.12.13(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) clsx: 2.1.1 events: 3.3.0 jose: 6.1.0 - livekit-client: 2.15.15(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.17.2(@types/dom-mediacapture-record@1.0.22) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) tslib: 2.8.1 usehooks-ts: 3.1.1(react@19.1.1) optionalDependencies: - '@livekit/krisp-noise-filter': 0.2.16(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22)) + '@livekit/krisp-noise-filter': 0.2.16(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22)) - '@livekit/krisp-noise-filter@0.2.16(livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22))': + '@livekit/krisp-noise-filter@0.2.16(livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22))': dependencies: - livekit-client: 2.15.15(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.17.2(@types/dom-mediacapture-record@1.0.22) optional: true '@livekit/mutex@1.1.1': {} @@ -4521,7 +4521,7 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.1 - '@livekit/protocol@1.42.2': + '@livekit/protocol@1.44.0': dependencies: '@bufbuild/protobuf': 1.10.1 @@ -7297,10 +7297,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 - livekit-client@2.15.15(@types/dom-mediacapture-record@1.0.22): + livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22): dependencies: '@livekit/mutex': 1.1.1 - '@livekit/protocol': 1.42.2 + '@livekit/protocol': 1.44.0 '@types/dom-mediacapture-record': 1.0.22 events: 3.3.0 jose: 6.1.0