Skip to content

feat(web): read a document's converted source text in the Documents pane (#197) - #199

Merged
KylinMountain merged 2 commits into
mainfrom
worktree-feat+workbench-doc-reader
Jul 22, 2026
Merged

feat(web): read a document's converted source text in the Documents pane (#197)#199
KylinMountain merged 2 commits into
mainfrom
worktree-feat+workbench-doc-reader

Conversation

@KylinMountain

Copy link
Copy Markdown
Collaborator

What

Closes #197 — the Documents pane listed each ingested doc (filename, type, hash) with no way to read the actual full text that ingestion produced under wiki/sources/. This adds a read-only reader for that converted source text.

Backend

  • New POST /api/v1/document/source (openkb/api_documents_router.py + openkb/documents.py, models in api_models.py). Resolves a document hash to its converted text:
    • short docs → wiki/sources/<doc_name>.md
    • long docs → wiki/sources/<doc_name>.json (per-page list[{page, content, images}]), pages joined by a thematic break
  • Keyed by hash (unique) rather than doc_name. Resolution prefers the registry's stored source_path, then falls back to the wiki/sources/<doc_name>.{md,json} convention ordered by the doc's type (long → .json first) so two docs sharing a doc_name each resolve to their own file. Path-traversal guarded.
  • Read-only: sources are do-not-edit artifacts, so there is no edit/delete counterpart here.

Frontend

  • Document rows are now clickable and open a wide read-only slide-out reader rendering the converted Markdown (MarkdownView), with a read-only badge, filename, type and page count.
  • Built on Radix Dialog (same pattern as KbSettingsSheet): focus trap, initial + return focus, Escape, background inert. Body scrolls independently; native find-in-page works (the whole doc is rendered, not virtualized). Content is fetched by hash, cached, and the parsed body is memoized, so re-opening is instant.
  • The open-reader target and the delete control are sibling buttons (no nested interactive markup); the per-hash cache is invalidated when the inventory changes so a reopen after recompile refetches.

Known limitation

Images embedded in long-doc pages are not rendered inline yet — they surface as a literal ![image](...) reference. This is a separate follow-up (needs Markdown image support + a wiki/sources/images/* serving route with the token-blob pattern). Note that OpenKB's ingestion also does not caption/OCR images, so figure/chart content does not reach the LLM regardless.

Verification

  • Backend: 1246 passed (adds tests for short/long/collision/malformed-JSON/404/auth paths); ruff + mypy clean.
  • Frontend: npm run build (i18n + tsc + vite) green.
  • Manually driven in a browser: short + long docs render; Escape closes and returns focus to the trigger; background is inert while open.

Review

Ran an xhigh code review over the diff; the 8 findings were addressed in the second commit (keyboard/focus/modal a11y, doc_name-collision resolution, malformed-JSON handling, cache invalidation). One finding (frontmatter stripping) was intentionally not applied — source docs render verbatim, unlike wiki-page OKF metadata.

…ane (#197)

The Documents pane listed each ingested doc (name/type/hash) with no way to
read the converted full text ingestion produced under wiki/sources/.

Backend: new POST /api/v1/document/source resolves a doc hash to its source
text — short docs read <doc_name>.md; long docs concatenate the per-page
<doc_name>.json (page text joined by a thematic break). Hash is the identifier
(unique, avoids doc_name/stem collisions); resolution prefers the registry's
stored source_path then falls back to the wiki/sources/<doc_name>.{md,json}
convention, with a path-traversal guard. Read-only (sources are do-not-edit).

Frontend: document rows are now clickable and open a wide read-only slide-out
reader (MarkdownView) — ESC/overlay/close to dismiss, independent scroll,
content cached + memoized per hash, native find-in-page preserved (no
virtualization). Delete stays inline (stopPropagation). Closed drawer is inert.

Known limitation: images embedded in long-doc pages are not rendered inline yet.
…197)

Correctness / a11y:
- Rebuild the reader drawer on Radix Dialog (like KbSettingsSheet) instead of
  a hand-rolled overlay: proper modal focus trap, initial + return focus,
  Escape, and background inert (was: aria-modal with none of it) [#4]. This
  also removes the hand-rolled window keydown listener that re-subscribed every
  render [#7].
- Restructure each document row so the open-reader target is a real <button>
  and the delete control is a SIBLING, not nested. Keyboard-activating delete
  no longer bubbles into opening the reader, and the invalid nested-interactive
  markup is gone [#1, #5].
- Resolve a source file by the doc's own type (long → .json first, else .md),
  so two docs sharing a doc_name each resolve to their own file rather than
  whichever extension is tried first [#2].
- Guard source reads: skip non-dict page entries, reject non-list JSON, and
  return a controlled 500 on corrupt/unreadable sources instead of an
  unhandled exception [#3].
- Invalidate the per-hash content cache when the inventory changes, so a
  reopen after recompile refetches instead of serving stale text [#6].

Fetch/cache/memoized body moved to DocumentsPane so they survive the drawer's
unmount-on-close. #8 (frontmatter stripping) intentionally not applied: source
docs render verbatim (a user's own frontmatter is content, unlike wiki-page OKF
metadata). Adds tests for the collision and malformed-JSON paths.
@KylinMountain
KylinMountain merged commit 8b1a569 into main Jul 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant