Skip to content

Commit 03726ba

Browse files
feat(shadcn): agent session view block (#1286)
1 parent a80e623 commit 03726ba

File tree

14 files changed

+1251
-58
lines changed

14 files changed

+1251
-58
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { StoryObj } from '@storybook/react-vite';
3+
import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider';
4+
import { AgentSessionView_01, AgentSessionView_01Props } from '@agents-ui';
5+
6+
export default {
7+
component: AgentSessionView_01,
8+
decorators: [AgentSessionProvider],
9+
render: (args: AgentSessionView_01Props) => <AgentSessionView_01 {...args} />,
10+
args: {
11+
className: 'h-screen w-screen',
12+
supportsChatInput: true,
13+
supportsVideoInput: true,
14+
supportsScreenShare: true,
15+
isPreConnectBufferEnabled: true,
16+
preConnectMessage: 'Agent is listening, ask it a question',
17+
audioVisualizerType: 'bar',
18+
audioVisualizerColor: undefined,
19+
audioVisualizerColorShift: 0,
20+
audioVisualizerBarCount: 5,
21+
audioVisualizerGridRowCount: 10,
22+
audioVisualizerGridColumnCount: 10,
23+
audioVisualizerRadialBarCount: 25,
24+
audioVisualizerRadialRadius: 80,
25+
audioVisualizerWaveLineWidth: 10,
26+
},
27+
argTypes: {
28+
supportsChatInput: { control: { type: 'boolean' } },
29+
supportsVideoInput: { control: { type: 'boolean' } },
30+
supportsScreenShare: { control: { type: 'boolean' } },
31+
isPreConnectBufferEnabled: { control: { type: 'boolean' } },
32+
preConnectMessage: { control: { type: 'text' } },
33+
audioVisualizerType: {
34+
control: { type: 'select', options: ['bar', 'wave', 'grid', 'radial', 'aura'] },
35+
},
36+
audioVisualizerColor: { control: { type: 'color' } },
37+
audioVisualizerColorShift: { control: { type: 'range', min: 0, max: 2, step: 0.1 } },
38+
audioVisualizerBarCount: { control: { type: 'range', min: 1, max: 21, step: 1 } },
39+
audioVisualizerGridRowCount: { control: { type: 'range', min: 3, max: 21, step: 2 } },
40+
audioVisualizerGridColumnCount: { control: { type: 'range', min: 3, max: 21, step: 2 } },
41+
audioVisualizerRadialBarCount: { control: { type: 'range', min: 4, max: 64, step: 4 } },
42+
audioVisualizerRadialRadius: { control: { type: 'range', min: 30, max: 120, step: 1 } },
43+
audioVisualizerWaveLineWidth: { control: { type: 'range', min: 1, max: 10, step: 0.1 } },
44+
},
45+
parameters: {
46+
layout: 'centered',
47+
actions: { handles: [] },
48+
},
49+
};
50+
51+
export const Default: StoryObj<AgentSessionView_01Props> = {
52+
args: {},
53+
};

packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
// Originally developed for Unicorn Studio
2-
// https://unicorn.studio
3-
//
4-
// Licensed under the Polyform Non-Resale License 1.0.0
5-
// https://polyformproject.org/licenses/non-resale/1.0.0/
6-
//
7-
// © 2026 UNCRN LLC
1+
/**
2+
* @license
3+
*
4+
* Originally developed for Unicorn Studio
5+
* https://unicorn.studio
6+
*
7+
* Licensed under the Polyform Non-Resale License 1.0.0
8+
* https://polyformproject.org/licenses/non-resale/1.0.0/
9+
*
10+
* © 2026 UNCRN LLC
11+
*/
812

913
'use client';
1014

@@ -437,11 +441,7 @@ export function AgentAudioVisualizerAura({
437441
amplitude={amplitude}
438442
frequency={frequency}
439443
brightness={brightness}
440-
className={cn(
441-
AgentAudioVisualizerAuraVariants({ size }),
442-
'overflow-hidden rounded-full',
443-
className,
444-
)}
444+
className={cn(AgentAudioVisualizerAuraVariants({ size }), className)}
445445
{...props}
446446
/>
447447
);

packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function cloneSingleChild(
4646
export const AgentAudioVisualizerBarElementVariants = cva(
4747
[
4848
'rounded-full transition-colors duration-250 ease-linear',
49-
'bg-transparent data-[lk-highlighted=true]:bg-current',
49+
'bg-current/10 data-[lk-highlighted=true]:bg-current',
5050
],
5151
{
5252
variants: {

packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,6 @@ export function AgentAudioVisualizerWave({
361361
className={cn(
362362
AgentAudioVisualizerWaveVariants({ size }),
363363
'mask-[linear-gradient(90deg,transparent_0%,black_20%,black_80%,transparent_100%)]',
364-
'overflow-hidden rounded-full',
365364
className,
366365
)}
367366
{...props}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
'use client';
2+
3+
import React, { useEffect, useRef, useState } from 'react';
4+
import { AnimatePresence, type MotionProps, motion } from 'motion/react';
5+
import { useAgent, useSessionContext, useSessionMessages } from '@livekit/components-react';
6+
import { AgentChatTranscript } from '@/components/agents-ui/agent-chat-transcript';
7+
import {
8+
AgentControlBar,
9+
type AgentControlBarControls,
10+
} from '@/components/agents-ui/agent-control-bar';
11+
import { Shimmer } from '@/components/ai-elements/shimmer';
12+
import { cn } from '@/lib/utils';
13+
import { TileLayout } from './tile-view';
14+
15+
const MotionMessage = motion.create(Shimmer);
16+
17+
const BOTTOM_VIEW_MOTION_PROPS: MotionProps = {
18+
variants: {
19+
visible: {
20+
opacity: 1,
21+
translateY: '0%',
22+
},
23+
hidden: {
24+
opacity: 0,
25+
translateY: '100%',
26+
},
27+
},
28+
initial: 'hidden',
29+
animate: 'visible',
30+
exit: 'hidden',
31+
transition: {
32+
duration: 0.3,
33+
delay: 0.5,
34+
ease: 'easeOut',
35+
},
36+
};
37+
38+
const CHAT_MOTION_PROPS: MotionProps = {
39+
variants: {
40+
hidden: {
41+
opacity: 0,
42+
transition: {
43+
ease: 'easeOut',
44+
duration: 0.3,
45+
},
46+
},
47+
visible: {
48+
opacity: 1,
49+
transition: {
50+
delay: 0.2,
51+
ease: 'easeOut',
52+
duration: 0.3,
53+
},
54+
},
55+
},
56+
initial: 'hidden',
57+
animate: 'visible',
58+
exit: 'hidden',
59+
};
60+
61+
const SHIMMER_MOTION_PROPS: MotionProps = {
62+
variants: {
63+
visible: {
64+
opacity: 1,
65+
transition: {
66+
ease: 'easeIn',
67+
duration: 0.5,
68+
delay: 0.8,
69+
},
70+
},
71+
hidden: {
72+
opacity: 0,
73+
transition: {
74+
ease: 'easeIn',
75+
duration: 0.5,
76+
delay: 0,
77+
},
78+
},
79+
},
80+
initial: 'hidden',
81+
animate: 'visible',
82+
exit: 'hidden',
83+
};
84+
85+
interface FadeProps {
86+
top?: boolean;
87+
bottom?: boolean;
88+
className?: string;
89+
}
90+
91+
export function Fade({ top = false, bottom = false, className }: FadeProps) {
92+
return (
93+
<div
94+
className={cn(
95+
'from-background pointer-events-none h-4 bg-linear-to-b to-transparent',
96+
top && 'bg-linear-to-b',
97+
bottom && 'bg-linear-to-t',
98+
className,
99+
)}
100+
/>
101+
);
102+
}
103+
104+
export interface AgentSessionView_01Props {
105+
/**
106+
* Message shown above the controls before the first chat message is sent.
107+
*
108+
* @default 'Agent is listening, ask it a question'
109+
*/
110+
preConnectMessage?: string;
111+
/**
112+
* Enables or disables the chat toggle and transcript input controls.
113+
*
114+
* @default true
115+
*/
116+
supportsChatInput?: boolean;
117+
/**
118+
* Enables or disables camera controls in the bottom control bar.
119+
*
120+
* @default true
121+
*/
122+
supportsVideoInput?: boolean;
123+
/**
124+
* Enables or disables screen sharing controls in the bottom control bar.
125+
*
126+
* @default true
127+
*/
128+
supportsScreenShare?: boolean;
129+
/**
130+
* Shows a pre-connect buffer state with a shimmer message before messages appear.
131+
*
132+
* @default true
133+
*/
134+
isPreConnectBufferEnabled?: boolean;
135+
136+
/** Selects the visualizer style rendered in the main tile area. */
137+
audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura';
138+
/** Primary hex color used by supported audio visualizer variants. */
139+
audioVisualizerColor?: `#${string}`;
140+
/** Hue shift intensity used by certain visualizers. */
141+
audioVisualizerColorShift?: number;
142+
/** Number of bars to render when `audioVisualizerType` is `bar`. */
143+
audioVisualizerBarCount?: number;
144+
/** Number of rows in the visualizer when `audioVisualizerType` is `grid`. */
145+
audioVisualizerGridRowCount?: number;
146+
/** Number of columns in the visualizer when `audioVisualizerType` is `grid`. */
147+
audioVisualizerGridColumnCount?: number;
148+
/** Number of radial bars when `audioVisualizerType` is `radial`. */
149+
audioVisualizerRadialBarCount?: number;
150+
/** Base radius of the radial visualizer when `audioVisualizerType` is `radial`. */
151+
audioVisualizerRadialRadius?: number;
152+
/** Stroke width of the wave path when `audioVisualizerType` is `wave`. */
153+
audioVisualizerWaveLineWidth?: number;
154+
/** Optional class name merged onto the outer `<section>` container. */
155+
className?: string;
156+
}
157+
158+
export function AgentSessionView_01({
159+
preConnectMessage = 'Agent is listening, ask it a question',
160+
supportsChatInput = true,
161+
supportsVideoInput = true,
162+
supportsScreenShare = true,
163+
isPreConnectBufferEnabled = true,
164+
165+
audioVisualizerType,
166+
audioVisualizerColor,
167+
audioVisualizerColorShift,
168+
audioVisualizerBarCount,
169+
audioVisualizerGridRowCount,
170+
audioVisualizerGridColumnCount,
171+
audioVisualizerRadialBarCount,
172+
audioVisualizerRadialRadius,
173+
audioVisualizerWaveLineWidth,
174+
ref,
175+
className,
176+
...props
177+
}: React.ComponentProps<'section'> & AgentSessionView_01Props) {
178+
const session = useSessionContext();
179+
const { messages } = useSessionMessages(session);
180+
const [chatOpen, setChatOpen] = useState(false);
181+
const scrollAreaRef = useRef<HTMLDivElement>(null);
182+
const { state: agentState } = useAgent();
183+
184+
const controls: AgentControlBarControls = {
185+
leave: true,
186+
microphone: true,
187+
chat: supportsChatInput,
188+
camera: supportsVideoInput,
189+
screenShare: supportsScreenShare,
190+
};
191+
192+
useEffect(() => {
193+
const lastMessage = messages.at(-1);
194+
const lastMessageIsLocal = lastMessage?.from?.isLocal === true;
195+
196+
if (scrollAreaRef.current && lastMessageIsLocal) {
197+
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight;
198+
}
199+
}, [messages]);
200+
201+
return (
202+
<section
203+
ref={ref}
204+
className={cn('bg-background relative z-10 h-full w-full overflow-hidden', className)}
205+
{...props}
206+
>
207+
<Fade top className="absolute inset-x-4 top-0 z-10 h-40" />
208+
{/* transcript */}
209+
210+
<div className="absolute top-0 bottom-[135px] flex w-full flex-col md:bottom-[170px]">
211+
<AnimatePresence>
212+
{chatOpen && (
213+
<motion.div
214+
{...CHAT_MOTION_PROPS}
215+
className="flex h-full w-full flex-col gap-4 space-y-3 transition-opacity duration-300 ease-out"
216+
>
217+
<AgentChatTranscript
218+
agentState={agentState}
219+
messages={messages}
220+
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"
221+
/>
222+
</motion.div>
223+
)}
224+
</AnimatePresence>
225+
</div>
226+
{/* Tile layout */}
227+
<TileLayout
228+
chatOpen={chatOpen}
229+
audioVisualizerType={audioVisualizerType}
230+
audioVisualizerColor={audioVisualizerColor}
231+
audioVisualizerColorShift={audioVisualizerColorShift}
232+
audioVisualizerBarCount={audioVisualizerBarCount}
233+
audioVisualizerRadialBarCount={audioVisualizerRadialBarCount}
234+
audioVisualizerRadialRadius={audioVisualizerRadialRadius}
235+
audioVisualizerGridRowCount={audioVisualizerGridRowCount}
236+
audioVisualizerGridColumnCount={audioVisualizerGridColumnCount}
237+
audioVisualizerWaveLineWidth={audioVisualizerWaveLineWidth}
238+
/>
239+
{/* Bottom */}
240+
<motion.div
241+
{...BOTTOM_VIEW_MOTION_PROPS}
242+
className="absolute inset-x-3 bottom-0 z-50 md:inset-x-12"
243+
>
244+
{/* Pre-connect message */}
245+
{isPreConnectBufferEnabled && (
246+
<AnimatePresence>
247+
{messages.length === 0 && (
248+
<MotionMessage
249+
key="pre-connect-message"
250+
duration={2}
251+
aria-hidden={messages.length > 0}
252+
{...SHIMMER_MOTION_PROPS}
253+
className="pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold"
254+
>
255+
{preConnectMessage}
256+
</MotionMessage>
257+
)}
258+
</AnimatePresence>
259+
)}
260+
<div className="bg-background relative mx-auto max-w-2xl pb-3 md:pb-12">
261+
<Fade bottom className="absolute inset-x-0 top-0 h-4 -translate-y-full" />
262+
<AgentControlBar
263+
variant="livekit"
264+
controls={controls}
265+
isChatOpen={chatOpen}
266+
isConnected={session.isConnected}
267+
onDisconnect={session.end}
268+
onIsChatOpenChange={setChatOpen}
269+
/>
270+
</div>
271+
</motion.div>
272+
</section>
273+
);
274+
}

0 commit comments

Comments
 (0)