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:
- No office formats —
.docx/.xlsx/.pptxare rejected at upload. - Answers don't point at their evidence — an
@hanswer names facts but the viewer can't show where they came from. - 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:
| Path | Fidelity | Scope | Highlighting |
|---|---|---|---|
| a) client-side docx-preview (docxjs, active 0.4.x) | good, HTML-limited | docx only | needs a SECOND highlight engine over HTML |
b) server-side LibreOffice headless soffice --headless --convert-to pdf | best-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; addmeta.onEvidence([{text}])firing withsnippetAround(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 fieldevidence(frozen wire shapes are extended, never mutated; archives gain provenance for free). - Client (shared sidecar): the per-page text index grows
w,hper item (PDF.js item.width× scale). Matching: normalized-concat + char→item map (thelocateSectionmachinery, generalized) finds the snippet's item run → group items into rows by y → one rect per row. - The scribble: a
.scribblemark div per row, swept left→right, row by row, with a leading pen dot and ±0.3° jitter — a GSAP timeline (scaleX0→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>withbackground-sizeanimated 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:
- Landing reveals — ScrollTrigger-staggered rise-ins replacing the plain
IO class flips (same
[data-reveal]contract, so tests keep passing). - Stat count-ups — gsap tweens with ease-out snap.
- 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).
- 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: attachevidenceto the answer message.public/v4/sidecar.js(shared): itemw,h;scribble(evidence)API (match → row rects → GSAP sweep; email<mark>variant); scroll-to.public/v7/v7.js+ CSS (+v4.cssmarks): GSAP landing motion, evidence click-through, typewriter answers.- Tests: unit — evidence fires with verbatim doc text (mock llm);
live —
ragsuite asserts the answer message carries matching evidence; browser — v7 suite clicks an answer →.scribblemarks appear over the demo agreement; reduced-motion path asserted.
Phase 2 — office ingestion (docx/xlsx/pptx) — next
server/execution/parsers/office.js: detectsofficeat boot (probe likepdftotext); 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.sourceFormatrecords 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
.docxvia pandoc (installed) — suite SKIPs whensofficeabsent (docling-suite pattern).
Phase 3 — enterprise plane + polish
- Resident returns evidence spans with
/kxp/askanswers (sealed underK_roomlike summaries; coordinator stays document-blind — it relays offsets, never content). Ephemeral fact-cache stores evidence beside answers. - Email deep-link highlighting for
@hanswers 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:
evidenceis optional-additive;publicRoomStateuntouched; 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
