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

@chai-ui/react

v1.4.1

Published

Chai UI React components — smart, drop-in UI for AI apps: Composer, Media Analyzer, Result Card, Edit Card, Side Panel, and the useComposer hook.

Readme

@chai-ui/react

Chai UI for React: drop-in components for the input and output surfaces of generative-media apps. Everything here is controlled — you own the state, these components render it and tell you when it changes.

Building with it, or pointing a coding agent at it? Read DESIGN.md (shipped in the package) for the usage rules: which component to pick, which tokens exist, the built-in AI states, and what not to build.

Chai's components support WCAG 2.2 AA: tested with axe and automated keyboard and focus checks, not by hand with a screen reader. DESIGN.md §6 has the details. Focus rings are visible by default, as WCAG requires. data-chai-focus-ring="hidden" on an ancestor hides them, for preview or showcase surfaces only. Composer's promptLabel names the prompt field (default: the placeholder), and ResultCard's altText sets image alt text (default: the prompt).

Quick start: Composer

Composer is the bar-layout input surface: a prompt field, an attach menu, a use-case chip, model/aspect-ratio pickers, and submit — all driven by which props you supply, not by four separate components.

import { useState } from "react";
import "@chai-ui/tokens/css";
import "@chai-ui/react/style.css";
import { Composer, type ComposerAttachment, type ComposerSubmitPayload } from "@chai-ui/react";

function App() {
  const [value, setValue] = useState("");
  const [attachments, setAttachments] = useState<ComposerAttachment[]>([]);

  function handleSubmit(payload: ComposerSubmitPayload) {
    // payload = { value, attachments, useCase, modelId, aspectRatio } —
    // everything Composer tracks, assembled for you. Send it to whichever
    // model/provider you've set up; Composer never calls one itself.
    console.log(payload);
  }

  return (
    <Composer
      value={value}
      onChange={setValue}
      attachments={attachments}
      onAttachmentsChange={setAttachments}
      onSubmit={handleSubmit}
    />
  );
}

That's the minimal case — no model picker, no aspect ratio, no attach capability beyond the default file picker. Every other control is additive:

<Composer
  value={value}
  onChange={setValue}
  attachments={attachments}
  onAttachmentsChange={setAttachments}
  // A use-case chip + model/aspect-ratio pickers only appear once `useCase`
  // is set — wire it from the attach menu's own picks, or set it yourself.
  useCase={useCase}
  onClearUseCase={() => setUseCase(null)}
  onAttachMenuSelect={(action) => {
    if (action === "create-image") setUseCase({ kind: "image", label: "Image" });
  }}
  // The Model menu lists Chai's suggested models for the use case, unless
  // you pass your own `models` (see "Suggested models"). Nothing is picked
  // for the person: submit waits until they pick a model or turn on
  // auto-select. Without `onModelChange` there's no menu, and the list's
  // first model runs.
  modelId={modelId}
  onModelChange={setModelId}
  // An "Auto-select model" switch appears at the top of the model menu so
  // the end user can flip it — omit `showAutoSelectToggle` to keep it a
  // fixed, builder-only setting with no visible control instead.
  autoSelectModel={autoSelectModel}
  showAutoSelectToggle
  onAutoSelectModelChange={setAutoSelectModel}
  // The Aspect ratio menu offers what the selected models take. Suggested
  // models carry their `aspectRatios`; give your own models theirs, e.g.
  // ["1:1", "16:9", "9:16"]. A model without them (image-to-video follows
  // the input image) shows no menu.
  aspectRatio={aspectRatio}
  onAspectRatioChange={setAspectRatio}
  // Omit onEnhance entirely to hide the "optimize prompt" button — e.g.
  // before you've wired an engine that can actually run the rewrite.
  onEnhance={(currentValue) => runEnhance(currentValue).then(setValue)}
  onSubmit={handleSubmit}
/>

The full prop list — including attachments' file-reading behavior, onUnsupportedFile, and disabled/submitDisabled — is documented as JSDoc directly on ComposerProps in src/Composer.tsx; every prop and event follows the same value/onChange shape used everywhere else in this package.

See the Composer page on the docs site for a live example with every opt-in feature.

No use case picked: a text request

A prompt typed with no use case picked is a text request (TEXT_USE_CASE): it gets a text reply rather than doing nothing. That's how to use the Composer as a plain chat bar for an LLM:

  • The model: useComposer sends it to textEngine with textModel when you set them, otherwise to ChaiProvider's reasoning model (default Claude Opus 5 on OpenRouter). Without a ChaiProvider or a textEngine, a mock text engine answers, so wrap your app in <ChaiProvider> for real replies.
  • No Model menu: the suggested lists cover image and video only, so a text request shows no Model menu. To offer a choice of text models, pass your own models.
  • The reply streams into the ResultCard as it's written, and cancel() stops it, keeping what has arrived.
<ChaiProvider>
  <Composer value={value} onChange={setValue} onSubmit={submit} submitting={busy} onAbort={cancel} />
  {run && <ResultCard results={run.results} prompt={run.request.prompt} onAction={() => {}} />}
</ChaiProvider>

For an app that's mostly about images, set the default instead; it's never shown as a chip, and it brings the suggested image models and the Aspect ratio menu:

<Composer defaultUseCase={{ kind: "image", label: "Image" }} … />

Several models, several use cases (builder opt-ins, off by default)

// Several models: each produces its own result, paged inside one ResultCard.
<Composer multiSelectModels modelIds={modelIds} onModelIdsChange={setModelIds} models={models} … />

// Image and Video from one prompt (advanced): a Model menu per use case,
// one model each picked by hand (no auto-select), and submit waits for both. Only Image and Video combine;
// nextUseCases makes any other pick replace the rest.
<Composer
  multiSelectUseCases
  useCases={useCases}
  onUseCasesChange={setUseCases}
  onAttachMenuSelect={(action) => {
    const picked = useCaseFor(action); // your mapping from menu item to use case
    if (picked) setUseCases((prev) => nextUseCases(prev, picked));
  }}
  modelsByKind={{ image: imageModels, video: videoModels }}
  modelIds={modelIds}
  onModelIdsChange={setModelIds}
  …
/>

Either way onSubmit receives selections: one { useCase, modelIds, models } entry per use case. An image arrives in seconds and a video can take minutes, so show each use case's run in its own ResultCard (useComposer's runs, one per use case) rather than paging them together.

With autoSelectModel on (not with multiSelectUseCases), the trigger reads "Auto-select" and useComposer picks the model at submit: the reasoning model reads the prompt and media and chooses from that use case's models. When the end user owns the switch (showAutoSelectToggle), auto-select belongs to the use case it was turned on for: picking another use case calls onAutoSelectModelChange(false), so the new one's model is picked by hand. Pass onModelChange (or onModelIdsChange) to the hook too, so the trigger shows the pick:

const { submit, routing } = useComposer({ engine, onModelChange: setModelId });

If routing can't run (no ChaiProvider, a failed reasoning call, an unlisted reply), the request runs on the first listed model and onRoutingFallback says why.

Attached images and the picked ratio

Attaching images doesn't change the Model menu or hide Aspect ratio. Each suggested model names its image-taking Fal endpoint (withImages), and the Fal engine calls it when images are attached. The picked ratio always wins: image models take it directly, and a video's start image is fitted to it before it's sent, cropped around its center by default or padded with black bars:

const { submit } = useComposer({ engine, startImageFit: "pad" }); // default "crop"

Quality checks: evaluations

Each result can carry pass or fail tags, shown on the ResultCard's flip side. They come from two places, and you can use either or both:

  • Your backend: add a top-level evaluations: [{ id, label, passed }] field to the JSON your route returns (for a stream, send it as its own data: chunk). The Fal and OpenRouter engines read it.
  • The opt-in judge: a model you name checks each text or image result against your criteria, one tag per check. It runs on the reasoning engine (ChaiProvider). It's off by default, with no default model. Without judgeChecks it runs one general "Matches prompt" check. What else counts as good is up to your app.
const { submit } = useComposer({
  engine,
  judgeModel: "google/gemini-3.5-flash",
  judgeChecks: [
    // Your criteria, in your words. This one is an example for a marketing app's images.
    { id: "on-brand", label: "On brand", criterion: "Photographic and polished, never cartoonish.", kind: "image" },
    ...DEFAULT_EVALUATION_CHECKS,
  ],
});

The judge's prompt is buildJudgePrompt (exported, so you can read it). Pass judgePrompt to replace it, calling buildJudgePrompt to add to it rather than rewrite it.

Analytics: onEvent

Every component reports what the person does and sees, one ChaiEvent each, for whatever analytics tool you use. Write one function and pass it to each component you use:

import posthog from "posthog-js";
import type { ChaiEvent } from "@chai-ui/react";

const track = (e: ChaiEvent) => posthog.capture(e.name, e.properties);

<Composer onEvent={track} … />
<ResultCard onEvent={track} … />
<SidePanel onEvent={track} … />
<MediaAnalyzer onEvent={track} … />
<EditCard onEvent={track} … />

Events cover submits, Stop, use case, model and option picks, attachments, Enhance, voice input, results shown (once each), result actions, paging, the details side; the side panel opening, closing, New chat, prompt tips (by position) and its page chip; the media analyzer's attach and remove, model, auto-select and prompt length picks, submit, Stop and each analysis finishing (done or error); and the edit card's regions added, confirmed and removed, zoom, paging through versions, download and share. Moving and resizing a region aren't reported. They're front end only: no prompt text, file names, URLs or outputs, only lengths, counts and Chai's own ids. Track model calls, latency and cost on your server, where they're accurate.

Where the bar sits: menuPlacement

Every menu in the bar (the + menu, Model, Aspect ratio, Voice, Genre, Frame rate) opens the same way. Tell Composer where the bar is docked:

<Composer menuPlacement="above" {...props} /> // docked to the bottom of the screen, like a chat app
<Composer menuPlacement="below" {...props} /> // docked to the top

Leave it out ("auto") for a bar that sits in the page: each menu is measured when it opens and goes below if it fits, else above if that fits, else toward the side with more room, so it's never cut off by the edge of the screen.

Your own font

style.css loads Roboto, Material Design 3's typeface. If your app has its own font, import @chai-ui/react/style-without-fonts.css in its place (the same styles, without Roboto) and point the components at your font:

:root { --chai-type-family-material: "Your Font", system-ui, sans-serif; }

Dark theme

The components have a dark theme. It's opt-in: set data-chai-theme="dark" on <html>, or on any element, and every component inside it switches.

<html data-chai-theme="dark">

Chai doesn't follow the system setting on its own, because your app decides its theme. Following it, overriding a single color, and the dark values themselves are in the @chai-ui/tokens README.

What else this package ships

  • MediaAnalyzer — drop in an image, video, or audio clip and get a generation-ready prompt back, with kind-aware model menus (suggested OpenRouter models unless you pass your own) and a prompt length picker. Wire it with useMediaAnalyzer(), which sends the media to ChaiProvider's reasoning model and returns the prompt: onSubmit={submit} submitting={analyzing} submitError={error}.
  • ResultCard — renders one prompt's generated results (image, video, audio, or text) with like/dislike/retry/download/share, a configurable "more" menu, pagination across multiple results, and an info flip showing the prompt and model metadata. Live example: chai-ui.com/docs/components/result-card.
  • EditCard — edit an attached image by marking regions on it, each with its own instruction, then page through the edits as versions. See "Editing an image" below.
  • SidePanel — a panel beside your page with a Composer and its answers, with opt-in page context and prompt tips. See "A side panel" below.
  • Primitives (src/primitives/) — Toggle (an MD3 switch), SearchMenu (the model/option menu behind every picker — Chai's own rather than Material's <md-menu>; DESIGN.md §3 says why), and Pagination (a progress-style pager any paginated surface can reuse). The components are built from these; they're also exported directly if you're assembling your own layout.

Suggested models

You don't have to write model lists. Chai ships suggested lists, checked against each provider's own catalog, and the components use them when you pass none:

| Use case | Export | Provider | |---|---|---| | Create image | SUGGESTED_IMAGE_MODELS | Fal | | Create video from a prompt | SUGGESTED_TEXT_TO_VIDEO_MODELS | Fal | | Create video from an image | SUGGESTED_IMAGE_TO_VIDEO_MODELS | Fal | | Edit image | SUGGESTED_EDIT_MODELS | Fal | | Media analysis, per media kind | SUGGESTED_ANALYSIS_MODELS | OpenRouter |

  • Composer offers the list for the current use case (suggestedModels(useCase)). It never picks for the person: with a Model menu, submit waits for a pick or auto-select; with no menu, the list's first model runs. MediaAnalyzer offers the analysis lists.
  • Your own list wins. Pass models (or modelsByKind) to replace a list, or [] for no Model menu. Nothing is required.
  • Each suggested model knows its inputs: its aspect ratios, and for Fal the field its image goes in (falInput), so the Fal engine sends exactly what that model expects.
  • They change only through package releases. Providers change faster, so check them while setting up your engine. It needs no key, runs no model and costs nothing:
import { checkSuggestedModels } from "@chai-ui/react";

const { ok, problems } = await checkSuggestedModels();
// problems: [{ provider: "fal", modelId, problem: "No longer takes these aspect_ratio values: 21:9." }]

It confirms each model still exists and is active, still takes the aspect ratios and image field Chai sends (Fal), and still accepts its media kind (OpenRouter). If something changed, pick another model or update the package once a release catches up.

useComposer — wiring Composer to a real engine

import { ChaiProvider, useComposer, createFalEngine, Composer, ResultCard } from "@chai-ui/react";

// The reasoning models write text (Enhance, routing, text requests) and
// are separate from the media engine. With no props,
// Claude Opus 5 handles text and images, Gemini 3.8 Flash handles video
// and audio, both on `createOpenRouterEngine()`. Without a provider,
// Composer's Enhance button stays hidden.
function Root() {
  return (
    <ChaiProvider>
      <App />
    </ChaiProvider>
  );
}

function App() {
  const { run, runs, submit, cancel, enhance, enhancing } = useComposer({
    engine: createFalEngine({ outputKind: "image" }),
  });
  // Every model's result from every use case, paged in one card.
  const results = runs.flatMap((r) => r.results);
  const busy = results.some((r) => r.status === "queued" || r.status === "running");

  return (
    <>
      <Composer
        value={value}
        onChange={setValue}
        onSubmit={submit}
        submitting={busy}
        onAbort={cancel}
        onEnhance={enhance ? (v) => enhance(v).then(setValue) : undefined}
        enhancing={enhancing}
        // ...the rest of Composer's own props, as above
      />
      {run && (
        <ResultCard
          results={results}
          prompt={run.request.prompt}
          onAction={(action, result) => {
            // like / dislike / retry / download / share / your "more" actions
          }}
        />
      )}
    </>
  );
}

Enhance's instruction is buildEnhancePrompt (exported, so you can read it). Pass enhancePrompt to useComposer to replace it, calling buildEnhancePrompt to add to it rather than rewrite it:

useComposer({
  engine,
  enhancePrompt: (value, mode, field) => `${buildEnhancePrompt(value, mode, field)}\n\nKeep our brand's warm, natural style.`,
});

submit takes Composer's ComposerSubmitPayload directly — pass it straight to onSubmit. It turns each selection into a real Request/Run/Result: creates a "queued" Result per model up front, then folds each one to "running" → "done"/"error" as the engine settles it. run is the first Run (or null before the first submit); runs holds one per use case when multiSelectUseCases is on. Pass ResultCard the results of every run, as above: it pages through them, and each page shows its own kind (image, video, audio or text). Passing only run.results would hide every use case after the first.

The server route. createFalEngine(), createVercelEngine() and the OpenRouter reasoning engine call /api/chai/* on your server, which adds the API keys. Add it with one file, then set FAL_KEY, OPENROUTER_API_KEY or AI_GATEWAY_API_KEY:

// Next.js: app/api/chai/[...path]/route.ts
import { createChaiHandler } from "@chai-ui/core/server";
export const { GET, POST, PUT } = createChaiHandler();

Other frameworks, and the authorize / allowedModels settings to add before going live, are in the @chai-ui/core README.

Without a server route. In a desktop app (Tauri, Electron), write an engine that hands each run to your app's backend. Pass it to useComposer({ engine: desktopEngine }) and to <ChaiProvider reasoningEngine={desktopEngine}>. The @chai-ui/core README has a Tauri example, voice input, and what Stop does there.

Every default is a suggestion. Swap any of them:

<ChaiProvider
  reasoningModel="your/text-and-image-model"
  // Cheaper video and audio analysis; kinds you leave out keep their default.
  reasoningModelByKind={{ video: "qwen/qwen3.8-omni-flash", audio: "qwen/qwen3.8-omni-flash" }}
  reasoningEngine={yourEngine}
/>

The defaults are exported as DEFAULT_REASONING_MODEL and DEFAULT_REASONING_MODEL_BY_KIND.

useComposer's own reasoningEngine + reasoningModel override the provider's, but only when both are set. Text requests go to textEngine (default: the reasoning engine, then a mock text engine), using textModel (default: the reasoning model) when no model is picked. Other kinds go to engineByKind[kind], then engine. With no model picked, the engine's own default model runs.

Stop. cancel() stops the current submit: wire it to Composer's onAbort, with submitting true while results are queued or running. Unfinished results turn "cancelled" (ResultCard shows "Stopped"; a streamed text result keeps what arrived), and a new submit cancels the previous one. Cancellation is best effort at the provider: Fal removes a queued request and signals a running one, which may still finish; OpenRouter stops a streaming request, and its billing, only for providers that support it (not Google); a non-streaming request keeps running. A custom engine receives signal in generate(): cancel at the provider if it can, then reject with an AbortError (abortError()).

Editing an image: EditCard

The attach menu's "Edit image" puts Composer in edit mode. Set the use case to EDIT_IMAGE_USE_CASE, pass the attached image to useComposer as editImage, and drop in the card:

import { Composer, EditCard, EDIT_IMAGE_USE_CASE, SUGGESTED_EDIT_MODELS, useComposer } from "@chai-ui/react";

const editImage = useCase?.edit ? (attachments.find((a) => a.kind === "image") ?? null) : null;
const { run, submit, edit } = useComposer({ engine: createFalEngine({ outputKind: "image" }), editImage });
// The pick decides how many regions it takes (Flux 3 Image's until one is picked).
const model = SUGGESTED_EDIT_MODELS.find((m) => m.id === modelId) ?? SUGGESTED_EDIT_MODELS[0];

{edit && <EditCard {...edit} maxRegions={model?.maxRegions} />}
{!edit && run && <ResultCard results={run.results} prompt={run.request.prompt} onAction={…} />}
<Composer
  // ...your other Composer props
  useCase={useCase}
  onAttachMenuSelect={(action) => {
    if (action === "edit-image") setUseCase(EDIT_IMAGE_USE_CASE);
  }}
  regions={edit?.regions}
  onRegionsChange={edit?.onRegionsChange}
  onSubmit={submit}
/>
  • Regions. "New region" adds a box with an instruction field. The check adds it to the prompt (a colored chip in the Composer); trash removes it. Drag to move, drag a corner to resize; by keyboard, arrows move and Shift
    • arrows resize. Zoom keeps the image inside the card; drag to pan.
  • Submitting. An edit needs the image plus a whole-image prompt, a region, or both. Regions carry their own instructions, so the prompt can stay empty.
  • Models. In edit mode Composer offers SUGGESTED_EDIT_MODELS: Flux 3 Image first, which reads each region as a box, then Nano Banana Pro and GPT Image 2. Offer any edit model you like; one without a regionFormat gets its regions described in words, which is less precise. Change how the prompt is built with useComposer's editPrompt.
  • Versions. Each edit's result is a new version, shown in the card with dots to page back. The next edit applies to the version showing.
  • Images only for now.

A side panel: SidePanel

A panel on one side of your page with a Composer and its answers, for quick model use without leaving the page. People can ask about the page they're on or about anything else:

import { SidePanel } from "@chai-ui/react";

<div style={{ display: "flex" }}>
  <main style={{ flex: 1 }}>{page}</main>
  <SidePanel
    title="Northwind Assistant"
    description="Ask about this page, or anything else."
    pageContext
    promptTips={["Summarize this page", "What changed for free accounts?", "Draft a polite out-of-office reply"]}
  />
</div>
  • Answers come from your ChaiProvider's reasoning engine, or pass textEngine (and textModel). There's no model menu, aspect ratio or Enhance in the panel; voice input works as on the Composer (voiceInput).
  • Page context is off by default. With pageContext, the panel reads the page's title, URL, selected text and main text (readPageContext: <main>, then <article>, then <body>, capped at 12,000 characters) and sends it with each question. A "This page" chip shows it's on; the person can remove it to ask something else, and put it back from the + menu. Pass a function instead of true to read the page your own way. Mark anything to leave out with data-chai-page-context="skip". Sending a page's text to a model is your decision to make on purpose.
  • Prompt tips: two to four short examples under "Getting started" (promptTipsLabel). A tap sends one. Include at least one that isn't about the page.
  • Layout: side="right" (default) or "left". mode="push" (default) takes its own width beside your content, so place it as a flex sibling; mode="overlay" floats over the page. contained keeps an overlay inside its positioned parent. Below 600px wide, both open as a full-screen sheet. --chai-side-panel-width sets the width (400px).
  • Opening: a launcher button opens it; Close clears the conversation, and New chat starts over without closing. Control it with open and onOpenChange, and set launcher={false} to use your own button. Opening moves focus to the prompt, Esc closes the panel (after any open menu), and closing returns focus to the launcher.
  • Your mark: logo replaces the default sparkle in the launcher, the greeting (44px) and the header (28px), shown as given (an img or svg fills the box) without the sparkle's green tile.
  • Follow-ups work: the last six exchanges go with each question (buildSidePanelPrompt).
  • Answers style: answers="simple" (default) shows text on the panel with like, dislike, retry and copy; "card" puts each answer in a ResultCard. footer adds a note under the Composer, such as a disclaimer.
  • Analytics: onEvent reports opening, closing, New chat, prompt tips (by position) and the page chip, plus the Composer's and the answers' own events.