Add specifications for gamification, generation, and R42 chat services

- Introduced gamification service spec detailing responsibilities, API surface, XP calculation, levels, streaks, badges, milestone cards, and heatmap data.
- Added generation service spec outlining the process for generating micro learning content, including API endpoints, AI call configuration, prompt strategies, and error handling.
- Created R42 chat service spec covering chatbot interactions, retrieval pipeline, prompt construction, response generation, and stateless design principles.
This commit is contained in:
RaymondVerhoef
2026-05-23 18:13:08 +02:00
parent dda20612e9
commit 472685f0d7
62 changed files with 11552 additions and 21 deletions

701
docs/frontend-spec.md Normal file
View File

@@ -0,0 +1,701 @@
# Frontend spec
## Responsibility
Single Next.js 14 codebase serving two distinct role-based experiences:
- `/admin/*` — content administration (document upload, KB review, curriculum)
- `/app/*` — employee learning experience (sessions, library, R42, gamification)
Mobile-first. Designed for 375px width, scales up. Installable as a PWA.
---
## Location
```
app/frontend/
├── src/
│ ├── app/ Next.js app router
│ │ ├── layout.tsx root layout — global stylesheet import
│ │ ├── page.tsx redirect → role-based landing
│ │ ├── admin/
│ │ │ ├── layout.tsx admin shell (sidebar nav)
│ │ │ ├── page.tsx admin dashboard
│ │ │ ├── documents/
│ │ │ │ └── page.tsx document upload + ingestion status
│ │ │ ├── knowledge/
│ │ │ │ ├── page.tsx theme batch review list
│ │ │ │ └── [themeId]/page.tsx theme detail + topic edit
│ │ │ └── curriculum/
│ │ │ └── page.tsx curriculum editor + regeneration
│ │ ├── app/
│ │ │ ├── layout.tsx employee shell (bottom nav + R42)
│ │ │ ├── page.tsx redirect → /app/session
│ │ │ ├── session/
│ │ │ │ └── page.tsx current week session
│ │ │ ├── library/
│ │ │ │ ├── page.tsx knowledge library browse
│ │ │ │ └── [topicId]/page.tsx topic detail
│ │ │ └── profile/
│ │ │ └── page.tsx gamification profile + heatmap + badges
│ │ ├── auth/
│ │ │ └── page.tsx login (PocketBase auth)
│ │ └── api/ Next.js API routes (thin proxies only)
│ ├── components/
│ │ ├── admin/ admin-specific components
│ │ ├── employee/ employee-specific components
│ │ ├── micro-learnings/ one component per micro learning type
│ │ ├── r42/ R42 chatbot components
│ │ ├── gamification/ heatmap, badges, leaderboard
│ │ └── ui/ shared primitives
│ ├── lib/
│ │ ├── pocketbase.ts PocketBase client (browser)
│ │ ├── services.ts typed API calls to backend services
│ │ ├── auth.ts auth helpers + role guards
│ │ └── hooks/ custom React hooks
│ └── types/
│ └── index.ts shared TypeScript types
├── public/
│ ├── manifest.json PWA manifest
│ ├── sw.js service worker (generated)
│ └── icons/ PWA icons (192, 512)
├── next.config.js
├── tailwind.config.ts
├── tsconfig.json
└── .env.example
```
---
## Stylesheet integration
`/stylesheet.css` lives at the repo root — not inside `app/frontend/`.
Import it as the first global stylesheet in `src/app/layout.tsx`:
```tsx
import '../../../stylesheet.css' // path from app/frontend/src/app/
import './globals.css' // Tailwind directives second
```
Rules:
- stylesheet.css is the authoritative visual style — never override it
- Where Tailwind utility classes conflict with stylesheet.css rules,
stylesheet.css wins
- Tailwind is used for layout, spacing, and elements not covered by the
stylesheet — match the visual language (spacing scale, colour, type) of
the existing stylesheet when doing so
- Inspect stylesheet.css before implementing any component — use its CSS
custom properties (if any) rather than hardcoding values
---
## PWA configuration
### next.config.js
Use `next-pwa` package to generate service worker and manifest wiring:
```javascript
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development'
})
module.exports = withPWA({
reactStrictMode: true,
})
```
### public/manifest.json
```json
{
"name": "Learning Platform",
"short_name": "Learn",
"description": "Employee knowledge and learning",
"start_url": "/app",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"orientation": "portrait",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
]
}
```
Note: set `theme_color` and `background_color` to match stylesheet.css
primary background after inspecting the file.
### Service worker caching strategy
- Static assets: cache-first
- PocketBase API calls: network-first, fall back to cache
- Backend service calls: network-only (no caching for dynamic content)
---
## Auth
PocketBase handles auth. Two roles: `admin` and `employee`.
### Login flow
```
/auth page → email + password form
PocketBase authWithPassword()
Store token in PocketBase SDK (persists in localStorage)
Read user.role from auth record
role === 'admin' → redirect to /admin
role === 'employee' → redirect to /app
```
### Route guards
Implement as Next.js middleware (`middleware.ts` at app root):
```typescript
// Admin routes: require role === 'admin'
// Employee routes: require role === 'employee'
// Unauthenticated: redirect to /auth
// Wrong role: redirect to correct landing
```
### PocketBase client (browser)
```typescript
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
```
Use `pb.authStore` for auth state. Use `pb.collection().getFullList()` etc.
for direct PocketBase reads. The frontend reads KB content (topics, micro
learnings) directly from PocketBase — it does not proxy through backend services.
### Service calls
Backend services (ingestion, generation, curriculum, chat, progress) are called
via typed fetch wrappers in `lib/services.ts`:
```typescript
// Example
export async function postComplete(payload: CompletePayload) {
const res = await fetch(`${PROGRESS_URL}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (!res.ok) throw new Error(`Complete failed: ${res.status}`)
return res.json() as Promise<CompleteResponse>
}
```
All service response types imported from `types/index.ts`.
---
## Admin app
### Shell layout (`admin/layout.tsx`)
Sidebar navigation on desktop, top navigation on mobile.
Nav items:
- Documents
- Knowledge base
- Curriculum
- (link back to employee app)
### Documents page (`admin/documents/page.tsx`)
**Upload section**
- Drag-and-drop file input: accepts .pdf, .md, .txt
- On upload: POST file to PocketBase storage →
then POST to ingestion service `/ingest` with document metadata
- Show upload confirmation with filename
**Job status list**
- Poll GET /status/:jobId every 3 seconds while status is not done/failed
- Show per-job progress:
- Status badge: queued / extracting / chunking / structuring / embedding / done / failed
- Progress bar derived from chunksEmbedded / chunksTotal
- On done: "N themes, N topics ready for review" → link to knowledge base
- On failed: error reason in red, no retry (admin re-uploads)
- Stop polling when status === 'done' or 'failed'
**Document history**
- List of all source_documents from PocketBase
- Columns: filename, format, status, ingested_at, chunk_count
---
### Knowledge base page (`admin/knowledge/page.tsx`)
Lists all Themes with status indicator.
**Theme card**
```
[Theme title] [status badge: draft / published]
N topics · from: filename.pdf
[Approve batch] [Edit] [Reject]
```
Approve batch:
- Calls PocketBase to set theme.status → 'published', all child topics → 'published'
- Triggers generation service: POST /generate-all with themeId
- Shows toast: "Generation queued for N topics"
Reject:
- Sets theme.status → 'rejected'
- Removes from list
Edit → navigates to `/admin/knowledge/[themeId]`
**Theme detail page (`admin/knowledge/[themeId]/page.tsx`)**
Displays all Topics in the Theme as editable cards.
Topic card fields (all editable inline):
- title (text input)
- body (textarea — rich enough for paragraphs, no full rich text editor needed)
- difficulty (select: introductory / intermediate / advanced)
- key_terms (tag input — comma-separated)
- related_topics (multi-select from published topics)
- prerequisite_topics (multi-select)
Save button per card — calls PocketBase PATCH on the topic record.
Below topic list: [Approve batch] button — approves all topics in the theme.
**Micro learning generation status**
After batch approval, show generation status per topic:
```
Concept explainer ✓ published
Scenario quiz ⏳ generating
Comparison card ✓ published
...
```
Poll micro_learnings collection filtered by topic until all 10 are published.
---
### Curriculum page (`admin/curriculum/page.tsx`)
**Current curriculum view**
26 weeks displayed as a list. Each week shows:
```
Week 7
[Theme: Holacratic roles]
Topics: Role definitions · Circle structure · Lead link responsibilities
Estimated: 25 min
[Edit week] [Admin notes]
```
**Regeneration banner**
When a pending regeneration is queued:
```
⚠ 8 new topics added. A new curriculum version is ready to preview.
[Preview changes] [Confirm regeneration] [Dismiss]
```
Preview: shows proposed schedule with diff highlighting — weeks that changed
are highlighted, weeks that stay the same are dimmed.
Confirm: calls POST /generate confirm on curriculum service →
applies new version to all active employees.
**Drag-to-reorder**
Each week row is draggable. Reordering calls PATCH /weeks/:weekId on the
curriculum service to swap theme assignments.
**Admin notes**
Inline text input per week — saved to curriculum_weeks.admin_notes.
---
## Employee app
### Shell layout (`app/layout.tsx`)
Bottom navigation bar (mobile-first):
```
[Session] [Library] [Profile]
```
R42 floating button: fixed position, bottom-right, above the nav bar.
Z-index above all content.
### Session page (`app/session/page.tsx`)
**Week header**
```
Week 7 of 26 · Cycle 1
[Theme title: Holacratic roles]
[Progress bar: N of 26 weeks complete]
```
**Topic list**
Each topic in the week's theme rendered as a card:
```
[Topic title]
[difficulty badge] [estimated: 10 min]
Choose how to learn this topic:
[Concept explainer] [Scenario quiz] [How-to] ...
(only published types shown as buttons)
[Completed types: ✓ Concept explainer]
```
Selecting a type opens the micro learning inline (no navigation — expands in
place on mobile). Employee reads/completes it, then taps [Mark complete].
On mark complete:
- POST to progress service `/complete`
- Response displays: commits earned + any new badges as a toast notification
- Topic card updates to show type as completed (✓)
- All types in topic completable in one session
**Week complete state**
When all topics in the week have at least one completed type:
```
🚀 Week 7 complete
You earned N commits
[Continue to Week 8]
```
Continue button calls POST /advance/:userId on curriculum service.
---
### Micro learning components
One component per type in `components/micro-learnings/`.
Each receives the `content` JSON field from the micro_learnings record.
| Component | Key interactions |
|---|---|
| ConceptExplainer | Render paragraphs + example — read only |
| ScenarioQuiz | Select option → reveal explanation — stateful |
| Misconceptions | Accordion: tap misconception to reveal correction |
| HowTo | Numbered steps — tap step to check it off |
| ComparisonCard | Two-column table — swipeable on mobile |
| ReflectionPrompt | Open text area → reveal model answer on submit |
| FlashcardSet | Flip card interaction — swipe through deck |
| CaseStudy | Scenario text + open questions — read only |
| GlossaryAnchor | Term card with definition + examples |
| MythVsEvidence | Myth card → tap to reveal evidence |
All components are self-contained. They receive content JSON and emit an
`onComplete` callback. They do not call any services directly.
```typescript
type MicroLearningProps = {
content: unknown // typed per component
onComplete: () => void
}
```
---
### Knowledge library (`app/library/page.tsx`)
**Browse view**
All published topics, grouped by Theme.
Search input: filters by title and key_terms in real time (client-side).
Filter chips: by difficulty (introductory / intermediate / advanced).
Each topic shown as a card:
```
[Topic title]
[Theme] · [difficulty badge]
[key terms as chips]
```
Tap → navigate to topic detail.
**Topic detail (`app/library/[topicId]/page.tsx`)**
```
[Topic title]
[Theme] · [difficulty]
[Topic body — rendered as paragraphs]
Key terms: [chip] [chip] [chip]
Related topics: [card] [card]
Prerequisite for: [card] [card]
How to learn this topic:
[micro learning type buttons — same as session view]
```
Completing a micro learning from the library records the completion via
progress service. Week_number is set to the employee's current week.
---
### Profile page (`app/profile/page.tsx`)
**Header**
```
[Display name]
[Level badge: Junior] [N commits]
[Current streak: 5 weeks] [Longest: 8 weeks]
```
**Heatmap**
GitHub-style contribution graph.
26 columns (weeks) × rows implied by completions per week.
Cell colour: 0 completions = lightest, 5+ completions = darkest.
Tap a cell → tooltip: "Week N · N completions".
Scrollable horizontally on mobile if needed.
Implementation: render as SVG or CSS grid — no charting library required.
```typescript
// Data from GET /profile/:userId → heatmap[]
// Colour scale: 4 levels based on completions count
// 0: var(--heatmap-0)
// 1: var(--heatmap-1)
// 2-3: var(--heatmap-2)
// 4+: var(--heatmap-3)
// Use CSS custom properties — values derived from stylesheet.css palette
```
**Badges**
Grid of earned badges. Unearned badges shown as locked (greyed out).
Tap badge → tooltip with award condition.
```
🥉 First commit ✓
🥈 Five sessions ✓
🥇 On a streak 🔒 (13 week streak needed)
⭐ Shipped 🔒
---
🏷 Governance nerd ✓
🏷 Deep reader 🔒 (3/5 case studies)
```
**Leaderboard tab**
Toggle between "My profile" and "Leaderboard".
Leaderboard: table of all employees from GET /leaderboard.
Columns: Name · Commits · Streak · Types used · Badges · Level.
Not ranked 1N. No sorting by the user — display order is commits descending.
Current employee row is highlighted.
**Activity feed tab**
Third tab: "Feed".
Milestone cards from GET /feed.
Most recent first.
```
🚀 Alex shipped the full curriculum
26 weeks · 847 commits · 3 badges
Longest streak: 18 weeks
[timestamp]
```
---
## R42 chatbot components (`components/r42/`)
### R42Button
Fixed position, bottom-right, above bottom nav bar.
Circle button with R42 label or icon.
Tap → opens R42Drawer.
```tsx
// Position: fixed, bottom: calc(nav-height + 16px), right: 16px
// Z-index: above all content, below modals
```
### R42Drawer
Slides up from bottom on mobile (sheet pattern).
On desktop: expands to a side panel.
```
[R42 header bar] [close ×]
─────────────────────────────────────────────
[Response area — scrollable]
Based on: [Holacratic roles ×] [Circle structure ×]
─────────────────────────────────────────────
[Type a question...] [Send →]
```
**State machine:**
```
idle → loading (query sent) → streaming → done
↘ out_of_scope
```
**Streaming implementation:**
```typescript
// POST /chat with fetch, read SSE stream
const response = await fetch(`${CHAT_URL}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, userId })
})
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const lines = decoder.decode(value).split('\n')
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const event = JSON.parse(line.slice(6))
if (event.type === 'chunk') appendText(event.text)
if (event.type === 'citations') setCitations(event.topics)
if (event.type === 'out_of_scope') setOutOfScope(event.text)
if (event.type === 'done') setDone()
}
}
```
**Citations**
Rendered as tappable pills below the response.
Tap → closes R42Drawer, navigates to `/app/library/[topicId]`.
**Out of scope response**
Render as a muted message (not an error state):
"This doesn't appear to be covered in the knowledge base.
You can browse the full library in the Knowledge section."
**Stateless by design**
Conversation cleared on drawer close. No history persisted.
Input cleared on send.
---
## Mobile-first layout rules
All layout decisions start at 375px and scale up.
- Bottom navigation: fixed, height 56px, icons + labels
- R42 button: 48px circle, positioned above nav bar
- Session topic cards: full width, stack vertically
- Micro learning components: full width, no horizontal scroll except
ComparisonCard (swipeable)
- Heatmap: horizontal scroll container on narrow screens
- Leaderboard table: horizontally scrollable on mobile, sticky name column
- Drawer/sheet pattern for R42 on mobile, side panel on desktop (breakpoint: 768px)
- Tap targets: minimum 44×44px on all interactive elements
- No hover-only interactions — all hover states have tap equivalents
---
## Environment variables
```
NEXT_PUBLIC_POCKETBASE_URL=http://localhost:8090
NEXT_PUBLIC_INGESTION_URL=http://localhost:3001
NEXT_PUBLIC_GENERATION_URL=http://localhost:3002
NEXT_PUBLIC_CURRICULUM_URL=http://localhost:3003
NEXT_PUBLIC_CHAT_URL=http://localhost:3004
NEXT_PUBLIC_PROGRESS_URL=http://localhost:3005
```
---
## Dependencies
```json
{
"dependencies": {
"next": "14",
"react": "^18",
"react-dom": "^18",
"pocketbase": "^0.21",
"next-pwa": "^5",
"zod": "^3"
},
"devDependencies": {
"typescript": "^5",
"tailwindcss": "^3",
"autoprefixer": "^10",
"postcss": "^8",
"@types/react": "^18",
"@types/node": "^20"
}
}
```
No component library. No charting library. No drag-and-drop library —
implement curriculum drag-to-reorder with native HTML5 drag API.
The heatmap is SVG or CSS grid — no D3.
---
## TypeScript strict mode requirements
- No `any` types
- All PocketBase collection responses typed against data-model.md schemas
- All service API responses typed against response types from each service spec
- Micro learning content JSON typed per type using discriminated union:
```typescript
type MicroLearningContent =
| { type: 'concept_explainer'; paragraphs: string[]; example: string }
| { type: 'scenario_quiz'; scenario: string; options: QuizOption[] }
| { type: 'misconceptions'; items: MisconceptionItem[] }
// ... all 10 types
```
- SSE event types as discriminated union
- No implicit any on event handlers
---
## What the frontend does NOT do
- Does not run AI calls directly — all AI goes through backend services
- Does not write to Qdrant — embedding is the ingestion service's responsibility
- Does not implement auth logic — delegates entirely to PocketBase SDK
- Does not implement curriculum generation — calls curriculum service
---
## Testing checkpoints
### Admin app
1. Upload a PDF → ingestion job created → status polls and updates → done state shows link
2. Theme batch appears after ingestion → approve → generation queued
3. Edit a topic title and body → save → changes persisted in PocketBase
4. Curriculum renders 26 weeks → drag week 3 and week 5 → order persists
5. Regeneration banner appears → preview shows → confirm applies new version
### Employee app
6. Login as employee → redirected to /app/session → correct week shown
7. Select micro learning type → content renders → mark complete → commits toast shown
8. Complete all topics in week → week complete state shown → advance to next week
9. Library browse → search filters results → topic detail renders body + related topics
10. Profile page → heatmap renders for current cycle → badges show locked/unlocked state
11. Leaderboard tab → all employees shown → current employee row highlighted
12. R42 button visible on every screen → opens drawer → question answered with citations
13. R42 citation tap → navigates to correct topic in library
14. Out-of-scope question → muted message shown, no citations
15. All screens render correctly at 375px width — no horizontal overflow except
intentional scroll containers