Skip to Content

@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.

Parameters:
NameTypeDefault
optionsCreateAiUIExtensionOptions
Returns:
StesuraUIExtension

CreateAiUIExtensionOptions

Options for createAiUIExtension: the text-menu props plus the chat settings.

NameTypeDefault
generateGenerateText

Text-action transport — typically createHttpGenerateText pointed at the text-action route.

userCurrentUser

Suggestion author shown on the tracked changes; defaults to the AI identity.

languagesAiTextMenuLanguage[]

Languages offered in the translate submenu. Defaults to five, labelled in the UI locale; value stays English because it is the prompt parameter.

chatApistring

Chat route (from ai-server’s createChatHandler). When provided, the extension adds the chat right panel and its Home-tab toolbar toggle.

chatHeadersRecord<string, string>

Extra headers sent with every chat request — pair with the route guard’s bearerToken. The text-menu/autocomplete transports carry their own headers via createHttpGenerateText’s init.

chatModelsAiChatModelOption[]

Models offered in the chat’s composer picker; mirror the chat route’s models allowlist. Omit to render no picker.

chatStorageKeystring

Persist the chat conversation in localStorage under this key — one key per document. Omit it and nothing is persisted.

importDocxImportDocx

Enables .docx chat attachments: pass importDocx from @stesura/docx, ideally lazy-loaded. Without it the picker offers PDFs only.

useChatTools() => readonly DefinedAiEditorTool[]

The editor tools the chat runs, as a hook: it runs inside the editor, so a tool that needs something only the editor has (the comments adapter) can be bound there. Return a stable list (useMemo). Its names must match the editor tools the chat route declares; a declared tool missing here answers “not available in this editor”. Defaults to the built-ins.

AiChatModelOption

A model the user can pick in the chat composer.

NameTypeDefault
idstring

Model id — must be a key of the server route’s models allowlist.

labelstring
descriptionstring

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.

Parameters:
NameTypeDefault
fileFile
schemaSchema<any, any>
Returns:
Promise<{ doc: Node; }>

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.

Parameters:
NameTypeDefault
__0AiTextMenuButtonProps
Returns:
Element

AiTextMenuButtonProps

Props for AiTextMenuButton; also the text-menu half of CreateAiUIExtensionOptions.

NameTypeDefault
generateGenerateText

Text-action transport — typically createHttpGenerateText pointed at the text-action route.

userCurrentUser

Suggestion author shown on the tracked changes; defaults to the AI identity.

languagesAiTextMenuLanguage[]

Languages offered in the translate submenu. Defaults to five, labelled in the UI locale; value stays English because it is the prompt parameter.

AiTextMenuLanguage

A translate target: value is what the prompt receives, label what the menu shows.

NameTypeDefault
valuestring
labelstring

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.

Parameters:
This function does not accept any parameters.
Returns:
Element

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.

Parameters:
NameTypeDefault
props{}
Returns:
ReactNode

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.

Parameters:
NameTypeDefault
editorIdstring
Returns:
AiChatStore

useAiChatUiValue

Subscribe to a slice of the chat UI state. Selectors must return stable references.

Parameters:
NameTypeDefault
storeAiChatStore
selector(state: AiChatUiState) => T
Returns:
T

AiChatStore

Per-editor chat state container; get one with getAiChatStore, read it with useAiChatUiValue.

NameTypeDefault
get() => AiChatUiState
subscribe(listener: () => void) => () => void
set(partial: Partial<AiChatUiState>) => void
setMode(mode: AiChatMode) => void
setModel(model: string | undefined) => void
recordSuggestion(toolCallId: string, trackChangesId: string) => void
recordReview(toolCallId: string, review: AiSuggestionReview) => void
clearConversation() => void

Reset everything conversation-scoped (attachments, in-flight conversions, suggestions, reviews). The thread itself is the runtime’s to reset.

confirmAttachmentHashes(hashes: string[]) => void

Merge server-confirmed attachment hashes (see confirmedAttachmentHashes).

keepAttachmentDocument(attachment: AiChatAttachment, doc: Node) => void

Keep a .docx attachment’s imported document for compare. Held by the attachment itself (weakly), not by its hash or name: two files may share either. It goes when the attachment is removed or the conversation cleared.

attachmentDocuments() => CompareAttachment[]

The .docx attachments with their imported documents, in attachment order: compare’s source.

bindConversation(storageKey: string | undefined, session: object) => void

Bind the store to the host’s conversation: its storage key, and the host mount (session) that owns an unpersisted one. A different key — or, with no key, a different host — is a different conversation, so the conversation-scoped slice resets and the key’s stored suggestions load (AIR-23); mode/model stay. Idempotent and listener-free (it runs during the host’s render, where notifying subscribers would be an update-during-render warning); the first read after it sees the reset.

toolSession() => AiToolSession

The conversation’s read ledger for the editing tools: what the agent has read, so its edits merge with changes made since. A new conversation (cleared, switched or restored) gets a new one: the model no longer has what an old ledger recorded.

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.

NameTypeDefault
panelContainerHTMLElement | null

DOM node the open chat surface offers; the host portals the thread UI into it.

attachmentsAiChatAttachment[]

Conversation-scoped reference documents, sent with every request.

attachmentsDroppednumber

How many selected files were dropped by the attachment cap (UI feedback).

attachOutcome{ attached: number; failed: string[]; } | null

Outcome of the last attach batch: what landed, and which files failed.

attachmentsConvertingnumber

Files accepted into an attach batch and still converting. Lives here, not in the button, so the cap counts them and a reopened panel sees them.

attachBatchnumber

Bumped by clearConversation: a batch that started under an older value drops its late results instead of landing them in the new conversation.

confirmedAttachmentHashesstring[]

Attachment hashes the server confirmed holding for this conversation — requests send refs for these instead of re-uploading (upload dedup).

toolBudgetExhaustedboolean

The tool-loop budget stopped the run mid-task. Rendered as a notice — without it the assistant just falls silent with tool chips on screen.

modeAiChatMode
modelstring | undefined

Selected model id; undefined = the server’s default.

suggestionsRecord<string, string>

toolCallId → trackChangesId for suggestions produced by edit tools.

reviewsRecord<string, AiSuggestionReview>

toolCallId → resolution chosen from the chat’s review chips.

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.

Parameters:
NameTypeDefault
storageKeystring
Returns:
void

hasChatHistory

True when a persisted conversation exists under this storage key.

Parameters:
NameTypeDefault
storageKeystring
Returns:
boolean

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.

Parameters:
NameTypeDefault
__0AiChatRuntimeHostProps
Returns:
Element

AiChatRuntimeHostProps

Props for AiChatRuntimeHost.

NameTypeDefault
apistring

Chat route created by ai-server’s createChatHandler.

headersRecord<string, string>

Extra headers sent with every chat request (e.g. the route’s bearer token).

userCurrentUser

Suggestion author for edits made from the chat.

storageKeystring

Persist the conversation in localStorage under this key — one key per document. Omitted, nothing is persisted: a shared default key leaked one document’s transcript into every other on the origin (AIR-20).

modelsAiChatModelOption[]

Models offered in the composer picker. Omit to render no picker (the server default is used). Mirror the server route’s models allowlist.

importDocxImportDocx

Enables .docx chat attachments; see CreateAiUIExtensionOptions.importDocx.

useChatTools() => readonly DefinedAiEditorTool[]

The editor tools this chat runs; see CreateAiUIExtensionOptions.useChatTools.

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.

Parameters:
This function does not accept any parameters.
Returns:
Element

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.

Parameters:
NameTypeDefault
__0{ store: AiChatStore; models?: AiChatModelOption[]; storageKey?: string; importDocx?: ImportDocx; }
Returns:
Element

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.

Parameters:
NameTypeDefault
__0{ user?: CurrentUser; store: AiChatStore; tools: readonly DefinedAiEditorTool[]; children?: ReactNode; }
Returns:
Element

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.

Parameters:
NameTypeDefault
options{ user?: CurrentUser; store?: AiChatStore; toolSet?: AiToolSet; }
Returns:
(toolName: string, input: unknown, toolCallId?: string | undefined) => Promise<string>
Last updated on