Skip to content

feat(wave2): citation trust layer — resurrect the signature feature#13

Merged
vikranthreddimasu merged 1 commit into
mainfrom
feat/wave2-citation-trust-layer
Apr 18, 2026
Merged

feat(wave2): citation trust layer — resurrect the signature feature#13
vikranthreddimasu merged 1 commit into
mainfrom
feat/wave2-citation-trust-layer

Conversation

@vikranthreddimasu

Copy link
Copy Markdown
Owner

Context

Wave 2 of 5 from PLAN.md. Wave 1 was #12 (stop-the-bleeding: data integrity, security, destructive guards).

The audits found that the amber left-rule citation treatment in `tokens.css` — the app's entire visual story around grounded vs synthesized answers — had never rendered in a single chat response. `MessageBubble` used plain ReactMarkdown with no renderer; the CSS classes were pure decoration. This wave wires it end to end.

Citations actually render now

  • Parse + wrap in MessageBubble. Assistant output is scanned for `[Source N]` / `[N]` markers (case-insensitive, with/without `#`). Each sentence containing a citation becomes a `.cited` block with the amber left-rule; each marker becomes a clickable `CitationMarker` — mono tabular-nums chip, keyboard-focusable, hover-styled.
  • Graceful fallback. Non-cited sentences and markdown with nested formatting pass through untouched; orphan marker indexes (model hallucinated a citation that doesn't exist in retrieved sources) render as a muted tooltip so we don't fake it.
  • Two-way link between message and source. `hoveredSourceIndex` is shared state in `AppShell`: hovering a citation highlights + scrolls its card; hovering a card highlights the corresponding citation chip. Clicking a citation opens the document preview with highlight text already populated.
  • Consolidated source-open path. `openSource(source: SourceChunk)` in `AppShell` is the single function for both citation click and source card click. Cross-notebook mode now uses `source.notebook_id` if present (the previous code hard-wired `activeNotebookId`, which was wrong for cross-notebook sources).

Source panel actually toggles + has an empty state

  • Toggle works. `SourcePanel` now checks `sourcePanelOpen` from the store. `Cmd+/` and the overflow-menu "Toggle sources" item were doing nothing — the component only hid on empty source count.
  • Empty affordance. Before first query: serif-italic "No sources yet" headline + muted hint. The 280px of right-side real estate isn't unexplained blankness anymore.
  • Slide-in animation. No more 280px layout jump when the panel appears/disappears — fade + 8px translate on mount.
  • Hover state. Cards lift with a cite-glow on hover; clicking opens the source. Keyboard Enter/Space also opens.
  • Token-based relevance colors (`--color-accent` / `--color-cite` / `--color-text-muted` instead of hardcoded hex).
  • Defensive clamp for `relevance_score` — if the backend sends a 0–1 float, bars were invisible. Now treated as a fraction and scaled to 0–100.
  • `[N]` markers on each card so citation number → card number is obvious.

DocumentPreview on the warm-stone system

  • Full token rewrite of `DocumentPreview.css`. Every value was a cold neutral (`#1f1f1f`, `#e5e5e5`, `#a3a3a3`); now matches the rest of the app. Radius from hardcoded 12px → `--radius-modal` (20px).
  • No more double PDF load. Prior code called `pdfjs.getDocument(url)` inside `onLoadSuccess` to scan for highlight text, re-downloading the whole file. Now we use the `PDFDocumentProxy` react-pdf already loaded.
  • Tighter highlight matching. The `matchRatio >= 0.3` word-overlap heuristic false-positived across whole paragraphs. Replaced with longest-consecutive-run + min-run floor; amber highlights hit the matched sentences only.
  • Keyboard works without a click. Overlay is programmatically focused via `requestAnimationFrame` on open. Arrow/Escape/±/= shortcuts no longer require a click-to-focus step. `role="dialog"` + `aria-modal` + `aria-label` added.
  • Emoji view-toggle dropped. `📄 Single` / `📑 All` → plain text labels. Emoji rendering is inconsistent across OS versions and cheapens the surface.
  • PDF highlight uses the amber cite tokens (`--color-cite` + `--color-cite-glow`). The file shipped Tailwind warning yellow `#fbbf24` until today.

Chat polish riding along

  • Aborted messages have a visible tag. Wave 1 made abort work correctly; this wave shows it — mono `stopped` chip + 0.68 opacity on the body so you can tell a truncated reply from a complete one.
  • Auto-scroll stops fighting the user. `isScrolledUp` was already in state but not gating the scroll-into-view effect; now it does.
  • `.cite-marker` gets hover + focus styling using `--color-cite-subtle` + `--color-cite-glow`.

Verification

  • `npm run build` clean (269 modules, 842ms)
  • No backend changes this wave

Not in this PR

  • Rename notebooks, delete documents, real upload progress, notification center, Ollama health banner (Wave 3)
  • Backend crash recovery, API timeouts, OOM guards (Wave 4)
  • Pane resize, a11y polish (Wave 5)

🤖 Generated with Claude Code

The amber left-rule citation treatment in tokens.css has existed since the
design system landed. It had never rendered in a single chat response —
MessageBubble used plain ReactMarkdown with no renderer. This wave wires it
end to end, fixes the dead source-panel toggle, and rewrites the document
preview on the warm-stone palette.

## Citations actually render now

- MessageBubble parses assistant output for [Source N] / [N] patterns (case-
  insensitive, with or without #) and:
  - Wraps each sentence containing a citation in .cited with the amber
    left-rule from tokens.css
  - Replaces each marker with a CitationMarker — a small mono tabular-nums
    chip that is clickable, keyboard-focusable, hover-styled, and carries
    title=document_name for quick identification
  - Degrades gracefully: non-cited sentences and mixed-children markdown
    nodes pass through untouched; orphan marker indexes render as a muted
    tooltip so we don't fake a source that wasn't retrieved
- AppShell owns a single openSource(source) function shared by:
  - Citation marker click in MessageBubble → opens DocumentPreview with
    highlight text already populated
  - Source card click in SourcePanel → same code path
- Hover is two-way linked via hoveredSourceIndex shared state — hovering a
  citation scrolls its card into view in the panel and highlights it; hovering
  a card also drives the same visual.
- Citation marker click now passes a full SourceChunk (including notebook_id)
  instead of just a path, which also fixes cross-notebook mode where
  activeNotebookId was the wrong id for the clicked source.

## Source panel is toggleable and has an empty affordance

- SourcePanel.tsx now respects sourcePanelOpen from the store. Cmd+/ (and
  the overflow menu) actually did nothing for the last release; the
  component returned null purely based on source count. Fixed.
- When toggled open but sources are empty (pre-first-query), we render a
  serif-italic "No sources yet" headline with a muted hint, so the 280px of
  right-side real estate isn't unexplained blankness.
- Slide-in + fade animation on mount so toggling back on doesn't feel like
  a layout jump; cards lift on hover with a subtle cite-glow.
- Relevance colors migrated to tokens (--color-accent / --color-cite /
  --color-text-muted). Score is also clamped to 0-100 defensively — if the
  backend ever returns a 0-1 float, bars no longer render invisibly.
- [N] marker added to each card header so the user can map a citation
  chip's number to the right card visually.

## DocumentPreview on-system rewrite

- DocumentPreview.css rewritten top-to-bottom on warm-stone tokens. Every
  value was a cold neutral (#1f1f1f, #e5e5e5, #a3a3a3) — it now matches the
  rest of the app exactly. Radius moved from a hardcoded 12px to
  --radius-modal (20px).
- Stopped the double PDF load. Prior code called pdfjs.getDocument(url)
  inside onLoadSuccess to scan for highlight text, redownloading the entire
  file. Now we use the PDFDocumentProxy react-pdf already loaded.
- Highlight match tightened. The 0.3 word-overlap threshold caused false
  positives across whole paragraphs. Replaced with a longest-consecutive-run
  heuristic and a min-run floor; the amber highlight now hits the matched
  sentences, not every paragraph that shares topic words.
- Overlay is programmatically focused on open (requestAnimationFrame), so
  Arrow/Escape/±/= all work without a click-to-focus step. role="dialog" +
  aria-modal + aria-label added.
- Replaced 📄/📑 emoji view-toggle with plain text labels ("Single" / "All
  pages"). Emoji rendering is inconsistent across OS versions and gave the
  UI a cheap feel compared to the rest of the surface.
- PDF text highlight now uses --color-cite + --color-cite-glow (Wave-1-era
  amber rebrand was applied to DESIGN.md but the component still shipped
  Tailwind warning yellow #fbbf24).

## Chat niceties that ride along

- Aborted assistant messages render with a "stopped" mono chip underneath
  and 0.68 opacity on the body so a user can tell a truncated reply from a
  full one.
- ChatView auto-scroll stops snapping the user back to bottom when they've
  scrolled up — the isScrolledUp signal was already in state, it just wasn't
  gating the scroll effect.
- .cite-marker styled as a pill-padded, hover-highlighted superscript with a
  focus ring using --color-cite-glow.

## Verified

- \`npm run build\`: clean, 269 modules, 842ms
- No backend changes this wave

Wave 3 (missing actions + error visibility) is next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vikranthreddimasu vikranthreddimasu merged commit d99696b into main Apr 18, 2026
2 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.

1 participant