composer
Composer bar
A compose bar pattern for generative AI input — prompt field, media attachment, and submit as standard features; use case, model and aspect ratio selection, smart prompt optimization, automatic model routing, and abort as advanced ones.
Code snippet
The bare minimum is a prompt bar for text input to generate media with the ability to abort. With no Model menu, it runs the first suggested image model.
For the stylesheets and the server route that holds your API keys, see Develop.
import { useState } from "react";import { Composer, ResultCard, createFalEngine, useComposer } from "@chai-ui/react";
const engine = createFalEngine({ outputKind: "image" });
export function Create() { const [value, setValue] = useState(""); const { run, runs, submit, cancel } = useComposer({ engine }); const results = runs.flatMap((r) => r.results); const busy = results.some((r) => r.status === "queued" || r.status === "running");
return ( <> <Composer value={value} onChange={setValue} defaultUseCase={{ kind: "image", label: "Image" }} onSubmit={submit} submitting={busy} onAbort={cancel} /> {run && <ResultCard results={results} prompt={run.request.prompt} onAction={() => {}} />} </> );}Add more
| Feature | Props | Notes |
|---|---|---|
| Input | ||
| Media attachments | attachments onAttachmentsChange | |
| Voice input | voiceInput={{ model }} | Any Fal speech-to-text model below, or { transcribe } for your own |
| Use cases | ||
| Use case picker | useCase onAttachMenuSelect | World, speech and music also need attachMenuActions |
| Text chat | defaultUseCase | Leave it out: with no use case picked, a submit is a text request. Needs ChaiProvider |
| Several at once | multiSelectUseCases useCases onUseCasesChange modelsByKind | Image and Video only, a Model menu each; other use cases run alone. Use nextUseCases |
| Create world | character onCharacterChange | The world's second prompt, the character |
| Models | ||
| Model menu | modelId onModelChange models | Chai's suggested models unless you pass your own |
| Several models | multiSelectModels modelIds onModelIdsChange | One result per model |
| Auto-select | showAutoSelectToggle autoSelectModel onAutoSelectModelChange | Needs ChaiProvider |
| Options | ||
| Aspect ratio | aspectRatio onAspectRatioChange | Image and video, from the picked models' aspectRatios. With images attached too; startImageFit fits a video's start image |
| Voice | speechVoice onSpeechVoiceChange | Create speech, from the picked model's voices |
| Genre | genres onGenresChange | Create music, from the picked models' genres |
| Frame rate | frameRate onFrameRateChange | Create world, from the models' frameRates |
| Layout | ||
| Menu direction | menuPlacement | Auto by default. "above" for a bar docked to the bottom, "below" docked to the top |
| AI help | ||
| Prompt enhance | onEnhance enhancing | Use useComposer's enhance. Needs ChaiProvider |
Preview controls
Shows the "optimize prompt" icon button — a builder omits onEnhance entirely to hide it, e.g. before wiring an engine that can actually run the rewrite.
Shows the mic left of submit. Recording is real (your browser asks for the microphone the first time); the text it adds is a placeholder, since no speech-to-text model is connected here. In your app, voiceInput names the speech-to-text model, and there's no mic without it.
Shows an "Auto-select model" switch at the top of the Model menu so the end user can flip it themselves. Off by default — a builder can still force autoSelectModel without this, just with no visible control for the end user to change it.
Advanced2 options
multiSelectModels: the Model menu allows several picks, and each model produces its own result, paged inside one Result card. Off by default.
multiSelectUseCases: pick Create image and Create video together from the "+" menu. Each gets its own Model menu, and submit waits for a model in both. Every other use case runs alone. Off by default; see Image and video together.
menuPlacement: which way the bar's menus open. Auto, the default, opens below when the menu fits, as on a start screen, and above when it doesn't. Above for a bar docked to the bottom of the screen, below for one docked to the top. See Docking the bar.
A Create world submit opens a viewer for the world and its controls. It's a stub for now: no world model runs on Fal or OpenRouter yet. Off, the request shows below the bar like other use cases.
Make every submit — retries included — fail, to preview the retry state (the submit button becomes a danger-tinted refresh icon, with a short message above it) on demand instead of wiring a real failing engine.
Makes "Optimize prompt" return a 260+ character rewrite, to preview Composer's own common-sense bailout: past that length the rewrite just appears instantly, skipping the gradient reveal rather than dragging it out.
Suggested models
The Model menu's lists when you pass no models. They ship with the package, checked against Fal. Nothing is picked for the person: with a Model menu, they pick or turn on auto-select; with no menu, the first model runs.
Create image5 models
With images attached, each runs as its edit endpoint (withImages), still in the picked ratio.
- Nano Banana Prostandard
- Nano Banana 2fast
- Flux 3 Imagestandard
- GPT Image 2slow
- Flux 3 Kleinfast
Create video4 models
With an image attached, each runs as its image-to-video endpoint. The image is fitted to the picked ratio first: cropped by default, or padded with startImageFit.
- MiniMax H3 Maxslow
- Seedance 2.5slow
- Kling v3 Proslow
- Veo 3.1 Faststandard
Edit image3 models
- Flux 3 Imagestandard
- Nano Banana Prostandard
- GPT Image 2slow
Create world5 models
Examples for the preview, not shipped: no world model is hosted on Fal or OpenRouter yet. Pass your own with models.
- LingBot-World 2.0fast
- Gemini Genie 3fast
- World-Labs-Marbleslow
- Micro-Worldstandard
- Matrix-Game 2.0fast
Create speech4 models
Fal text-to-speech models for the preview, not shipped: Create speech is opt-in, so you name the models. Pass them with models, each with the voices from its Fal schema.
- Eleven v4standard
- Gemini 3.8 Flash TTSstandard
- xAI TTSstandard
- Kokorofast
Create music4 models
Fal music models for the preview, not shipped: Create music is opt-in, so you name the models. Pass them with models, each with its genres. No music model takes genre as an input, so the picks are written into the prompt.
- Lyria 3 Prostandard
- MiniMax Music 2.6standard
- Eleven Music v2.5slow
- Stable Audio 2.5fast
Voice input5 models
Fal speech-to-text models, not shipped: you choose one. Paste its id into voiceInput={{ model: "…" }}.
- Wizperfal-ai/wizper
- ElevenLabs Scribe v2fal-ai/elevenlabs/speech-to-text/scribe-v2
- Cohere Transcribefal-ai/cohere-transcribe
- Nemotron ASRnvidia/nemotron-asr-multilingual/asr
- Fal Speech-to-Text Turbofal-ai/speech-to-text/turbo
Image and video together
An advanced option for creative workflows: multiSelectUseCases makes an image and a video from one prompt. Only Image and Video combine, because they're the only use cases that can share a prompt and one Enhance rewrite; nextUseCases makes any other pick replace them. Each use case has its own Model menu and needs a model before submit. The image arrives in seconds and the video can take minutes, so show each run in its own Result card rather than paging them together.
import { useState } from "react";import { Composer, ResultCard, createFalEngine, nextUseCases, useComposer, type ComposerAttachMenuAction, type ComposerUseCase,} from "@chai-ui/react";
const engineByKind = { image: createFalEngine({ outputKind: "image" }), video: createFalEngine({ outputKind: "video" }),};
const USE_CASES: Partial<Record<ComposerAttachMenuAction, ComposerUseCase>> = { "create-image": { kind: "image", label: "Image" }, "create-video": { kind: "video", label: "Video" },};
export function CreateImageAndVideo() { const [value, setValue] = useState(""); const [useCases, setUseCases] = useState<ComposerUseCase[]>([]); const [modelIds, setModelIds] = useState<string[]>([]); const { runs, submit, cancel } = useComposer({ engineByKind }); const busy = runs.some((r) => r.results.some((x) => x.status === "queued" || x.status === "running"));
return ( <> <Composer value={value} onChange={setValue} multiSelectUseCases useCases={useCases} onUseCasesChange={setUseCases} onAttachMenuSelect={(action) => { const picked = USE_CASES[action]; if (picked) setUseCases((prev) => nextUseCases(prev, picked)); }} modelIds={modelIds} onModelIdsChange={setModelIds} onSubmit={submit} submitting={busy} onAbort={cancel} /> {/* One card per use case: the image lands in seconds, the video in minutes. */} {runs.map((run) => ( <ResultCard key={run.id} results={run.results} prompt={run.request.prompt} onAction={() => {}} /> ))} </> );}