Docs

Document Viewer 2.0

Office-format support and live evidence-scribble highlighting — a planning doc extending the Sidecar viewer.

Planned 2026-07-28. Companion to Document Sidecar (the viewer this extends) and Ephemeral Architecture (the plane rules any evidence flow must respect). Deliverable of the "good document viewer + accurate runtime highlighting + typewriter agent scribble" request.


1 · Where the viewer stands today

The sidecar (SIDECAAR-phase work, V4/V7-shared module public/v4/sidecar.js) renders PDF via self-hosted PDF.js (canvas + a private text-item index {str,x,y} per page) and email threads via a structured per-message DOM renderer. Highlighting today is section-granular: grey veils, the active band, the yellow discussed-page border. Three gaps:

  1. No office formats — .docx/.xlsx/.pptx are rejected at upload.
  2. Answers don't point at their evidence — an @h answer names facts but the viewer can't show where they came from.
  3. Highlighting is band-level, not text-accurate — we can frame a section, not sweep the exact sentence.

2 · Research outcome — viewer selection

PDF → keep PDF.js (Mozilla, vendored 4.0.379). Its text layer exposes per-fragment coordinates; viewport.convertToViewportRectangle() translates PDF user-space rects (bottom-left origin) into canvas space, which is exactly what accurate runtime highlighting needs — the standard approach documented by Nutrient, MESCIUS and the overlay-div pattern: highlights are absolutely-positioned divs in a page-anchored overlay, so they survive scroll/zoom. Commercial viewers (Nutrient/Apryse/PDF.js Express) were rejected: license + CDN coupling breaks Align's local-first, no-CDN doctrine.

Office → convert at the boundary, view as PDF. Two candidate paths:

PathFidelityScopeHighlighting
a) client-side docx-preview (docxjs, active 0.4.x)good, HTML-limiteddocx onlyneeds a SECOND highlight engine over HTML
b) server-side LibreOffice headless soffice --headless --convert-to pdfbest-in-class (Baeldung, survey)docx/xlsx/pptx/odt/…FREE — reuses the one PDF pipeline

Locked decision: (b). One viewer, one text-layer index, one highlighter, one sections parser, one embedding path — office files simply become PDFs at upload. soffice 7.3 is already installed on this machine; no new runtime dependency. (Throughput note for later scale: LibreOffice listener mode / Gotenberg-style service cuts per-conversion cost 50-75% — irrelevant at Align's one-doc-per-room cadence.) docx-preview stays on file as the client-side fallback if a deployment lacks LibreOffice.

Email → keep the structured renderer. No embeddable Outlook viewer exists (re-confirmed in Document Sidecar research); parsing libs (postal-mime / msgreader) solve parsing, not viewing. Accurate highlighting in email = substring <mark> wraps inside the message-body DOM — simpler than PDF and fully text-accurate.

3 · Live evidence scribble (the typewriter agent-highlight)

The accuracy problem is upstream, not in the viewer: the client can only highlight exactly what the answer was grounded on if the answer carries its evidence. Guessing by fuzzy-matching answer text against the doc is the inaccurate path — we refuse it.

  • Server (context layer): knowledge.answer() already ranks dense hits; add meta.onEvidence([{text}]) firing with snippetAround(top-hit, standalone) — a contiguous, verbatim span of document text (that contiguity is what makes client matching exact). Execution (socket-api) attaches it to the posted moderator message as an optional additive field evidence (frozen wire shapes are extended, never mutated; archives gain provenance for free).
  • Client (shared sidecar): the per-page text index grows w,h per item (PDF.js item .width × scale). Matching: normalized-concat + char→item map (the locateSection machinery, generalized) finds the snippet's item run → group items into rows by y → one rect per row.
  • The scribble: a .scribble mark div per row, swept left→right, row by row, with a leading pen dot and ±0.3° jitter — a GSAP timeline (scaleX 0→1, transform-origin:left, sequential with slight overlap) so it reads as the agent running a highlighter across the page while the answer types out. Email variant: <mark> with background-size animated 0→100%.
  • Triggers: auto-scribble when the sidecar is open when an evidence-bearing answer lands; every evidence answer bubble is clickable → opens the sidecar, scrolls to the span, replays the scribble.
  • Chat side (V7): moderator answers render with a typewriter character reveal (≤2.5s, capped by length) so the scribble and the "agent writing" land as one gesture.
  • Fail-soft ladder: snippet not found in text layer (scanned PDF, hyphen splits) → fall back to the existing section band; no GSAP / reduced motion → instant static marks; no evidence on the message (compliant plane, persona answers) → nothing new, current behaviour.

4 · Thoughtful GSAP (V7 — scoped, all additive)

GSAP is already vendored (/vendor/gsap + ScrollTrigger). V7 adopts it for exactly four jobs, each with the existing IO/CSS fallback kept intact:

  1. Landing reveals — ScrollTrigger-staggered rise-ins replacing the plain IO class flips (same [data-reveal] contract, so tests keep passing).
  2. Stat count-ups — gsap tweens with ease-out snap.
  3. Hero-scene drift — a few px of parallax on the viewer card + bubbles against scroll; the CSS two-phase loop stays CSS (it must run headless).
  4. The scribble + typewriter timelines (§3) — the one place GSAP is load-bearing for feel, never for correctness.

5 · Phases

Phase 1 — evidence pipe + scribble + GSAP (team plane) — SHIPPED 2026-07-28

Delivered as the annotation tool calls (Architecture §11), which subsume the scribble: @h let's mark it / highlight / add a comment <text> / make a note <text> place a per-author pastel highlighter sweep plus a colour-coded card on the document, live for the crew. Anchoring resolves phrase → evidence → section, all constrained to the section under discussion, with approx (page-1) as an asserted failure state. Evidence chunks ride the annotation record; knowledge.anchorFor composes the query by specificity and skips chunks already claimed by earlier marks. Two silent bugs were fixed on the way (lazy region computation; heading matching that preferred a common word inside a sentence) — both documented in §11.

Still open from the original Phase 1 sketch: the typewriter reveal of the moderator's answer text in chat, and GSAP-sequenced row-by-row sweeping (the sweep currently animates as a single CSS scaleX per row).

original plan
  • server/context/knowledge.js: meta.onEvidence (snippet of top dense hit).
  • server/execution/socket-api.js: attach evidence to the answer message.
  • public/v4/sidecar.js (shared): item w,h; scribble(evidence) API (match → row rects → GSAP sweep; email <mark> variant); scroll-to.
  • public/v7/v7.js + CSS (+v4.css marks): GSAP landing motion, evidence click-through, typewriter answers.
  • Tests: unit — evidence fires with verbatim doc text (mock llm); live — rag suite asserts the answer message carries matching evidence; browser — v7 suite clicks an answer → .scribble marks appear over the demo agreement; reduced-motion path asserted.

Phase 2 — office ingestion (docx/xlsx/pptx) — next

  • server/execution/parsers/office.js: detect soffice at boot (probe like pdftotext); upload of office MIME/extensions → convert in a temp dir → hand the PDF buffer to the EXISTING pdf path (sections, index, sidecar, scribble all inherit). room.doc.sourceFormat records provenance; the original file is retained for the record alongside the annotated PDF.
  • Fail-soft: no soffice → 422 with a candid "convert to PDF and re-upload".
  • Fixtures: generate .docx via pandoc (installed) — suite SKIPs when soffice absent (docling-suite pattern).

Phase 3 — enterprise plane + polish

  • Resident returns evidence spans with /kxp/ask answers (sealed under K_room like summaries; coordinator stays document-blind — it relays offsets, never content). Ephemeral fact-cache stores evidence beside answers.
  • Email deep-link highlighting for @h answers grounded in thread messages.
  • Office → sidecar page-map textures verified; large-workbook guardrails.

6 · Critical files

server/context/knowledge.js · server/execution/socket-api.js · server/execution/parsers/office.js (new) · server/execution/http-api.js (office MIME gate) · public/v4/sidecar.js · public/v7/v7.js / v7.css / public/v4/v4.css · test/v7-test.js, test/rag-stop-test.js, new test/office-test.js.

7 · Risks & mitigations

  • Snippet↔text-layer mismatch (ligatures, hyphenation, multi-column): normalized matching + prefix fallback; ultimate fallback = section band. Never block the answer on the highlight.
  • LibreOffice fidelity/perf: 7.3 headless is heavy (~1-2s/doc, RAM); one doc per room makes this a non-issue; convert with a hard timeout, fail-soft.
  • Wire discipline: evidence is optional-additive; publicRoomState untouched; compliant plane ships nothing until Phase 3's sealed design.
  • Scribble on rotated/scanned pages: rects come from the same transform as rendering, so rotation is safe; scanned pages have no text layer → band fallback, marked "approx" like bubbles.

8 · Verification

Phase 1: npm run test:v7 (scribble drive), npm run qa fast set; manual — ask @h about the option agreement with the sidecar open and watch the sweep. Phase 2: test/office-test.js (pandoc-made docx → sections found → scribble works on the converted PDF); upload a real xlsx through the tunnel.

Last updated on

On this page