Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/src/content/docs/configuration/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ attributes on the `<claudius-chat>` web component.
| `locale` | `"en" \| "es" \| "fr" \| "de"` | auto-detected | UI language; see [Localization](/configuration/localization/) |
| `translations` | `Partial<ClaudiusTranslations>` | built-in | Override individual UI strings |
| `triggers` | `Trigger[]` | `undefined` | Proactive triggers; see [Proactive triggers](/configuration/triggers/) |
| `plugins` | `ClaudiusPlugin[]` | `undefined` | Message middleware run around each send (`onBeforeSend` / `onAfterReceive` / `onError`); see [Plugins](/plugins/) |

## Web component attributes

Expand All @@ -41,8 +42,10 @@ attributes on the `<claudius-chat>` web component.
></claudius-chat>
```

`locale`, `translations`, and `triggers` are not available as attributes — use
`window.ClaudiusConfig` or the React component for those.
`locale`, `translations`, `triggers`, and `plugins` are not available as
attributes — use `window.ClaudiusConfig` or the React component for those.
(`plugins` are functions, so set them in JS via `window.ClaudiusConfig` or as a
React prop.)

## Checking the deployed version

Expand Down
230 changes: 213 additions & 17 deletions docs/src/content/docs/plugins/index.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,233 @@
---
title: Plugins & Tools
description: Extension points available today, and the planned plugin SDK and tool registry.
description: The Claudius plugin SDK — client and server middleware for message transforms, PII redaction, canned responses, and analytics.
---

## Extension points today
Claudius plugins are small middleware that run around the chat message
lifecycle. The same three lifecycle hooks exist on the **client** (the React
widget) and the **server** (the Worker), so you can inject context, redact PII,
route to different models, log analytics events, or answer locally — without
forking the widget.

Claudius doesn't have a plugin API yet, but several supported extension points
cover most customization needs:
## Lifecycle hooks

| Hook | Runs | Can |
|------|------|-----|
| `onBeforeSend` | before a message is sent | modify it, replace it, answer locally (`respondWith`), or cancel it (`abort`) |
| `onAfterReceive` | after the reply arrives | modify or replace the reply |
| `onError` | when a send fails | observe the error; on the client, recover with a fallback reply |

All hooks may be `async`. A hook that throws is caught and logged, so a single
misbehaving plugin will not break the chat — write security-sensitive
transforms (like redaction) defensively.

## Client plugins (widget)

Pass an array of plugins to `ChatWidget`. Hooks run in array order.

```tsx
import { ChatWidget, pluginRedactPII, pluginAnalytics } from "claudius-chat-widget";

export function App() {
return (
<ChatWidget
apiUrl="https://your-worker.workers.dev"
plugins={[
pluginRedactPII(),
pluginAnalytics({ onEvent: (e) => console.log(e) }),
]}
/>
);
}
```

`onBeforeSend` returns the (possibly modified) message — and the returned
message is what is **both displayed and sent**, so a redaction is visible to the
user.

### Short-circuiting

The `onBeforeSend` context can answer without a network call, or cancel the
send entirely:

```ts
import type { ClaudiusPlugin } from "claudius-chat-widget";

const slashCommands: ClaudiusPlugin = {
name: "slash-commands",
onBeforeSend(message, ctx) {
if (message.content === "/clear") return ctx.abort();
if (message.content.startsWith("/help")) {
ctx.respondWith("Type a question and press Enter.");
}
},
};
```

- `ctx.respondWith(reply)` — render `reply` as the assistant message and skip
the API. `reply` is a string or `{ content, sources }`.
- `ctx.abort()` — drop the message and render nothing.

`onError` can recover the same way, replacing the error UI with a reply:

```ts
const fallback: ClaudiusPlugin = {
name: "fallback",
onError: (_err, ctx) =>
ctx.respondWith("We're offline right now — email help@example.com."),
};
```

### Reference plugins

Three plugins ship with `claudius-chat-widget`.

**`pluginAnalytics`** — emit a structured event for every sent message, reply,
and error:

```ts
import { pluginAnalytics } from "claudius-chat-widget";

pluginAnalytics({
onEvent: (event) => window.gtag?.("event", event.type, event),
includeContent: false, // record only character counts, not message text
});
```

**`pluginRedactPII`** — strip emails, phone numbers, SSNs, and card-like
numbers before the message leaves the browser:

```ts
import { pluginRedactPII } from "claudius-chat-widget";

pluginRedactPII(); // sensible defaults
pluginRedactPII({ replacement: "***", redactReplies: true });
```

**`pluginCannedResponses`** — answer matched intents locally, with no API call:

```ts
import { pluginCannedResponses } from "claudius-chat-widget";

pluginCannedResponses({
rules: [
{ match: "hours", reply: "We're open 9–5, Mon–Fri." },
{ match: /pricing|cost/i, reply: "See https://example.com/pricing." },
],
});
```

### Writing your own

A plugin is an object with a `name` and any of the three hooks:

```ts
import type { ClaudiusPlugin } from "claudius-chat-widget";

const pageContext: ClaudiusPlugin = {
name: "page-context",
onBeforeSend(message) {
return { ...message, content: `[on ${location.pathname}] ${message.content}` };
},
};
```

## Server plugins (Worker)

The Worker exposes the equivalent hooks as Hono middleware. Server hooks
operate on the whole request (the messages array) and the reply string, rather
than on a single widget message.

Register plugins in `worker/src/index.ts` — the file ships this block with an
empty list, so just drop your plugins in:

```ts
import { chatPlugins, pluginRedactPII } from "./plugins";

const serverPlugins = [pluginRedactPII({ redactReplies: true })];
if (serverPlugins.length > 0) {
app.use("/api/chat", chatPlugins(serverPlugins));
}
```

### Server hooks

```ts
import type { ClaudiusServerPlugin } from "./plugins";

const example: ClaudiusServerPlugin = {
name: "example",
onBeforeSend(messages, ctx) {
// Inspect ctx.env, transform messages, or short-circuit the model:
// ctx.respondWith("a canned reply");
return messages;
},
onAfterReceive(reply) {
return reply.trim();
},
onError(error) {
console.error("chat failed:", error.message);
},
};
```

- `onBeforeSend(messages, ctx)` — return a new messages array, or call
`ctx.respondWith(text)` to answer without calling Claude.
- `onAfterReceive(reply, ctx)` — return a new reply string.
- `onError(error, ctx)` — observe failures.

### Server reference plugins

The same three plugins, adapted for the request lifecycle:

```ts
import {
chatPlugins,
pluginAnalytics,
pluginRedactPII,
pluginCannedResponses,
} from "./plugins";

const serverPlugins = [
pluginRedactPII(), // redact every message before it reaches Claude
pluginCannedResponses({
rules: [{ match: /refund/i, reply: "See our refund policy at /refunds." }],
}),
pluginAnalytics({ onEvent: (e) => console.log(e) }),
];

app.use("/api/chat", chatPlugins(serverPlugins));
```

Redacting server-side is defense in depth — it runs even for clients that don't
ship the widget redactor. A canned (short-circuit) response skips the model, and
with it the built-in D1 analytics for that turn.

## Notes

- Hooks run in array order; the first `respondWith` / `abort` wins and stops
that hook's chain.
- A throwing hook is caught and logged — it never breaks the chat.
- The client and server interfaces are parallel but not identical: a widget
message carries an `id` and `sources`; the server works on plain
`{ role, content }` messages.

## Other extension points

These complement plugins and cover most customization without code:

| Extension point | What it controls |
|-----------------|------------------|
| [System prompt](/configuration/worker/#system-prompt) (`worker/src/system-prompt.ts`) | The bot's personality, knowledge, guardrails, and FAQ answers |
| [Translations](/configuration/localization/) | Every user-facing UI string |
| [Theming options](/configuration/theming/) and `widget/tailwind.config.ts` | Colors, fonts, radii |
| [Theming](/configuration/theming/) and `widget/tailwind.config.ts` | Colors, fonts, radii |
| [Proactive triggers](/configuration/triggers/) | When the widget opens or greets proactively |
| Worker env vars (`CLAUDE_MODEL`, `MAX_TOKENS`, rate limits) | Model behavior and abuse protection |
| Fork-level: `widget/src/` and `worker/src/` | Anything else — both packages are small, typed, and well-tested |

For testing integrations, the widget exports a `MockChatApiClient` test
utility (`widget/src/test-utils/`) that fakes the API client without network
calls.

## Planned

These are tracked on the [v1.3.0 and v2.0.0 milestones](https://github.com/PMDevSolutions/Claudius/milestones):

- **Anthropic tool-use / function calling** with a declarative tool registry,
so the bot can call your APIs mid-conversation —
[#51](https://github.com/PMDevSolutions/Claudius/issues/51)
- **Plugin/hook SDK** for message middleware: pre-send and post-receive
transforms — [#45](https://github.com/PMDevSolutions/Claudius/issues/45)
- **Plugin SDK RFC for Claudius v2** — the longer-term plugin architecture —
[#79](https://github.com/PMDevSolutions/Claudius/issues/79)

Nothing on this list is shipped yet. If you need one of these, comment on the
issue — it helps prioritization.
9 changes: 9 additions & 0 deletions widget/src/components/ChatWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { resolveTranslations, type LocaleCode } from "../locales";
import { useTheme } from "../theme/useTheme";
import type { ClaudiusThemeInput } from "../theme/types";
import type { ClaudiusPlugin } from "../plugins/types";

/** Corner of the viewport the widget docks to. */
export type WidgetPosition =
Expand Down Expand Up @@ -66,6 +67,12 @@
translations?: Partial<ClaudiusTranslations>;
/** Proactive open/greeting rules evaluated against the current page. */
triggers?: Trigger[];
/**
* Middleware run around each message: `onBeforeSend`, `onAfterReceive`, and
* `onError`. Hooks run in array order and may modify, replace, or
* short-circuit messages. See {@link ClaudiusPlugin}.
*/
plugins?: ClaudiusPlugin[];
}

function readDismissed(): boolean {
Expand Down Expand Up @@ -111,6 +118,7 @@
locale,
translations: translationOverrides,
triggers,
plugins,
}: ChatWidgetProps) {
const [isOpen, setIsOpen] = useState(false);
const [greeting, setGreeting] = useState<string | null>(null);
Expand All @@ -129,6 +137,7 @@
storageKeyPrefix,
timeoutMs: requestTimeoutMs,
translations,
plugins,
});
const toggleRef = useRef<HTMLButtonElement>(null);
const prevOpenRef = useRef(isOpen);
Expand Down Expand Up @@ -273,5 +282,5 @@
}

// Re-export for convenience
export { defaultTranslations, createTranslations };

Check warning on line 285 in widget/src/components/ChatWidget.tsx

View workflow job for this annotation

GitHub Actions / Validate Widget

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components

Check warning on line 285 in widget/src/components/ChatWidget.tsx

View workflow job for this annotation

GitHub Actions / Validate Widget

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
export type { ClaudiusTranslations };
Loading
Loading