Embeddable Widget
Embed the AI assistant on any external site with a script snippet, gated by an origin allowlist rather than a reader session.
Embed the AI assistant on any external site with a script snippet, gated by an origin allowlist rather than a reader session.
The AI assistant isn’t confined to a tenant’s own docs site. A
<script> snippet mounts the same assistant — as a floating chat bubble — on any external
site an owner controls: a marketing site, a product app, a support portal. It’s configured
from Settings → Widget.
Built: the Settings → Widget page (Availability, Authorized domains, Installation), the
/api/widget/[widgetId]/chat endpoint, and the /api/widget/embed.js loader script —
real markdown rendering (headings, lists, tables, links, images), on-demand Mermaid
diagrams, dark/light/system theming, the full init() option surface below, a
programmatic API (open/close/ask/update/reset/destroy), and event hooks.
Not yet built: a widget-specific rate limit beyond the shared AI billing gate, and
analytics that distinguish widget-originated questions from in-docs ones.
The in-docs assistant trusts a same-origin request and, on a gated site, a reader’s session cookie. A widget embedded on someone else’s domain has neither:
From Settings → Widget:
https://docs.example.com). Paths and wildcards aren’t
supported; add every subdomain that needs it individually.<head> or <body> of the external
site:<script type="module" src="https://your-domain/api/widget/embed.js"></script>
<script type="module">
await window.PapervineAssistant.init({
id: "widget_...",
});
</script>
Or, if you’d rather not add a second script block, put the widget id directly on the
loader tag as a data-widget-id attribute — it auto-mounts the default bubble with no
init() call at all:
<script
type="module"
src="https://your-domain/api/widget/embed.js"
data-widget-id="widget_..."
></script>
Both methods produce the same widget. Reach for init() when you want to pass options
(below) or later trigger the widget programmatically; the single tag is the smaller
footprint when you just want the default floating bubble with no customization.
The Widget ID shown on the same page is what init() takes — it’s already filled into
the copyable snippet, so most owners never need to touch it directly.
init() takes a config object beyond just id:
| Option | Type | Default | Effect |
|---|---|---|---|
theme | "dark" | "light" | "system" | "dark" | The panel’s color scheme. "system" follows the host page’s prefers-color-scheme at mount time. |
title | string | "Ask the docs assistant" | The panel header text. |
placeholder | string | "Ask a question…" | The input field’s placeholder. |
disclaimer | string | false | "Responses are generated using AI and may contain mistakes." | The line shown under the header. false omits it entirely. |
defaultOpen | boolean | false | Opens the panel immediately on mount instead of waiting for a launcher click. |
variant | "widget" | "modal" | "panel" | "widget" | "widget" is the default floating bubble + panel; "modal" is a centered overlay with a backdrop; "panel" docks a full-height panel to a screen edge. |
side | "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | "bottom" | Which screen edge the launcher (and, in "panel" mode, the panel itself) anchors to. |
align | "start" | "center" | "end" | "end" | Position along that edge. |
zIndex | number | a very high value | Override if the widget needs to sit above (or below) other high-z-index elements on your page. |
accent | CSS color | — | Recolors the send button, links, and other accent surfaces. |
radius | CSS length | "16px" | The panel’s corner radius. |
font | CSS font-family value | system font stack | The panel’s font. |
logo | string | { light, dark } | — | Replaces the emoji launcher with an image. A object swaps per resolved theme. |
<script type="module">
await window.PapervineAssistant.init({
id: "widget_...",
theme: "light",
title: "Ask us anything",
accent: "#7c3aed",
starterQuestions: ["How do I get started?", "Where are the API docs?"],
});
</script>
The floating launcher bubble itself always stays a fixed dark style regardless of
theme (unless replaced by logo/trigger) — it sits on your page’s own background,
which the widget doesn’t control, so it needs to read clearly against either a light or
dark page. Only the opened panel (and any Mermaid diagrams inside it) follow your theme
choice.
Beyond init(), window.PapervineAssistant exposes runtime methods that operate on
whatever instance is currently mounted — each is a no-op before init() resolves or
after destroy(), rather than throwing, so your own code can call these opportunistically
(e.g. from a page-wide keyboard shortcut) without guarding every call:
window.PapervineAssistant.open(); // open the panel
window.PapervineAssistant.close(); // close the panel
window.PapervineAssistant.ask("How do I…?"); // open + send a question
window.PapervineAssistant.update({ theme: "light" }); // apply new options live
window.PapervineAssistant.reset(); // clear the conversation
window.PapervineAssistant.destroy(); // unmount entirely
update() covers what can sensibly change on an already-mounted instance — theme,
title, placeholder, disclaimer, accent, radius, font, zIndex — without
clearing the conversation. Structural options (variant, side/align, logo,
trigger) are simplest to change with destroy() followed by a fresh init().
init() also accepts event and error callbacks for building a “headless” integration
that drives its own UI off the widget’s activity:
await window.PapervineAssistant.init({
id: "widget_...",
event: (e) => console.log(e.type), // "init" | "ask" | "update" | "reset" | "destroy"
error: (e) => console.log(e.code, e.retryable, e.status),
});
Both are wrapped so a mistake in your own callback can never break the widget itself.
The loader script is a small, dependency-free bundle — no framework, no build step — that
mounts into an isolated shadow DOM
node, so the host page’s CSS can never leak in or the widget’s styles leak out. It renders
a floating launcher button and a chat panel, and streams the assistant’s answer token by
token from /api/widget/{widgetId}/chat — the same agentic retrieval loop (searchDocs /
readPage / listPages / searchApi) that powers the in-docs assistant and the
MCP server, just reached over a different, cross-origin transport.
Usage against the widget draws from the same AI credit pool as the in-docs assistant and automations — there’s one billing gate per organization, not a separate budget per surface.
The widget renders the assistant’s answer as real markdown — headings, lists (including nested ones), tables, links, images, and code — built directly as DOM nodes rather than by injecting HTML, so nothing in the model’s own output can ever run as script.
A fenced code block labeled mermaid renders as an actual diagram: the widget loads the Mermaid library
on demand, only when an answer actually contains one, from a pinned, exact version — never
a floating tag, so it can’t change without a deliberate update on our side. If that load
fails for any reason (a restrictive Content-Security-Policy on the host page, a network
issue, or a diagram the model wrote with invalid syntax), the widget falls back to showing
the diagram’s raw source in a code block — an answer never breaks because a diagram
couldn’t render.
{ light, dark }trigger | string | — | Turns the launcher into a text pill with this label instead of a bare circle. |
starterQuestions | string[] | — | Up to 3 pills shown above the input until the first message is sent — the same config shape as the in-docs assistant’s starter questions. |
suggestions | string | "Suggestions" | The heading shown above starterQuestions. |
supportEmail | string | — | Adds a “Contact support” mailto: link in the header, for a human fallback. |
dismissOnInteractOutside | boolean | false | Closes the panel when the visitor clicks anywhere outside the widget. |
nonce | string | — | Copied onto the widget’s <style> tag, for sites with a strict style-src CSP. |