Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 56s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m13s
Forgejo Docker Build / Build Docker image (push) Successful in 14s
Forgejo Docker Build / Deploy to the host (push) Failing after 1s
The pathway existed but was reachable only by API. It now has a tab of its own next to the Learning Hub — related, not the same thing, and sitting together is how someone discovers the difference — visible to every signed-in user with no role gate in the markup. Generate a deck or an article, see everything you have made, download each as PowerPoint, Word or PDF, delete what you no longer want. The screen says "Private to you" and "Nobody else sees these", because the distinction from published Learning content is the thing a person needs to understand before typing a patient's condition into it. Downloads are fetched rather than linked: an <a href> cannot carry the Authorization header. The blob is saved under the filename the server chose and the object URL is revoked afterwards. Resource titles come from a model, so rows are built as elements and a title is only ever assigned to textContent. The e2e stack now joins danvics_convert too. It could previously reach only Postgres and Redis, so a PDF download failed there in a way production would not — which did at least prove the degradation path works: with Gotenberg unreachable the response is "PDF conversion is unavailable right now. PowerPoint and Word still work", and the other two formats download unaffected. Verified in a browser as an ordinary user: the tab appears and opens, the form swaps slide count for word count when the format changes, the library lists their own work, and pptx, docx and pdf all download with sensible filenames (36360, 13285 and 68310 bytes). Also documents retrieval sizing in docs/retrieval-tuning.md — the per-feature budgets, and RERANKER_TOP_K, which caps all of them and had until now appeared in no configuration file at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
93 lines
3.3 KiB
Markdown
93 lines
3.3 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 / TXT / MD / HTML / CSV / JSON, ≤ 100 MB each, max 10 files |
|
|
| 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
|
|
|
|
Uses `pptxgenjs`.
|
|
|
|
- 16:9 widescreen.
|
|
- Bottom-right slide numbers.
|
|
- Supported Markdown elements: headings, sub-headings, bold, italic, inline
|
|
code, numbered + bulleted lists, code blocks (grey background), blockquotes
|
|
(blue accent bar), tables with alternating rows.
|
|
- Mixed content per slide allowed.
|
|
|
|
## Semantic search
|
|
|
|
| | |
|
|
|---|---|
|
|
| Store | `pgvector` on `learning_content.embedding VECTOR(768)` |
|
|
| Index | IVFFLAT, cosine distance |
|
|
| Primary model | Google Vertex `text-embedding-005` (768 dims) |
|
|
| 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).
|