feat(widget): embeddable, brandable chat widget — one script tag from pipeline to production chat#1586
Conversation
…get (rocketride-org#1582) New workspace package packaging RocketRide's existing public chat surface as something teams can put on their own sites. One ~72 KB (21 KB gzip) React-free bundle, two consumption modes: - <rocketride-chat engine-url=... auth=...> web component (shadow DOM, style isolation both ways) rendered inline anywhere; attributes are observed live (title, accent, welcome, placeholder, theme=light|dark| auto), theming via --rr-* CSS custom properties, default accent #5f2167. - One-script-tag floating bubble: data-* attributes on the script tag configure a launcher button that opens a panel hosting the same element (position, Escape-to-close, aria-expanded). Auth uses ONLY the pipeline's public authorization key — the same {host}/chat?auth={public_auth} mechanism the chat source node already publishes; JSDoc and docs state explicitly that the engine API key and private token must never appear in a page. The connection layer reuses the browser-compatible 'rocketride' TS SDK (bundled from source, Node 'ws' path stubbed; SDK itself untouched), maps live SSE status to a 'thinking' indicator, and keeps a 6-turn history per question. Assistant output goes through an escape-first safe renderer (allowlist: paragraphs, line breaks, inline/fenced code, bold, http(s)-only links with rel="noopener noreferrer") — no raw model HTML ever reaches the DOM. A11y: role=log with aria-live=polite, labeled controls, Enter/ Shift+Enter, focus-visible outlines, reduced-motion aware. 112 jsdom tests (XSS corpus, component states, theming, loader config, connection protocol) plus a demo page for both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Embedding guide for both consumption modes with copy-paste snippets, the full attribute / CSS-custom-property / events reference, and a security section: authenticate with the pipeline's public key only — never the engine API key or private token — plus CORS/TLS notes for exposing an engine to browsers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🤖 Internal: Discord sync markerAuto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete. |
nihalnihalani
left a comment
There was a problem hiding this comment.
Self-review from the author: inline notes on the security and design decisions most worth a reviewer's attention. Full evidence chain (112 jsdom tests, three independent XSS audits against the built bundle, two live-engine e2e runs on the public-key-only path, browser visual check) is in the PR description.
| */ | ||
|
|
||
| /** Escapes the five HTML-significant characters. Applied before any formatting. */ | ||
| export function escapeHtml(text: string): string { |
There was a problem hiding this comment.
The renderer's contract is escape-first: ALL input is HTML-escaped before any formatting is applied, and formatting only ever emits an allowlist (p/br/pre/code/strong/a with http(s)-only hrefs, rel="noopener noreferrer"). No external markdown dependency, no raw innerHTML of model output anywhere.
The recursive token-restore at line ~114 is the fix for a genuine major our internal devil's advocate caught: the original single-pass String.replace restore could mis-restore when one stashed fragment contained another token sentinel (inline code inside a markdown link label). Three separate audits ran attack corpora against the built bundle — the implementation suite, the DA's 35+ fresh fixtures (including attribute breakouts and CSS-injection attempts through the theming attributes), and the independent verifier's 5 DOM-parsed payloads.
| // The SDK owns reconnection: linear backoff, retries forever. | ||
| persist: true, | ||
| // Never fall back to ambient env credentials (e.g. ROCKETRIDE_APIKEY). | ||
| env: {}, |
There was a problem hiding this comment.
env: {} is deliberate and load-bearing: it prevents the SDK from picking up any ambient ROCKETRIDE_APIKEY fallback, so the widget can only ever authenticate with the value explicitly passed as auth — which the docs, JSDoc, and demo all define as the pipeline's public key (the same credential the chat node's published /chat?auth={public_auth} link uses). Verified live twice: on the published ghcr engine image and on an engine built from develop, the built bundle connected, chatted, and received answers using only the public pk_ key.
| * Turns a single `<script>` tag into a floating chat "bubble": a launcher | ||
| * button fixed to a corner of the page which opens a panel hosting the | ||
| * `<rocketride-chat>` web component. Configuration is read from the script | ||
| * tag's `data-*` attributes via `document.currentScript`: |
There was a problem hiding this comment.
The one-script-tag mode reads its config from the script tag's own data-* attributes via document.currentScript — no global config object, no init call, matching the embed ergonomics non-developers expect from Flowise/Dify-style widgets. The loader guards double-initialization (script included twice, element already defined), renders the launcher as a real <button> with aria-expanded, and closes on Escape. Everything mounts the same <rocketride-chat> element the inline mode uses, so there is exactly one code path for the chat surface itself.
| @@ -0,0 +1,66 @@ | |||
| { | |||
There was a problem hiding this comment.
Packaging decisions worth confirming: (1) the bundle inlines the rocketride SDK from packages/client-typescript/src via esbuild alias — the SDK has no standalone npm build step in-repo, and a single-file embed must inline it regardless; the SDK itself is untouched (zero diff; its full test suite ran green as part of verification). (2) The SDK's optional Node-only ws import is aliased to a stub so esbuild doesn't drag Node built-ins into a browser bundle. (3) unpkg/jsdelivr fields point at the IIFE so the script-tag snippet works from a CDN the moment this publishes. Bundle: ~72 KB raw / ~21 KB gzip, React-free (verified by dist inspection).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughIntroduces the ChangesChat widget implementation
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant HostPage
participant ChatWidget
participant WidgetConnection
participant RocketRideEngine
HostPage->>ChatWidget: configure web component or script tag
ChatWidget->>WidgetConnection: connect with engine URL and public auth
WidgetConnection->>RocketRideEngine: establish persistent chat connection
HostPage->>ChatWidget: submit message
ChatWidget->>WidgetConnection: send question and replay history
WidgetConnection->>RocketRideEngine: submit pipeline question
RocketRideEngine-->>WidgetConnection: status updates and answers
WidgetConnection-->>ChatWidget: update transcript and state
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/README-chat-widget.md`:
- Line 90: Update the RocketRide description in the README to hyphenate
“open-source” when it directly modifies “developer-native AI pipeline platform,”
preserving the rest of the sentence unchanged.
In `@packages/chat-widget/esbuild.js`:
- Line 82: Update the esbuild configuration’s target option from ES2020 to
ES2022, preserving the existing build configuration around it.
In `@packages/chat-widget/src/loader.ts`:
- Around line 462-487: Update initFromScript to add a page-level deduplication
guard before mountChatBubble, in addition to the existing per-script
MOUNTED_FLAG check. Store and check the guard on the relevant page document so
separate loader script elements cannot create multiple bubbles or SDK
connections, while preserving the current null return and first-mount behavior.
In `@packages/chat-widget/src/styles.ts`:
- Around line 363-373: Update the dark-theme focus-visible selector to include
.rr-retry alongside .rr-input and .rr-send, so all three controls use
var(--_accent-text) for their outline color in dark mode.
In `@packages/chat-widget/tsconfig.json`:
- Around line 13-18: Remove the redundant noImplicitAny, noImplicitThis,
strictNullChecks, and strictPropertyInitialization entries from the TypeScript
configuration. Keep noImplicitReturns and noUnusedLocals unchanged because they
are independent of the strict setting.
In `@packages/chat-widget/tsconfig.test.json`:
- Around line 10-15: Remove the redundant baseUrl and paths entries from
tsconfig.test.json, relying on the inherited configuration from tsconfig.json.
Preserve the existing rocketride resolution through the base configuration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f2fe83cc-ce1c-4091-8cd1-260b9057fee7
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (28)
docs/README-chat-widget.mddocs/README-clients.mdpackages/chat-widget/.gitignorepackages/chat-widget/README.mdpackages/chat-widget/demo/index.htmlpackages/chat-widget/esbuild.jspackages/chat-widget/jest.config.jspackages/chat-widget/package.jsonpackages/chat-widget/scripts/tasks.jspackages/chat-widget/src/component.tspackages/chat-widget/src/connection.tspackages/chat-widget/src/entry-iife.tspackages/chat-widget/src/index.tspackages/chat-widget/src/loader.tspackages/chat-widget/src/render.tspackages/chat-widget/src/stubs/ws.tspackages/chat-widget/src/styles.tspackages/chat-widget/src/types.tspackages/chat-widget/tests/component.test.tspackages/chat-widget/tests/connection.test.tspackages/chat-widget/tests/helpers.tspackages/chat-widget/tests/loader.test.tspackages/chat-widget/tests/render.test.tspackages/chat-widget/tests/theming.test.tspackages/chat-widget/tsconfig.jsonpackages/chat-widget/tsconfig.test.jsonpackages/chat-widget/tsconfig.types.jsonpnpm-workspace.yaml
…cleanups CodeRabbit review round on rocketride-org#1586: - initFromScript now guards at page level (existing bubble host in the document) — two separate script-tag includes load two module scopes, so the per-script flag alone could double-mount; regression test added. - Dark-theme focus-visible override now covers .rr-retry, matching .rr-input/.rr-send. - tsconfig cleanups (redundant strict-family flags; inherited baseUrl/paths in the test config) and a README hyphenation fix. esbuild target stays ES2020 deliberately — the workspace convention (client-typescript, chat-ui tsconfigs) and wider embed reach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#Hire-Me-RocketRide
Contribution Type
Feature —
rocketride-chat-widget: an embeddable, brandable chat widget. One script tag from a deployed pipeline to production chat on any website.Closes #1582
Summary
The engine already generates a public chat surface per pipeline (
{host}/chat?auth={public_auth}, with the public key deliberately separated from the private token) — but its only consumer was the standalone chat-ui app. A finished pipeline couldn't become something an end user touches on your site. This PR packages that existing surface the way the category leaders do (Flowiseflowise-embed, Langflow's Share pane component, Dify's app widget — and OpenAI's ChatKit, the piece that survived the Agent Builder shutdown):title,accent,welcome,placeholder,theme=light|dark|auto) +--rr-*CSS custom properties that pierce the shadow boundary; RocketRide violet#5f2167default./chat?auth=link uses. JSDoc, README, and the demo all state that the engine API key and private token must never appear in a page. Assistant output goes through an escape-first safe renderer (allowlist: paragraphs, code, bold, http(s)-only links withrel="noopener noreferrer") — raw model HTML never reaches the DOM.role=log aria-live=polite, labeled controls,aria-expandedlauncher, Escape-to-close, reduced-motion awareness.rocketrideTS SDK (bundled from source with the Node-onlywspath stubbed); zero diff to the SDK, proven by its own full test suite staying green.Validation
Unit / static (no server): 112/112 jsdom tests, clean typecheck.
img onerror,javascript:/data:/vbscript:and scheme-obfuscated URLs, attribute breakouts, fenced-code-language injection, markdown-label HTML) plus the devil's-advocate pass ran 35+ fresh adversarial fixtures against the built bundle, and the independent verifier added 5 more DOM-parsed payloads — everything inert; onlyp/br/pre/code/strong/asurvive.gitleaksclean; MIT headers on all sources; frozen-lockfile install clean.Live end-to-end — twice, on real engines:
ghcr.io/rocketride-org/rocketride-engine:latest): an admin deploys a chat-source pipeline via the SDK; the task exposes its publicpk_key; the widget's built bundle — using only that public key — connects (connecting → connected), sends a question, and receives the pipeline's answer array over the real protocol.develop: the identical script returns the real (mock-LLM) answer text end to end.Visual check: the demo page was loaded in a browser — inline widget and bubble mode both render correctly, including the branded panel and the clean error + Retry state when no engine is reachable.
Review notes
packages/client-typescript/src(it has no standalone npm build; single-file embeds must inline it regardless). Build is wired into the repo builder (chat-widget:build/test) ordered after the SDK's type generation.pnpm-workspace.yamlregistration + docs. No engine, server, or existing-app changes.🤖 Generated with Claude Code
Summary by CodeRabbit
<rocketride-chat>web components and a floating launcher bubble.rr-message/rr-errorevents, and safely rendered assistant output.