diff --git a/docs/storybook/stories/agents-ui/AgentAudioVisualizerGrid.stories.tsx b/docs/storybook/stories/agents-ui/AgentAudioVisualizerGrid.stories.tsx new file mode 100644 index 000000000..df2480124 --- /dev/null +++ b/docs/storybook/stories/agents-ui/AgentAudioVisualizerGrid.stories.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { + AgentSessionProvider, + useMicrophone, +} from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AgentAudioVisualizerGrid, AgentAudioVisualizerGridProps } from '@agents-ui'; + +export default { + component: AgentAudioVisualizerGrid, + decorators: [AgentSessionProvider], + render: (args: AgentAudioVisualizerGridProps) => { + const audioTrack = useMicrophone(); + + return ; + }, + args: { + default: 'lg', + state: 'connecting', + radius: 5, + interval: 100, + rowCount: 10, + columnCount: 10, + }, + argTypes: { + size: { + options: ['icon', 'sm', 'md', 'lg', 'xl'], + control: { type: 'radio' }, + }, + state: { + options: [ + 'idle', + 'disconnected', + 'pre-connect-buffering', + 'connecting', + 'initializing', + 'listening', + 'thinking', + 'speaking', + 'failed', + ], + control: { type: 'radio' }, + }, + radius: { + control: { type: 'range', min: 1, max: 50, step: 1 }, + }, + interval: { + control: { type: 'range', min: 1, max: 1000, step: 1 }, + }, + rowCount: { + control: { type: 'range', min: 1, max: 40, step: 1 }, + }, + columnCount: { + control: { type: 'range', min: 1, max: 40, step: 1 }, + }, + className: { control: { type: 'text' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Demo1: StoryObj = { + args: { + className: + 'gap-4 [&_>_*]: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)]', + }, +}; + +export const Demo2: StoryObj = { + args: { + className: + 'gap-2 [&_>_*]:w-4 [&_>_*]:h-1 [&_>_*]:bg-foreground/10 [&_>_[data-lk-highlighted=true]]:bg-[#F9B11F] [&_>_[data-lk-highlighted=true]]:shadow-[0px_0px_14.8px_2px_#F9B11F]', + }, +}; + +export const Demo3: StoryObj = { + args: { + className: + 'gap-4 [&_>_*]:size-2 [&_>_*]:rounded-full [&_>_*]:bg-foreground/10 [&_>_[data-lk-highlighted=true]]:bg-[#1F8CF9] [&_>_[data-lk-highlighted=true]]:shadow-[0px_0px_14.8px_2px_#1F8CF9]', + transformer: (index: number, rowCount: number, columnCount: number) => { + const rowMidPoint = Math.floor(rowCount / 2); + const distanceFromCenter = Math.sqrt( + Math.pow(rowMidPoint - (index % columnCount), 2) + + Math.pow(rowMidPoint - Math.floor(index / columnCount), 2), + ); + + return { + opacity: 1 - distanceFromCenter / columnCount, + transform: `scale(${1 - (distanceFromCenter / (columnCount * 2)) * 1.75})`, + }; + }, + }, +}; + +export const Demo4: StoryObj = { + args: { + className: + 'gap-x-2.5 gap-y-1 [&_>_*]:w-3 [&_>_*]:h-px [&_>_*]:my-2 [&_>_*]:rotate-45 [&_>_*]:bg-foreground/10 [&_>_*]:rotate-45 [&_>_*]:scale-100 [&_>_[data-lk-highlighted=true]]:bg-[#FFB6C1] [&_>_[data-lk-highlighted=true]]:shadow-[0px_0px_8px_2px_rgba(255,182,193,0.4)] [&_>_[data-lk-highlighted=true]]:rotate-[405deg] [&_>_[data-lk-highlighted=true]]:scale-200', + }, +}; diff --git a/docs/storybook/stories/agents-ui/AgentAudioVisualizerRadial.stories.tsx b/docs/storybook/stories/agents-ui/AgentAudioVisualizerRadial.stories.tsx new file mode 100644 index 000000000..f71c609b3 --- /dev/null +++ b/docs/storybook/stories/agents-ui/AgentAudioVisualizerRadial.stories.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { + AgentSessionProvider, + useMicrophone, +} from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AgentAudioVisualizerRadial, AgentAudioVisualizerRadialProps } from '@agents-ui'; + +export default { + component: AgentAudioVisualizerRadial, + decorators: [AgentSessionProvider], + render: (args: AgentAudioVisualizerRadialProps) => { + const audioTrack = useMicrophone(); + + return ; + }, + args: { + size: 'lg', + state: 'connecting', + radius: undefined, + }, + argTypes: { + size: { + options: ['icon', 'sm', 'md', 'lg', 'xl'], + control: { type: 'radio' }, + }, + state: { + options: [ + 'idle', + 'disconnected', + 'pre-connect-buffering', + 'connecting', + 'initializing', + 'listening', + 'thinking', + 'speaking', + 'failed', + ], + control: { type: 'radio' }, + }, + barCount: { + control: { type: 'range', min: 4, max: 64, step: 4 }, + }, + radius: { + control: { type: 'range', min: 1, max: 500, step: 1 }, + }, + className: { control: { type: 'text' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Default: StoryObj = { + args: {}, +}; diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx index 6cae6e905..a4b0f92e1 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx @@ -18,7 +18,7 @@ import { import { useAgentAudioVisualizerBarAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-bar'; import { cn } from '@/lib/utils'; -export function cloneSingleChild( +function cloneSingleChild( children: ReactNode | ReactNode[], props?: Record, key?: unknown, diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx new file mode 100644 index 000000000..c76e1da4f --- /dev/null +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx @@ -0,0 +1,216 @@ +import React, { + type ReactNode, + type CSSProperties, + memo, + useMemo, + Children, + cloneElement, + isValidElement, +} from 'react'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client'; +import { + type AgentState, + type TrackReferenceOrPlaceholder, + useMultibandTrackVolume, +} from '@livekit/components-react'; +import { cn } from '@/lib/utils'; +import { + type Coordinate, + useAgentAudioVisualizerGridAnimator, +} from '@/hooks/agents-ui/use-agent-audio-visualizer-grid'; + +function cloneSingleChild( + children: ReactNode | ReactNode[], + props?: Record, + key?: unknown, +) { + return Children.map(children, (child) => { + // Checking isValidElement is the safe way and avoids a typescript error too. + if (isValidElement(child) && Children.only(children)) { + const childProps = child.props as Record; + if (childProps.className) { + // make sure we retain classnames of both passed props and child + props ??= {}; + props.className = cn(childProps.className as string, props.className as string); + props.style = { + ...(childProps.style as CSSProperties), + ...(props.style as CSSProperties), + }; + } + return cloneElement(child, { ...props, key: key ? String(key) : undefined }); + } + return child; + }); +} + +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)]', + ], + { + variants: { + size: { + icon: ['gap-[2px] [&_>_*]:size-[4px]'], + sm: ['gap-[4px] [&_>_*]:size-[4px]'], + md: ['gap-[8px] [&_>_*]:size-[8px]'], + lg: ['gap-[8px] [&_>_*]:size-[8px]'], + xl: ['gap-[8px] [&_>_*]:size-[8px]'], + }, + }, + defaultVariants: { + size: 'md', + }, + }, +); + +export interface GridOptions { + radius?: number; + interval?: number; + rowCount?: number; + columnCount?: number; + transformer?: (index: number, rowCount: number, columnCount: number) => CSSProperties; + className?: string; + children?: ReactNode; +} + +const sizeDefaults = { + icon: 3, + sm: 5, + md: 5, + lg: 5, + xl: 5, +}; + +function useGrid( + size: VariantProps['size'] = 'md', + columnCount = sizeDefaults[size as keyof typeof sizeDefaults], + rowCount = sizeDefaults[size as keyof typeof sizeDefaults], +) { + return useMemo(() => { + const _columnCount = columnCount; + const _rowCount = rowCount ?? columnCount; + const items = new Array(_columnCount * _rowCount).fill(0).map((_, idx) => idx); + + return { columnCount: _columnCount, rowCount: _rowCount, items }; + }, [columnCount, rowCount]); +} + +interface GridCellProps { + index: number; + state: AgentState; + interval: number; + transformer?: (index: number, rowCount: number, columnCount: number) => CSSProperties; + rowCount: number; + columnCount: number; + volumeBands: number[]; + highlightedCoordinate: Coordinate; + children: ReactNode; +} + +const GridCell = memo(function GridCell({ + index, + state, + interval, + transformer, + rowCount, + columnCount, + volumeBands, + highlightedCoordinate, + children, +}: GridCellProps) { + if (state === 'speaking') { + const y = Math.floor(index / columnCount); + const rowMidPoint = Math.floor(rowCount / 2); + const volumeChunks = 1 / (rowMidPoint + 1); + const distanceToMid = Math.abs(rowMidPoint - y); + const threshold = distanceToMid * volumeChunks; + const isHighlighted = volumeBands[index % columnCount] >= threshold; + + return cloneSingleChild(children, { + 'data-lk-index': index, + 'data-lk-highlighted': isHighlighted, + }); + } + + let transformerStyle: CSSProperties | undefined; + if (transformer) { + transformerStyle = transformer(index, rowCount, columnCount); + } + + const isHighlighted = + highlightedCoordinate.x === index % columnCount && + highlightedCoordinate.y === Math.floor(index / columnCount); + + const transitionDurationInSeconds = interval / (isHighlighted ? 1000 : 100); + + return cloneSingleChild(children, { + 'data-lk-index': index, + 'data-lk-highlighted': isHighlighted, + style: { + transitionProperty: 'all', + transitionDuration: `${transitionDurationInSeconds}s`, + transitionTimingFunction: 'ease-out', + ...transformerStyle, + }, + }); +}); + +export type AgentAudioVisualizerGridProps = GridOptions & { + state: AgentState; + audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder; + className?: string; + children?: ReactNode; +} & VariantProps; + +export function AgentAudioVisualizerGrid({ + size = 'md', + state, + radius, + rowCount: _rowCount = 5, + columnCount: _columnCount = 5, + transformer, + interval = 100, + className, + children, + audioTrack, +}: AgentAudioVisualizerGridProps) { + const { columnCount, rowCount, items } = useGrid(size, _columnCount, _rowCount); + const highlightedCoordinate = useAgentAudioVisualizerGridAnimator( + state, + rowCount, + columnCount, + interval, + radius, + ); + const volumeBands = useMultibandTrackVolume(audioTrack, { + bands: columnCount, + loPass: 100, + hiPass: 200, + }); + + return ( +
+ {items.map((idx) => ( + + {children ??
} + + ))} +
+ ); +} diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx new file mode 100644 index 000000000..637c1e762 --- /dev/null +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx @@ -0,0 +1,153 @@ +import { useMemo } from 'react'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client'; +import { + type AgentState, + type TrackReferenceOrPlaceholder, + useMultibandTrackVolume, +} from '@livekit/components-react'; +import { cn } from '@/lib/utils'; +import { useAgentAudioVisualizerRadialAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-radial'; + +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', + ], + { + 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 AgentAudioVisualizerRadialProps { + state?: AgentState; + radius?: number; + barCount?: number; + audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder; + className?: string; +} + +export function AgentAudioVisualizerRadial({ + size, + state, + radius, + barCount, + audioTrack, + className, +}: AgentAudioVisualizerRadialProps & VariantProps) { + const _barCount = useMemo(() => { + if (barCount) { + return barCount; + } + switch (size) { + case 'icon': + case 'sm': + return 12; + default: + return 24; + } + }, [barCount, size]); + + const volumeBands = useMultibandTrackVolume(audioTrack, { + bands: _barCount, + loPass: 80, + hiPass: 200, + }); + + const sequencerInterval = useMemo(() => { + switch (state) { + case 'connecting': + case 'listening': + return 500; + case 'initializing': + return 250; + case 'thinking': + return Infinity; + default: + return 1000; + } + }, [state, _barCount]); + + const distanceFromCenter = useMemo(() => { + if (radius) { + return radius; + } + switch (size) { + case 'icon': + return 6; + case 'xl': + return 128; + case 'lg': + return 64; + case 'sm': + return 16; + case 'md': + default: + return 32; + } + }, [size, radius]); + + if (_barCount % 4 !== 0) { + console.warn('barCount should be divisible by 4 for optimal visual results'); + } + + const highlightedIndices = useAgentAudioVisualizerRadialAnimator( + state, + _barCount, + sequencerInterval, + ); + const bands = useMemo( + () => (audioTrack ? volumeBands : new Array(_barCount).fill(0)), + [audioTrack, volumeBands, _barCount], + ); + + const dotSize = useMemo(() => { + return (distanceFromCenter * Math.PI) / _barCount; + }, [distanceFromCenter, _barCount]); + + return ( +
+ {bands.map((band, idx) => { + const angle = (idx / _barCount) * Math.PI * 2; + + return ( +
+
+
+ ); + })} +
+ ); +} diff --git a/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-grid.ts b/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-grid.ts new file mode 100644 index 000000000..ea7188afa --- /dev/null +++ b/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-grid.ts @@ -0,0 +1,114 @@ +import { useEffect, useState } from 'react'; +import { type AgentState } from '@livekit/components-react'; + +export interface Coordinate { + x: number; + y: number; +} + +export function generateConnectingSequence(rows: number, columns: number, radius: number) { + const seq = []; + const centerY = Math.floor(rows / 2); + + // Calculate the boundaries of the ring based on the ring distance + const topLeft = { + x: Math.max(0, centerY - radius), + y: Math.max(0, centerY - radius), + }; + const bottomRight = { + x: columns - 1 - topLeft.x, + y: Math.min(rows - 1, centerY + radius), + }; + + // Top edge + for (let x = topLeft.x; x <= bottomRight.x; x++) { + seq.push({ x, y: topLeft.y }); + } + + // Right edge + for (let y = topLeft.y + 1; y <= bottomRight.y; y++) { + seq.push({ x: bottomRight.x, y }); + } + + // Bottom edge + for (let x = bottomRight.x - 1; x >= topLeft.x; x--) { + seq.push({ x, y: bottomRight.y }); + } + + // Left edge + for (let y = bottomRight.y - 1; y > topLeft.y; y--) { + seq.push({ x: topLeft.x, y }); + } + + return seq; +} + +export function generateListeningSequence(rows: number, columns: number) { + const center = { x: Math.floor(columns / 2), y: Math.floor(rows / 2) }; + const noIndex = { x: -1, y: -1 }; + + return [center, noIndex, noIndex, noIndex, noIndex, noIndex, noIndex, noIndex, noIndex]; +} + +export function generateThinkingSequence(rows: number, columns: number) { + const seq = []; + const y = Math.floor(rows / 2); + for (let x = 0; x < columns; x++) { + seq.push({ x, y }); + } + for (let x = columns - 1; x >= 0; x--) { + seq.push({ x, y }); + } + + return seq; +} + +export function useAgentAudioVisualizerGridAnimator( + state: AgentState, + rows: number, + columns: number, + interval: number, + radius?: number, +): Coordinate { + const [index, setIndex] = useState(0); + const [sequence, setSequence] = useState(() => [ + { + x: Math.floor(columns / 2), + y: Math.floor(rows / 2), + }, + ]); + + useEffect(() => { + const clampedRadius = radius + ? Math.min(radius, Math.floor(Math.max(rows, columns) / 2)) + : Math.floor(Math.max(rows, columns) / 2); + + if (state === 'thinking') { + setSequence(generateThinkingSequence(rows, columns)); + } else if (state === 'connecting' || state === 'initializing') { + const sequence = [...generateConnectingSequence(rows, columns, clampedRadius)]; + setSequence(sequence); + } else if (state === 'listening') { + setSequence(generateListeningSequence(rows, columns)); + } else { + setSequence([{ x: Math.floor(columns / 2), y: Math.floor(rows / 2) }]); + } + setIndex(0); + }, [state, rows, columns, radius]); + + useEffect(() => { + if (state === 'speaking') { + return; + } + + const indexInterval = setInterval(() => { + setIndex((prev) => { + return prev + 1; + }); + }, interval); + + return () => clearInterval(indexInterval); + }, [interval, columns, rows, state, sequence.length]); + + return sequence[index % sequence.length]; +} diff --git a/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-radial.ts b/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-radial.ts new file mode 100644 index 000000000..9c83d92d2 --- /dev/null +++ b/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-radial.ts @@ -0,0 +1,73 @@ +import { useEffect, useRef, useState } from 'react'; +import { type AgentState } from '@livekit/components-react'; + +function generateConnectingSequenceBar(columns: number): number[][] { + const seq = []; + const center = Math.floor(columns / 2); + + for (let x = 0; x < columns; x++) { + seq.push([x, (x + center) % columns]); + } + + return seq; +} + +function generateListeningSequenceBar(columns: number): number[][] { + const divisor = columns > 8 ? columns / 4 : 2; + + return Array.from({ length: divisor }, (_, idx) => [ + ...Array(Math.floor(columns / divisor)) + .fill(1) + .map((_, idx2) => idx2 * divisor + idx), + ]); +} + +export const useAgentAudioVisualizerRadialAnimator = ( + state: AgentState | undefined, + barCount: number, + interval: number, +): number[] => { + const [index, setIndex] = useState(0); + const [sequence, setSequence] = useState([[]]); + + useEffect(() => { + if (state === 'thinking') { + setSequence(generateListeningSequenceBar(barCount)); + } else if (state === 'connecting' || state === 'initializing') { + setSequence(generateConnectingSequenceBar(barCount)); + } else if (state === 'listening') { + setSequence(generateListeningSequenceBar(barCount)); + } else if (state === undefined || state === 'speaking') { + setSequence([new Array(barCount).fill(0).map((_, idx) => idx)]); + } else { + setSequence([[]]); + } + setIndex(0); + }, [state, barCount]); + + const animationFrameId = useRef(null); + useEffect(() => { + let startTime = performance.now(); + + const animate = (time: DOMHighResTimeStamp) => { + const timeElapsed = time - startTime; + + if (timeElapsed >= interval) { + setIndex((prev) => prev + 1); + startTime = time; + } + + animationFrameId.current = requestAnimationFrame(animate); + }; + + animationFrameId.current = requestAnimationFrame(animate); + + return () => { + if (animationFrameId.current !== null) { + cancelAnimationFrame(animationFrameId.current); + } + }; + }, [interval, barCount, state, sequence.length]); + + return sequence[index % sequence.length]; +}; diff --git a/packages/shadcn/index.ts b/packages/shadcn/index.ts index b84395d14..9578af194 100644 --- a/packages/shadcn/index.ts +++ b/packages/shadcn/index.ts @@ -3,3 +3,5 @@ export * from './components/agents-ui/agent-track-toggle'; export * from './components/agents-ui/agent-track-control'; export * from './components/agents-ui/agent-disconnect-button'; export * from './components/agents-ui/agent-control-bar'; +export * from './components/agents-ui/agent-audio-visualizer-radial'; +export * from './components/agents-ui/agent-audio-visualizer-grid'; diff --git a/packages/shadcn/registry.json b/packages/shadcn/registry.json index ae187cc79..9253c311c 100644 --- a/packages/shadcn/registry.json +++ b/packages/shadcn/registry.json @@ -64,7 +64,7 @@ "name": "agent-control-bar", "type": "registry:component", "title": "Agent Control Bar", - "description": "A control bar for managing media tracks (microphone, camera, screen share) with appropriate icons and loading states.", + "description": "A control bar for managing media tracks (microphone, camera, screen share), disconnecting the agent session, and opening the chat input, with appropriate icons and loading states.", "files": [ { "path": "components/agents-ui/agent-control-bar.tsx", @@ -113,6 +113,50 @@ "lucide-react" ], "registryDependencies": ["utils"] + }, + { + "name": "agent-audio-visualizer-radial", + "type": "registry:component", + "title": "Audio Radial Visualizer", + "description": "A radial bar visualizer for audio tracks.", + "files": [ + { + "path": "components/agents-ui/agent-audio-visualizer-radial.tsx", + "type": "registry:component" + }, + { + "path": "hooks/agents-ui/use-agent-audio-visualizer-radial.ts", + "type": "registry:hook" + } + ], + "dependencies": [ + "livekit-client@^2.0.0", + "@livekit/components-react@^2.0.0", + "class-variance-authority" + ], + "registryDependencies": ["utils"] + }, + { + "name": "agent-audio-visualizer-grid", + "type": "registry:component", + "title": "Audio Grid Visualizer", + "description": "A grid visualizer for audio tracks.", + "files": [ + { + "path": "components/agents-ui/agent-audio-visualizer-grid.tsx", + "type": "registry:component" + }, + { + "path": "hooks/agents-ui/use-agent-audio-visualizer-grid.ts", + "type": "registry:hook" + } + ], + "dependencies": [ + "livekit-client@^2.0.0", + "@livekit/components-react@^2.0.0", + "class-variance-authority" + ], + "registryDependencies": ["utils"] } ] }