Embedding Smarter Chat
This guide is for developers who want to add a Smarter chatbot to their own website or application, with the @smarter.sh/ui-chat npm package. You need a deployed LLMClient and a React 19 application built with a bundler, such as Vite, webpack, Rollup or Next.js. You don’t need to know anything about Smarter’s backend.
Before You Start
Smarter Chat runs in your visitors’ browsers and talks directly to the Smarter platform. Check these three things first, because each of them fails in the browser rather than in your build.
1. The LLMClient is deployed. Deploy it with smarter deploy llmclient <name>, or from the
web console. A deployed LLMClient has its own host:
https://<name>.<account-number>.api.<smarter-domain>/
for example https://stackademy.3141-5926-5359.api.example.com/, or the verified
custom domain that you gave it. This url is the
apiUrl that you pass to the component.
2. Your site’s origin is allowed. Requests from your page to Smarter are cross-origin, and are sent with credentials, so the origin must be allowed explicitly; a wildcard is not allowed. List it in the LLMClient’s manifest, and apply it again:
spec:
config:
allowedOrigins:
- https://www.example.com
- http://localhost:5173 # your development server
An origin is a scheme, a host and an optional port, without a path. The chat calls one host only,
the LLMClient’s (see The Chat API Contract). A platform operator can also
allow an origin for every LLMClient, in CORS_ALLOWED_ORIGINS or CORS_ALLOWED_ORIGIN_REGEXES.
3. The browser can authenticate, if the LLMClient requires it. An LLMClient requires
authentication when it has at least one active api key
attached. Then pass a Smarter api key to the component as apiKey, which it sends as
Authorization: Token <apiKey>. A deployed LLMClient without api keys is public: anyone who can
reach its host can chat with it, without an apiKey, and every prompt is charged to the
LLMClient’s owner. Give a public LLMClient a budget
if that matters to you.
Warning
An apiKey in a web page is public: anyone who can view the page can read it, and use it. Use
a key that is dedicated to this page, and revoke it if it is abused. Your web console login
doesn’t help here: the Smarter session cookie is SameSite=Lax, so browsers don’t send it with
requests from other sites.
What the page receives is the public configuration: the LLMClient’s name, welcome message, example prompts, model and system prompt, but not, for example, its owner’s profile or the session’s diagnostic histories, which only the workbench sees.
Installation
npm install @smarter.sh/ui-chat
react and react-dom 19 are peer dependencies. katex, which typesets math, and
mermaid, which draws diagrams, are installed with the package.
Quick Start
import { createRoot } from "react-dom/client";
import { SmarterChat } from "@smarter.sh/ui-chat";
import "@smarter.sh/ui-chat/dist/ui-chat.css";
import "katex/dist/katex.min.css";
createRoot(document.getElementById("chat")!).render(
<div style={{ height: 600 }}>
<SmarterChat
apiUrl="https://stackademy.3141-5926-5359.api.example.com/"
apiKey={import.meta.env.VITE_SMARTER_API_KEY}
showConsole={false}
/>
</div>,
);
That’s all. On its first render, the component fetches the LLMClient’s configuration, displays its welcome message and example prompts, and is ready to chat. The chat fills the height of its parent element, so give the parent a height.
showConsole={false} hides the developer Console, which is on by default and suits only wide
pages, such as an internal tool. See Features.
Props
Only apiUrl is required. The defaults suit a page that embeds one public LLMClient.
Prop |
Default |
Description |
|---|---|---|
|
(required) |
The LLMClient’s url. The chat appends |
|
|
A Smarter api key, sent as |
|
|
Show the developer Console beside the chat. |
|
|
Show the Sandbox mode / Production mode button, which shows and hides the backend’s own messages, and start in sandbox mode. Without it, the chat is in production mode: the thread is the conversation, without the system prompt, tool results or Smarter’s notes. |
|
|
Display a running prompt’s progress, streamed as Server-Sent Events. A platform that doesn’t stream answers with JSON, and the chat shows a typing indicator instead. |
|
|
Log to the browser console. The LLMClient’s configuration can also turn it on. |
|
|
The domain whose cookies the chat reads. Empty means the page’s own domain. The chat reads a cookie only if the page’s hostname is in this domain. |
|
|
The cookie in which the chat saves its chat session’s key. |
|
one day |
The session cookie’s lifetime, in milliseconds. After it, the visitor starts a new chat. |
|
|
The name of a Django CSRF cookie, whose value is sent as the |
|
|
A CSRF token to send instead of the cookie’s value. |
|
|
The cookie in which the chat saves the LLMClient’s debug mode. |
|
|
An id of the page request, sent as the |
|
|
The url of a Smarter server log stream, for the Console’s Server Logs tab. Only useful to developers who are logged in to the web console on the same domain. |
Recipes
A floating chat widget
import { useState } from "react";
import { SmarterChat } from "@smarter.sh/ui-chat";
export function ChatWidget() {
const [open, setOpen] = useState(false);
return (
<>
<button className="chat-launcher" onClick={() => setOpen(!open)}>
Chat with us
</button>
{open && (
<div style={{ position: "fixed", right: 24, bottom: 80, width: 400, height: 600, zIndex: 1000 }}>
<SmarterChat apiUrl="https://support.3141-5926-5359.api.example.com/" apiKey={apiKey} showConsole={false} />
</div>
)}
</>
);
}
Closing the widget unmounts the chat, but not its session: the session’s key is in a cookie, so the conversation is restored when the widget opens again.
Next.js
The component uses browser apis (cookies, localStorage, EventSource), so render it on the
client only:
"use client";
import dynamic from "next/dynamic";
import "@smarter.sh/ui-chat/dist/ui-chat.css";
import "katex/dist/katex.min.css";
const SmarterChat = dynamic(() => import("@smarter.sh/ui-chat").then((m) => m.SmarterChat), { ssr: false });
export default function Chat() {
return (
<div style={{ height: "80vh" }}>
<SmarterChat
apiUrl={process.env.NEXT_PUBLIC_SMARTER_LLMCLIENT_URL!}
apiKey={process.env.NEXT_PUBLIC_SMARTER_API_KEY}
showConsole={false}
/>
</div>
);
}
Several chatbots on one site
The chat saves its session’s key in a cookie for the page’s path, so two LLMClients on two pages,
/sales/ and /support/, keep separate sessions. Two chats on the same page must use different
cookies:
<SmarterChat apiUrl={salesUrl} apiKey={apiKey} sessionCookieName="sales_session_key" showConsole={false} />
<SmarterChat apiUrl={supportUrl} apiKey={apiKey} sessionCookieName="support_session_key" showConsole={false} />
A sandbox for your own team
An internal tool can show everything the workbench shows:
<SmarterChat apiUrl={apiUrl} apiKey={apiKey} toggleMetadata showConsole />
Styling
Import @smarter.sh/ui-chat/dist/ui-chat.css (or its alias, @smarter.sh/ui-chat/style.css)
once. It contains the component’s own css, and that of
chatscope, the chat UI kit that it is built on.
Import katex/dist/katex.min.css too. It’s the stylesheet and fonts of math in the LLM’s
responses, and is left out of ui-chat.css so that your bundler emits KaTeX’s fonts as separate
files, which browsers download only when an equation uses them. Without it, math is displayed twice:
as plain text, and as unstyled MathML.
The component’s root element has the class SmarterChat, and its parts have stable class names
prefixed smarter-chat-, for example:
Class |
Element |
|---|---|
|
The thread. |
|
Smarter’s notes, errors, and system and tool messages. |
|
A step of a running prompt. |
|
A code block, with its header and copy button. |
|
An image in a message. |
|
The Console. |
Override them from your own stylesheet, loaded after ui-chat.css.
Bundling Notes
Mermaid is imported only when a message first has a diagram, so your bundler splits it into separate chunks that are downloaded only then.
The UMD bundle,
dist/smarter-chat-library.umd.js, expectsReact,ReactDOMandjsxRuntimeglobals. It can’t import Mermaid, and displays diagrams as code, unless the page mapsmermaidto Mermaid’s ES module with an import map, for example tohttps://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs. Prefer a bundler.Production builds of the component strip its
console.debugcalls.
What the Component Stores in the Browser
Name |
Where |
Contents |
|---|---|---|
|
cookie |
The chat session’s key, for the page’s path, |
|
cookie |
The LLMClient’s debug mode. |
|
localStorage |
The Console’s layout, once the visitor changes it. |
The chat session itself, its messages, is stored by Smarter, not in the browser. Mention the session cookie in your site’s cookie notice if you have one.
Troubleshooting
Open the browser’s developer tools, and look at the console and the network tab.
Symptom |
Cause and fix |
|---|---|
“blocked by CORS policy” in the console |
Your origin isn’t in the LLMClient’s |
The header reads “Smarter Chat is not available”, with “LLMClient not found.” |
|
“blocked by CORS policy” from an origin in |
The LLMClient was deployed before its Ingress stopped using Traefik’s CORS middleware, which answers preflight requests itself. Deploy it again. |
“Forbidden. Please provide a valid API key.”, or an http 401 or 403 |
The LLMClient has an api key, and |
“Unexpected response from the Smarter api (http …)”, with another status |
The response wasn’t JSON, for example a proxy’s html error page, or a server error. The message includes the start of the response. |
The chat never leaves “Configuring workbench…” |
The configuration request is pending. Check that |
A blank area where the chat should be |
The parent element has no height. Give it one. |
Math is shown twice, as text and as unstyled symbols |
|
The prompt’s progress never appears, only a typing indicator |
A proxy in front of Smarter buffers the response, or |
Every reload starts a new chat |
The browser blocks the session cookie, or |
Versions
The package follows semantic versioning, and its
changelog lists every
release. It asks the platform for the current version of the api contract, and reads the previous one from
older platforms, so a new version of the package works with older Smarter platforms; when a feature
needs a newer platform, its release notes say so. Pin a minor version, ~0.7.0, if you want to
review changes before your visitors see them.