Skip to content

feat: inline citations and source cards for RAG results - #161

Merged
PAMulligan merged 24 commits into
mainfrom
56-inline-citations
Sep 29, 2026
Merged

PAMulligan merged 24 commits into
mainfrom
56-inline-citations

Conversation

@PAMulligan

Copy link
Copy Markdown
Contributor

Closes #56

What

Opt-in citations option (boolean | { maxSources, favicons }, default off, fails closed) on ChatWidget, ClaudiusConfig, <claudius-chat citations citations-max-sources citations-favicons>, and widget.citations in client configs. When it is on and a reply carries sources:

  • in-range [n] markers in the reply render as numbered accent chips; [1, 3] and [1][3] both work, out-of-range or malformed brackets stay text;
  • a collapsible Sources footer under the reply lists one card per source (favicon from the source's own origin with no referrer, title link, 200-character snippet, domain), replacing the source icon and sidebar for that widget; maxSources (default 5) cards show before "Show all";
  • clicking a chip opens the footer, scrolls to the card, focuses it, and flashes a ring;
  • the live region and read-aloud skip the markers.

The widget sends citations: true with each request. The worker then numbers its RAG excerpts in the system prompt to match sources (two chunks of one page share a number) and appends a citing instruction. Independently of the flag, every source now carries a snippet, sources are limited to the excerpts that fit maxContextChars (so the prompt and the cards always agree), and /api/chat/stream announces event: sources with the first upstream event so chips render while the text streams. done still carries sources.

Docs: new configuration/citations.md; widget options, localization, clients, theming, RAG, and a new streaming section in the REST reference; CLAUDE.md. Design and plan are in docs/plans/2026-09-27-inline-citations-*.md.

Behaviour changes outside the opt-in

  • URLs inside **bold** or *italic* are now linked (the inline renderer's leaf step is shared).
  • RAG deployments whose context budget drops an excerpt no longer return that page as a source. With the defaults nothing is dropped.
  • A widget without the option keeps today's source timing: early sources are attached to the streaming reply only when citations are on.

Decisions made without you (see the design doc for the reasoning)

  1. Opt-in and fail closed, against the issue's implied default-on, for the same reason as Conversation export (Markdown, JSON, copy-to-clipboard) #55 (the floating @1 tag).
  2. The footer replaces the icon and sidebar only when the option is on.
  3. The widget drives the worker through the request flag; no new worker variable.
  4. An early sources SSE event in addition to done.sources, emitted with the first upstream event so a first-event upstream error is still a JSON 503.
  5. Favicons from <origin>/favicon.ico, no third party, no referrer, favicons: false to switch off. Cards mount only when the footer is opened, so no request happens before that.
  6. Footer collapsed by default.
  7. Snippets are 200 characters, built by the worker from the first matching chunk, cut again by the widget for sources from plugins.
  8. Sources limited to excerpts that fit the budget.
  9. [1, 3] groups supported as well as [1][3].
  10. Bold and italic text goes through the same link and citation step.

Verification

Check Result
widget vitest run 785 passed
worker vitest run 177 passed
scripts vitest run 93 passed
widget tsc --noEmit, eslint src/, prettier --check, typedoc --emit none, vite build (lib + iife), publint, attw clean (2 pre-existing react-refresh warnings)
Playwright, chromium-desktop, system Chrome 21 passed, including the new citations.spec.ts
docs astro build 63 pages, anchors resolve
size-limit within the raised budgets

Bundle deltas against main (measured with size-limit --json on both trees, limits set to measured + 5%): claudius.iife.js gzip 71,954 → 74,454 B (+2,500), brotli +2,162, raw +9,462; claudius.js gzip +3,065; CSS gzip +60.

Review

An independent whole-branch review ran before this PR was opened: no critical findings, two important (a first-event upstream error would have opened the SSE before failing, and a marker glued to a URL broke the link) and five minor. All seven are fixed with a failing test first (fix: commits). The worker's local tsc -p . has 15 pre-existing errors in attachments.ts typings that predate this branch.

🤖 Generated with Claude Code

PAMulligan and others added 24 commits September 27, 2026 20:28
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…est opts in

Sources now come from the excerpts that fit the context budget, so the
numbers in the prompt and on the cards are the same list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Text inside bold and italic now goes through the same link step as the
rest of a line, so URLs inside ** are linked too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…<claudius-chat>

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uest field

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…re it

An upstream failure delivered as the first stream event (an overload)
must still reach the route before the SSE response opens, so it stays a
JSON 503 the client retries instead of an in-band STREAM_ERROR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Early sources are attached to the streaming reply only when the option
is on, so an embed that never opted in shows its source icon on done as
before, even against a worker that announces sources early.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Measured against main (967a227) with size-limit --json on both trees,
limits set to measured + 5%:

  claudius.iife.js  raw 222981 -> 232443 B (+9462), gzip 71954 -> 74454 B (+2500), brotli 62828 -> 64990 B (+2162)
  claudius.js       raw 115459 -> 129310 B (+13851), gzip 31881 -> 34946 B (+3065), brotli 27235 -> 29542 B (+2307)
  claudius.css      raw 24604 -> 24970 B (+366), gzip 5177 -> 5237 B (+60), brotli 4556 -> 4604 B (+48)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…while streaming

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… snippets

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Deploying chat-widget with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9bd8262
Status: ✅  Deploy successful!
Preview URL: https://8899972c.chat-widget-ejc.pages.dev
Branch Preview URL: https://56-inline-citations.chat-widget-ejc.pages.dev

View logs

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
claudius.iife.js (raw) 227.03 KB (+4.26% 🔺)
claudius.iife.js (gzip) 72.71 KB (+3.49% 🔺)
claudius.iife.js (brotli) 63.44 KB (+3.41% 🔺)
claudius.js (raw) 126.31 KB (+12.03% 🔺)
claudius.js (gzip) 34.15 KB (+9.68% 🔺)
claudius.js (brotli) 28.88 KB (+8.6% 🔺)
claudius.css (raw) 24.38 KB (+1.49% 🔺)
claudius.css (gzip) 5.11 KB (+1.16% 🔺)
claudius.css (brotli) 4.5 KB (+1.06% 🔺)

@PAMulligan
PAMulligan merged commit c899b84 into main Sep 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inline citations and source cards for RAG results

1 participant