Composer

import { Composer, ComposerTool, ASK } from '@hanzo/ui/chat'

The stacked composer

A field over a toolbar, the send control at the end of it.

Enter to send
export function Stacked() {
const [draft, setDraft] = useState('')
return <Composer value={draft} onChange={setDraft} onSend={() => setDraft('')} hint="Enter to send" />
}

One line, with the context above and the controls below

A coding composer.

Build
export function Coding() {
const [draft, setDraft] = useState('')
const [repo, setRepo] = useState({ owner: 'hanzo-inc', name: 'cloud' })
const [branch, setBranch] = useState('main')
return (
<YStack width="100%" maxW={768} pt={260}>
<Composer inline value={draft} onChange={setDraft} onSend={() => setDraft('')} placeholder="Describe a task or ask a question" head={ <> <ChipSelect name="Environment" label="Default" onChange={() => {}} items={[{ id: 'd', label: 'Default' }]} /> <RepoSelect value={repo} onChange={setRepo} load={async () => ({ repos: [repo] })} /> <BranchSelect value={branch} onChange={setBranch} load={async () => ({ branches: ['main', 'dev'] })} /> </> } foot={ <> <ComposerTool label="Attach" icon={<Plus size={14} />} /> <ComposerTool label="Voice" icon={<Mic size={14} />} /> <ComposerTool label="Mode" text="Build" caret /> <XStack flex={1} /> <ChipSelect name="Model" label="Zen 5" onChange={() => {}} items={[{ id: 'z', label: 'Zen 5' }]} quiet /> </> } />
</YStack>
)
}

Busy

The send mark becomes Stop while a turn is in flight.

export function Busy() {
return <Composer inline value="" busy onChange={() => {}} onSend={() => {}} onStop={() => {}} />
}

Types

export interface ComposerProps extends Stack {
value: string
onChange: (value: string) => void
/** Called when the draft is submitted. Clearing `value` is the host's job. */
onSend: () => void
/** Called instead of `onSend` while `busy`. Omit to disable stopping. */
onStop?: () => void
/** A turn is in flight: the send control becomes stop. */
busy?: boolean
disabled?: boolean
placeholder?: string
/** Row floor for the field. It grows with the draft from here. */
rows?: number
/** Ceiling for that growth, px. Past it the field scrolls instead. */
maxHeight?: number
/** Toolbar slot, rendered at the start of the footer row. */
children?: ReactNode
/** Hint shown at the end of the footer row. */
hint?: string
/**
* The accessible name of the field, independent of `placeholder`.
*
* A placeholder that rotates renames the control on every tick and gets the
* field re-announced mid-sentence. The name is stable; the placeholder is
* decoration.
*/
label?: string
/** The field's own props — a ref, a testid, focus handlers, a paste handler. */
field?: FieldProps
/** Replaces the send control. `children` is the START of the footer row, so a
* second commit mode has nowhere else to go. */
send?: ReactNode
/**
* Drawn ABOVE the frame — the row of context a draft is sent with (where it
* runs, which repository, which branch). Outside the frame on purpose: it
* says what the draft is about, and the field says what it is.
*/
head?: ReactNode
/**
* Drawn UNDER the frame — attach, voice, mode at its start and the model at
* its end, as the host arranges them. Unlike `children`, which lives inside
* the frame beside the send control.
*/
foot?: ReactNode
/**
* One line: the field and the send control share a single row inside the
* frame, and the send control is a glyph at the field's end — Enter's own
* mark while idle, Stop while busy. `children` and `hint` join that row
* before it. Unset, the frame is the stacked field-over-toolbar it has
* always been.
*/
inline?: boolean
}
export interface ComposerToolProps extends Row {
/** The control's name — what a screen reader says and the tooltip shows. */
label: string
icon?: ReactNode
/** Words drawn after the icon — a mode, a model. Absent, the tool is icon-only. */
text?: string
/** A trailing caret: this opens a menu. */
caret?: boolean
onPress?: () => void
disabled?: boolean
}

Exports

ComposerComposerToolASKtype ComposerPropstype ComposerToolProps