shadimage
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 · medium
animationmotion

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

Browse all components in this library →