Skip to content

Commit d41cf29

Browse files
refactor(shadcn): replace ai-elements chat components with shadcn base (#1365)
1 parent ac72064 commit d41cf29

21 files changed

Lines changed: 609 additions & 717 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from 'react';
2+
import { Decorator } from '@storybook/react-vite';
3+
import { useSessionContext } from '@livekit/components-react';
4+
import { DataPacket_Kind, Participant, type RemoteParticipant, RoomEvent } from 'livekit-client';
5+
6+
/**
7+
* Mirrors `LegacyDataTopic.CHAT` from `@livekit/components-core` (not re-exported from
8+
* `@livekit/components-react`, so it's inlined here rather than adding a new dependency).
9+
*/
10+
const LEGACY_CHAT_TOPIC = 'lk-chat-topic';
11+
12+
const MOCK_AGENT_PARTICIPANT = new Participant('mock-agent-sid', 'agent', 'Agent');
13+
14+
export type MockConversationMessage = {
15+
id: string;
16+
from: 'user' | 'agent';
17+
message: string;
18+
};
19+
20+
/**
21+
* Populates the current session's transcript with a scripted conversation, without a real
22+
* connection or backend agent. Works by emitting fake `RoomEvent.DataReceived` events on the
23+
* legacy chat topic -- the same public SDK event the chat pipeline listens on -- so any
24+
* component reading messages via `useSessionMessages()` renders them as if they were received
25+
* for real.
26+
*
27+
* Must be nested inside a decorator that provides `SessionContext` (e.g. `AgentSessionProvider`).
28+
*/
29+
export function withMockConversation(messages: MockConversationMessage[]): Decorator {
30+
return (Story) => {
31+
const { room } = useSessionContext();
32+
33+
React.useEffect(() => {
34+
messages.forEach(({ id, from, message }) => {
35+
const payload = new TextEncoder().encode(
36+
JSON.stringify({ id, timestamp: Date.now(), message }),
37+
) as Uint8Array<ArrayBuffer>;
38+
// `RoomEvent.DataReceived` is typed as remote-only, but `setupChat` (the only consumer of
39+
// this event) reads `from` as a plain `Participant`, so this cast is safe -- the real
40+
// local participant is a `LocalParticipant`, not a `RemoteParticipant` either.
41+
const participant = (from === 'user'
42+
? room.localParticipant
43+
: MOCK_AGENT_PARTICIPANT) as unknown as RemoteParticipant;
44+
room.emit(
45+
RoomEvent.DataReceived,
46+
payload,
47+
participant,
48+
DataPacket_Kind.RELIABLE,
49+
LEGACY_CHAT_TOPIC,
50+
);
51+
});
52+
}, [room]);
53+
54+
return <>{Story()}</>;
55+
};
56+
}

docs/storybook/.storybook/tailwind.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import 'tailwindcss';
2+
@import 'shadcn/tailwind.css';
23

34
/* where to source the shadcn styles from */
45
@source './**/*.{js,ts,jsx,tsx}';

docs/storybook/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@
2727
"@storybook/react": "^10.1.4",
2828
"@storybook/react-vite": "^10.1.11",
2929
"@storybook/testing-library": "^0.2.2",
30-
"@tailwindcss/postcss": "^4",
31-
"@tailwindcss/vite": "^4.1.17",
30+
"@tailwindcss/postcss": "^4.3.2",
31+
"@tailwindcss/vite": "^4.3.2",
3232
"autoprefixer": "^10.4.22",
3333
"babel-loader": "^10.0.0",
3434
"eslint-config-lk-custom": "workspace:*",
3535
"eslint-plugin-storybook": "10.1.4",
3636
"next-themes": "^0.4.6",
3737
"postcss": "^8.5.15",
38+
"shadcn": "^4.13.0",
3839
"storybook": "^10.1.4",
39-
"tailwindcss": "^4.1.17",
40+
"tailwindcss": "^4.3.2",
41+
"tslib": "^2.6.2",
4042
"tsx": "^4.0.0",
4143
"typescript": "5.8.2",
4244
"vite": "^8.0.0",

docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,41 @@
11
import React from 'react';
22
import { StoryObj } from '@storybook/react-vite';
3+
import { within, userEvent } from 'storybook/test';
34
import { useTheme } from 'next-themes';
45
import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider';
6+
import {
7+
MockConversationMessage,
8+
withMockConversation,
9+
} from '../../.storybook/lk-decorators/MockConversation';
510
import { AgentSessionView_01, AgentSessionView_01Props } from '@livekit/agents-ui';
611

12+
const SAMPLE_CONVERSATION: MockConversationMessage[] = [
13+
{ id: '1', from: 'agent', message: 'Hi, how can I help you today?' },
14+
{ id: '2', from: 'user', message: 'Hi, how are you?' },
15+
{ id: '3', from: 'agent', message: "I'm good, thank you!" },
16+
{ id: '4', from: 'user', message: 'This is a longer message that should wrap to the next line.' },
17+
{
18+
id: '5',
19+
from: 'agent',
20+
message: "Great, I'm responding with an even longer message to see how it wraps.",
21+
},
22+
...Array.from({ length: 15 }, (_, index) => ({
23+
id: `${6 + index}`,
24+
from: (index % 2 === 0 ? 'user' : 'agent') as MockConversationMessage['from'],
25+
message:
26+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
27+
})),
28+
];
29+
730
export default {
831
component: AgentSessionView_01,
932
decorators: [AgentSessionProvider],
1033
render: (args: AgentSessionView_01Props) => {
1134
const { resolvedTheme = 'dark' } = useTheme();
12-
return (
13-
<AgentSessionView_01 themeMode={resolvedTheme as 'dark' | 'light'} {...args} />
14-
);
35+
return <AgentSessionView_01 themeMode={resolvedTheme as 'dark' | 'light'} {...args} />;
1536
},
1637
args: {
17-
className: 'h-screen w-screen',
38+
className: 'h-screen',
1839
supportsChatInput: true,
1940
supportsVideoInput: true,
2041
supportsScreenShare: true,
@@ -49,11 +70,21 @@ export default {
4970
audioVisualizerWaveLineWidth: { control: { type: 'range', min: 1, max: 10, step: 0.1 } },
5071
},
5172
parameters: {
52-
layout: 'centered',
73+
layout: 'fullscreen',
5374
actions: { handles: [] },
5475
},
5576
};
5677

5778
export const Default: StoryObj<AgentSessionView_01Props> = {
5879
args: {},
5980
};
81+
82+
export const WithConversation: StoryObj<AgentSessionView_01Props> = {
83+
decorators: [withMockConversation(SAMPLE_CONVERSATION)],
84+
args: {},
85+
play: async ({ canvasElement }) => {
86+
const canvas = within(canvasElement);
87+
const transcriptToggle = await canvas.findByRole('button', { name: 'Toggle transcript' });
88+
await userEvent.click(transcriptToggle);
89+
},
90+
};

packages/shadcn/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Agents UI is the easiest way to build agentic voice applications faster on top of LiveKit primitives.
44

5-
Agents UI is a component library built on top of [shadcn/ui](https://ui.shadcn.com/) and [AI Elements](https://ai-sdk.dev/elements) to accelerate building agentic applications on top of LiveKit's real-time platform. It provides pre-built components like controling IO, managing sessions, rendering transcripts, visualing audio streams, and more.
5+
Agents UI is a component library built on top of [shadcn/ui](https://ui.shadcn.com/) to accelerate building agentic applications on top of LiveKit's real-time platform. It provides pre-built components like controling IO, managing sessions, rendering transcripts, visualing audio streams, and more.
66

77
## Components
88

@@ -103,7 +103,6 @@ After installation, no additional setup is needed. The component's styles (Tailw
103103
packages/shadcn/
104104
├── components/
105105
│ ├── agents-ui/ # LiveKit agent-specific components
106-
│ ├── ai-elements/ # Reusable AI conversation components
107106
│ ├── ui/ # Base UI primitives (shadcn/ui style)
108107
│ └── session-provider.tsx
109108
├── hooks/

packages/shadcn/components.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,5 @@
1717
"lib": "@/lib",
1818
"hooks": "@/hooks"
1919
},
20-
"iconLibrary": "lucide",
21-
"registries": {
22-
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json"
23-
}
20+
"iconLibrary": "lucide"
2421
}

packages/shadcn/components/agents-ui/agent-chat-indicator.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const motionAnimationProps = {
1616
},
1717
visible: {
1818
opacity: [0.5, 1],
19-
scale: [1, 1.2],
19+
scale: [0.9, 1],
2020
transition: {
2121
type: 'spring' as const,
2222
bounce: 0,
@@ -34,7 +34,7 @@ const motionAnimationProps = {
3434
const agentChatIndicatorVariants = cva('bg-muted-foreground inline-block size-2.5 rounded-full', {
3535
variants: {
3636
size: {
37-
sm: 'size-2.5',
37+
sm: 'size-3',
3838
md: 'size-4',
3939
lg: 'size-6',
4040
},

packages/shadcn/components/agents-ui/agent-chat-transcript.tsx

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
import { type ComponentProps } from 'react';
44
import { type AgentState, type ReceivedMessage } from '@livekit/components-react';
5+
import { Streamdown } from 'streamdown';
6+
import { Bubble, BubbleContent } from '@/components/ui/bubble';
7+
import { Message, MessageContent } from '@/components/ui/message';
58
import {
6-
Conversation,
7-
ConversationContent,
8-
ConversationScrollButton,
9-
} from '@/components/ai-elements/conversation';
10-
import { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';
9+
MessageScroller,
10+
MessageScrollerButton,
11+
MessageScrollerContent,
12+
MessageScrollerItem,
13+
MessageScrollerProvider,
14+
MessageScrollerViewport,
15+
} from '@/components/ui/message-scroller';
1116
import { AgentChatIndicator } from '@/components/agents-ui/agent-chat-indicator';
1217
import { AnimatePresence } from 'motion/react';
1318

@@ -52,28 +57,38 @@ export function AgentChatTranscript({
5257
...props
5358
}: AgentChatTranscriptProps) {
5459
return (
55-
<Conversation className={className} {...props}>
56-
<ConversationContent>
57-
{messages.map((receivedMessage) => {
58-
const { id, timestamp, from, message } = receivedMessage;
59-
const time = new Date(timestamp);
60-
const messageOrigin = from?.isLocal ? 'user' : 'assistant';
61-
const locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US';
62-
const title = time.toLocaleTimeString(locale, { timeStyle: 'full' });
60+
<MessageScrollerProvider autoScroll defaultScrollPosition="last-anchor">
61+
<MessageScroller className={className} {...props}>
62+
<MessageScrollerViewport>
63+
<MessageScrollerContent aria-busy={agentState === 'thinking'}>
64+
{messages.map((receivedMessage) => {
65+
const { id, timestamp, from, message } = receivedMessage;
66+
const time = new Date(timestamp);
67+
const isUser = from?.isLocal;
68+
const locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US';
69+
const title = time.toLocaleTimeString(locale, { timeStyle: 'full' });
6370

64-
return (
65-
<Message key={id} title={title} from={messageOrigin}>
66-
<MessageContent>
67-
<MessageResponse>{message}</MessageResponse>
68-
</MessageContent>
69-
</Message>
70-
);
71-
})}
72-
<AnimatePresence>
73-
{agentState === 'thinking' && <AgentChatIndicator size="sm" />}
74-
</AnimatePresence>
75-
</ConversationContent>
76-
<ConversationScrollButton />
77-
</Conversation>
71+
return (
72+
<MessageScrollerItem key={id} messageId={id} scrollAnchor={isUser}>
73+
<Message align={isUser ? 'end' : 'start'} title={title}>
74+
<MessageContent>
75+
<Bubble align={isUser ? 'end' : 'start'} variant={isUser ? 'secondary' : 'ghost'}>
76+
<BubbleContent>
77+
<Streamdown>{message}</Streamdown>
78+
</BubbleContent>
79+
</Bubble>
80+
</MessageContent>
81+
</Message>
82+
</MessageScrollerItem>
83+
);
84+
})}
85+
<AnimatePresence>
86+
{agentState === 'thinking' && <AgentChatIndicator size="sm" />}
87+
</AnimatePresence>
88+
</MessageScrollerContent>
89+
</MessageScrollerViewport>
90+
<MessageScrollerButton />
91+
</MessageScroller>
92+
</MessageScrollerProvider>
7893
);
7994
}

packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@ import {
88
AgentControlBar,
99
type AgentControlBarControls,
1010
} from '@/components/agents-ui/agent-control-bar';
11-
import { Shimmer } from '@/components/ai-elements/shimmer';
1211
import { cn } from '@/lib/utils';
1312
import { TileLayout } from './tile-view';
1413

15-
const MotionMessage = motion.create(Shimmer);
16-
1714
const BOTTOM_VIEW_MOTION_PROPS: MotionProps = {
1815
variants: {
1916
visible: {
@@ -218,12 +215,12 @@ export function AgentSessionView_01({
218215
{isChatOpen && (
219216
<motion.div
220217
{...CHAT_MOTION_PROPS}
221-
className="flex h-full w-full flex-col gap-4 space-y-3 transition-opacity duration-300 ease-out"
218+
className="h-full w-full transition-opacity duration-300 ease-out"
222219
>
223220
<AgentChatTranscript
224221
agentState={agentState}
225222
messages={messages}
226-
className="mx-auto w-full max-w-2xl [&_.is-user>div]:rounded-[22px] [&>div>div]:px-4 [&>div>div]:pt-40 md:[&>div>div]:px-6"
223+
className="mx-auto w-full max-w-2xl px-4 md:px-6 **:data-[slot=message-scroller-content]:pb-4 **:data-[slot=message-scroller-content]:pt-40"
227224
/>
228225
</motion.div>
229226
)}
@@ -252,20 +249,18 @@ export function AgentSessionView_01({
252249
{isPreConnectBufferEnabled && (
253250
<AnimatePresence>
254251
{messages.length === 0 && (
255-
<MotionMessage
252+
<motion.p
256253
key="pre-connect-message"
257-
duration={2}
258254
aria-hidden={messages.length > 0}
259255
{...SHIMMER_MOTION_PROPS}
260-
className="pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold"
256+
className="shimmer shimmer-duration-2000 pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold"
261257
>
262258
{preConnectMessage}
263-
</MotionMessage>
259+
</motion.p>
264260
)}
265261
</AnimatePresence>
266262
)}
267263
<div className="bg-background relative mx-auto max-w-2xl pb-3 md:pb-12">
268-
<Fade bottom className="absolute inset-x-0 top-0 h-4 -translate-y-full" />
269264
<AgentControlBar
270265
variant="livekit"
271266
controls={controls}

packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function TileLayout({
109109
const videoHeight = agentVideoTrack?.publication.dimensions?.height ?? 0;
110110

111111
return (
112-
<div className="absolute inset-x-0 top-8 bottom-32 z-50 md:top-12 md:bottom-40">
112+
<div className="pointer-events-none absolute inset-x-0 top-8 bottom-32 z-50 md:top-12 md:bottom-40">
113113
<div className="relative mx-auto h-full max-w-2xl px-4 md:px-0">
114114
<div className={cn(tileViewClassNames.grid)}>
115115
{/* Agent */}

0 commit comments

Comments
 (0)