# R42 spec: the in-app chatbot R42 is a knowledge-base-grounded assistant available on every screen. It runs client-side and is grounded by local TF-IDF retrieval — **no vector database**. - **UI:** `src/components/chat/` — `ChatLauncher.jsx`, `ChatWindow.jsx`, `useChat.js` - **Prompt/tool:** `src/components/chat/prompts.js` - **Retrieval/validation:** `src/components/chat/rag.js` + `src/lib/retrieval.js` - **KB writes:** `src/lib/kbStore.js` --- ## Conversation - The brand mark (`src/components/ui/Mark.jsx`) renders R42 in idle / typing / error states. - Messages persist per user in `localStorage` under `chat:thread:{userId}`, capped at **50** messages. Only roughly the last **12** turns are sent to the API; older context is truncated with a notice. - A greeting message seeds an empty thread. - Each turn calls `callLLM` (fast/standard Claude tier — low latency matters for chat). - The chat header has a **clear** button (trash icon). It confirms, then wipes `chat:thread:{userId}` and reseeds the greeting via `clearThread` in `useChat.js`. --- ## Grounding (RAG via TF-IDF) `buildKbContext` in `rag.js`: 1. Build / reuse the TF-IDF index over `topics` (`src/lib/retrieval.js`). 2. Retrieve the top **10** topics for the user's message. Scoring is exact-token TF-IDF **plus a compound-word fallback**: an unmatched query token (≥6 chars) also matches a document term when they share a ≥6-char stem or one contains the other, at a reduced weight. This recovers Dutch compounds — e.g. a `pensioen` query matches `pensioenregeling` and `partnerpensioen`. 3. Always include topics whose `id` or `label` appears verbatim in the message. 4. Include relations only when **both** endpoints are in the retrieved set. 5. Inject up to ~1000 chars of generated content for up to **5** topics — verbatim-mentioned first, then the highest-ranked retrieved ones — so a query that never names a topic exactly still gets rich content for what it matched. 6. Append a short KB hash so the cached context busts when topics change. If the summarised context is still too thin, R42 can call the `lookup_topic` tool to pull a topic's full description and learning content on demand. The system prompt (`prompts.js`) is assembled as cacheable blocks: a stable preamble (role, tasks, style, "answer only from the KB"), the KB context block, and a per-turn tail with the user's name and admin/non-admin flag. --- ## Proposing knowledge-graph edits R42 may call `propose_graph_delta` with: - `reason` — one sentence - `topics` — **max 3**, each `{ id (kebab-case), label, type (concept|role|process), description }` - `relations` — **max 5**, each `{ source, target, type (related_to|depends_on|part_of|executed_by) }` `validateDelta` (`rag.js`) dedupes by topic id and case-folded label, rejects relations with missing endpoints or self-references, and enforces the 3/5 caps. Invalid deltas are dropped. A confirmation chip appears inline: - **Admin → Yes:** `kbStore.applyDelta` writes topics/relations to PocketBase immediately. - **Non-admin → Yes:** `kbStore.appendSuggestion` queues a `pending` entry in `kb:suggestions` (localStorage) for admin review. Admins review the queue in `src/components/admin/SuggestionsQueue.jsx` (approve re-runs `applyDelta`; reject marks it rejected). `kbStore` dispatches `respellion:kb-updated` after writes so the D3 graph and queue refresh. --- ## Quiz integrity `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on quiz start and clears it on every non-quiz phase and on unmount, dispatching a `respellion:quiz-state` event. `ChatLauncher` hides the FAB while this flag is set, so users cannot consult R42 mid-quiz. **Never bypass this.**