pediatric-ai-scribe-v3/docs/learning-hub.md
Daniel bf4f895f2c
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m55s
Forgejo Docker Build / Build Docker image (push) Successful in 9s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
fix: article uploads are 10 MB, type-checked both ways, and sniffed
The ceiling was 100 MB per file with ten files allowed at once, and every file
is held whole in memory to be parsed — so the old limit let a single request ask
for a gigabyte of heap. A source article that size is not a thing anyone
uploads here. Now 10 MB, defined once and used by both the multer limit and the
post-upload check.

The filter accepted `allowed mime OR allowed extension`, so naming a file .pdf
was enough on its own, whatever it declared — and the extension is chosen by
whoever uploads. Both are required now.

Neither of those sees any bytes: multer filters on the headers, before the file
has arrived. verifySources() runs once the buffer exists and refuses a file
whose contents are not what its type claims, using the same helper as documents,
S3 uploads and assistant attachments. It runs before extraction, because an
extractor handed a malformed file is where the damage would happen.

The CMS screen said 100 MB and listed four of the ten accepted formats; it now
says what the server actually does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 19:25:41 +02:00

101 lines
4 KiB
Markdown

# Learning Hub
A CMS + content-delivery module for clinical education material inside the
app. Supports articles, clinical pearls, quizzes, and Marp-rendered
presentations with PPTX export. Quiz questions are stored alongside article
content and can optionally be generated by AI from uploaded source material.
## Content types
| Type | Description |
|---|---|
| `article` | Rich HTML body with an optional attached quiz |
| `pearl` | Short clinical snippet (no quiz, no heavy media) |
| `quiz` | Standalone quiz (no article body) |
| `presentation` | Marp markdown rendered as slides; PPTX export supported |
## User-facing features
- Browse by category.
- Three search modes:
- **Keyword** — Postgres full-text.
- **Semantic** — pgvector cosine similarity on the embedding column.
- **Hybrid** — weighted merge of both result sets.
- Articles render with sanitized HTML (DOMPurify, loaded via SRI-pinned cdnjs).
- Quizzes: multiple-choice, multi-select, true/false. Score computed on submit,
per-question explanations revealed after.
- Presentation viewer: modal with keyboard / swipe navigation.
- Progress: `learning_progress` stores per-attempt score + total.
## CMS (moderator / admin)
- Tiptap rich-text editor for article body.
- Draft / published toggle.
- Category assignment.
- Quiz builder: add/remove questions, add/remove options, mark correct, enter
explanation.
- Marp editor for presentations with live preview.
## AI content generation
`POST /api/admin/learning/generate` takes one of:
| Input | Notes |
|---|---|
| `topic` | Plain-text description of the topic |
| Uploaded files | PDF / DOCX / PPTX / ODT / EPUB / TXT / MD / HTML / CSV / JSON, ≤ 10 MB each, max 10 files. The declared type must be in the allowlist *and* match the extension, and the bytes are sniffed before anything parses them. |
| WebDAV path | Pulled from the user's connected Nextcloud instance |
Parameters: `model` (from the provider whitelist), `slideCount` for
presentations, `wordCount` for articles.
File uploads pass the `src/utils/fileType.js` magic-byte check so a
mismatched extension is rejected before it reaches the parser.
## Marp → PPTX export
`POST /api/admin/learning/generate-pptx` writes the markdown to a temp
directory and runs `pandoc` against
`assets/learning/slides-reference.pptx`. The reference deck carries the fonts,
palette and slide layouts, so restyling the export means editing that file in
PowerPoint — not changing code.
Images are handled before pandoc sees the markdown. Each
`/api/generated-images/{id}` link is resolved through the ownership check and
written beside the deck under a name this route chooses; any link that does not
resolve to one of those is dropped. Pandoc resolves image links against the
filesystem, so passing an arbitrary local path through would embed that file
into the deck.
This is the Learning Hub's own path and is separate from My Resources, which
renders decks with python-pptx from a typed deck rather than from markdown —
see [`my-resources.md`](my-resources.md).
## Semantic search
| | |
|---|---|
| Store | `pgvector` on `learning_content.embedding VECTOR(768)` |
| Index | IVFFLAT, cosine distance |
| Primary model | `vertex/text-embedding-005` (768 dims), served through LiteLLM |
| Fallback model | OpenAI `text-embedding-3-small` (truncated to 768 to match the column) |
Embeddings are generated on content publish + on every edit. If the embedding
provider is unreachable, the content still saves — keyword search remains
available.
## Tables
| Table | Purpose |
|---|---|
| `learning_categories` | Top-level groupings |
| `learning_content` | Articles / pearls / quizzes / presentations. Body + `embedding` vector. |
| `learning_questions` | Quiz question prompts (FK to content) |
| `learning_options` | Answer options (FK to question) |
| `learning_progress` | Per-user attempt history |
## Retrieval sizing
How many corpus excerpts the Clinical Assistant, the Learning Hub and My
Resources each receive, and the reranker cap that overrides all three:
[retrieval-tuning.md](retrieval-tuning.md).