Skip to content

Review Workspace

Nabeel Wasif edited this page Sep 29, 2026 · 1 revision

Review Workspace

The screen where a document becomes a set of decisions: what is on it, how each control behaves, and how the client keeps its state.

Canonical sources: docs/editor.md (components) · docs/client-state.md (store and hooks) · DESIGN.md (visual language).

Companion pages: Search and Rules · Hush Assistant · Export and Anonymisation.


1. The pages

Route What it is
/ Landing page and the upload panel: drag and drop, the preset chooser (with its disclaimer), the retention window. One file opens its workspace; several become a batch.
/documents This session's documents, grouped by batch (groupByBatch): cards with review progress, retention, status, retry and delete; limits (rate gauges and daily quotas) and usage (tokens, time, cost). Polls every 4 s only while something is processing.
/workspace/:id The editor: the processing screen while the document works, then review.
/batches/:id The batch: every document's status and counts, Retry, Review links, carried decisions (batch rules with their reach, removable), and the batch download button. Polls every 4 s while anything is processing.
/restore Upload an exported file and its vault, and get the original back. See Export and Anonymisation.
/about What the project is.

2. The workspace

Providers                ThemeProvider (forced dark) · StoreProvider · TooltipProvider · Toaster
Workspace                the orchestrator: status routing, stream, refresh, shortcuts
 ├─ WorkspaceHeader      filename, kind, "looked for <preset>", batch chip + prev/next,
 │                       retention, status, batch download, Export
 ├─ PageNavigator        thumbnail rail: real renders with accepted redactions drawn on,
 │                       search-hit badges
 ├─ DocumentCanvas       format dispatch
 │   ├─ PdfViewer        rendered page + RedactionLayer + SearchBoxes
 │   ├─ DocxViewer       paginated runs
 │   ├─ EmlViewer        foldable headers / bodies / attachments
 │   ├─ TextViewer       txt · rtf · pptx
 │   ├─ ImageCanvas      pixels + OCR words + regions + SearchBoxes
 │   └─ SpreadsheetGrid  sheets, rows, columns, cells (hidden ones marked)
 ├─ SearchBar            the find strip (Ctrl/⌘+F, /)
 ├─ RedactionInspector   tabs: Redactions | Rules   (MobileInspector: a bottom sheet)
 ├─ EditorToolbar        select / redact tools, search, Hush, shortcuts, zoom, undo/redo
 ├─ ExportDialog         method, style, metadata, labels, variants → report, vault
 ├─ RuleDialog           pattern, scope, live preview
 ├─ HushPanel            the assistant (right rail, or a sheet on narrow screens)
 ├─ ShortcutSheet        every key binding (?)
 └─ LiveAnnouncer        screen-reader live regions: review position, search position

Status routing

  • Still processing: the ProcessingScreen (status pill and progress from the SSE stream). Closing the tab does not cancel anything.
  • Failed before extraction: the failure screen, with Retry only if the failure code is retryable.
  • Failed after extraction (e.g. rule-too-broad): the editor opens with a FailureNotice banner. Manual redaction is still real, and an empty suggestion list must never read as "nothing to redact".
  • An expanded mailbox: ExpandedNotice instead of an editor, offering Download redacted mailbox (that one upload, rebuilt as .mbox).
  • When the stream reports ready or failed, the workspace refreshes once per status.

The header says what was searched for

"looked for Names and contact details". An empty inspector reads as "nothing to redact" unless you know the search was narrowed. See Redaction Presets.


3. The canvas

Pages load one at a time

The editor asks for the outline first (GET /content?view=outline: kind, metadata, sheets, page numbers), then the page on screen, then its neighbours. Rail thumbnails request their page when they come within 400 px of view. At most 24 pages stay in memory, least recently used first out. A 1,992-page text file opens after ~9 KB instead of 17 MB. See Streaming IO.

What the colours mean

Look Meaning
Dashed red a suggestion: proposed, not decided
Solid black accepted: this is what the export will produce
Red underline the selected redaction
Grey / dimmed rejected ("kept in file")
Amber a search hit, never a decision; stronger amber with an edge is the current hit
Red + rule badge created by a rule

The chrome is forced dark so the white document surface stays the strongest element on screen, and black overlays show exactly what the export will look like.

Redactions are painted by their exact characters (since 1.9.0)

In text-flow viewers (DOCX, TXT, RTF, PPTX, EML), highlights use the CSS Custom Highlight API over the text the viewer already drew, so a redaction covers exactly its characters and not the whole line it sits on. (Before 1.9.0, a line holding one redacted ID was drawn fully black, which overstated what the export removes.) Browsers without the API fall back to marking the whole line. In geometry viewers (PDF, image, OCR), boxes come from boxesForRedaction, the same function the exporter uses: what is drawn is a promise about the exported file.

Making a redaction by hand

Gesture Result
Click a word Redacts the token under the pointer (wordAt): letters and digits joined by the punctuation identifiers, emails and phone numbers use, so EMP-10007, jane.doe1@example.com, Doe. Grouped values (a card number, a spaced IBAN, a phone number) are taken whole; a name is taken a word at a time. On PDFs, the character comes from measured glyph positions.
Click inside an existing redaction Selects it
Drag across text Redacts exactly the selection, across lines if need be, with whitespace trimmed at either end
Drag a rectangle (redact tool) on a PDF or image A region redaction, if larger than MIN_DRAG_PX
Click a cell, row or column header A cell / row / column redaction
Enter / Space on a focused span Redacts the span, or selects the redaction already in it

Touch drawing of regions is not supported yet (#45).

Format-specific viewers

  • EML: sections fold. Headers are open; of the text/plain and HTML bodies, one is open (HTML by default, since it is what the recipient saw); attachments are folded, with a count. A section holding undecided suggestions opens regardless of the default, because a default the code chose must never be the reason something went unread. HTML bodies are drawn from the markdown the extractor produced, and no message content ever reaches an href, src or other fetchable attribute, so a tracking pixel cannot phone home from a reviewer's screen.
  • Spreadsheets: CSV and TSV are reviewed in the same grid as workbooks. Hidden and very-hidden sheets get a banner and a tab marker, with counts of hidden rows and columns. A sensitive column arrives as one column suggestion.
  • Images: OCR words are hit targets; face regions arrive as whole-head boxes; blur and pixelate are export styles, not canvas states.

4. The inspector

Right rail on wide screens (xl), bottom sheet on narrow ones. Two tabs: Redactions and Rules (see Search and Rules).

  • Occurrence groups. Suggestions are grouped by category|normalizedText (selectOccurrenceGroups), so seventeen "John Smith"s are one row and one decision.
  • Filters: source (All / AI / Manual / Rules), category chips (top 8).
  • Per-group actions: Redact this (the first occurrence) · Redact all N · Everywhere (a literal document rule) · Whole batch (a literal batch rule, offered only when the batch has more than one document) · Ignore (reject every occurrence).
  • Accept all / Reject all act on every filtered suggestion, not just the visible ones. Accept all leaves ignored suggestions alone (fixed in 1.5.0), and ignored rows are marked and dimmed.
  • Method per suggestion (mask, pseudonymize, tokenize, encrypt), limited to what the category and the target allow. See Export and Anonymisation.
  • Confidence is a band (high / medium / low) plus a percentage: a hint for triage, never a verdict.

5. Keyboard and accessibility

Press ? for the shortcut sheet. It is rendered from SHORTCUTS in lib/editor/shortcuts.ts, the only list of bindings (toolbar tooltips read from it too), so this wiki does not copy it. Named ones worth knowing: Ctrl/⌘+F or / search, Ctrl/⌘+K Hush, Esc closes search or returns to the select tool, Ctrl/⌘+Z undo.

Two rules hold for every binding (tests/shortcuts.test.ts): nothing fires while you are typing in a field, and nothing destructive is bound to a bare key. Accept and reject act on the current selection only, and undo is one keystroke away.

  • Redactions on the canvas are focusable buttons with descriptive labels and aria-pressed. Per-word hit targets are out of the tab order, because a page of prose as hundreds of tab stops is worse than none. The inspector list is the complete keyboard path to every suggestion.
  • LiveAnnouncer describes the whole position, not the last change: "7 of 12 suggestions reviewed, 4 accepted, 5 left to review". Search has its own region ("7 of 132 matches").
  • Zoom: out / in, a log-scale slider (100% in the middle; a native range input for keyboards and screen readers), a percentage that resets to 100%, fit width and fit page. Fit modes survive a resize.
  • Download and Report are real <a> links styled as buttons, so link semantics and middle-click still work.
  • Still open: a screen-reader pass by someone who uses one daily, and a colour-contrast audit (#42).

6. Retention, usage and limits

  • RetentionControl offers only windows that would actually push the expiry out, computed from creation and capped at 72 h. A document at the ceiling has no options, and the limit is stated rather than discovered by trying.
  • Usage (/api/documents/:id/usage, and the session total on /documents) shows tokens, duration and calls, plus a currency figure only when prices are configured. An invented price is worse than no price. The panel also reports a degraded model pass and why.
  • LimitsCard shows rate limits (pace, refilling within a minute) and daily quotas (volume, per UTC day) side by side, because hitting either produces the same refusal. On a self-hosted install it says there are no quotas instead of drawing empty bars. /api/limits deliberately does not spend from the bucket it reports on.

7. Client state

A Redux Toolkit store, created lazily per browser session (makeStore() in StoreProvider, never a module singleton, so SSR and hydration cannot share one), with seven slices:

Slice Owns
document the DocumentSummary, the outline, and at most PAGE_CACHE_LIMIT = 24 pages
redactions redaction entities and ids, selection, filters, and undo/redo
editor viewport: page, zoom, tool, fit mode, active sheet, grid selection
processing run status, progress, event log, suggestion count
ui export dialog, shortcut sheet, mobile sheet, page panel
rules every rule in scope for the open document
search the query, options, and hits, each keyed by the question it answers

Undo/redo covers decisions only (page turns and zoom are view, not decision). It keeps up to 50 snapshots of entities and ids, materialised with Immer's current() before copying. Spreading the draft directly once made undo restore the state it was meant to replace, and a test now catches that. Since 1.5.0 an undo saves only the redactions it changed: before, pressing Ctrl+Z in a tab holding stale decisions could overwrite every decision on the document. Server reloads (redactionsReplaced) and streamed suggestions are not undoable, while removing a rule is.

Main hooks: use-redactions (accept, reject, create, rules, undo, redo, all persisted through /api/documents/:id/redactions), use-processing-stream (SSE with indexed resume), use-normalized-document (outline and pages), use-search, use-rules, use-learned-shapes (Hush's offers), use-batch-export, use-retry-document, use-shortcuts.


See also

Clone this wiki locally