View as markdown

Hooks

<AvatarWidget> is built on top of a set of hooks you can use directly. Skip the prebuilt UI when you need a custom look — your own avatar dock, your own toolbar, your own transcript view.

useLiveKitSession

The session lifecycle hook. Connects to the agent's LiveKit room, manages the transcript, exposes audio/video tracks.

ts
import { useLiveKitSession, type SessionOptions } from "@livelayer/react";

function useLiveKitSession(options: SessionOptions): UseLiveKitSessionResult;

Options

ts
interface SessionOptions {
  agentId: string;
  baseUrl?: string;
  apiKey?: string;
  sessionEndpoint?: string;
  sessionBody?: Record<string, unknown>;
  onDataMessage?: (msg: Record<string, unknown>) => void;
}

Return value

ts
interface UseLiveKitSessionResult {
  // State
  connectionState: ConnectionState;       // "idle" | "connecting" | "connected" | "disconnected" | "error"
  agentState: AgentState;                 // "idle" | "listening" | "thinking" | "speaking"
  transcript: TranscriptEntry[];          // [{ id, role, text, final }]
  agentConfig: AgentConfig | null;        // { name, avatarImageUrl, idleLoopUrl }
  videoElement: HTMLVideoElement | null;  // live agent video — attach to your own DOM
  audioElement: HTMLAudioElement | null;  // agent voice — attach somewhere mounted
  canResume: boolean;                     // session can resume within 5 minutes after disconnect
  error: string | null;

  // Actions
  connect(): Promise<void>;
  disconnect(): void;

  // Underlying access (for advanced cases)
  getRoom(): Room | null;                 // raw livekit-client Room
  session: LiveKitSession | null;         // raw SDK session
}

Example: minimal "talk to agent" UI

tsx
import { useLiveKitSession } from "@livelayer/react";

export function MyAgent() {
  const session = useLiveKitSession({ agentId: "agt_abc123" });

  if (session.connectionState === "idle") {
    return <button onClick={session.connect}>Talk to agent</button>;
  }
  if (session.connectionState === "connecting") {
    return <p>Connecting…</p>;
  }
  if (session.connectionState === "error") {
    return <p>Error: {session.error}</p>;
  }

  return (
    <div>
      <p>Agent state: {session.agentState}</p>
      <ul>
        {session.transcript.map((entry) => (
          <li key={entry.id}>
            <strong>{entry.role}:</strong> {entry.text}
          </li>
        ))}
      </ul>
      <button onClick={session.disconnect}>End</button>
    </div>
  );
}

useLiveKitSession does not publish the mic for you. If you're driving the session manually, also use useMicrophoneState and pass it the room from session.getRoom().

useMicrophoneState

Manages mic track lifecycle: publish, mute, unmute, switch device.

ts
import { useMicrophoneState } from "@livelayer/react";

function MicButton({ room }: { room: Room | null }) {
  const mic = useMicrophoneState(room);

  return (
    <button
      onClick={() => (mic.isMuted ? mic.unmute() : mic.mute())}
      disabled={!mic.isReady}>
      {mic.isMuted ? "🎤 Unmute" : "🔇 Mute"}
    </button>
  );
}

Returns { isReady, isMuted, mute, unmute, switchDevice, error }.

useAudioLevel

Web-Audio analyser for visualizing mic input level. Runs at 60fps in a requestAnimationFrame loop without re-rendering React.

tsx
import { useAudioLevel } from "@livelayer/react";

function MicLevelMeter({ room }: { room: Room | null }) {
  const ref = useRef<HTMLDivElement>(null);

  useAudioLevel(room, (level) => {
    // level: 0 to 1
    if (ref.current) ref.current.style.transform = `scale(${1 + level * 0.4})`;
  });

  return <div ref={ref} className="mic-pulse" />;
}

Pass a callback that receives a 0..1 level on every animation frame.

useCameraState / useScreenShareState

Same shape as useMicrophoneState, but for camera and screen-share tracks. Most agents are voice-only — these are useful when building a video-conferencing-style flow.

useMediaDevices

Enumerate available mics and cameras for a settings UI:

tsx
import { useMediaDevices } from "@livelayer/react";

function DevicePicker() {
  const { microphones, cameras, refresh } = useMediaDevices();

  return (
    <select>
      {microphones.map((d) => (
        <option key={d.deviceId} value={d.deviceId}>{d.label}</option>
      ))}
    </select>
  );
}

useAgentInfo

Fetch agent metadata (name, avatar, idle loop) from the server. Useful before connecting — show the agent's photo on a "Talk to me" button.

ts
import { useAgentInfo } from "@livelayer/react";

function PreConnectCard({ agentId }: { agentId: string }) {
  const { agent, loading, error } = useAgentInfo(agentId);

  if (loading) return <p>Loading…</p>;
  if (error || !agent) return null;

  return (
    <button onClick={onConnect}>
      <img src={agent.avatarImageUrl} alt={agent.name} />
      Talk to {agent.name}
    </button>
  );
}

useTranscript

Subscribes to transcript updates from a session. Returns the current array of entries.

ts
import { useTranscript } from "@livelayer/react";

const transcript = useTranscript(session);

(Most consumers just read session.transcript from useLiveKitSession directly. useTranscript is exposed for cases where you have a session instance from elsewhere.)

useDisplayMode / useDisplayModePersistence

Display state machine. useDisplayMode exposes [mode, setMode]; useDisplayModePersistence syncs to localStorage.

ts
import { useDisplayMode, useDisplayModePersistence } from "@livelayer/react";

const [mode, setMode] = useDisplayMode("expanded");
useDisplayModePersistence(mode, setMode, "ll-widget-key");

useIsMobile

Viewport width check (default breakpoint 640px).

ts
import { useIsMobile } from "@livelayer/react";

const isMobile = useIsMobile(); // boolean
const isVerySmall = useIsMobile(480); // custom breakpoint

usePathname / useRouteMatch

Pathname and route-matching helpers — fall back to window.location if no React Router or Next.js context is detected.

ts
import { usePathname, useRouteMatch } from "@livelayer/react";

const pathname = usePathname();
const shouldRender = useRouteMatch(pathname, ["/app/**"], ["/app/admin/**"]);

Putting it all together — a custom dock

tsx
import {
  useLiveKitSession,
  useMicrophoneState,
  useAudioLevel,
  useAgentInfo,
} from "@livelayer/react";
import { useRef, useEffect } from "react";

export function CustomAgentDock({ agentId }: { agentId: string }) {
  const session = useLiveKitSession({ agentId });
  const room = session.getRoom();
  const mic = useMicrophoneState(room);
  const { agent } = useAgentInfo(agentId);
  const videoRef = useRef<HTMLDivElement>(null);
  const levelRef = useRef<HTMLDivElement>(null);

  useAudioLevel(room, (level) => {
    if (levelRef.current) {
      levelRef.current.style.opacity = String(0.3 + level * 0.7);
    }
  });

  useEffect(() => {
    if (session.videoElement && videoRef.current) {
      videoRef.current.appendChild(session.videoElement);
    }
  }, [session.videoElement]);

  if (!agent) return null;

  return (
    <div className="my-agent-dock">
      <div ref={videoRef} className="agent-video" />
      <div ref={levelRef} className="mic-pulse" />
      <button onClick={mic.isMuted ? mic.unmute : mic.mute}>
        {mic.isMuted ? "🎤" : "🔇"}
      </button>
      <button onClick={session.disconnect}>End</button>
    </div>
  );
}

You're now driving the whole stack — session, mic, audio level — from your own component. The widget package becomes a library, not a layout.