@assemble-inc/chat-widget
v0.6.4
Published
Embeddable agentic chat widget powered by Anthropic
Readme
@assemble-inc/chat-widget
An embeddable, general-purpose AI chat widget. Drop it into any React app — or any plain HTML page — to give users a floating chat interface backed by Anthropic Claude and MCP (Model Context Protocol). Fully configurable at embed time: system prompt, model, knowledge base, UI copy, and more.
Installation
pnpm add @assemble-inc/chat-widget
# or
npm install @assemble-inc/chat-widget
# or
yarn add @assemble-inc/chat-widgetPeer dependencies — make sure these are already in your project:
pnpm add react react-domQuick Start
1. Mount the widget
import { Widget } from "@assemble-inc/chat-widget";
export default function App() {
return (
<>
{/* your app */}
<Widget
config={{
systemPrompt: "You are a helpful assistant for Acme Corp.",
title: "Ask Us Anything",
placeholder: "Type your question...",
}}
/>
</>
);
}The widget renders as a fixed floating button in the bottom-right corner (configurable). Clicking it opens the chat panel. Styles are injected automatically when you import Widget; you can still import @assemble-inc/chat-widget/style.css explicitly if your bundler strips side effects. If your app uses Tailwind v3 PostCSS, do not re-run Tailwind on the widget stylesheet (only autoprefixer) — the CSS is already compiled with Tailwind v4.
2. Add the server-side handler
The simplest setup is to mount the handler on your existing app server so that the widget's default endpoint: '/api/chat' resolves correctly:
// your existing Express/Next/etc. server
import { createChatHandler } from "@assemble-inc/chat-widget/server";
app.use(express.json());
app.post(
"/api/chat",
createChatHandler({
apiKey: process.env.ANTHROPIC_API_KEY!,
mcpServerUrl: process.env.MCP_SERVER_URL!,
mcpBearerToken: process.env.MCP_BEARER_TOKEN,
}),
);If you prefer to run a separate API server (e.g. on port 3006), you must tell the widget where to find it — otherwise it will POST to your frontend server and get a 404:
// Pass the full URL when the API server is on a different port
<Widget config={{ endpoint: "http://localhost:3006/api/chat" }} />Or proxy /api to the API server in your frontend dev config (Vite example):
// vite.config.ts
server: {
proxy: {
"/api": "http://localhost:3006",
},
},3. Environment variables
ANTHROPIC_API_KEY=sk-ant-...
MCP_SERVER_URL=https://your-mcp-server.example.com/mcp
MCP_BEARER_TOKEN=your-bearer-token # optional
SYSTEM_PROMPT="You are..." # optional server-side overrideScript-tag Embed (no React required)
For non-React apps, use the self-contained IIFE bundle:
<link rel="stylesheet" href="https://cdn.example.com/asm-widget/index.css" />
<script src="https://cdn.example.com/asm-widget/embed.js"></script>
<script>
AsmWidget.init({
endpoint: "https://my-server.com/api/chat",
systemPrompt: "You are a helpful assistant.",
title: "Help",
});
</script>Build the embed bundle:
pnpm run build:embed # outputs dist/embed.jsConfiguration Reference
WidgetConfig
All configuration is passed as a single config prop. Every field is optional.
| Field | Type | Default | Description |
| ----------------- | --------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------- |
| endpoint | string | '/api/chat' | Backend chat endpoint URL. |
| systemPrompt | string | — | System prompt for this embed. Forwarded to the server; server-side value wins when both are set. |
| context | string | — | Additional context appended after the system prompt (e.g. current page, user role). Forwarded to the server. |
| mcpServerUrl | string | — | MCP server URL for this embed. Must appear in the server's mcpCredentials allowlist; falls back to default. |
| model | string | — | Claude model ID (e.g. 'claude-opus-4-5'). Forwarded to the server; server-side value wins when both are set. |
| temperature | number | — | Response creativity, 0–1. Forwarded to the server; server-side value wins when both are set. |
| initialMessages | Array<{ role, content }> | — | Seed messages shown when the widget first opens. |
| persist | boolean | false | Save the conversation to sessionStorage across page navigations. |
| user | { id?, name?, role? } | — | Forwarded in every request body for server-side personalisation or logging. |
| title | string | 'Chat' | Header title. |
| logo | string | Built-in logo | URL to a custom logo image. |
| theme | WidgetTheme | — | Semantic color and typography overrides for whitelabeling (see below). |
| welcomeHeading | string | — | Default assistant message shown when the conversation is empty. |
| copy | WidgetCopy | — | Override built-in error and loading status text. |
| placeholder | string | — | Textarea placeholder text. |
| showCloseButton | boolean | — | Header chevron that dismisses the panel. false hides it. Default: shown in floating layout or when onClose is set in docked layout. |
| layout | 'floating' \| 'docked' | 'floating' | 'floating': FAB + overlay panel. 'docked': fills the host container. |
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Widget anchor corner. Only applies when layout is 'floating'. |
WidgetTheme
Pass semantic tokens via config.theme to rebrand the widget without writing custom CSS:
| Field | Applies to |
| ------------ | -------------------------------------------------------------------------- |
| primary | Header title, FAB (closed), icons, MCQ selected state, send button border |
| accent | Hover and focus rings |
| userBubble | User message bubbles |
| fabOpen | FAB when the panel is open (defaults to userBubble) |
| surface | Panel shell, assistant bubbles, composer bar |
| content | Chat content area background |
| muted | Secondary text and loading indicators |
| text | Message body text |
| fontFamily | Widget font stack |
You can also override tokens with CSS on the widget root:
.asm-widget {
--asm-primary: #003366;
--asm-user-bubble: #dbeafe;
--asm-font-family: 'Helvetica Neue', sans-serif;
}WidgetCopy
| Field | Default | Description |
| --------- | ---------------------------------------------------- | ------------------------------------------------ |
| error | "Sorry, I'm having trouble connecting right now." | Shown when the chat endpoint fails. \n for breaks. |
| loading | "This should only take a moment…" | Shown while waiting for the first response token. |
WidgetProps
| Prop | Type | Default | Description |
| -------------- | -------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
| config | WidgetConfig | — | All widget configuration (see above). |
| open | boolean | — | Controls open/close state from outside (controlled mode). |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called whenever the widget opens or closes. |
| onMessage | (msg: { role: string; content: string }) => void | — | Called after each new assistant message is received. |
| onError | (error: Error) => void | — | Called when a stream error occurs. |
| onClose | () => void | — | Called when the close button is clicked. Use config.showCloseButton: false to hide the chevron while keeping this handler. |
| className | string | — | Extra class names on the widget root. |
| style | React.CSSProperties | — | Extra inline styles on the widget root (merged with config.theme). |
| children | ReactNode | — | Custom content shown in the empty state. Replaces the built-in empty state entirely. |
Server Reference
createChatHandler(config)
Returns an Express-compatible (req, res) => Promise<void> request handler that streams Claude responses via your MCP server.
| Option | Type | Required | Description |
| ---------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| apiKey | string | Yes | Anthropic API key. |
| mcpServerUrl | string | Yes | Default MCP server URL. Used when no per-embed URL is provided or the requested URL is not in the allowlist. |
| mcpBearerToken | string | No | Bearer token for the default MCP server. |
| model | string | No | Claude model ID. Takes precedence over any model sent by the widget. Defaults to claude-sonnet-4-5. |
| systemPrompt | string | No | Operator system prompt. Takes precedence over any system prompt sent by the widget. |
| temperature | number | No | Model temperature (0–1). Takes precedence over any temperature sent by the widget. |
| mcpCredentials | Record<string, string> | No | Allowlist of additional MCP server URLs → bearer tokens. Enables per-embed MCP routing. Credentials never leave the server. |
Precedence rules
For systemPrompt, model, temperature, and mcpServerUrl, the server-side value always takes precedence over what the widget sends. The resolution order is:
ChatHandlerConfig value → widget config value → built-in defaultMulti-context deployments
A single server can serve multiple embeds pointing at different MCP servers:
createChatHandler({
apiKey: process.env.ANTHROPIC_API_KEY!,
mcpServerUrl: process.env.MCP_SERVER_URL!, // default fallback
mcpBearerToken: process.env.MCP_BEARER_TOKEN,
mcpCredentials: {
"https://mcp.acme.com/hr": process.env.MCP_HR_TOKEN,
"https://mcp.acme.com/it": process.env.MCP_IT_TOKEN,
},
});Each embed specifies which server it wants via config.mcpServerUrl. Bearer tokens are looked up server-side — they are never exposed to the browser.
Examples
Minimal embed
<Widget config={{ systemPrompt: "You are a helpful assistant." }} />Whitelabeled embed
<Widget
config={{
title: "Ask Acme",
logo: "https://acme.com/logo.svg",
theme: {
primary: "#003366",
accent: "#93c5fd",
userBubble: "#dbeafe",
surface: "#f8fafc",
content: "#ffffff",
text: "#334155",
fontFamily: "'Helvetica Neue', sans-serif",
},
}}
/>Fully configured embed
<Widget
config={{
endpoint: "https://api.acme.com/chat",
systemPrompt: "You are an HR assistant for Acme Corp.",
mcpServerUrl: "https://mcp.acme.com/hr",
model: "claude-opus-4-5",
temperature: 0.3,
title: "Ask HR",
welcomeHeading: "How can we help?",
placeholder: "Ask an HR question...",
position: "bottom-left",
persist: true,
user: { id: currentUser.id, name: currentUser.name },
}}
onMessage={({ content }) => analytics.track("chat_message", { content })}
onOpenChange={(open) => setHelpOpen(open)}
/>Custom empty state (children)
<Widget config={{ title: "Support", placeholder: "Describe your issue..." }}>
<div>
<h3>How can we help?</h3>
<button onClick={() => sendMessage("Reset my password")}>
Reset my password
</button>
<button onClick={() => sendMessage("Billing question")}>
Billing question
</button>
</div>
</Widget>Local Development
# Install dependencies
pnpm install
# Copy and fill in environment variables
cp .env.example .env
# Start the Vite dev server (port 3003) + Express API server (port 3006)
pnpm run devThe Vite dev server proxies /api/* to http://localhost:3006.
pnpm run lint # type-check without emitting
pnpm run build # build the React library (dist/)
pnpm run build:embed # build the standalone IIFE script (dist/embed.js)Publishing
pnpm version patch # bug fixes (0.1.x)
pnpm version minor # new features (0.x.0)
pnpm version major # breaking changes (x.0.0)
pnpm publish