# 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 } ``` 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