@chai-ui/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/DESIGN.md +207 -0
  2. package/LICENSE +21 -0
  3. package/README.md +265 -0
  4. package/dist/ChaiProvider.d.ts +49 -0
  5. package/dist/ChaiProvider.d.ts.map +1 -0
  6. package/dist/ChaiProvider.js +54 -0
  7. package/dist/ChaiProvider.js.map +1 -0
  8. package/dist/Composer.d.ts +169 -0
  9. package/dist/Composer.d.ts.map +1 -0
  10. package/dist/Composer.js +389 -0
  11. package/dist/Composer.js.map +1 -0
  12. package/dist/MediaAnalyzer.d.ts +102 -0
  13. package/dist/MediaAnalyzer.d.ts.map +1 -0
  14. package/dist/MediaAnalyzer.js +250 -0
  15. package/dist/MediaAnalyzer.js.map +1 -0
  16. package/dist/ResultCard.d.ts +55 -0
  17. package/dist/ResultCard.d.ts.map +1 -0
  18. package/dist/ResultCard.js +409 -0
  19. package/dist/ResultCard.js.map +1 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +11 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/primitives/Pagination.d.ts +19 -0
  25. package/dist/primitives/Pagination.d.ts.map +1 -0
  26. package/dist/primitives/Pagination.js +13 -0
  27. package/dist/primitives/Pagination.js.map +1 -0
  28. package/dist/primitives/SearchMenu.d.ts +69 -0
  29. package/dist/primitives/SearchMenu.d.ts.map +1 -0
  30. package/dist/primitives/SearchMenu.js +88 -0
  31. package/dist/primitives/SearchMenu.js.map +1 -0
  32. package/dist/primitives/Toggle.d.ts +9 -0
  33. package/dist/primitives/Toggle.d.ts.map +1 -0
  34. package/dist/primitives/Toggle.js +28 -0
  35. package/dist/primitives/Toggle.js.map +1 -0
  36. package/dist/primitives/index.d.ts +4 -0
  37. package/dist/primitives/index.d.ts.map +1 -0
  38. package/dist/primitives/index.js +4 -0
  39. package/dist/primitives/index.js.map +1 -0
  40. package/dist/style.css +1860 -0
  41. package/dist/use-latest.d.ts +7 -0
  42. package/dist/use-latest.d.ts.map +1 -0
  43. package/dist/use-latest.js +17 -0
  44. package/dist/use-latest.js.map +1 -0
  45. package/dist/useComposer.d.ts +75 -0
  46. package/dist/useComposer.d.ts.map +1 -0
  47. package/dist/useComposer.js +191 -0
  48. package/dist/useComposer.js.map +1 -0
  49. package/dist/useMediaAnalyzer.d.ts +37 -0
  50. package/dist/useMediaAnalyzer.d.ts.map +1 -0
  51. package/dist/useMediaAnalyzer.js +114 -0
  52. package/dist/useMediaAnalyzer.js.map +1 -0
  53. package/package.json +88 -0
  54. package/src/ChaiProvider.tsx +88 -0
  55. package/src/Composer.tsx +961 -0
  56. package/src/MediaAnalyzer.tsx +527 -0
  57. package/src/ResultCard.tsx +980 -0
  58. package/src/index.ts +36 -0
  59. package/src/primitives/Pagination.tsx +41 -0
  60. package/src/primitives/SearchMenu.tsx +284 -0
  61. package/src/primitives/Toggle.tsx +43 -0
  62. package/src/primitives/index.ts +3 -0
  63. package/src/style.css +1860 -0
  64. package/src/use-latest.ts +18 -0
  65. package/src/useComposer.ts +289 -0
  66. package/src/useMediaAnalyzer.ts +157 -0
package/DESIGN.md ADDED
@@ -0,0 +1,207 @@
1
+ # CHAI UI — DESIGN.md
2
+
3
+ The rules for building with `@chai-ui/react`, written for the people and
4
+ the coding agents who integrate it. The API reference is the JSDoc on each
5
+ component's props and the docs site's `llms.txt`. This file covers how to
6
+ use the pieces correctly: which component to pick, which tokens to use,
7
+ which states you get for free, and what not to build.
8
+
9
+ If this file and the code disagree, the code wins. Please report the
10
+ mismatch.
11
+
12
+ ## 1. Pick the component, don't rebuild it
13
+
14
+ | You need | Use | Not |
15
+ |---|---|---|
16
+ | A prompt box with attach, model and aspect-ratio pickers, submit | `Composer` | A hand-built textarea + button row |
17
+ | Media (image, video, audio) in, a generation-ready prompt out | `MediaAnalyzer` + `useMediaAnalyzer` | A file input plus your own model call |
18
+ | One prompt's generated output (image, video, audio, text) | `ResultCard` | A custom media card |
19
+ | Submit → engine → result state, wired together | `useComposer` | Your own fetch/loading/error state |
20
+ | One reasoning model shared by every component | `ChaiProvider`, `useReasoning` | A text model picked per component |
21
+ | A switch, an option menu, a pager | `Toggle`, `SearchMenu`, `Pagination` | Another UI kit's versions inside a CHAI surface |
22
+
23
+ These don't exist yet. Don't import them, and don't invent stand-ins that
24
+ use these names: `Results`, `Compare`, `ModelPicker`,
25
+ `ParamControls`, `Presets`, `RunSummary`, `MediaViewer`, `createHttpEngine`.
26
+
27
+ ## 2. Integration rules
28
+
29
+ 1. **Load the CSS in order:** `@chai-ui/tokens/css` first, then
30
+ `@chai-ui/react/style.css`. Component styles read the token variables.
31
+ 2. **Everything is controlled.** Every value comes in as a prop and every
32
+ change comes out as a callback (`value` / `onChange`). Keep the state in
33
+ your app.
34
+ 3. **Advanced selection is the builder's opt-in.** `multiSelectModels`
35
+ and `multiSelectUseCases` are off by default. Turn them on only where
36
+ comparing models or making several media types at once is the point.
37
+ 4. **A callback's presence is the opt-in.** Omit `onEnhance` and there's
38
+ no optimize button. Omit `onAbort` and there's no stop button. Omit
39
+ `moreActions` and there's no ⋯ menu. Don't pass no-op handlers just to
40
+ make a control appear.
41
+ 5. **API keys never reach the browser.** Engines call one same-origin
42
+ route, `/api/chai/*`, served by `createChaiHandler` from
43
+ `@chai-ui/core/server`, which adds the key. Never pass a key to
44
+ `createFalEngine` or `createOpenRouterEngine`. In production, set the
45
+ handler's `authorize` and `allowedModels`.
46
+ 6. **Media generation and reasoning are separate choices.** `engine`
47
+ generates media and defaults to Fal.ai. `ChaiProvider`'s
48
+ reasoning models power Enhance, routing and text requests. Defaults:
49
+ Claude Opus 5 for text and images, Gemini 3.8 Flash for video and
50
+ audio, on OpenRouter. They're suggestions; respect a builder's own
51
+ `reasoningModel`, `reasoningModelByKind` and `reasoningEngine`.
52
+ 7. **Only show what's real.** Show cost only when the engine reports it.
53
+ Don't show model rankings or leaderboard positions you can't keep up to
54
+ date. Don't show a control the selected model can't honor.
55
+
56
+ ## 3. Material components, and where Chai uses its own
57
+
58
+ Chai builds on Material Design 3's web components where they fit, and
59
+ uses its own component where they don't. Both kinds take every color from
60
+ Chai tokens (§4), so they look like one system.
61
+
62
+ | Native Material component | Where Chai uses it |
63
+ |---|---|
64
+ | `<md-fab>` | The submit, retry and stop button |
65
+ | `<md-switch>` (`Toggle`) | Auto-select model, and any switch you add |
66
+ | `<md-checkbox>` | The checkboxes on rows in a multi-select model menu |
67
+ | `<md-outlined-text-field>` | The search field in searchable menus |
68
+ | `<md-focus-ring>` | The focus ring on all of the above |
69
+
70
+ Chai's own components are the menus (`SearchMenu` and the attach menu),
71
+ the use-case chips, `Pagination`, and the cards. The menus are the main
72
+ difference, because Material has an `<md-menu>`:
73
+
74
+ - **Placement.** `<md-menu>` opens as a floating popover anchored to a
75
+ corner of its trigger. Chai's menus open as a panel directly under their
76
+ trigger, inside the component, as the Figma designs show.
77
+ - **Features `<md-menu>` doesn't have.** Chai's model menus need a search
78
+ field, grouped sections with headings (one per use case), a switch
79
+ pinned at the top (auto-select), and multi-select. `<md-menu>` closes
80
+ when an item is picked and has none of these.
81
+ - **Portability.** Chai isn't tied to Material. Its own components restyle
82
+ entirely through tokens, which keeps the door open to other design
83
+ systems later.
84
+
85
+ They still follow Material's conventions where they apply: a trailing
86
+ check mark for single-select, MD3's own checkbox for multi-select,
87
+ and Material's type scale. Each option is a plain button: reach it with
88
+ Tab, pick it with Enter or Space; `aria-pressed` marks what's picked.
89
+
90
+ ## 4. Tokens
91
+
92
+ All visual values come from `@chai-ui/tokens`, which builds from one JSON
93
+ source into CSS variables, JS/TS and a Figma token set. **It is the only
94
+ source of color:** no component contains a hard-coded color, and Material's
95
+ colors are driven by Chai tokens, not the other way around.
96
+
97
+ | Layer | Pattern | Use it for |
98
+ |---|---|---|
99
+ | Root palette | `--chai-color-root-{gray, green, amber, red}-*` | Nothing directly. It only feeds the layers below. `green-50` (`#009747`) is the brand green. |
100
+ | Brand and state | `--chai-color-semantic-{accent, accent-hover, accent-subtle, accent-soft, accent-text, text-on-accent, warning, warning-subtle, danger, danger-subtle}` | The brand green and state colors. Use `accent` for fills, icons and focus rings, and `accent-text` for green text (`accent` is only 3.8:1 on white). |
101
+ | Controls | `--chai-color-semantic-control-{surface, surface-hover, surface-raised, track, divider, border, border-soft, border-faint, text-strong, text, text-muted}` | The pure grays the components are built from. |
102
+ | Overlays and media | `--chai-color-semantic-{overlay, on-overlay, scrim, media-backdrop}` | Buttons and backdrops on top of images and video |
103
+ | Space | `--chai-space-0` … `--chai-space-8` (0, 4, 8, 12, 16, 24, 32, 48, 64px) | Padding, gaps, margins |
104
+ | Radius | `--chai-radius-{sm, md, lg, full}` | Corners |
105
+ | Type | `--chai-type-{family, size, weight, leading, tracking}-*` | Text |
106
+ | Motion | `--chai-motion-duration-{fast, base, slow, steep}`, `--chai-motion-easing-{standard, growth}` | Transitions |
107
+ | Effects | `--chai-shadow-menu`, `--chai-gradient-ai` | Floating menus, and the AI shimmer after prompt optimization |
108
+ | Component | `--chai-composer-{bg, border, hover, placeholder, text}`, `--chai-media-analyzer-{bg, border, hover, text}` | Restyling that one component. Each defaults to a control role. |
109
+ | Material | `--md-sys-color-*` | Nothing in your code. They exist so the Material switch, button and text field follow Chai tokens. |
110
+
111
+ Rules:
112
+
113
+ - **Never invent a token name.** If it isn't listed above or in
114
+ `@chai-ui/tokens/css`, it doesn't exist, and `var()` on an unknown name
115
+ silently falls back to nothing.
116
+ - **Never put a literal color on a component.** Use a semantic role. If a
117
+ design needs a color that has no role yet, add the role to
118
+ `packages/tokens/src/tokens.json` instead of writing the hex.
119
+ - **Restyle by overriding variables, not selectors.** Override the
120
+ semantic roles on `:root` to restyle everything, including the Material
121
+ components, or set a component variable like `--chai-composer-bg` on
122
+ `.chai-composer` to restyle one component. Don't target internal
123
+ `.chai-*__*` class names. Those are implementation details and can
124
+ change without notice.
125
+
126
+ ## 5. AI states you get for free — use them, don't replace them
127
+
128
+ | Situation | Built-in behavior | Your part |
129
+ |---|---|---|
130
+ | Generation failed | `Composer`'s submit button becomes a retry action and an inline message appears (`submitError`) | Set `submitError`, and clear it on success or when the prompt changes. **No error dialogs or toasts for a failed generation.** |
131
+ | Request in flight | Submit becomes a stop button (`submitting` + `onAbort`) | Pass `useComposer`'s `cancel` as `onAbort`; results show "Stopped" |
132
+ | Prompt too thin | Optimize button rewrites the prompt, then offers undo (`onEnhance`) | Run the rewrite and call `onChange` with the new text |
133
+ | No use case picked | The prompt is a text request and gets a text reply | Set `defaultUseCase` if your app's default should be image or video |
134
+ | Result pending | `ResultCard` shows a spinner for `queued` / `running` | Nothing. `useComposer` sets the status. |
135
+ | Long text answer | `ResultCard` keeps line breaks, scrolls past 400px with a visible scrollbar, and follows streaming text unless the person scrolls up | Change the height with `textMaxHeight` if needed |
136
+ | Text streaming in | `ResultCard` renders partial text with a caret, marked `aria-busy` | Use an engine that calls `onText` (OpenRouter does, and so does the mock text engine). `useComposer` wires it. |
137
+ | Result failed | `ResultCard` shows the error message in place | Nothing |
138
+ | Try again | `ResultCard`'s retry fires `onAction("retry", result)` | Re-run the **same** model call, not a different one |
139
+ | Feedback | Like/dislike fire `onAction`, and the displayed vote comes from `getVote` | Store votes yourself |
140
+ | Details | The flip side shows prompt, model, tokens, cost and duration | Supply `models` so ids resolve to labels |
141
+ | Quality checks | Each result's `evaluations` render on the flip side: green with a check if it passed, red with a cross if it failed | Run your checks in your backend proxy and add a top-level `evaluations: [{ id, label, passed }]` field to the JSON it returns (for a stream, send it as its own `data:` chunk). The built-in Fal and OpenRouter engines read that field, a custom engine returns it from `generate`, and `useComposer` puts it on the matching `Result`. CHAI never runs or scores anything itself, and there are no confidence scores. |
142
+
143
+ `ResultCard` is where a run ends. Don't add a "use as input" action that
144
+ feeds a result back into a `Composer`. Chaining steps like that is
145
+ deliberately out of scope.
146
+
147
+ ## 6. Accessibility
148
+
149
+ - Every icon-only button in CHAI has an accessible name. In `moreActions`,
150
+ `label` is the item's visible and announced text, so pass an `icon` that
151
+ is `aria-hidden`.
152
+ - Animations (the optimize reveal, the streaming caret) are disabled under
153
+ `prefers-reduced-motion`. Your own motion around CHAI should do the same.
154
+ - **Focus rings are on by default, as WCAG 2.2 AA requires** (2.4.7 Focus
155
+ Visible). Every control shows a ring when it
156
+ has keyboard focus: buttons and menu items use the browser's own ring,
157
+ MD3 elements use `<md-focus-ring>`, and the Composer rings its whole pill
158
+ while the prompt field is focused. Hover fills don't replace these,
159
+ because hover never appears for someone navigating with a keyboard.
160
+ - **To hide them**, set `data-chai-focus-ring="hidden"` on any ancestor.
161
+ Every CHAI component inside it stops drawing focus rings, and your own
162
+ controls under the same ancestor are unaffected. Remove the attribute to
163
+ turn them back on:
164
+
165
+ ```tsx
166
+ <div data-chai-focus-ring={showFocusRings ? undefined : "hidden"}>…</div>
167
+ // or page-wide:
168
+ document.documentElement.dataset.chaiFocusRing = "hidden";
169
+ ```
170
+
171
+ **Hiding focus rings breaks WCAG 2.2 AA (and 2.1 AA) compliance.** Keyboard users
172
+ lose any indication of where they are. Only hide them on showcase or
173
+ preview surfaces, like the docs site's component previews, not in a
174
+ production app. Don't set `tabIndex={-1}` on CHAI controls either.
175
+ - Streaming text is `aria-busy` until it settles, so screen readers read
176
+ the finished text once instead of every chunk.
177
+ - **Supports WCAG 2.2 AA.** Chai's components are built to support WCAG 2.2 AA and are tested
178
+ against it with automated checks: axe (WCAG 2.0, 2.1 and 2.2, levels A
179
+ and AA) on every component state in the test suite, plus keyboard and
180
+ focus tests in a browser. They haven't been tested by hand with a
181
+ screen reader, so this is support, not a certification. Compliance is
182
+ judged for a whole app, so test yours, including with a screen reader.
183
+
184
+ What that covers: contrast, focus rings, a name for every control
185
+ (`Composer`'s `promptLabel`, `Toggle`'s `ariaLabel`), alt text for
186
+ generated images (`ResultCard`'s `altText`, default the prompt), 24px
187
+ targets, and keyboard use: video play/pause (Tab to the video, Space)
188
+ and seeking (a slider: arrow keys, Home, End), the turned-away side of a
189
+ flipped card is `inert` and focus follows the flip, and the expanded
190
+ view is a dialog (focus inside, Escape closes, focus returns to Expand).
191
+
192
+ ## 7. What's enforced
193
+
194
+ Inside this repo, `pnpm lint` runs `scripts/lint-tokens.mjs` over the
195
+ packages' CSS:
196
+
197
+ - It fails on `var(--…)` names that don't exist (invented tokens).
198
+ - It fails on any hard-coded color, custom properties included.
199
+ - It fails on direct use of Material's `--md-sys-color-*` roles, which
200
+ must go through the Chai token they map to.
201
+
202
+ TypeScript enforces the component APIs: controlled props, typed submit
203
+ payloads, and typed engine and result shapes. `ResultCard`'s `onAction`
204
+ is a plain `string` so it can carry your own `moreActions` ids, which
205
+ means a misspelled action name isn't caught. Nothing checks your
206
+ app's own CSS or markup yet, so sections 1–6 above are guidance there, not
207
+ guarantees.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kazarazat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # @chai-ui/react
2
+
3
+ React components for the input and output surfaces of generative-media
4
+ apps. Everything here is controlled — you own the state, these components
5
+ render it and tell you when it changes.
6
+
7
+ Building with it, or pointing a coding agent at it? Read
8
+ [`DESIGN.md`](./DESIGN.md) (shipped in the package) for the usage rules:
9
+ which component to pick, which tokens exist, the built-in AI states, and
10
+ what not to build.
11
+
12
+ Chai's components support WCAG 2.2 AA: tested with axe and automated
13
+ keyboard and focus checks, not by hand with a screen reader. DESIGN.md §6
14
+ has the details. Focus rings are visible by default, as WCAG requires.
15
+ `data-chai-focus-ring="hidden"` on an ancestor hides them, for preview or
16
+ showcase surfaces only. `Composer`'s `promptLabel` names the prompt field
17
+ (default: the placeholder), and `ResultCard`'s `altText` sets image alt
18
+ text (default: the prompt).
19
+
20
+ ## Quick start: `Composer`
21
+
22
+ `Composer` is the `bar`-layout input surface: a prompt field, an attach
23
+ menu, a use-case chip, model/aspect-ratio pickers, and submit — all driven
24
+ by which props you supply, not by four separate components.
25
+
26
+ ```tsx
27
+ import { useState } from "react";
28
+ import "@chai-ui/tokens/css";
29
+ import "@chai-ui/react/style.css";
30
+ import { Composer, type ComposerAttachment, type ComposerSubmitPayload } from "@chai-ui/react";
31
+
32
+ function App() {
33
+ const [value, setValue] = useState("");
34
+ const [attachments, setAttachments] = useState<ComposerAttachment[]>([]);
35
+
36
+ function handleSubmit(payload: ComposerSubmitPayload) {
37
+ // payload = { value, attachments, useCase, modelId, aspectRatio } —
38
+ // everything Composer tracks, assembled for you. Send it to whichever
39
+ // model/provider you've set up; Composer never calls one itself.
40
+ console.log(payload);
41
+ }
42
+
43
+ return (
44
+ <Composer
45
+ value={value}
46
+ onChange={setValue}
47
+ attachments={attachments}
48
+ onAttachmentsChange={setAttachments}
49
+ onSubmit={handleSubmit}
50
+ />
51
+ );
52
+ }
53
+ ```
54
+
55
+ That's the minimal case — no model picker, no aspect ratio, no attach
56
+ capability beyond the default file picker. Every other control is additive:
57
+
58
+ ```tsx
59
+ <Composer
60
+ value={value}
61
+ onChange={setValue}
62
+ attachments={attachments}
63
+ onAttachmentsChange={setAttachments}
64
+ // A use-case chip + model/aspect-ratio pickers only appear once `useCase`
65
+ // is set — wire it from the attach menu's own picks, or set it yourself.
66
+ useCase={useCase}
67
+ onClearUseCase={() => setUseCase(null)}
68
+ onAttachMenuSelect={(action) => {
69
+ if (action === "create-image") setUseCase({ kind: "image", label: "Image" });
70
+ }}
71
+ models={models}
72
+ modelId={modelId}
73
+ onModelChange={setModelId}
74
+ // An "Auto-select model" switch appears at the top of the model menu so
75
+ // the end user can flip it — omit `showAutoSelectToggle` to keep it a
76
+ // fixed, builder-only setting with no visible control instead.
77
+ autoSelectModel={autoSelectModel}
78
+ showAutoSelectToggle
79
+ onAutoSelectModelChange={setAutoSelectModel}
80
+ aspectRatios={aspectRatios}
81
+ aspectRatio={aspectRatio}
82
+ onAspectRatioChange={setAspectRatio}
83
+ // Omit onEnhance entirely to hide the "optimize prompt" button — e.g.
84
+ // before you've wired an engine that can actually run the rewrite.
85
+ onEnhance={(currentValue) => runEnhance(currentValue).then(setValue)}
86
+ onSubmit={handleSubmit}
87
+ />
88
+ ```
89
+
90
+ The full prop list — including `attachments`' file-reading behavior,
91
+ `onUnsupportedFile`, and `disabled`/`submitDisabled` — is documented as
92
+ JSDoc directly on `ComposerProps` in `src/Composer.tsx`; every prop and
93
+ event follows the same `value`/`onChange` shape used everywhere else in
94
+ this package.
95
+
96
+ See the [Composer page on the docs site](https://chai-ui.com/docs/components/composer-bar)
97
+ for a live example with every opt-in feature.
98
+
99
+ ### No use case picked: a text request
100
+
101
+ A prompt typed with no use case picked is a text request
102
+ (`TEXT_USE_CASE`): it gets a text reply rather than doing nothing. For an
103
+ app that's mostly about images, set the default instead; it's never shown
104
+ as a chip:
105
+
106
+ ```tsx
107
+ <Composer defaultUseCase={{ kind: "image", label: "Image" }} … />
108
+ ```
109
+
110
+ ### Several models, several use cases (builder opt-ins, off by default)
111
+
112
+ ```tsx
113
+ // Several models: each produces its own result, paged inside one ResultCard.
114
+ <Composer multiSelectModels modelIds={modelIds} onModelIdsChange={setModelIds} models={models} … />
115
+
116
+ // Several use cases at once (at most one per kind): the Model menu shows a
117
+ // section per picked use case, and onSubmit gets one selection per use case.
118
+ <Composer
119
+ multiSelectUseCases
120
+ useCases={useCases}
121
+ onUseCasesChange={setUseCases}
122
+ modelsByKind={{ image: imageModels, video: videoModels }}
123
+ modelIds={modelIds}
124
+ onModelIdsChange={setModelIds}
125
+ …
126
+ />
127
+ ```
128
+
129
+ Either way `onSubmit` receives `selections`: one `{ useCase, modelIds, models }`
130
+ entry per use case.
131
+
132
+ With `autoSelectModel` on, the trigger reads "Auto-select" and
133
+ `useComposer` picks the model at submit: the reasoning model reads the
134
+ prompt and media and chooses from that use case's `models`. Pass
135
+ `onModelChange` (or `onModelIdsChange`) to the hook too, so the trigger
136
+ shows the pick:
137
+
138
+ ```tsx
139
+ const { submit, routing } = useComposer({ engine, onModelChange: setModelId });
140
+ ```
141
+
142
+ If routing can't run (no `ChaiProvider`, a failed reasoning call, an
143
+ unlisted reply), the request runs on the first listed model and
144
+ `onRoutingFallback` says why.
145
+
146
+ ## What else this package ships
147
+
148
+ - **`MediaAnalyzer`** — drop in an image, video, or audio clip and get a
149
+ generation-ready prompt back, with kind-aware model menus and a prompt
150
+ length picker. Wire it with `useMediaAnalyzer()`, which sends the media
151
+ to `ChaiProvider`'s reasoning model and returns the prompt:
152
+ `onSubmit={submit} submitting={analyzing} submitError={error}`.
153
+ - **`ResultCard`** — renders one prompt's generated results (image, video,
154
+ audio, or text) with like/dislike/retry/download/share, a configurable
155
+ "more" menu, pagination across multiple results, and an info flip showing
156
+ the prompt and model metadata. Live example:
157
+ [chai-ui.com/docs/components/result-card](https://chai-ui.com/docs/components/result-card).
158
+ - **Primitives** (`src/primitives/`) — `Toggle` (an MD3 switch),
159
+ `SearchMenu` (the model/option menu behind every picker — Chai's own rather than Material's `<md-menu>`; DESIGN.md §3 says why), and
160
+ `Pagination` (a progress-style pager any paginated surface can reuse).
161
+ The components are built from these; they're also exported directly if
162
+ you're assembling your own layout.
163
+
164
+ ## `useComposer` — wiring `Composer` to a real engine
165
+
166
+ ```tsx
167
+ import { ChaiProvider, useComposer, createFalEngine, Composer, ResultCard } from "@chai-ui/react";
168
+
169
+ // The reasoning models write text (Enhance, routing, text requests) and
170
+ // are separate from the media engine. With no props,
171
+ // Claude Opus 5 handles text and images, Gemini 3.8 Flash handles video
172
+ // and audio, both on `createOpenRouterEngine()`. Without a provider,
173
+ // Composer's Enhance button stays hidden.
174
+ function Root() {
175
+ return (
176
+ <ChaiProvider>
177
+ <App />
178
+ </ChaiProvider>
179
+ );
180
+ }
181
+
182
+ function App() {
183
+ const { run, submit, cancel, enhance, enhancing } = useComposer({
184
+ engine: createFalEngine({ outputKind: "image" }),
185
+ });
186
+ const busy = run?.results.some((r) => r.status === "queued" || r.status === "running") ?? false;
187
+
188
+ return (
189
+ <>
190
+ <Composer
191
+ value={value}
192
+ onChange={setValue}
193
+ onSubmit={submit}
194
+ submitting={busy}
195
+ onAbort={cancel}
196
+ onEnhance={enhance ? (v) => enhance(v).then(setValue) : undefined}
197
+ enhancing={enhancing}
198
+ // ...the rest of Composer's own props, as above
199
+ />
200
+ {run && (
201
+ <ResultCard
202
+ results={run.results}
203
+ prompt={run.request.prompt}
204
+ onAction={(action, result) => {
205
+ // like / dislike / retry / download / share / your "more" actions
206
+ }}
207
+ />
208
+ )}
209
+ </>
210
+ );
211
+ }
212
+ ```
213
+
214
+ `submit` takes `Composer`'s `ComposerSubmitPayload` directly — pass it
215
+ straight to `onSubmit`. It turns each selection into a real the design
216
+ `Request`/`Run`/`Result`: creates a `"queued"` `Result` per model up
217
+ front, then folds each one to `"running"` → `"done"`/`"error"` as the
218
+ engine settles it. `run` is the first `Run` (or `null` before the first
219
+ submit); `runs` holds one per use case when `multiSelectUseCases` is on.
220
+ `ResultCard` renders `run.results` directly, paginating when there's more
221
+ than one.
222
+
223
+ **The server route.** `createFalEngine()` and the OpenRouter reasoning
224
+ engine call `/api/chai/*` on your server, which adds the API keys. Add it
225
+ with one file, then set `FAL_KEY` and `OPENROUTER_API_KEY`:
226
+
227
+ ```ts
228
+ // Next.js: app/api/chai/[...path]/route.ts
229
+ import { createChaiHandler } from "@chai-ui/core/server";
230
+ export const { GET, POST, PUT } = createChaiHandler();
231
+ ```
232
+
233
+ Other frameworks, and the `authorize` / `allowedModels` settings to add
234
+ before going live, are in the `@chai-ui/core` README.
235
+
236
+ Every default is a suggestion. Swap any of them:
237
+
238
+ ```tsx
239
+ <ChaiProvider
240
+ reasoningModel="your/text-and-image-model"
241
+ // Cheaper video and audio analysis; kinds you leave out keep their default.
242
+ reasoningModelByKind={{ video: "qwen/qwen3.8-omni-flash", audio: "qwen/qwen3.8-omni-flash" }}
243
+ reasoningEngine={yourEngine}
244
+ />
245
+ ```
246
+
247
+ The defaults are exported as `DEFAULT_REASONING_MODEL` and
248
+ `DEFAULT_REASONING_MODEL_BY_KIND`.
249
+
250
+ `useComposer`'s own `reasoningEngine` + `reasoningModel` override the
251
+ provider's, but only when both are set. Text requests go to `textEngine`
252
+ (default: the reasoning engine, then a mock text engine), using
253
+ `textModel` (default: the reasoning model) when no model is picked. Other kinds go to `engineByKind[kind]`, then `engine`. With no
254
+ model picked, the engine's own default model runs.
255
+
256
+ **Stop.** `cancel()` stops the current submit: wire it to Composer's
257
+ `onAbort`, with `submitting` true while results are queued or running.
258
+ Unfinished results turn `"cancelled"` (ResultCard shows "Stopped"; a
259
+ streamed text result keeps what arrived), and a new submit cancels the
260
+ previous one. Cancellation is best effort at the provider: Fal removes a
261
+ queued request and signals a running one, which may still finish;
262
+ OpenRouter stops a *streaming* request, and its billing, only for
263
+ providers that support it (not Google); a non-streaming request keeps
264
+ running. A custom engine receives `signal` in `generate()`: cancel at the
265
+ provider if it can, then reject with an `AbortError` (`abortError()`).
@@ -0,0 +1,49 @@
1
+ import { type ReactNode } from "react";
2
+ import { type DroppedMedia, type GenerationEngine, type MediaKind } from "@chai-ui/core";
3
+ /**
4
+ * The suggested reasoning model for text and images: prompt enhancement,
5
+ * auto-select routing and image analysis. A default, not a requirement —
6
+ * pass any model id `reasoningEngine` can run.
7
+ */
8
+ export declare const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
9
+ /**
10
+ * Suggested per-kind overrides for media Claude can't read. Gemini takes
11
+ * video and audio directly. For a cheaper option, swap in
12
+ * `"qwen/qwen3.8-omni-flash"`, which takes both too.
13
+ */
14
+ export declare const DEFAULT_REASONING_MODEL_BY_KIND: Partial<Record<MediaKind, string>>;
15
+ /** The reasoning engine and models every CHAI component shares. */
16
+ export interface Reasoning {
17
+ engine: GenerationEngine;
18
+ /** The model for text and any attachment kind without its own entry in `modelByKind`. */
19
+ model: string;
20
+ modelByKind: Partial<Record<MediaKind, string>>;
21
+ }
22
+ export interface ChaiProviderProps {
23
+ /** Text, image and routing model. Defaults to `DEFAULT_REASONING_MODEL`. */
24
+ reasoningModel?: string;
25
+ /**
26
+ * Per-attachment-kind models, merged over `DEFAULT_REASONING_MODEL_BY_KIND`
27
+ * one kind at a time, e.g. `{ video: "qwen/qwen3.8-omni-flash" }` keeps
28
+ * the default for audio. Map a kind to `reasoningModel`'s value to use
29
+ * one model for everything.
30
+ */
31
+ reasoningModelByKind?: Partial<Record<MediaKind, string>>;
32
+ /**
33
+ * The engine the reasoning models run on. Defaults to OpenRouter,
34
+ * independent of the generation engine: Fal's own LLM access is an
35
+ * OpenRouter proxy, so defaulting to it would double-proxy.
36
+ */
37
+ reasoningEngine?: GenerationEngine;
38
+ children?: ReactNode;
39
+ }
40
+ export declare function ChaiProvider({ reasoningModel, reasoningModelByKind, reasoningEngine, children }: ChaiProviderProps): import("react").JSX.Element;
41
+ /** The nearest `ChaiProvider`'s reasoning engine and models, or `undefined` outside one. */
42
+ export declare function useReasoning(): Reasoning | undefined;
43
+ /**
44
+ * The reasoning model for a call with these attachments: the first
45
+ * attachment whose kind has its own model wins (a clip plus a still goes
46
+ * to the model that can read the clip), else `reasoning.model`.
47
+ */
48
+ export declare function reasoningModelFor(reasoning: Reasoning, attachments?: DroppedMedia[]): string;
49
+ //# sourceMappingURL=ChaiProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChaiProvider.d.ts","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,EAA0B,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAEjH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAG9E,CAAC;AAEF,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,gBAAgB,CAAC;IACzB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAID,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D;;;;OAIG;IACH,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAgBlH;AAMD,4FAA4F;AAC5F,wBAAgB,YAAY,IAAI,SAAS,GAAG,SAAS,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,GAAE,YAAY,EAAO,GAAG,MAAM,CAMhG"}
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { createOpenRouterEngine } from "@chai-ui/core";
4
+ /**
5
+ * The suggested reasoning model for text and images: prompt enhancement,
6
+ * auto-select routing and image analysis. A default, not a requirement —
7
+ * pass any model id `reasoningEngine` can run.
8
+ */
9
+ export const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
10
+ /**
11
+ * Suggested per-kind overrides for media Claude can't read. Gemini takes
12
+ * video and audio directly. For a cheaper option, swap in
13
+ * `"qwen/qwen3.8-omni-flash"`, which takes both too.
14
+ */
15
+ export const DEFAULT_REASONING_MODEL_BY_KIND = {
16
+ video: "google/gemini-3.8-flash",
17
+ audio: "google/gemini-3.8-flash",
18
+ };
19
+ const ReasoningContext = createContext(undefined);
20
+ export function ChaiProvider({ reasoningModel, reasoningModelByKind, reasoningEngine, children }) {
21
+ const engine = useMemo(() => reasoningEngine ?? createOpenRouterEngine(), [reasoningEngine]);
22
+ const model = reasoningModel ?? DEFAULT_REASONING_MODEL;
23
+ const video = reasoningModelByKind?.video;
24
+ const audio = reasoningModelByKind?.audio;
25
+ const image = reasoningModelByKind?.image;
26
+ const text = reasoningModelByKind?.text;
27
+ const value = useMemo(() => ({
28
+ engine,
29
+ model,
30
+ modelByKind: { ...DEFAULT_REASONING_MODEL_BY_KIND, ...dropUndefined({ video, audio, image, text }) },
31
+ }), [engine, model, video, audio, image, text]);
32
+ return _jsx(ReasoningContext.Provider, { value: value, children: children });
33
+ }
34
+ function dropUndefined(obj) {
35
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
36
+ }
37
+ /** The nearest `ChaiProvider`'s reasoning engine and models, or `undefined` outside one. */
38
+ export function useReasoning() {
39
+ return useContext(ReasoningContext);
40
+ }
41
+ /**
42
+ * The reasoning model for a call with these attachments: the first
43
+ * attachment whose kind has its own model wins (a clip plus a still goes
44
+ * to the model that can read the clip), else `reasoning.model`.
45
+ */
46
+ export function reasoningModelFor(reasoning, attachments = []) {
47
+ for (const { kind } of attachments) {
48
+ const model = reasoning.modelByKind[kind];
49
+ if (model)
50
+ return model;
51
+ }
52
+ return reasoning.model;
53
+ }
54
+ //# sourceMappingURL=ChaiProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChaiProvider.js","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAA4D,MAAM,eAAe,CAAC;AAEjH;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAC;AAEjE;;;;GAIG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAuC;IACjF,KAAK,EAAE,yBAAyB;IAChC,KAAK,EAAE,yBAAyB;CACjC,CAAC;AAUF,MAAM,gBAAgB,GAAG,aAAa,CAAwB,SAAS,CAAC,CAAC;AAqBzE,MAAM,UAAU,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAqB;IACjH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,eAAe,IAAI,sBAAsB,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAC7F,MAAM,KAAK,GAAG,cAAc,IAAI,uBAAuB,CAAC;IACxD,MAAM,KAAK,GAAG,oBAAoB,EAAE,KAAK,CAAC;IAC1C,MAAM,KAAK,GAAG,oBAAoB,EAAE,KAAK,CAAC;IAC1C,MAAM,KAAK,GAAG,oBAAoB,EAAE,KAAK,CAAC;IAC1C,MAAM,IAAI,GAAG,oBAAoB,EAAE,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,MAAM;QACN,KAAK;QACL,WAAW,EAAE,EAAE,GAAG,+BAA+B,EAAE,GAAG,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE;KACrG,CAAC,EACF,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAC3C,CAAC;IACF,OAAO,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA6B,CAAC;AACzF,CAAC;AAED,SAAS,aAAa,CAAmB,GAAM;IAC7C,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAe,CAAC;AAClG,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAoB,EAAE,cAA8B,EAAE;IACtF,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC,KAAK,CAAC;AACzB,CAAC"}