# 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
