@assistant-ui/react
Version:
Open-source TypeScript/React library for building production-grade AI chat experiences
132 lines (122 loc) • 4.69 kB
text/typescript
"use client";
import { useCallback } from "react";
import { useAui } from "@assistant-ui/store";
import { flushTapSync } from "@assistant-ui/tap";
import { useComposerSend } from "@assistant-ui/core/react";
import type { ComposerSendOptions } from "@assistant-ui/core/store";
import {
type TriggerPopoverAriaProps,
useComposerInputDisabled,
useComposerInputValue,
useTriggerPopoverAriaProps,
} from "../primitives/composer/useComposerInputState";
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
export type Unstable_UseComposerInputOptions = {
/**
* Disables the input in addition to the composer's own disabled sources
* (thread disabled, active dictation). When disabled, `isDisabled` is `true`
* and `canSend` is `false`.
*/
disabled?: boolean | undefined;
};
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
export type Unstable_ComposerInput = {
/** Current composer text, or `""` when the composer is not editing. */
value: string;
/**
* Writes `text` into the composer, mirroring `ComposerPrimitive.Input`:
* a no-op unless the composer is editing; the controlled value stays in
* sync within the same tick.
*/
setText(text: string): void;
/**
* Sends the current message when `canSend` is `true`; otherwise a no-op.
* Accepts the same options as the composer send action.
*/
send(options?: ComposerSendOptions): void;
/**
* Whether the input is disabled, combining the `disabled` option with the
* composer's own disabled sources (thread disabled, active dictation).
*/
isDisabled: boolean;
/**
* Whether a send is currently available. Matches `ComposerPrimitive.Send`
* gating (non-empty editing composer, not running without queue support) and
* is additionally `false` while `isDisabled`.
*/
canSend: boolean;
};
/**
* Headless bridge to the composer's text value and send action, for building a
* custom composer input without `ComposerPrimitive.Input`. It is a thin bridge,
* not a second input: it does not own keyboard behavior, autosize, IME or
* contentEditable sync, paste/drop attachments, focus management, or rich-text
* state. Spread `unstable_useTriggerPopoverAriaProps()` onto your element for
* trigger-popover combobox semantics.
*
* @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
*
* @example
* ```tsx
* const { value, setText, send, isDisabled, canSend } = unstable_useComposerInput();
* <textarea
* value={value}
* disabled={isDisabled}
* onChange={(e) => setText(e.target.value)}
* onKeyDown={(e) => {
* if (e.nativeEvent.isComposing || e.keyCode === 229) return;
*
* if (e.key === "Enter" && !e.shiftKey && canSend) {
* e.preventDefault();
* send();
* }
* }}
* />
* ```
*/
export function unstable_useComposerInput(
options?: Unstable_UseComposerInputOptions,
): Unstable_ComposerInput {
const aui = useAui();
const value = useComposerInputValue();
const isDisabled = useComposerInputDisabled(options?.disabled);
const setText = useCallback(
(text: string) => {
if (!aui.composer.getState().isEditing) return;
flushTapSync(() => {
aui.composer.setText(text, { origin: "user" });
});
},
[aui],
);
const { send: rawSend, disabled: sendDisabled } = useComposerSend();
const canSend = !sendDisabled && !isDisabled;
const send = useCallback(
(sendOptions?: ComposerSendOptions) => {
if (!canSend) return;
rawSend(sendOptions);
},
[canSend, rawSend],
);
return { value, setText, send, isDisabled, canSend };
}
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
export type Unstable_TriggerPopoverAriaProps = TriggerPopoverAriaProps;
/**
* ARIA combobox attributes for the focused element (typically the composer
* input) describing the open trigger popover, per the WAI-ARIA editable
* combobox pattern. Returns an empty object outside a `TriggerPopoverRoot` or
* when no popover is open. Spread these last so they take precedence over any
* matching ARIA props you set yourself, mirroring `ComposerPrimitive.Input`.
*
* @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
*
* @example
* ```tsx
* const aria = unstable_useTriggerPopoverAriaProps();
* <textarea {...aria} />
* ```
*/
export function unstable_useTriggerPopoverAriaProps(): Unstable_TriggerPopoverAriaProps {
return useTriggerPopoverAriaProps();
}