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.
import { useLiveKitSession, type SessionOptions } from "@livelayer/react";
function useLiveKitSession(options: SessionOptions): UseLiveKitSessionResult;
Options
interface SessionOptions {
agentId: string;
baseUrl?: string;
apiKey?: string;
sessionEndpoint?: string;
sessionBody?: Record<string, unknown>;
onDataMessage?: (msg: Record<string, unknown>) => void;
}
Return value
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
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.
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.
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:
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.
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.
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.
import { useDisplayMode, useDisplayModePersistence } from "@livelayer/react";
const [mode, setMode] = useDisplayMode("expanded");
useDisplayModePersistence(mode, setMode, "ll-widget-key");
useIsMobile
Viewport width check (default breakpoint 640px).
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.
import { usePathname, useRouteMatch } from "@livelayer/react";
const pathname = usePathname();
const shouldRender = useRouteMatch(pathname, ["/app/**"], ["/app/admin/**"]);
Putting it all together — a custom dock
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.
Read next
- Controlled session — wrap these hooks into a context provider
- TypeScript types — reference for
AgentState,ConnectionState, etc. <livelayer-widget>web component — the same SDK, framework-agnostic