Skip to content

Commit b01278a

Browse files
migrate to agents-ui
1 parent 3f63ed4 commit b01278a

34 files changed

+1583
-717
lines changed

components.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
"cssVariables": true,
1111
"prefix": ""
1212
},
13+
"iconLibrary": "lucide",
1314
"aliases": {
1415
"components": "@/components",
1516
"utils": "@/lib/utils",
1617
"ui": "@/components/ui",
1718
"lib": "@/lib",
1819
"hooks": "@/hooks"
1920
},
20-
"iconLibrary": "phosphor"
21+
"registries": {
22+
"@agents-ui": "http://livekit.io/ui/r/{name}.json",
23+
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json"
24+
}
2125
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use client';
2+
3+
import React, {
4+
type CSSProperties,
5+
Children,
6+
type ReactNode,
7+
cloneElement,
8+
isValidElement,
9+
useMemo,
10+
} from 'react';
11+
import { type VariantProps, cva } from 'class-variance-authority';
12+
import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client';
13+
import {
14+
type AgentState,
15+
type TrackReferenceOrPlaceholder,
16+
useMultibandTrackVolume,
17+
} from '@livekit/components-react';
18+
import { useAgentAudioVisualizerBarAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-bar';
19+
import { cn } from '@/lib/utils';
20+
21+
function cloneSingleChild(
22+
children: ReactNode | ReactNode[],
23+
props?: Record<string, unknown>,
24+
key?: unknown
25+
) {
26+
return Children.map(children, (child) => {
27+
// Checking isValidElement is the safe way and avoids a typescript error too.
28+
if (isValidElement(child) && Children.only(children)) {
29+
const childProps = child.props as Record<string, unknown>;
30+
if (childProps.className) {
31+
// make sure we retain classnames of both passed props and child
32+
props ??= {};
33+
props.className = cn(childProps.className as string, props.className as string);
34+
props.style = {
35+
...(childProps.style as CSSProperties),
36+
...(props.style as CSSProperties),
37+
};
38+
}
39+
return cloneElement(child, { ...props, key: key ? String(key) : undefined });
40+
}
41+
return child;
42+
});
43+
}
44+
45+
export const AgentAudioVisualizerBarVariants = cva(
46+
[
47+
'relative flex items-center justify-center',
48+
'[&_>_*]:rounded-full [&_>_*]:transition-colors [&_>_*]:duration-250 [&_>_*]:ease-linear',
49+
'[&_>_*]:bg-transparent [&_>_*]:data-[lk-highlighted=true]:bg-current',
50+
],
51+
{
52+
variants: {
53+
size: {
54+
icon: ['h-[24px] gap-[2px]', '[&_>_*]:w-[4px] [&_>_*]:min-h-[4px]'],
55+
sm: ['h-[56px] gap-[4px]', '[&_>_*]:w-[8px] [&_>_*]:min-h-[8px]'],
56+
md: ['h-[112px] gap-[8px]', '[&_>_*]:w-[16px] [&_>_*]:min-h-[16px]'],
57+
lg: ['h-[224px] gap-[16px]', '[&_>_*]:w-[32px] [&_>_*]:min-h-[32px]'],
58+
xl: ['h-[448px] gap-[32px]', '[&_>_*]:w-[64px] [&_>_*]:min-h-[64px]'],
59+
},
60+
},
61+
defaultVariants: {
62+
size: 'md',
63+
},
64+
}
65+
);
66+
67+
export interface AgentAudioVisualizerBarProps {
68+
state?: AgentState;
69+
barCount?: number;
70+
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
71+
className?: string;
72+
children?: ReactNode | ReactNode[];
73+
}
74+
75+
export function AgentAudioVisualizerBar({
76+
size = 'md',
77+
state = 'connecting',
78+
barCount,
79+
audioTrack,
80+
className,
81+
children,
82+
}: AgentAudioVisualizerBarProps & VariantProps<typeof AgentAudioVisualizerBarVariants>) {
83+
const _barCount = useMemo(() => {
84+
if (barCount) {
85+
return barCount;
86+
}
87+
switch (size) {
88+
case 'icon':
89+
case 'sm':
90+
return 3;
91+
default:
92+
return 5;
93+
}
94+
}, [barCount, size]);
95+
96+
const volumeBands = useMultibandTrackVolume(audioTrack, {
97+
bands: _barCount,
98+
loPass: 100,
99+
hiPass: 200,
100+
});
101+
102+
const sequencerInterval = useMemo(() => {
103+
switch (state) {
104+
case 'connecting':
105+
return 2000 / _barCount;
106+
case 'initializing':
107+
return 2000;
108+
case 'listening':
109+
return 500;
110+
case 'thinking':
111+
return 150;
112+
default:
113+
return 1000;
114+
}
115+
}, [state, _barCount]);
116+
117+
const highlightedIndices = useAgentAudioVisualizerBarAnimator(
118+
state,
119+
_barCount,
120+
sequencerInterval
121+
);
122+
123+
const bands = useMemo(
124+
() => (state === 'speaking' ? volumeBands : new Array(_barCount).fill(0)),
125+
[state, volumeBands, _barCount]
126+
);
127+
128+
return (
129+
<div className={cn(AgentAudioVisualizerBarVariants({ size }), className)}>
130+
{bands.map((band: number, idx: number) =>
131+
children ? (
132+
<React.Fragment key={idx}>
133+
{cloneSingleChild(children, {
134+
'data-lk-index': idx,
135+
'data-lk-highlighted': highlightedIndices.includes(idx),
136+
style: { height: `${band * 100}%` },
137+
})}
138+
</React.Fragment>
139+
) : (
140+
<div
141+
key={idx}
142+
data-lk-index={idx}
143+
data-lk-highlighted={highlightedIndices.includes(idx)}
144+
style={{ height: `${band * 100}%` }}
145+
/>
146+
)
147+
)}
148+
</div>
149+
);
150+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { VideoTrack, type VideoTrackProps } from '@livekit/components-react';
2+
3+
export function AgentAvatarVideoTrack(props: VideoTrackProps) {
4+
return <VideoTrack {...props} />;
5+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { type VariantProps, cva } from 'class-variance-authority';
2+
import { motion } from 'motion/react';
3+
import { cn } from '@/lib/utils';
4+
5+
const motionAnimationProps = {
6+
variants: {
7+
hidden: {
8+
opacity: 0,
9+
scale: 0.1,
10+
transition: {
11+
duration: 0.1,
12+
ease: 'linear',
13+
},
14+
},
15+
visible: {
16+
opacity: [0.5, 1],
17+
scale: [1, 1.2],
18+
transition: {
19+
type: 'spring',
20+
bounce: 0,
21+
duration: 0.5,
22+
repeat: Infinity,
23+
repeatType: 'mirror' as const,
24+
},
25+
},
26+
},
27+
initial: 'hidden',
28+
animate: 'visible',
29+
exit: 'hidden',
30+
};
31+
32+
const agentChatIndicatorVariants = cva('bg-muted-foreground inline-block size-2.5 rounded-full', {
33+
variants: {
34+
size: {
35+
sm: 'size-2.5',
36+
md: 'size-4',
37+
lg: 'size-6',
38+
},
39+
},
40+
defaultVariants: {
41+
size: 'md',
42+
},
43+
});
44+
45+
export interface AgentChatIndicatorProps extends VariantProps<typeof agentChatIndicatorVariants> {
46+
className?: string;
47+
}
48+
49+
export function AgentChatIndicator({ size, className }: AgentChatIndicatorProps) {
50+
return (
51+
<motion.span
52+
{...motionAnimationProps}
53+
transition={{ duration: 0.1, ease: 'linear' }}
54+
className={cn(agentChatIndicatorVariants({ size }), className)}
55+
/>
56+
);
57+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use client';
2+
3+
import { AnimatePresence } from 'motion/react';
4+
import { type AgentState, type ReceivedMessage } from '@livekit/components-react';
5+
import { AgentChatIndicator } from '@/components/agents-ui/agent-chat-indicator';
6+
import {
7+
Conversation,
8+
ConversationContent,
9+
ConversationScrollButton,
10+
} from '@/components/ai-elements/conversation';
11+
import { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';
12+
13+
export interface AgentChatTranscriptProps {
14+
agentState?: AgentState;
15+
messages?: ReceivedMessage[];
16+
className?: string;
17+
}
18+
19+
export function AgentChatTranscript({
20+
agentState,
21+
messages = [],
22+
className,
23+
}: AgentChatTranscriptProps) {
24+
return (
25+
<Conversation className={className}>
26+
<ConversationContent>
27+
{messages.map((receivedMessage) => {
28+
const { id, timestamp, from, message } = receivedMessage;
29+
const locale = navigator?.language ?? 'en-US';
30+
const messageOrigin = from?.isLocal ? 'user' : 'assistant';
31+
const time = new Date(timestamp);
32+
const title = time.toLocaleTimeString(locale, { timeStyle: 'full' });
33+
34+
return (
35+
<Message key={id} title={title} from={messageOrigin}>
36+
<MessageContent>
37+
<MessageResponse>{message}</MessageResponse>
38+
</MessageContent>
39+
</Message>
40+
);
41+
})}
42+
<AnimatePresence>
43+
{agentState === 'thinking' && <AgentChatIndicator size="sm" />}
44+
</AnimatePresence>
45+
</ConversationContent>
46+
<ConversationScrollButton />
47+
</Conversation>
48+
);
49+
}

0 commit comments

Comments
 (0)