1- import { CSSProperties , ComponentType , JSX , memo , useMemo } from 'react' ;
1+ import { CSSProperties , type ReactNode , memo , useMemo } from 'react' ;
2+ import { type VariantProps , cva } from 'class-variance-authority' ;
23import { LocalAudioTrack , RemoteAudioTrack } from 'livekit-client' ;
34import {
45 type AgentState ,
56 type TrackReferenceOrPlaceholder ,
67 useMultibandTrackVolume ,
78} from '@livekit/components-react' ;
8- import { cn } from '@/lib/utils' ;
9+ import { cloneSingleChild , cn } from '@/lib/utils' ;
910import { type Coordinate , useGridAnimator } from './hooks/useGridAnimator' ;
1011
11- type GridComponentType =
12- | ComponentType < { style ?: CSSProperties ; className ?: string } >
13- | keyof JSX . IntrinsicElements ;
12+ export const audioGridVisualizerVariants = cva (
13+ [
14+ 'grid' ,
15+ '[&_>_*]:size-1 [&_>_*]:rounded-full' ,
16+ '[&_>_*]: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)]' ,
17+ ] ,
18+ {
19+ variants : {
20+ size : {
21+ icon : [ 'gap-[2px] [&_>_*]:size-[4px]' ] ,
22+ sm : [ 'gap-[4px] [&_>_*]:size-[4px]' ] ,
23+ md : [ 'gap-[8px] [&_>_*]:size-[8px]' ] ,
24+ lg : [ 'gap-[8px] [&_>_*]:size-[8px]' ] ,
25+ xl : [ 'gap-[8px] [&_>_*]:size-[8px]' ] ,
26+ } ,
27+ } ,
28+ defaultVariants : {
29+ size : 'md' ,
30+ } ,
31+ }
32+ ) ;
1433
1534export interface GridOptions {
1635 radius ?: number ;
1736 interval ?: number ;
1837 rowCount ?: number ;
1938 columnCount ?: number ;
20- className ?: string ;
21- baseClassName ?: string ;
22- offClassName ?: string ;
23- onClassName ?: string ;
24- GridComponent ?: GridComponentType ;
2539 transformer ?: ( index : number , rowCount : number , columnCount : number ) => CSSProperties ;
40+ className ?: string ;
41+ children ?: ReactNode ;
2642}
2743
28- function useGrid ( options : GridOptions ) {
44+ const sizeDefaults = {
45+ icon : 3 ,
46+ sm : 5 ,
47+ md : 5 ,
48+ lg : 5 ,
49+ xl : 5 ,
50+ } ;
51+
52+ function useGrid (
53+ size : VariantProps < typeof audioGridVisualizerVariants > [ 'size' ] = 'md' ,
54+ columnCount = sizeDefaults [ size as keyof typeof sizeDefaults ] ,
55+ rowCount = sizeDefaults [ size as keyof typeof sizeDefaults ]
56+ ) {
2957 return useMemo ( ( ) => {
30- const { columnCount = 5 , rowCount } = options ;
31-
3258 const _columnCount = columnCount ;
3359 const _rowCount = rowCount ?? columnCount ;
3460 const items = new Array ( _columnCount * _rowCount ) . fill ( 0 ) . map ( ( _ , idx ) => idx ) ;
3561
3662 return { columnCount : _columnCount , rowCount : _rowCount , items } ;
37- } , [ options ] ) ;
63+ } , [ columnCount , rowCount ] ) ;
3864}
3965
4066interface GridCellProps {
4167 index : number ;
4268 state : AgentState ;
43- options : GridOptions ;
69+ interval : number ;
70+ transformer ?: ( index : number , rowCount : number , columnCount : number ) => CSSProperties ;
4471 rowCount : number ;
45- volumeBands : number [ ] ;
4672 columnCount : number ;
73+ volumeBands : number [ ] ;
4774 highlightedCoordinate : Coordinate ;
48- Component : GridComponentType ;
75+ children : ReactNode ;
4976}
5077
5178const GridCell = memo ( function GridCell ( {
5279 index,
5380 state,
54- options,
81+ interval,
82+ transformer,
5583 rowCount,
56- volumeBands,
5784 columnCount,
85+ volumeBands,
5886 highlightedCoordinate,
59- Component ,
87+ children ,
6088} : GridCellProps ) {
61- const { interval = 100 , baseClassName, onClassName, offClassName, transformer } = options ;
62-
6389 if ( state === 'speaking' ) {
6490 const y = Math . floor ( index / columnCount ) ;
6591 const rowMidPoint = Math . floor ( rowCount / 2 ) ;
6692 const volumeChunks = 1 / ( rowMidPoint + 1 ) ;
6793 const distanceToMid = Math . abs ( rowMidPoint - y ) ;
6894 const threshold = distanceToMid * volumeChunks ;
69- const isOn = volumeBands [ index % columnCount ] >= threshold ;
95+ const isHighlighted = volumeBands [ index % columnCount ] >= threshold ;
7096
71- return < Component className = { cn ( baseClassName , isOn ? onClassName : offClassName ) } /> ;
97+ return cloneSingleChild ( children , {
98+ 'data-lk-index' : index ,
99+ 'data-lk-highlighted' : isHighlighted ,
100+ } ) ;
72101 }
73102
74103 let transformerStyle : CSSProperties | undefined ;
75104 if ( transformer ) {
76105 transformerStyle = transformer ( index , rowCount , columnCount ) ;
77106 }
78107
79- const isOn =
108+ const isHighlighted =
80109 highlightedCoordinate . x === index % columnCount &&
81110 highlightedCoordinate . y === Math . floor ( index / columnCount ) ;
82111
83- const transitionDurationInSeconds = interval / ( isOn ? 1000 : 100 ) ;
84-
85- return (
86- < Component
87- style = { {
88- transitionProperty : 'all' ,
89- transitionDuration : `${ transitionDurationInSeconds } s` ,
90- transitionTimingFunction : 'ease-out' ,
91- ...transformerStyle ,
92- } }
93- className = { cn ( baseClassName , isOn ? onClassName : offClassName ) }
94- />
95- ) ;
112+ const transitionDurationInSeconds = interval / ( isHighlighted ? 1000 : 100 ) ;
113+
114+ return cloneSingleChild ( children , {
115+ 'data-lk-index' : index ,
116+ 'data-lk-highlighted' : isHighlighted ,
117+ style : {
118+ transitionProperty : 'all' ,
119+ transitionDuration : `${ transitionDurationInSeconds } s` ,
120+ transitionTimingFunction : 'ease-out' ,
121+ ...transformerStyle ,
122+ } ,
123+ } ) ;
96124} ) ;
97125
98- export interface AudioGridVisualizerProps {
126+ export type AudioGridVisualizerProps = GridOptions & {
99127 state : AgentState ;
100- options : GridOptions ;
101128 audioTrack ?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder ;
102- }
129+ className ?: string ;
130+ children ?: ReactNode ;
131+ } & VariantProps < typeof audioGridVisualizerVariants > ;
103132
104- export function AudioGridVisualizer ( { state, options, audioTrack } : AudioGridVisualizerProps ) {
105- const { radius, interval = 100 , className, GridComponent = 'div' } = options ;
106- const { columnCount, rowCount, items } = useGrid ( options ) ;
133+ export function AudioGridVisualizer ( {
134+ size = 'md' ,
135+ state,
136+ radius,
137+ rowCount : _rowCount = 5 ,
138+ columnCount : _columnCount = 5 ,
139+ transformer,
140+ interval = 100 ,
141+ className,
142+ children,
143+ audioTrack,
144+ } : AudioGridVisualizerProps ) {
145+ const { columnCount, rowCount, items } = useGrid ( size , _columnCount , _rowCount ) ;
107146 const highlightedCoordinate = useGridAnimator ( state , rowCount , columnCount , interval , radius ) ;
108147 const volumeBands = useMultibandTrackVolume ( audioTrack , {
109148 bands : columnCount ,
@@ -113,21 +152,23 @@ export function AudioGridVisualizer({ state, options, audioTrack }: AudioGridVis
113152
114153 return (
115154 < div
116- className = { cn ( 'grid gap-1' , className ) }
155+ className = { cn ( audioGridVisualizerVariants ( { size } ) , className ) }
117156 style = { { gridTemplateColumns : `repeat(${ columnCount } , 1fr)` } }
118157 >
119158 { items . map ( ( idx ) => (
120159 < GridCell
121160 key = { idx }
122161 index = { idx }
123162 state = { state }
124- options = { options }
163+ interval = { interval }
164+ transformer = { transformer }
125165 rowCount = { rowCount }
126166 columnCount = { columnCount }
127167 volumeBands = { volumeBands }
128168 highlightedCoordinate = { highlightedCoordinate }
129- Component = { GridComponent }
130- />
169+ >
170+ { children ?? < div /> }
171+ </ GridCell >
131172 ) ) }
132173 </ div >
133174 ) ;
0 commit comments