- 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.
21 KiB
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:
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:
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
{
"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):
// Admin routes: require role === 'admin'
// Employee routes: require role === 'employee'
// Unauthenticated: redirect to /auth
// Wrong role: redirect to correct landing
PocketBase client (browser)
// 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:
// 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
/ingestwith 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.
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.
// 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.
// 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:
// 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
{
"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
anytypes - 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:
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
- Upload a PDF → ingestion job created → status polls and updates → done state shows link
- Theme batch appears after ingestion → approve → generation queued
- Edit a topic title and body → save → changes persisted in PocketBase
- Curriculum renders 26 weeks → drag week 3 and week 5 → order persists
- Regeneration banner appears → preview shows → confirm applies new version
Employee app
- Login as employee → redirected to /app/session → correct week shown
- Select micro learning type → content renders → mark complete → commits toast shown
- Complete all topics in week → week complete state shown → advance to next week
- Library browse → search filters results → topic detail renders body + related topics
- Profile page → heatmap renders for current cycle → badges show locked/unlocked state
- Leaderboard tab → all employees shown → current employee row highlighted
- R42 button visible on every screen → opens drawer → question answered with citations
- R42 citation tap → navigates to correct topic in library
- Out-of-scope question → muted message shown, no citations
- All screens render correctly at 375px width — no horizontal overflow except intentional scroll containers