- 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.
702 lines
21 KiB
Markdown
702 lines
21 KiB
Markdown
# 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 1–N. 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
|