@stesura/ai-react
React UI for the AI feature: the text-menu dropdown, the chat panel and the UI extension that
wires both into StesuraEditor. Most hosts only need createAiUIExtension; see the
AI guide for setup. The headless core is @stesura/ai.
UI extension
createAiUIExtension
UI extension adding the AI text-menu dropdown and (optionally) the chat
panel. Transports are the host’s choice — pair with
createHttpGenerateText from @stesura/ai and @stesura/ai-server
routes:
const aiUIExtension = createAiUIExtension({
generate: createHttpGenerateText(`${basePath}/api/ai/text-action`),
chatApi: `${basePath}/api/ai/chat`,
});The chat runtime lives in an always-mounted host (overlay slot), so the
conversation, pending reviews, mode and model selection survive closing
the panel or switching to another one; the panel is only a viewport. The
host (and the chat stack’s chunk) mounts on the panel’s first open — see
AiChatRuntimeGate.
| Name | Type | Default |
|---|---|---|
options | CreateAiUIExtensionOptions |
CreateAiUIExtensionOptions
Options for createAiUIExtension: the text-menu props plus the chat settings.
AiChatModelOption
A model the user can pick in the chat composer.
| Name | Type | Default |
|---|---|---|
id | stringModel id — must be a key of the server route’s | |
label | string | |
description | string |
ImportDocx
Parses a .docx into a document of schema — importDocx from
@stesura/docx. Injected by the host so ai-react doesn’t depend on docx.
| Name | Type | Default |
|---|---|---|
file | File | |
schema | Schema<any, any> |
Text menu
For hosts that place the AI dropdown themselves. Mount AiTextActionAnnouncer somewhere that
outlives the menu, so screen readers hear the outcome of a run that finishes after it closes.
AiTextMenuButton
“AI” dropdown for the text menu. Every action lands as track-changes
suggestions via runTextAction. The run’s state lives in a per-editor store,
not here: the text menu unmounts on any typing keydown, and the run must
keep its pending guard (and its announcement, rendered by
AiTextActionAnnouncer) when that happens.
| Name | Type | Default |
|---|---|---|
__0 | AiTextMenuButtonProps |
AiTextMenuButtonProps
Props for AiTextMenuButton; also the text-menu half of CreateAiUIExtensionOptions.
AiTextMenuLanguage
A translate target: value is what the prompt receives, label what the menu shows.
| Name | Type | Default |
|---|---|---|
value | string | |
label | string |
AiTextActionAnnouncer
The text actions’ live region. Mounted by the UI extension where it outlives the text menu, so the outcome is heard even after the menu has closed — success lands silently as track changes otherwise.
Chat panel controls
AI_CHAT_PANEL_ACTION
"aiChat" — the chat’s right-panel action id. Pass it to toggleRightPanel from
useRightPanelControls to open or close the chat programmatically.
AiChatToolbarGroup
Home-tab toolbar group with the button that toggles the AI chat panel.
| Name | Type | Default |
|---|---|---|
props | {} |
Chat state
The chat’s UI state lives in a per-editor store, so custom controls can read the mode, model, attachments or pending reviews.
getAiChatStore
The chat store for one editor, created on first access. editorId is the
ROOT editor id — inside the editor tree, useRootEditorId() from
@stesura/editor-react is the way to get it.
| Name | Type | Default |
|---|---|---|
editorId | string |
useAiChatUiValue
Subscribe to a slice of the chat UI state. Selectors must return stable references.
| Name | Type | Default |
|---|---|---|
store | AiChatStore | |
selector | (state: AiChatUiState) => T |
AiChatStore
Per-editor chat state container; get one with getAiChatStore, read it with useAiChatUiValue.
AiChatUiState
Chat UI state that must outlive the panel: the runtime host (AiChatRuntimeHost, always mounted) and the panel surface (mounted only while open) are sibling branches of the editor tree, so they share it through this store instead of React context.
AiSuggestionReview
"accepted" | "rejected" — the resolution recorded for an edit tool call from its review chip.
Chat history
clearChatHistory
Delete the persisted conversation under this storage key (the extension’s
chatStorageKey). Does not reset a chat that
is currently mounted.
| Name | Type | Default |
|---|---|---|
storageKey | string |
hasChatHistory
True when a persisted conversation exists under this storage key.
| Name | Type | Default |
|---|---|---|
storageKey | string |
Custom chat layouts
The chat components live on @stesura/ai-react/chat, not the package root: they pull in
assistant-ui, the AI SDK and react-markdown, which createAiUIExtension only loads when the
panel first opens. Import from this entry only for a layout that mounts them directly.
AiChatRuntimeHost
Always-mounted owner of the chat runtime and its state (conversation, attachments, mode, model, pending reviews). Rendered by the AI UI extension in the editor’s overlay slot — NOT inside the right panel — so closing or switching panels never destroys the conversation. The visible chat UI is portalled into whatever surface (panel, drawer) publishes a container on the per-editor chat store.
Must render inside the editor’s react-prosemirror context.
| Name | Type | Default |
|---|---|---|
__0 | AiChatRuntimeHostProps |
AiChatRuntimeHostProps
Props for AiChatRuntimeHost.
AiChatPanel
The chat right-panel surface. Deliberately dumb: it only publishes its DOM container on the per-editor chat store — the always-mounted AiChatRuntimeHost portals the actual thread UI (and owns all chat state) into it, so closing the panel or switching to another panel never destroys the conversation.
AiChatThread
The visible chat UI — portalled into the panel container by the runtime host, so it always renders inside the host’s AssistantRuntimeProvider and the editor’s react-prosemirror context.
| Name | Type | Default |
|---|---|---|
__0 | { store: AiChatStore; models?: AiChatModelOption[]; storageKey?: string; importDocx?: ImportDocx; } |
AiToolRegistrations
Registers the given editor tools as frontend tools on the surrounding
assistant runtime: the model calls them, the editor executes them, results
flow back automatically. A tool’s kind decides the UI (read → status
text, edit / annotate → review chip, config → confirm card). Server tools
are not registered: they run in the route, and the thread renders them
through its fallback (status, and the approval card for side effects).
Must render inside both AssistantRuntimeProvider and the editor’s react-prosemirror context. The list may change between renders: each tool registers from its own component, keyed by name.
| Name | Type | Default |
|---|---|---|
__0 | { user?: CurrentUser; store: AiChatStore; tools: readonly DefinedAiEditorTool[]; children?: ReactNode; } |
useAiChatToolExecutor
Editor-side executor for chat tool calls. Chat-UI agnostic: wire it into whatever runtime drives the conversation (AI SDK useChat’s onToolCall, an assistant-ui tool fallback, etc.) — call it with the tool name and input, send the resolved string back as the tool result.
toolSet holds the editor tools this chat can run (default: the built-ins).
With a store, the current chat mode gates the tools ask mode excludes
(defence in depth behind the server-side gate) and edit-tool suggestions are
recorded per tool call (pass toolCallId) so the review chips can
accept/reject them. Edits carry the signed-in user as their requester.
Never rejects. The underlying editor hook throws when no view is mounted — which a streamed tool call can hit in a remount window (StrictMode, locale switch) — and an exception here kills the whole run, while an error string is a result the model recovers from like any other tool failure.
| Name | Type | Default |
|---|---|---|
options | { user?: CurrentUser; store?: AiChatStore; toolSet?: AiToolSet; } |