Skip to content

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

Open
KylinMountain wants to merge 2 commits into
mainfrom
worktree-feat+workbench-doc-reader
Open

feat(web): read a document's converted source text in the Documents pane (#197)#199
KylinMountain wants to merge 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Documents pane shows only title, no way to read a document's actual converted text

1 participant