Getting started
Sir Chats-a-Lot is an open-source React widget that drops an LLM-driven chat panel onto your site. It answers visitor questions from your published content via Gnosys Web, and renders forms / buttons / confirms inline when a text reply isn't enough.
This doc walks you through the five-minute install, the configuration surface, the API contract between the widget and your server, and the basics of theming and self-hosting.
Install
Add the package:
npm install sir-chats-a-lotMount it in your React app (Next.js App Router shown; the same component pattern works anywhere you can render React):
"use client";
import {
SirChatsALotProvider,
SirChatsALotLauncher,
SirChatsALotWidget,
} from "sir-chats-a-lot";
import type { AgentRequest, SirChatsALotApi } from "sir-chats-a-lot";
import "sir-chats-a-lot/styles";
const api: SirChatsALotApi = async (request: AgentRequest) => {
const res = await fetch("/api/scal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
};
export function AppScal() {
return (
<SirChatsALotProvider productName="My SaaS" api={api}>
<SirChatsALotLauncher />
<SirChatsALotWidget />
</SirChatsALotProvider>
);
}You'll also need a server route at /api/scal that talks to your LLM provider. The repo ships examples/nextjs-anthropic and examples/nextjs-openai as reference implementations.
Configuration
The provider accepts these props (all optional except productName and api):
| Prop | Type | Default | Purpose |
|---|---|---|---|
productName | string | — | Shown in the launcher and header |
api | (req) => Promise<res> | — | Your server-route caller |
rateLimitMs | number | 2000 | Cooldown between sends/submits |
maxMessageLength | number | 2000 | Character cap per message |
maxMessages | number | 50 | Conversation history cap |
greeting | string | — | First assistant message when chat opens |
subtitle | string | "AI assistant" | Header subtitle |
theme | object | — | Color/radius/font overrides |
user | object | — | Optional visitor context sent to your route |
onError | fn | — | Observability hook |
onMessage | fn | — | Per-message hook |
onClose | fn | — | Runs when the panel closes |
persist | "none" | "session" | "local" | "none" | Conversation storage mode |
persistKey | string | — | Required when persist is not "none" |
clearOnClose | boolean | false | Clears the thread when the panel closes |
idleTimeoutMs | number | — | Auto-closes after inactivity |
modes | ModeRegistry | — | Named flows opened by intent |
launcher | object | — | Launcher position, label, and aria label |
Knowledge layer
SCAL pairs with gnosys/web — a zero-dependency static knowledge index. The CLI crawls your site (sitemap, directory, or URL list), produces markdown, and builds a TF-IDF index with optional build-time embedding vectors for hybrid semantic search. Your server route loads the index and feeds matches to the LLM each turn.
Setup
Install Gnosys, initialize a web config, and build the index:
npm install gnosys
npx gnosys web init
npx gnosys web buildThen add "postbuild": "gnosys web build" to your package.json scripts so the index regenerates on every deploy.
Semantic search (optional)
Add embedding vectors at build time when you want hybrid semantic ranking. The route embeds only the user's query; if vectors are missing, the provider key is unavailable, the embed request fails, or the vector model does not match, search falls back to the lexical index.
npx gnosys web build --embeddings openaiUse the same embedding model at build and query time. The generated route handles the check; the OpenAI example uses text-embedding-3-small, and the Anthropic example uses Voyage when configured.
Surfaces API
Every turn, your server returns an AgentResponse. It can include text, a surface, or both. The LLM picks which surface (if any) to send based on what the visitor needs next.
Built-in surface types
| Type | Purpose |
|---|---|
quickActions | Row of tappable suggestion cards |
form | Labeled fields with validation and a submit button |
confirm | Yes/no dialog |
composed | Mixed text/field/button/divider blocks |
Response shape
{
"message": "Two paths to install — pick one:",
"surface": {
"type": "quickActions",
"props": {
"actions": [
{ "id": "npm", "label": "NPM package" },
{ "id": "cli", "label": "scal init" },
{ "id": "manual", "label": "Manual route" }
]
}
},
"sources": [
{ "title": "Installation guide", "path": "/docs/install" }
]
}When the visitor interacts with a surface, the widget sends an AgentAction back with the payload. Your server feeds that to the LLM and the next turn decides what to do next — reply, send a follow-up surface, or finish.
Theming
Every visual aspect is driven by CSS custom properties (--scal-color-accent, --scal-radius-panel, etc.). Override via the theme prop or define CSS variables on the host page. Host theme detection also recognizes common aliases such as --scl-accent.
import { SirChatsALotProvider } from "sir-chats-a-lot";
<SirChatsALotProvider
productName="My SaaS"
api={api}
theme={{
colors: {
accent: "#d49b3a",
background: "#0a100e",
elevated: "#17211f",
foreground: "#e3ddca",
muted: "#a8a091",
border: "#2b3431",
accentForeground: "#0a100e",
},
radius: {
panel: "14px",
button: "999px",
input: "10px",
},
typography: {
fontFamily: "Inter, system-ui, sans-serif",
},
}}
>
...
</SirChatsALotProvider>Self-hosting
SCAL is browser-only. Your server route owns the API key, rate limiting (server-side, in addition to the widget's client-side cooldown), prompt-injection defense, and content moderation. The package never makes outbound LLM calls itself.
The minimum your route needs to do:
- Accept a POST with the AgentRequest JSON body
- Retrieve relevant content from your knowledge index (Gnosys Web or other)
- Call your LLM with the system prompt + retrieved context + conversation history
- Validate the LLM's JSON response, returning an AgentResponse
- Return
{ blocked: true }for moderation failures, or{ error: "..." }for system failures