@flowajs/react-viewer
v0.2.1
Published
React components for rendering flowa pipeline outputs: citation-grounded Markdown, PDF viewer with bbox highlights, triage workspace.
Readme
@flowajs/react-viewer
React components for rendering flowa pipeline outputs.
Install
pnpm add @flowajs/react-viewer
# Plus the peer dependencies, if not already installed:
pnpm add react react-dom @mantine/core @mantine/hooks @tabler/icons-reactExports
LlmContent— renders LLM-generated Markdown via react-markdown (GFM tables, raw HTML escaped) with click-to-resolve citation links. Links that aren't a valid#cite:AuthorYearresolving in the supplied mapping render as plain text.PdfHighlightViewer— react-pdf-based viewer with 0–1000 normalized bbox highlight overlays.MarkdownHighlightViewer— the Markdown analogue: renders a paper's assembled Markdown and highlights a citation's code-point anchor span (one<mark>per overlapping text node, so a table-row-spanning quote leaves the table intact).parseCitationsFromMarkdown(md)— extract(paperId, quote)pairs from citation links of the form[display](#cite:AuthorYear "verbatim quote").parseCiteHref(href)/isCitationHref(href)— parse / validate citation fragment URLs.matchFilesToPapers(filenames, papers)— match uploaded files to papers by filename: a main paper PDF is<id>.pdf, a supplement is<id>[_ ]supp…(any extension), where<id>is a PubMed id or an encoded DOI. Returns{ mains, supplements, unmatched }, supplements sorted lexicographically.parseSupplementFilename(name)exposes the supplement-naming rule on its own.
Citation contract
Citation links use the form:
[display text](#cite:AuthorYear "verbatim quote")AuthorYear matches [A-Za-z]+\d+ and must resolve in the supplied
PaperIdMapping.byAuthorYear. The title attribute carries the verbatim quote
used to resolve a bbox. LlmContent renders any link that fails this format or
whose AuthorYear is absent from the mapping as plain text — raw HTML is
escaped (no rehype-raw), so untrusted model output can't inject markup.
Bbox coordinate scale
All highlight bboxes use a 0–1000 normalized scale (left/top/right/bottom), 1-indexed pages. The viewer rescales to rendered pixels.
Styles
The package ships a pre-built stylesheet at @flowajs/react-viewer/styles.css.
Import it once (e.g. in your top-level page or _app.tsx):
import "@flowajs/react-viewer/styles.css";The bundle contains only the Tailwind utilities used by the package itself —
no Preflight reset, so it won't fight your existing base styles (Mantine's
own reset stays in effect). The utilities sit outside any cascade layer, so
unlayered component styles such as Mantine's cannot outrank them by layer
order, and theme values are inlined rather than defined as variables on
:root.
If your app builds its own Tailwind stylesheet, import it after this one.
Class names the two share (text-sm, gap-2, …) then resolve to your
definitions, whichever Tailwind major your app is on.
Consumers do not need a Tailwind toolchain. The CSS is statically built
at package release time; nothing in your tailwind.config needs to point at
node_modules/@flowajs/react-viewer.
SSR
PdfHighlightViewer lazy-imports react-pdf on mount, so it is safe to
render on a server (it returns a <Loader> placeholder until react-pdf
loads in the browser). No dynamic(() => …, { ssr: false }) wrapper needed.
LlmContent is server-render-safe.
Viewing controls
The toolbar in the pane zooms the document and turns it counter-clockwise in
quarter turns.
Highlights and the scroll to a quote follow the turn. Each page keeps its own
/Rotate entry and turns from there, so a landscape table page in a portrait
paper stays upright until the user turns it. Like zoom, rotation can be
controlled from outside through rotation and onRotationChange; it resets
when another document opens.
The find box (the magnifier in the toolbar, or Ctrl+F / Cmd+F with focus in
the pane, or on nothing in particular after the pane was the last thing used)
searches the text pdf.js reads from the document: case-insensitively, across
line breaks and end-of-line hyphenation, with straight quotes and hyphens
matching their typographic forms. It marks every match and steps through them
with Enter and Shift+Enter, collecting at most 1000 and saying so. Focus
elsewhere on the page keeps the browser's own find; pass
captureFindShortcut={false} to keep the browser's find everywhere. A page
whose text cannot be read is skipped, and a scanned document without a text
layer reports "Text unavailable".
Loading feedback
While a document downloads, the pane shows the bytes received and, when the
server reports a content length, the total and a progress bar. A download that
fails, or a viewer bundle that fails to load, shows the cause with a Retry
button, and a download that goes quiet for fifteen seconds offers Retry too.
Retry re-uses the same pdfUrl; onLoadError receives the error and a
classification (kind, and the HTTP status where there is one), so a
consumer that hands out short-lived URLs can mint a new one on a 403, and a
changed pdfUrl starts a fresh load. A fresh load that fails calls
onLoadError again, so the consumer bounds its own retries. The package does
not cache a failed import of react-pdf, so the retry (or the next mount) asks
the bundler again; whether the bundler re-fetches a chunk that failed is up to
it.
pdf.js fetches a large document in byte ranges, so the first page renders after
a few small requests, only when the response lets it: the server has to answer
with Accept-Ranges: bytes and a Content-Length, the body must not be
content-encoded (a gzip-compressed object disables ranges), and for a
cross-origin file the CORS policy has to expose Accept-Ranges, which is not
on the browser's safelist (S3 and MinIO need ExposeHeaders in the bucket's
CORS rule for this; exposing Content-Range too is harmless). pdf.js also uses
ranges only for files above twice its chunk size, about 128 KB. Without ranges
pdf.js buffers the whole body before parsing, so nothing renders until the last
byte arrives, whether or not the file is linearised.
Worker assets
PdfHighlightViewer requires the consumer to serve pdf.worker.min.mjs and
cmaps/ somewhere reachable, then pass URLs via workerSrc and cMapUrl
props:
<PdfHighlightViewer
pdfUrl={url}
highlights={highlights}
workerSrc="/pdfjs/pdf.worker.min.mjs"
cMapUrl="/pdfjs/cmaps/"
/>Both files ship with react-pdf under node_modules/react-pdf/dist/. Copy
them into your public assets at install time (e.g. via a postinstall script).
Provenance
Every published version carries a sigstore provenance attestation. To verify:
npm audit signatures
# Or, for one package:
npm view @flowajs/react-viewerLicense
MIT.
