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.

ℹ Note
The widget is browser-only. API keys, rate limiting, and content moderation live in YOUR server route — never in the package. That's an intentional liability boundary.

Install

Add the package:

npm install sir-chats-a-lot

Mount it in your app (Next.js App Router shown; the same pattern works in Remix, Astro, or plain React):

app/scal.tsx
"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):

PropTypeDefaultPurpose
productNamestringShown in the launcher and header
api(req) => Promise&lt;res&gt;Your server-route caller
rateLimitMsnumber2000Cooldown between sends/submits
maxMessageLengthnumber2000Character cap per message
maxMessagesnumber50Conversation history cap
greetingstringFirst assistant message when chat opens
subtitlestring"AI assistant"Header subtitle
themeobjectColor/radius/font overrides
onErrorfnObservability hook
onMessagefnPer-message hook

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

Then add "postbuild": "gnosys web build" to your package.json scripts so the index regenerates on every deploy.

ℹ Note
You don't have to use Gnosys. The widget contract is provider-agnostic; bring your own RAG, vector DB, or whatever your stack already has.

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

TypePurpose
quickActionsRow of tappable suggestion cards
formLabeled fields with validation and a submit button
confirmYes/no dialog
composedMixed text/field/button/divider blocks

Response shape

AgentResponse
{
  "message": "Two paths to install — pick one:",
  "surface": {
    "type": "quickActions",
    "props": {
      "actions": [
        { "id": "npm",    "label": "NPM package" },
        { "id": "script", "label": "Script tag" },
        { "id": "docker", "label": "Docker" }
      ]
    }
  },
  "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, etc.). Override via the theme prop or plain CSS targeting the widget container.

import { SirChatsALotProvider } from "sir-chats-a-lot";

<SirChatsALotProvider
  productName="My SaaS"
  api={api}
  theme={{
    colors: {
      accent: "#d49b3a",
      background: "#0a100e",
      foreground: "#e3ddca",
    },
    radius: { card: 14, button: 999 },
    fontFamily: {
      body: "Inter, system-ui, sans-serif",
      display: "Big Shoulders Display, 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

FAQ