npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alexkroman1/aai-ui

v18.0.0

Published

The browser client for AssemblyAI Agent SDK agents: React 19 hooks and components over a framework-agnostic session core (WebSocket + microphone + playback).

Downloads

4,389

Readme

@alexkroman1/aai-ui

The browser client for AssemblyAI Agent SDK agents: React 19 hooks and components over a framework-agnostic session core (WebSocket + microphone + playback).

npm i @alexkroman1/aai-ui react react-dom

Every agent gets this UI for free — aai dev and deployed agents serve a default client built from this package. Install it directly when the agent has its own client.tsx.

UI integration

The hooks here are what a page reads an agent with: the live call, the state the agent projects, the tool calls as they run, and the durable runs it started. There is no route to write. The agent server already serves the session socket and the workflow HTTP API, so a component talks to a live agent with no glue file in between — a client is one client.tsx calling one mount.

Two front doors, two mounts. A voice agent's page calls mountClient() and talks to a live session. A workflow app's page (workflowApp(), or agent({ page: "static" })) calls mountPage() and talks to the workflow HTTP API — no session, no socket, no microphone. Both are still client.tsx, still React, still the same theme tokens.

Agent agent.ts

The session's state lives in a slot, and syncState is what the browser sees — pushed after every tool call, so the client never rebuilds it from events:

import { agent, sessionSlot } from "@alexkroman1/aai";

const desk = sessionSlot("desk", () => ({ symbols: [] as string[] }));

export default agent({
  name: "Market Desk",
  systemPrompt: "You look up quotes and keep the caller's watchlist.",
  syncState: desk.projection((state) => ({ symbols: state.symbols })),
});

Tool tools/get_quote.ts

A tool is a file, named by its own filename. Nothing registers it, and agent() takes no tools field:

import { tool } from "@alexkroman1/aai";
import { z } from "zod";

export default tool({
  description: "Look up the latest price for one ticker symbol",
  inputSchema: z.object({ symbol: z.string() }),
  execute: async ({ symbol }, ctx) => {
    const res = await fetch(`https://api.example.com/quote/${symbol}`, {
      headers: { authorization: `Bearer ${ctx.env.QUOTES_API_KEY}` },
      signal: ctx.signal,
    });
    return (await res.json()) as { symbol: string; price: number };
  },
});

UI component for a tool quote-card.tsx

useToolResult fires once per settled call of one named tool, with the result already parsed. A component that mounts late still receives the results of calls that finished before it — a result is a value the UI is driven from, not a moment:

import { useState } from "react";
import { useToolResult } from "@alexkroman1/aai-ui";

type Quote = { symbol: string; price: number };

export default function QuoteCard() {
  const [quote, setQuote] = useState<Quote>();
  useToolResult<Quote>("get_quote", (result) => setQuote(result));

  if (!quote) return null;
  return (
    <p>
      {quote.symbol} — {quote.price.toFixed(2)}
    </p>
  );
}

useToolCallStart is the same hook for the other end of the call — the pending invocation and its arguments, for a spinner rather than a value. Either one takes the tool's own shape as a type argument, derived from the tool module by a type-only import that is erased and so pulls no server code into the bundle: useToolResult<InferToolOutput<typeof getQuote>>(…) for the result, useToolCallStart<InferToolInput<typeof getQuote>>(…) for the arguments.

Client client.tsx

mountClient() mounts the default chat shell — start screen, transcript, controls — with your components in it. sidebar takes the COMPONENT, not an element:

import "@alexkroman1/aai-ui/styles.css";
import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
import QuoteCard from "./quote-card.tsx";

function Watchlist() {
  // Whatever `syncState` projected, live. Null until the first push, which is
  // a moment the UI has to render.
  const desk = useAgentState<{ symbols: string[] }>();
  return (
    <div>
      <QuoteCard />
      {desk?.symbols.map((symbol) => <div key={symbol}>{symbol}</div>)}
    </div>
  );
}

mountClient({
  name: "Market Desk",
  sidebar: Watchlist,
  // Icon and label per tool, for the transcript's tool rows.
  tools: { get_quote: { icon: "📈", label: "Fetching quote" } },
  theme: { primary: "#2f9e44" },
});

The type argument above restates a shape the agent already knows. Move the slot and its projection into a shared.ts both ends import, pass the projection itself — useAgentState(deskProjection) — and the state is typed from the projection and the pre-first-push frame derived from it, so there is nothing to restate and no branch for the first render.

Pass component instead of sidebar to replace the whole shell. It renders inside the same providers, so every hook here works in it unchanged.

A workflow app

mountPage() mounts a form over the agent's workflows and installs no session. <WorkflowFields> renders one control per scalar property of the workflow's own input schema, so adding a field to the schema adds it to the page:

import "@alexkroman1/aai-ui/styles.css";
import {
  Form,
  mountPage,
  SubmitButton,
  UploadProgressBar,
  useWorkflowSubmit,
  WorkflowFields,
  WorkflowProgress,
} from "@alexkroman1/aai-ui";

function App() {
  const { submitForm, run, pending, upload, error } =
    useWorkflowSubmit("digest");
  return (
    <main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
      <Form onSubmit={submitForm} error={error}>
        <WorkflowFields workflow="digest" />
        <SubmitButton pending={pending}>Summarize</SubmitButton>
      </Form>
      {/* The upload is its own wait: the run does not exist until the bytes
          are in, so nothing else on the page can describe it. */}
      <UploadProgressBar upload={upload} />
      {/* What the run has SAID, from `stepReport()` in its steps. */}
      <WorkflowProgress runId={run?.runId} />
      {run?.status === "completed" && <pre>{JSON.stringify(run.output)}</pre>}
    </main>
  );
}

mountPage({ name: "Digest", component: App });

Naming the workflow's def — useWorkflowSubmit<typeof digest>("digest"), off a type-only import of agent.ts — is what makes submit() take the declared input and run.status === "completed" narrow to a typed run.output. The import is erased, so it pulls no server code into the bundle.

What is in the package

The API reference groups by TypeScript kind. This is the same surface grouped by what it is for.

Mounts — mountClient(), mountPage(), the config each takes and the handle each returns (ClientConfig, ClientHandle, PageConfig, PageHandle). fetchClientConfig() reads the agent's declared name/greeting on a page, which mountClient() does for itself.

The live call — useSession() for the whole snapshot plus the controls (start, toggle, cancel, reset, resetState, restart, disconnect, end); useSessionSelector() for one slice, with useSessionStatus(), useSessionError() and useSessionActions() as the narrow reads a custom chrome repeats; useUserTranscript() for the caller's in-progress turn, which keeps null (silent) and "" (speech, no words yet) apart; useConversation() for the interleaved transcript with nothing rendered. createBrowserSession() is the same session as a plain store with an immutable snapshot per change, for a non-React client. SessionSnapshot, AgentState, ChatMessage, ToolCallInfo, SessionError and SessionErrorCode come with it.

What the agent pushes — useAgentState() (the syncState projection), useToolResult() / useToolCallStart() (tool calls as they run), and useEvent() (custom events from ctx.send, and the one to prefer in new code over reading tool results).

Chat chrome — ChatView is the whole surface; ConsoleShell, MessageList, Controls, Markdown, ToolCallRow, SessionErrorBanner, StartScreen, SidebarLayout, AutoScroll, BulletList, Facts and Button are its parts, exported so a custom chrome reuses them rather than approximating them. AutoScroll is the one to reach for directly: it is the only scroll-pinning implementation here, and it needs a bounded height. AGENT_STATE_LABELS and WORKFLOW_STATUS_LABELS are the default state words, so a chrome overrides the one term it has a better word for.

Forms — Form collects typed values off the DOM on submit. Field, TextField, NumberField, TextAreaField, SelectField, CheckboxField, FileField and SubmitButton are the controls; WorkflowFields generates them from a schema. A plain <input name="x"> works identically.

Workflows — createWorkflowApi() is the client; useWorkflowSubmit() / useWorkflowStream() (start one and watch it), useWorkflowRun() (watch one by id), useWorkflowRuns() (the history), useWorkflowProgress() (what a run has written), useWorkflows() (the listing), useDownloadUrl() (an upload a run produced, as a URL a DOM element accepts), useRunKey() (the storage-backed key a submission is recovered by). <WorkflowProgress> and <UploadProgressBar> are the rendered halves. WorkflowRun, WorkflowSummary, WorkflowInputOf, WorkflowOutputOf and isTerminal() are the vocabulary.

Theme — useTheme() and ClientTheme.

Other subpaths

The root export is the whole client API. Two subpaths sit beside it, neither of them something a client.tsx reaches for:

| Subpath | Reach for it when | | --- | --- | | /client-dir | serving the prebuilt default client from Node — defaultClientDir(), the filesystem path createRuntimeServer({ clientDir }) wants | | /internal | never, from application code: the plumbing mountClient() installs for itself (the session and theme providers, the default shell's URL chips, the tool-config context, the pre-connection lookup). Not a public API and not covered by semver |

Documentation

Full API reference: https://alexkroman.github.io/agent/