agents-ui
blockagent-session-view-01source: ok · hit
Agent Session View
A full-screen agent session block with transcript, controls, and I/O media tiles.
npx shadcn add "https://livekit.com/ui/r/agent-session-view-01.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationmotion
Registry dependencies
utils@agents-ui/agent-control-bar@agents-ui/agent-chat-transcript@agents-ui/agent-audio-visualizer-aura@agents-ui/agent-audio-visualizer-bar@agents-ui/agent-audio-visualizer-grid@agents-ui/agent-audio-visualizer-radial@agents-ui/agent-audio-visualizer-wave
Files4 · source cached 10/8/2026, 1:41:41 PM
components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx
'use client';
import React, { useEffect, useRef, useState } from 'react';
import { AnimatePresence, type MotionProps, motion } from 'motion/react';
import { useAgent, useSessionContext, useSessionMessages } from '@livekit/components-react';
import { AgentChatTranscript } from '@/components/agents-ui/agent-chat-transcript';
import {
AgentControlBar,
type AgentControlBarControls,
} from '@/components/agents-ui/agent-control-bar';
import { cn } from '@/lib/utils';
import { TileLayout } from './tile-view';
import { type AgentAudioVisualizerAuraProps } from '@/components/agents-ui/agent-audio-visualizer-aura';
import { type AgentAudioVisualizerBarProps } from '@/components/agents-ui/agent-audio-visualizer-bar';
import { type AgentAudioVisualizerGridProps } from '@/components/agents-ui/agent-audio-visualizer-grid';
import { type AgentAudioVisualizerRadialProps } from '@/components/agents-ui/agent-audio-visualizer-radial';
import { type AgentAudioVisualizerWaveProps } from '@/components/agents-ui/agent-audio-visualizer-wave';
/**
* Configures the visualizer style rendered in the main tile area. `type` selects the
* visualizer, and the remaining fields are exactly the props of the matching
* `AgentAudioVisualizer*` component (e.g. `type: 'grid'` accepts `rowCount`/`columnCount`,
* `type: 'wave'` accepts `lineWidth`, etc).
*/
export type AudioVisualizerConfig =
| ({ type?: 'bar' } & AgentAudioVisualizerBarProps)
| ({ type: 'wave' } & AgentAudioVisualizerWaveProps)
| ({ type: 'grid' } & AgentAudioVisualizerGridProps)
| ({ type: 'radial' } & AgentAudioVisualizerRadialProps)
| ({ type: 'aura' } & AgentAudioVisualizerAuraProps);
const DEFAULT_CONTROLS: AgentControlBarControls = {
leave: true,
microphone: true,
chat: false,
camera: false,
screenShare: false,
};
const BOTTOM_VIEW_MOTION_PROPS: MotionProps = {
variants: {
visible: {
opacity: 1,
translateY: '0%',
},
hidden: {
opacity: 0,
translateY: '100%',
},
},
initial: 'hidden',
animate: 'visible',
exit: 'hidden',
transition: {
duration: 0.3,
delay: 0.5,
ease: 'easeOut',
},
};
const CHAT_MOTION_PROPS: MotionProps = {
variants: {
hidden: {
opacity: 0,
transition: {
ease: 'easeOut',
duration: 0.3,
},
},
visible: {
opacity: 1,
transition: {
delay: 0.2,
ease: 'easeOut',
duration: 0.3,
},
},
},
initial: 'hidden',
animate: 'visible',
exit: 'hidden',
};
const SHIMMER_MOTION_PROPS: MotionProps = {
variants: {
visible: {
opacity: 1,
transition: {
ease: 'easeIn',
duration: 0.5,
delay: 0.8,
},
},
hidden: {
opacity: 0,
transition: {
ease: 'easeIn',
duration: 0.5,
delay: 0,
},
},
},
initial: 'hidden',
animate: 'visible',
exit: 'hidden',
};
interface FadeProps {
top?: boolean;
bottom?: boolean;
className?: string;
}
export function Fade({ top = false, bottom = false, className }: FadeProps) {
return (
<div
className={cn(
'from-background pointer-events-none h-4 bg-linear-to-b to-transparent',
top && 'bg-linear-to-b',
bottom && 'bg-linear-to-t',
className,
)}
/>
);
}
export interface AgentSessionView_01Props {
/**
* Theme mode forwarded to the aura visualizer (`audioVisualizer.type === 'aura'`) so
* the shader's blend mode adapts to the theme mode.
* Ignored by other visualizer types.
*/
themeMode?: 'dark' | 'light';
/**
* Message shown above the controls before the first chat message is sent.
*
* @default 'Agent is listening, ask it a question'
*/
preConnectMessage?: string;
/**
* An object with the following keys: leave, microphone, screenShare, camera, chat.
* Each key maps to a boolean value that determines whether the control is displayed.
*
* @default {
* leave: true,
* microphone: true,
* chat: false,
* camera: false,
* screenShare: false,
* }
*/
controls?: AgentControlBarControls;
/**
* Shows a pre-connect buffer state with a shimmer message before messages appear.
*
* @default true
*/
isPreConnectBufferEnabled?: boolean;
/**
* Configures the visualizer style rendered in the main tile area.
*
* @default { type: 'bar' }
*/
audioVisualizer?: AudioVisualizerConfig;
/** Optional class name merged onto the outer `<section>` container. */
className?: string;
/** Called when the user clicks the leave control, in addition to ending the session. */
onDisconnect?: () => void;
}
export function AgentSessionView_01({
preConnectMessage = 'Agent is listening, ask it a question',
controls = DEFAULT_CONTROLS,
isPreConnectBufferEnabled = true,
audioVisualizer = { type: 'bar' },
themeMode,
onDisconnect,
ref,
className,
...props
}: React.ComponentProps<'section'> & AgentSessionView_01Props) {
const session = useSessionContext();
const { messages } = useSessionMessages(session);
const [isChatOpen, setIsChatOpen] = useState(false);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const { state: agentState } = useAgent();
useEffect(() => {
const lastMessage = messages.at(-1);
const lastMessageIsLocal = lastMessage?.from?.isLocal === true;
if (scrollAreaRef.current && lastMessageIsLocal) {
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight;
}
}, [messages]);
const finalControls = {
...DEFAULT_CONTROLS,
...controls,
};
return (
<section
ref={ref}
className={cn(
'@container/agent-session-block bg-background relative z-10 h-full w-full overflow-hidden',
className,
)}
{...props}
>
<Fade top className="absolute inset-x-4 top-0 z-10 h-40" />
{/* transcript */}
<AnimatePresence>
{isChatOpen && (
<motion.div
{...CHAT_MOTION_PROPS}
className="absolute inset-x-0 top-0 bottom-[135px] overflow-hidden @md/agent-session-block:bottom-[170px]"
>
<AgentChatTranscript
agentState={agentState}
messages={messages}
className="mx-auto max-w-2xl **:data-[slot=message-scroller-content]:p-4 **:data-[slot=message-scroller-content]:pt-40! @md/agent-session-block:**:data-[slot=message-scroller-content]:p-6"
/>
</motion.div>
)}
</AnimatePresence>
{/* Tile layout */}
<TileLayout isChatOpen={isChatOpen} themeMode={themeMode} audioVisualizer={audioVisualizer} />
{/* Bottom */}
<motion.div
{...BOTTOM_VIEW_MOTION_PROPS}
className="absolute inset-x-3 bottom-0 z-50 @md/agent-session-block:inset-x-12"
>
{/* Pre-connect message */}
{isPreConnectBufferEnabled && (
<AnimatePresence>
{messages.length === 0 && (
<motion.p
key="pre-connect-message"
aria-hidden={messages.length > 0}
{...SHIMMER_MOTION_PROPS}
className="shimmer shimmer-duration-2000 pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm text-muted-foreground"
>
{preConnectMessage}
</motion.p>
)}
</AnimatePresence>
)}
<div className="bg-background relative mx-auto max-w-2xl pb-3 @md/agent-session-block:pb-12">
<AgentControlBar
variant="livekit"
controls={finalControls}
isChatOpen={isChatOpen}
isConnected={session.isConnected}
onDisconnect={onDisconnect ?? session.end}
onIsChatOpenChange={setIsChatOpen}
/>
</div>
</motion.div>
</section>
);
}
components/agents-ui/blocks/agent-session-view-01/components/audio-visualizer.tsx
'use client';
import React from 'react';
import { useVoiceAssistant } from '@livekit/components-react';
import { motion, type MotionProps } from 'motion/react';
import { cn } from '@/lib/utils';
import {
AgentAudioVisualizerAura,
type AgentAudioVisualizerAuraProps,
} from '@/components/agents-ui/agent-audio-visualizer-aura';
import {
AgentAudioVisualizerBar,
type AgentAudioVisualizerBarProps,
} from '@/components/agents-ui/agent-audio-visualizer-bar';
import {
AgentAudioVisualizerGrid,
type AgentAudioVisualizerGridProps,
} from '@/components/agents-ui/agent-audio-visualizer-grid';
import {
AgentAudioVisualizerRadial,
type AgentAudioVisualizerRadialProps,
} from '@/components/agents-ui/agent-audio-visualizer-radial';
import {
AgentAudioVisualizerWave,
type AgentAudioVisualizerWaveProps,
} from '@/components/agents-ui/agent-audio-visualizer-wave';
const MotionAgentAudioVisualizerAura = motion.create(AgentAudioVisualizerAura);
const MotionAgentAudioVisualizerBar = motion.create(AgentAudioVisualizerBar);
const MotionAgentAudioVisualizerGrid = motion.create(AgentAudioVisualizerGrid);
const MotionAgentAudioVisualizerRadial = motion.create(AgentAudioVisualizerRadial);
const MotionAgentAudioVisualizerWave = motion.create(AgentAudioVisualizerWave);
function getGridSize(rowCount: number, columnCount: number): AgentAudioVisualizerBarProps['size'] {
const totalCount = rowCount * columnCount;
if (totalCount < 100) {
return 'xl';
} else if (totalCount < 200) {
return 'lg';
} else if (totalCount < 300) {
return 'md';
}
return 'sm';
}
function getBarSize(barCount: number): AgentAudioVisualizerBarProps['size'] {
if (barCount <= 5) {
return 'xl';
} else if (barCount <= 10) {
return 'lg';
} else if (barCount <= 15) {
return 'md';
} else if (barCount <= 30) {
return 'sm';
}
return 'icon';
}
function getBarClassName(size: AgentAudioVisualizerBarProps['size']) {
if (size == 'xl') {
return 'size-[450px] *:min-h-[64px] *:w-[64px] gap-4';
} else if (size == 'lg') {
return 'size-[450px] *:min-h-[48px] *:w-[48px]';
} else if (size == 'md') {
return 'size-[350px] @md/agent-session-block:size-[450px] *:min-h-[32px] *:w-[32px]';
} else if (size == 'sm') {
return 'size-[300px] @md/agent-session-block:size-[450px] *:min-h-[16px] *:w-[16px]';
}
return 'size-[300px] @md/agent-session-block:size-[450px] *:min-h-[4px] *:w-[4px]';
}
interface AudioVisualizerProps extends MotionProps {
isChatOpen: boolean;
type: 'bar' | 'wave' | 'grid' | 'radial' | 'aura';
config?:
| AgentAudioVisualizerBarProps
| AgentAudioVisualizerWaveProps
| AgentAudioVisualizerGridProps
| AgentAudioVisualizerRadialProps
| AgentAudioVisualizerAuraProps;
themeMode?: 'dark' | 'light';
className?: string;
}
export function AudioVisualizer({
themeMode,
type,
config,
className,
...props
}: AudioVisualizerProps) {
const { state, audioTrack } = useVoiceAssistant();
// `state`/`audioTrack` always reflect the live agent session; any same-named field on
// `config` is ignored by spreading it before these are set explicitly below.
switch (type) {
case 'aura': {
return (
<MotionAgentAudioVisualizerAura
{...config}
state={state}
themeMode={themeMode}
audioTrack={audioTrack}
className={cn('size-[300px] @md/agent-session-block:size-[450px]', className)}
{...props}
/>
);
}
case 'wave': {
return (
<motion.div className={className} {...props}>
<MotionAgentAudioVisualizerWave
{...config}
state={state}
audioTrack={audioTrack}
className="size-[300px] @md/agent-session-block:size-[450px]"
/>
</motion.div>
);
}
case 'grid': {
const {
size,
radius,
rowCount = 15,
columnCount = 15,
} = config as AgentAudioVisualizerGridProps;
return (
<MotionAgentAudioVisualizerGrid
{...config}
size={size ?? getGridSize(rowCount, columnCount)}
state={state}
audioTrack={audioTrack}
rowCount={rowCount}
columnCount={columnCount}
radius={radius ?? Math.round(Math.min(rowCount, columnCount) / 4)}
className={cn(
'size-[350px] gap-0 p-20 *:place-self-center @md/agent-session-block:size-[450px]',
className,
)}
{...props}
/>
);
}
case 'radial': {
const { radius = 100, barCount = 25 } = config as AgentAudioVisualizerRadialProps;
return (
<motion.div className={className} {...props}>
<MotionAgentAudioVisualizerRadial
{...config}
size="xl"
state={state}
radius={radius}
barCount={barCount}
audioTrack={audioTrack}
className="size-[450px]"
/>
</motion.div>
);
}
default: {
const { size, barCount = 5 } = (config as AgentAudioVisualizerBarProps) ?? {};
const _size = size ?? getBarSize(barCount);
const sizedClassName = getBarClassName(_size);
return (
<MotionAgentAudioVisualizerBar
{...config}
size={_size}
state={state}
barCount={barCount}
audioTrack={audioTrack}
className={cn(sizedClassName, className)}
{...props}
>
<span className="min-h-2.5 w-2.5 rounded-full transition-colors duration-250 ease-linear bg-current/10 data-[lk-highlighted=true]:bg-current" />
</MotionAgentAudioVisualizerBar>
);
}
}
}
components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx
import React, { useMemo } from 'react';
import {
useLocalParticipant,
useTracks,
useVoiceAssistant,
VideoTrack,
type TrackReference,
} from '@livekit/components-react';
import { Track } from 'livekit-client';
import { AnimatePresence, motion, type MotionProps } from 'motion/react';
import { type AudioVisualizerConfig } from './agent-session-block';
import { cn } from '@/lib/utils';
import { AudioVisualizer } from './audio-visualizer';
const ANIMATION_TRANSITION: MotionProps['transition'] = {
type: 'spring',
stiffness: 675,
damping: 75,
mass: 1,
};
const tileViewClassNames = {
// GRID
// 2 Columns x 3 Rows
grid: [
'h-full w-full',
'grid gap-x-2 place-content-center',
'grid-cols-[1fr_1fr] grid-rows-[90px_1fr_90px]',
],
// Agent
// chatOpen: true,
// hasSecondTile: true
// layout: Column 1 / Row 1
// align: x-end y-center
agentChatOpenWithSecondTile: ['col-start-1 row-start-1', 'self-center justify-self-end'],
// Agent
// chatOpen: true,
// hasSecondTile: false
// layout: Column 1 / Row 1 / Column-Span 2
// align: x-center y-center
agentChatOpenWithoutSecondTile: ['col-start-1 row-start-1', 'col-span-2', 'place-content-center'],
// Agent
// chatOpen: false
// layout: Column 1 / Row 1 / Column-Span 2 / Row-Span 3
// align: x-center y-center
agentChatClosed: ['col-start-1 row-start-1', 'col-span-2 row-span-3', 'place-content-center'],
// Second tile
// chatOpen: true,
// hasSecondTile: true
// layout: Column 2 / Row 1
// align: x-start y-center
secondTileChatOpen: ['col-start-2 row-start-1', 'self-center justify-self-start'],
// Second tile
// chatOpen: false,
// hasSecondTile: false
// layout: Column 2 / Row 2
// align: x-end y-end
secondTileChatClosed: ['col-start-2 row-start-3', 'place-content-end'],
};
export function useLocalTrackRef(source: Track.Source) {
const { localParticipant } = useLocalParticipant();
const publication = localParticipant.getTrackPublication(source);
const trackRef = useMemo<TrackReference | undefined>(
() => (publication ? { source, participant: localParticipant, publication } : undefined),
[source, publication, localParticipant],
);
return trackRef;
}
interface TileLayoutProps {
themeMode?: 'dark' | 'light';
isChatOpen: boolean;
audioVisualizer?: AudioVisualizerConfig;
}
export function TileLayout({ themeMode, isChatOpen, audioVisualizer }: TileLayoutProps) {
const { videoTrack: agentVideoTrack } = useVoiceAssistant();
const [screenShareTrack] = useTracks([Track.Source.ScreenShare]);
const cameraTrack: TrackReference | undefined = useLocalTrackRef(Track.Source.Camera);
const isCameraEnabled = cameraTrack && !cameraTrack.publication.isMuted;
const isScreenShareEnabled = screenShareTrack && !screenShareTrack.publication.isMuted;
const hasSecondTile = isCameraEnabled || isScreenShareEnabled;
const animationDelay = isChatOpen ? 0 : 0.15;
const isAvatar = agentVideoTrack !== undefined;
const videoWidth = agentVideoTrack?.publication.dimensions?.width ?? 0;
const videoHeight = agentVideoTrack?.publication.dimensions?.height ?? 0;
const { type: audioVisualizerType = 'bar', ...audioVisualizerConfig } = audioVisualizer ?? {};
return (
<div className="pointer-events-none absolute inset-x-0 top-8 bottom-24 z-50 @md/agent-session-block:top-12 @md/agent-session-block:bottom-40">
<div className="relative mx-auto h-full max-w-2xl px-4 @md/agent-session-block:px-0">
<div className={cn(tileViewClassNames.grid)}>
{/* Agent */}
<div
className={cn([
'grid',
!isChatOpen && tileViewClassNames.agentChatClosed,
isChatOpen && hasSecondTile && tileViewClassNames.agentChatOpenWithSecondTile,
isChatOpen && !hasSecondTile && tileViewClassNames.agentChatOpenWithoutSecondTile,
])}
>
<AnimatePresence mode="popLayout">
{!isAvatar && (
// Audio Agent
<motion.div
key="agent"
layoutId="agent"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
...ANIMATION_TRANSITION,
delay: animationDelay,
}}
className={cn('relative aspect-square h-[90px]')}
>
<AudioVisualizer
key="audio-visualizer"
initial={{ scale: 1 }}
animate={{ scale: isChatOpen ? 0.2 : 1 }}
transition={{
...ANIMATION_TRANSITION,
delay: animationDelay,
}}
type={audioVisualizerType}
config={audioVisualizerConfig}
themeMode={themeMode}
isChatOpen={isChatOpen}
className={cn(
'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
'bg-background rounded-[50px] border border-transparent transition-[border,drop-shadow]',
isChatOpen && 'border-input shadow-2xl/10 delay-200',
)}
style={{ color: audioVisualizer?.color }}
/>
</motion.div>
)}
{isAvatar && (
// Avatar Agent
<motion.div
key="avatar"
layoutId="avatar"
initial={{
scale: 1,
opacity: 1,
maskImage:
'radial-gradient(circle, rgba(0, 0, 0, 1) 0, rgba(0, 0, 0, 1) 20px, transparent 20px)',
filter: 'blur(20px)',
}}
animate={{
maskImage:
'radial-gradient(circle, rgba(0, 0, 0, 1) 0, rgba(0, 0, 0, 1) 500px, transparent 500px)',
filter: 'blur(0px)',
borderRadius: isChatOpen ? 6 : 12,
}}
transition={{
...ANIMATION_TRANSITION,
delay: animationDelay,
maskImage: {
duration: 1,
},
filter: {
duration: 1,
},
}}
className={cn(
'overflow-hidden bg-black drop-shadow-xl/80',
isChatOpen ? 'h-[90px]' : 'h-auto w-full',
)}
>
<VideoTrack
width={videoWidth}
height={videoHeight}
trackRef={agentVideoTrack}
className={cn(isChatOpen && 'size-[90px] object-cover')}
/>
</motion.div>
)}
</AnimatePresence>
</div>
<div
className={cn([
'grid',
isChatOpen && tileViewClassNames.secondTileChatOpen,
!isChatOpen && tileViewClassNames.secondTileChatClosed,
])}
>
{/* Camera & Screen Share */}
<AnimatePresence>
{((cameraTrack && isCameraEnabled) || (screenShareTrack && isScreenShareEnabled)) && (
<motion.div
key="camera"
layout="position"
layoutId="camera"
initial={{
opacity: 0,
scale: 0,
}}
animate={{
opacity: 1,
scale: 1,
}}
exit={{
opacity: 0,
scale: 0,
}}
transition={{
...ANIMATION_TRANSITION,
delay: animationDelay,
}}
className="aspect-square size-[90px] drop-shadow-lg/20"
>
<VideoTrack
trackRef={cameraTrack || screenShareTrack}
width={(cameraTrack || screenShareTrack)?.publication.dimensions?.width ?? 0}
height={(cameraTrack || screenShareTrack)?.publication.dimensions?.height ?? 0}
className="bg-muted aspect-square size-[90px] rounded-md object-cover"
/>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
}
components/agents-ui/blocks/agent-session-view-01/index.ts
export * from './components/agent-session-block';
More from agents-ui
Agent Audio Visualizer AuracomponentAgent Audio Visualizer BarcomponentAgent Audio Visualizer GridcomponentAgent Audio Visualizer RadialcomponentAgent Audio Visualizer WavecomponentAgent Chat IndicatorcomponentAgent Chat TranscriptcomponentAgent Control Barcomponent
Browse all components in this library →