Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Chat

There is one chat component. ChatWidget is the full-control component: streaming text, tool-call cards, confirmations, ask-user cards, session history, theming, and an imperative handle. AmodalChat (in the package root) is a preset of the same component: ChatWidget with position="inline" and serverUrl defaulted from the nearest AmodalProvider. Pick the entry point, not a different widget.

ChatWidget is exported from both the package root and the @amodalai/react/widget subpath; the subpath exists so a chat-only embed doesn't import the whole SDK, and it also ships the widget's building blocks for composing a custom chat surface (see Building blocks).

Installation

npm install @amodalai/react

Quick start

Inside an AmodalProvider, the preset is enough:

import { AmodalChat } from "@amodalai/react";
import "@amodalai/react/style.css";
 
<AmodalChat user={{ id: userId }} />;

For any other position, or outside a provider, use ChatWidget directly:

import { ChatWidget } from "@amodalai/react/widget";
import "@amodalai/react/widget/style.css";
 
<ChatWidget
  serverUrl="https://your-agent.example.com"
  user={{ id: userId }}
  position="floating"
  historyEnabled
/>;

Props

AmodalChat accepts every ChatWidget prop except position (pinned to inline); its serverUrl is optional and defaults to the provider's runtimeUrl.

Connection and identity

PropDefaultMeaning
serverUrlrequiredBase URL of the agent's runtime server.
usernone{ id: string }; client-side label only — not sent to the runtime, which derives identity from the auth session.
getTokennoneReturns a bearer token for authenticated requests (sync or async).
scopeIdnoneMulti-tenant or per-object isolation: labels sessions and partitions memory/stores per value.
scopeContextnoneKey-value pairs injected into connection API calls via contextInjection.

scopeId and sessionId are related but not interchangeable. scopeId tells the runtime which tenant, user, workspace, case, or record the agent is operating inside. The active sessionId is the exact conversation. Passing the same scopeId keeps data access scoped, but it does not by itself resume the previous chat. Use resumeSessionId or persist the id from onStateChange when the same thread should survive page navigation.

Behavior

PropDefaultMeaning
positionfloatingSee Positions.
defaultOpenfalseStart open (togglable positions only).
historyEnabledfalseSession history drawer.
showHeader / showInput / showFeedbacktrue / true / falseHeader bar; input bar; thumbs up/down on assistant messages.
sessionTypenoneWhich curated surface (skills, tools, knowledge) loads into the session.
deployIdactive deployPin a specific deployment.
initialMessagenoneAuto-sent once on mount.
resumeSessionIdnoneLoad an existing session on mount and continue sending on that session; takes precedence over initialMessage.

Callbacks

PropMeaning
onToolCall(call)A tool call completed; receives the full ToolCallInfo.
onKBProposal(proposal)The agent proposed a knowledge-base update.
onEvent(event)Every widget event (agent-driven and interaction).
onSessionCreated(sessionId)First stream init returned a session id.
onStreamEnd()The SSE stream ended.
onStateChange({ sessionId, messages })Session state changed (for external persistence).

Extension points

PropMeaning
themeSee Theming.
widgetsWidgetRegistry of custom renderers for rich inline widgets.
inlineBlockRenderersRenderers for block types the widget doesn't render natively; native types (text, ask_choice, proposal) cannot be overridden.
streamFnCustom transport ((text, signal, images?, attachments?) => AsyncIterable<SSEEvent>); replaces the built-in chat API call for non-standard endpoints.
entityExtractorsReplaces the default entity extractor for entity events.

Imperative handle

The ref exposes ChatWidgetHandle:

const chat = useRef<ChatWidgetHandle>(null);
chat.current?.sendMessage("Summarize today's alerts"); // as if the user typed it
chat.current?.getSessionId();

Keeping A Scoped Chat Alive

For product embeds, the common pattern is one chat thread per app scope. Store the session id under the scope id and restore it when that panel remounts:

const storageKey = `amodal-session:${scopeId}`;
const [resumeSessionId] = useState(() => localStorage.getItem(storageKey));
 
<ChatWidget
  serverUrl={serverUrl}
  user={{ id: userId }}
  scopeId={scopeId}
  scopeContext={{ tenant_id: tenantId, page: "case-review" }}
  resumeSessionId={resumeSessionId ?? undefined}
  onStateChange={({ sessionId }) => {
    if (sessionId) localStorage.setItem(storageKey, sessionId);
  }}
/>;

Use a different storage key when you want a different conversation boundary. For example, tenant:${tenantId} creates one assistant thread for the whole tenant, while case:${caseId} creates a separate assistant thread for each case.

Positions

PositionBehavior
inlineRenders in-place within your layout
floatingFloating button that expands into a chat panel
rightFixed panel on the right side
bottomFixed panel at the bottom

Theming

The theme prop covers the common cases:

<ChatWidget
  serverUrl={serverUrl}
  user={{ id: userId }}
  theme={{
    mode: "auto", // 'light' | 'dark' | 'auto' (follows prefers-color-scheme)
    primaryColor: "#6e56cf",
    borderRadius: "12px",
    headerText: "Ask the agent",
    placeholder: "Type a message…",
    verboseTools: true, // full tool-call params, results, timing
  }}
/>

Other ChatTheme fields: backgroundColor, fontFamily, fontSize, userBubbleColor, agentBubbleColor, toolCallColor, emptyStateText.

Which key drives which surface

Each theme key sets one CSS custom property that drives specific surfaces. primaryColor is shared: the floating launcher, the send button, focus rings, and accent text all read --pcw-primary, so you cannot recolor the launcher independently through the prop (override the CSS variable on a narrower selector for that).

theme keyCSS variableSurfaces it controls
primaryColor--pcw-primaryFloating launcher, send button, focus rings, accent text/links
backgroundColor--pcw-bgPanel and header background
userBubbleColor--pcw-user-bubbleUser message bubbles
agentBubbleColor--pcw-agent-bubbleAgent bubbles and inline cards
toolCallColor--pcw-tool-call-bgTool-call card background
borderRadius--pcw-radiusPanel, bubbles, buttons
fontFamily / fontSize--pcw-font / --pcw-font-sizeAll widget text
headerText--pcw-header-textHeader title label
placeholder--pcw-placeholderInput placeholder
emptyStateText--pcw-empty-state-textEmpty-conversation message
modedata-theme / prefers-color-schemeLight vs. dark palette (see below)

† These variables carry a text string, not a style value, and the text is rendered by the component rather than read back from the variable. Set the prop to change the copy; overriding the CSS variable alone has no effect.

Surfaces with no matching theme key (header/body text, muted text, borders, panel tint, shadow) are CSS-only: --pcw-text, --pcw-text-muted, --pcw-border, --pcw-panel-tint, --pcw-user-text, --pcw-shadow. Override those directly.

For anything the prop doesn't cover, override the CSS custom properties (no Tailwind dependency):

.pcw-widget {
  --pcw-primary: #6e56cf;
  --pcw-bg: #ffffff;
  --pcw-text: #1a1a1a;
  --pcw-border: #e5e5e5;
  --pcw-radius: 12px;
}

Console Embed page vs. props

The console's Embed page stores a widget configuration server-side (position, theme, toggles) so operators can tune the widget without a code commit. That config does two things:

  • It drives the Preview on the Embed page.
  • It generates the React snippet you copy into your host app.

A self-mounted <ChatWidget> never fetches it at runtime. The snippet is a point-in-time copy — the server values are baked into the props when you copy it — and once mounted the widget reads only its props. The props are the single source of truth; there is no precedence to resolve.

Practical consequences:

  • Changing the Embed config later does not update an already-shipped snippet. Re-copy the snippet (or wire the values into your own config) to pick up changes.
  • Any prop you edit by hand after copying wins, because it is the only value the widget sees.

SSE events

The widget handles these event types from the runtime:

EventDescription
text_deltaStreaming text output
tool_call_startTool execution beginning
tool_call_resultTool execution complete
skill_activatedSkill activation
widgetWidget rendered inline
confirmation_requiredWrite operation needs approval
doneResponse complete

Building blocks

For a custom chat surface, the /widget subpath exports the pieces ChatWidget is made of:

  • Components: MessageList, InputBar, SessionHistory, StreamingIndicator, ToolCallCard, AskUserCard, KBProposalCard, SkillPill, TagEditor, FormattedText, and WidgetRenderer (with its WidgetRegistry type).
  • Hooks: useChat (the widget's chat state machine), useWidgetEvents, useSessionHistory.
  • Chat API: listSessions, getSessionHistory, createSession.
  • Theme utilities: defaultTheme, applyTheme, mergeTheme.

The provider-based hooks and headless clients live in the package root; see @amodalai/react.