Skip to content

feat(citation): add ≈ approximate-match marker and popover polish#428

Merged
bensonwong merged 11 commits intomainfrom
feat/approximate-citation-marker
Apr 15, 2026
Merged

feat(citation): add ≈ approximate-match marker and popover polish#428
bensonwong merged 11 commits intomainfrom
feat/approximate-citation-marker

Conversation

@bensonwong
Copy link
Copy Markdown
Collaborator

Summary

  • Adds a marker to citation highlights, keyhole strips, and drawer items when the model's inline display text (claimText) differs from the matched source text (sourceMatch) — surfaces approximate citation matches visually without hiding them
  • Renames i18n key popover.displayedAspopover.claimedAs and updates copy to "claimed as …" across all locales (en, es, fr, vi)
  • Replaces amber hover effect in HighlightedSourceContext using a Tailwind group/anchor pattern so the marker dims at rest and turns amber on hover/focus
  • Flattens claim block styles in markdownToHtml report shell: removes border-radius from cowork notice, removes border-radius + border-left primary color accent from the claim block (uses --dc-foreground instead), and drops the unused tier2Open progressive-disclosure flag
  • Removes scroll-lock RAF logic from usePopoverPosition and EvidenceTray in favor of overscroll-behavior: contain
  • Adds capture-snapshot.ts and template.ts to examples/basic-verification for one-shot snapshot capture and local re-render of verified output (debugging/dev aid); exports shared step helpers from shared.ts
  • Removes 29 now-redundant usePopoverViewState unit tests that duplicated integration-level coverage

Test plan

  • Open a citation whose claimText differs from sourceMatch — confirm appears beside the highlight in the popover and keyhole strip, and "claimed as …" annotation appears below
  • Open a citation whose claimText equals sourceMatch — confirm no marker appears anywhere
  • Hover/focus the highlight span — confirm turns amber; move away — confirm it returns to subtle
  • Open a citation in the drawer — confirm and "claimed as …" appear in the drawer item footer when applicable
  • Run markdownToHtml report output and confirm claim block has no rounded corners and uses foreground accent color
  • Confirm all locales (es, fr, vi) show translated "claimed as" text instead of old "displayed as" copy
  • Run npm test — all remaining tests pass

Benson and others added 7 commits April 15, 2026 11:24
… contain

Drops acquireScrollLock/releaseScrollLock from usePopoverViewState (and the
isOpen param that gated it). The popover now sets overscrollBehavior: contain
on its container, which natively absorbs wheel/touch momentum at its edges
without layout shifts or body padding hacks. Removes SCROLL_LOCK_LAYOUT_SHIFT_EVENT
listener from Popover.tsx and cleans up scroll lock tests and mocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a boolean `isApproximate` prop (claimText !== sourceMatch) that
flows from DefaultPopoverContent/CitationDrawer down to HighlightedSourceContext.
When set, a ≈ glyph appears beside the highlight span and turns amber on hover
(via Tailwind's named group-hover/anchor variant). The annotation row now reads
"claimed as …" instead of "(displayed as …)" to better reflect intent.

Renames i18n key `popover.displayedAs` → `popover.claimedAs` across all
locale files (en, es, fr, vi).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove border-radius from cowork notice and claim blocks for a sharper,
document-like appearance. Change claim left-border and label color from
--dc-primary to --dc-foreground / --dc-muted-foreground to reduce visual
noise on the static HTML export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ed step fns

capture-snapshot.ts: uploads a source PDF, re-keys fixture citations onto the
fresh attachmentId, calls verifyAttachment, and writes a *-snapshot.json to
output/ so template.ts can re-render without hitting the API again.

template.ts: loads a snapshot (newest by mtime, by safeName, or by explicit
path), calls renderVerifiedHtml, and writes an HTML file. Accepts --md= to
substitute hand-edited visible text for quick copy iteration.

shared.ts: exports stepUpload / stepWrapPrompts / stepCallLlm so both
step-runner and capture-snapshot share the same pipeline logic. Also replaces
report.shareUrl with a constructed deepcitation.com portal URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DefaultPopoverContent/PopoverFallbackView: align isApproximate to use
  citation.sourceMatch (not verification.sourceSnippet) so both code
  paths compare the same field
- DefaultPopoverContent/PopoverFallbackView: use precomputed isApproximate
  in annotation row instead of inline condition
- DefaultPopoverContent: incorporate annotation div layout polish
  (flex items-center, adjusted margins, text-md ≈ glyph)
- HighlightedSourceContext: motion-safe:transition-colors + group-focus-within
  so ≈ marker respects prefers-reduced-motion and responds to keyboard focus
- capture-snapshot: whitelist allowed providers before path interpolation
  to prevent directory traversal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
agui-chat-deepcitation Ignored Ignored Preview Apr 15, 2026 6:46pm
deepcitation-langchain-rag-chat Ignored Ignored Preview Apr 15, 2026 6:46pm
mastra-rag-deepcitation Ignored Ignored Preview Apr 15, 2026 6:46pm
nextjs-ai-sdk-deepcitation Ignored Ignored Preview Apr 15, 2026 6:46pm

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

✅ Playwright Test Report

Status: Tests passed

📊 Download Report & Snapshots (see Artifacts section)

What's in the Visual Snapshots

The gallery includes visual snapshots for:

  • 🖥️ Desktop showcase (all variants × all states)
  • 📱 Mobile showcase (iPhone SE viewport)
  • 📟 Tablet showcase (iPad viewport)
  • 🔍 Popover states (verified, partial, not found)
  • 🔗 URL citation variants

Run ID: 24472095998

@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Code Review

Overall this is a clean, well-scoped PR. The approximate-match marker idea is solid, the Tailwind group/anchor pattern is idiomatic, and replacing the JS scroll-lock RAF with overscroll-behavior: contain is the right direction. A few things worth addressing before merge:


Bug: text-md is not a valid Tailwind class

src/react/DefaultPopoverContent.tsx, line ~599

<span aria-hidden="true" className="mr-1 text-amber-500 dark:text-amber-400 text-md"></span>

text-md is not a Tailwind utility — the correct class is text-base (or a specific size like text-sm). Without a valid size class the character will inherit the container font-size rather than the intended one. The same glyph in HighlightedSourceContext and CitationDrawer doesn't have this issue, so the inconsistency is only here.


Locale semantic mismatch

The English key was renamed from popover.displayedAspopover.claimedAs and the copy changed from "(displayed as …)" to "claimed as …" — a meaningful semantic shift. The non-English overrides were updated to use the new key, but the translated strings still say "displayed as" (mostrado como / affiché comme / hiển thị là). That's inconsistent with the English intent. Either update the translations or leave a TODO comment so translators know a re-translation is needed.


overscroll-behavior: contain may not fully replace the scroll lock

The previous acquireScrollLock() prevented the body from scrolling while the popover was in expanded-page view. overscroll-behavior: contain only stops scroll chaining (momentum overflow) — it doesn't block body scroll when the popover content is shorter than its container and therefore not itself scrolling. On mobile Safari in particular, if the user swipes past the top/bottom of the popover content without triggering its own scroll, the body will still scroll. This may be acceptable given the complexity trade-off, but it should be called out in the test plan so QA knows to validate on real devices.


Missing API key guard in capture-snapshot.ts

const dc = new DeepCitation({ apiKey: process.env.DEEPCITATION_API_KEY! });

The non-null assertion silences TypeScript but passes undefined to the constructor at runtime if the env var is absent. The script already validates the fixture path and PDF — add the same check here for consistency and a better DX:

const apiKey = process.env.DEEPCITATION_API_KEY;
if (!apiKey) {
  console.error("❌ DEEPCITATION_API_KEY is not set — add it to .env");
  process.exit(1);
}
const dc = new DeepCitation({ apiKey });

--md= path not existence-checked in template.ts

: readFileSync(resolve(mdOverride), "utf-8")

If the user passes a --md= path that doesn't exist, readFileSync throws a raw Node error rather than the styled fail() message used everywhere else. A quick guard keeps the UX consistent:

if (!existsSync(resolve(mdOverride))) fail(`--md file not found: ${mdOverride}`);

Hardcoded portal URL in shared.ts

const portalUrl = `https://deepcitation.com/verifications/${report.id}`;

If the API response already has a shareUrl field (it did before this change), constructing the URL from a hardcoded host breaks staging/dev environments. If the field was intentionally removed from the API response, fine — but worth a comment explaining why the URL is being built locally rather than reading it from the response.


Minor: isApproximate computed three times

!!claimText && !!sourceMatch && claimText !== sourceMatch appears independently in PopoverFallbackView, DefaultPopoverContent, and CitationDrawerItemComponent. The logic is trivial so this isn't a blocker, but it's the kind of thing that diverges when one site adds a trim/normalize step and the others don't. A shared one-liner helper (or computing it one level up and passing it down) would keep it DRY.


The scroll-lock removal and overscrollBehavior: contain placement in the popover inline style are otherwise clean. The group/anchor Tailwind pattern for the hover-amber effect is exactly right.

Benson and others added 4 commits April 15, 2026 12:32
…ortBody

Render remaining report sections flat instead of inside a <details> wrapper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ndering

Details wrapper and "Full Report" summary were removed; tests now assert
flat section order and the absence of <details>.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ./html-utils package export for generateReviewVariants and ReportStyle,
making the report generation API accessible without importing from the CLI path.

Also make the approximate citation marker (≈) always amber rather than
revealing only on hover/focus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bensonwong bensonwong merged commit fd5d6c2 into main Apr 15, 2026
13 of 14 checks passed
@bensonwong bensonwong deleted the feat/approximate-citation-marker branch April 15, 2026 18:50
bensonwong pushed a commit that referenced this pull request Apr 16, 2026
tsup.config.ts and package.json exports reference html-utils but the
source file was missing from this branch (added on main in #428).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bensonwong added a commit that referenced this pull request Apr 17, 2026
* feat(deepcitation): export renderVerifiedHtml from public API

* feat(sdk): replace publishVerificationReport with createReport; add report CLI command

* fix(viewTransition): defer CDN repositioning until animation completes; fix transitionDepth decrement timing

CDN scheduleReposition now polls isViewTransitioning() in a rAF loop instead
of firing reposition() immediately — prevents a mid-animation reposition
that was snapping the popover before the collapse ghost finished.

_transitionDepth is now decremented inside the onDone callback fired when
both animations (ghost + content reveal) finish, rather than before the
animation starts. This means isViewTransitioning() stays true for the full
duration of the collapse, keeping the CDN deferral loop active throughout.

* feat(markdownToHtml): add claim card + MODEL meta item; remove audience preset

Header now renders an optional claim card (blockquote with eyebrow label)
between the H1 and the meta strip when options.claim is provided. Inline
markdown (bold/italic/code) is formatted; HTML is escaped; whitespace-only
values are silently ignored.

The AUDIENCE meta item is replaced by MODEL, shown when options.model is
set. The audience feature (AUDIENCE_PRESETS, AudiencePreset, AUDIENCE_CONFIG,
--audience CLI flag) is removed entirely — width and tier2Open are hardcoded
to their general-preset values (960px / true), keeping the output unchanged
for existing callers.

* chore: delete teardown-v14.png; downgrade tsconfig.jest ignoreDeprecations to 5.0

teardown-v14.png was a leftover debugging screenshot.
ignoreDeprecations "6.0" was a forward-looking value that TS 5.x doesn't
recognize — reverted to "5.0" which matches the installed compiler.

* fix: make onDone required in runPageCollapseGhostAnimation; add scheduleReposition circuit-breaker

onDone was optional but the only call site always passes it — optional chaining
was masking an invariant. Making it required lets TypeScript enforce that
_transitionDepth will always be decremented by callers.

scheduleReposition now accepts a retriesLeft counter (default 30 ≈ 500 ms at
60fps) so the rAF polling loop self-terminates if _transitionDepth ever gets
stuck, falling through to reposition() rather than looping indefinitely.

Update cdnPopover source-invariant test to match the new call-site signature.

* refactor: fix scheduleReposition event-handler bug; clean up minor quality issues

scheduleReposition was used as an event listener for resize/ResizeObserver.
The default-parameter circuit breaker (retriesLeft = 30) was bypassed because
event listeners receive an Event object as the first argument — Event > 0 is
NaN, so the retry condition was always false during resize-triggered
repositions. Split into a clean public scheduleReposition() (no params) and a
private deferReposition(retriesLeft) that owns the retry loop.

Also: remove unnecessary `as boolean` cast from cfg literal; fix leading
whitespace in claimCard template literal (would have been emitted verbatim in
HTML output); remove stale "AUDIENCE hidden on default general" comment from
test (audience feature was removed on this branch).

* fix(ci): resolve lint-and-validate and test failures

- Replace deprecated moduleResolution "node10" with "node16" in
  tsconfig.jest.json to fix TS5107 errors with TypeScript 6.x in CI
- Fix biome formatting: remove trailing blank lines, sort imports
- Complete markdownToHtml CSS token refactor (DC_ROOT_TOKENS variables)
- Add Test Failure Policy to engineering-rules.md

* fix(ci): restore ignoreDeprecations "6.0" for TS6 moduleResolution compat

TypeScript 6.x normalizes module:CommonJS + any nodeX resolution to
node10 semantics internally and emits TS5107. The only suppressor is
"ignoreDeprecations": "6.0" — as the error message itself instructs.
Reverts the node16 attempt (no effect on TS5107 with CommonJS module).

* fix(animation): anchor page-expand ghost to annotation position via data attrs

- EvidenceKeyhole now sets data-dc-source-anchor-x/y on the keyhole
  element so buildGhostTarget can compute imageOffsetLeft + anchorX×imageW
  instead of always using the viewport center (srcW/2, srcH/2).
  Fixes ghost overshoot when a width-filled image has annotation off-center.
- buildGhostTargetFromViewport keeps viewport-center anchor (miss/not_found
  has no annotation; fall through to srcW/2 is correct there).
- Add will-change: filter to ghost container for blur compositor hint;
  add will-change: transform to ghost img to avoid per-frame repaints.
- Locate-icon pulse: slower timing (120/80ms), neutral --dc-muted-foreground
  color, style moved to button so transform applies at button level.
- Pulse stage renamed grow→flash; update Playwright assertions accordingly.
- tsconfig.jest.json: upgrade moduleResolution to node16, drop ignoreDeprecations.

* wip: animation debug tooling (taking over from worktree)

* fix(animation-debug): address review findings

- Remove vt-expand/vt-collapse from FROZEN_KINDS (no call sites yet)
- Drop NODE_ENV guard from installConsoleApi so CT env installs it
- Add define to playwright-ct.config.ts to ensure dev mode in Vite
- Add evidence.textItems + verifiedSourceMatch to harness fixture so
  resolveEvidenceSourceAnchorRatio returns a non-null anchor ratio
- Switch toBeVisible → toBeAttached for 0×0 aim overlay container
- Rename misleading test name for scaleDuration edge-case test
- Delete src/html-utils.ts (dead re-export, no importers)

* refactor(animation-debug): simplify debug store and harness

- Export stepAnimation/pauseAnimation/playAnimation from store directly
- Replace window.__dcAnimationDebug casts in ControlBar with store calls
- Remove redundant serverSnapshot wrappers; pass getDebugSnapshot directly
- Inline serverFalse lambda in AimOverlay
- Replace useState+useEffect imageSrc with useMemo in AnimationDebugHarness
- Add RAF equality guard to prevent spurious re-renders in AimAlignmentPanel

* fix(animation-debug): add missing type field to harness citation fixture

* fix: add missing src/html-utils.ts entry point

tsup.config.ts and package.json exports reference html-utils but the
source file was missing from this branch (added on main in #428).

* fix(ci): restore ignoreDeprecations "6.0" for TS6 moduleResolution compat
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