Compare commits
84 Commits
feat/ai-pi
...
fe99381cb7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe99381cb7 | ||
|
|
3af105bccd | ||
| 5f34a6f825 | |||
|
|
9395ea11fe | ||
| e310b6d85b | |||
| 2b2921f6ed | |||
|
|
2274de4de7 | ||
|
|
eb08c4ad96 | ||
|
|
218f6e7d64 | ||
|
|
43a71e2110 | ||
|
|
d1a1cc913c | ||
|
|
8160242be5 | ||
|
|
d38c75beb1 | ||
|
|
6a1f5556a9 | ||
|
|
ad5be01b70 | ||
|
|
ce276f0296 | ||
|
|
47a738fde3 | ||
|
|
9fb22b8090 | ||
|
|
6309ae716b | ||
|
|
6ea8860b96 | ||
|
|
7b6a5b4bf0 | ||
|
|
3aa32c383e | ||
|
|
07af2783dc | ||
|
|
7066f881f9 | ||
|
|
febc9dc7f2 | ||
|
|
46d2735237 | ||
|
|
daec9cf5b5 | ||
|
|
9b3d7fde8c | ||
|
|
84e7468841 | ||
|
|
9ea5d5444d | ||
|
|
a653812cd8 | ||
|
|
f16438c1bc | ||
|
|
7164317908 | ||
|
|
3c04bab1b9 | ||
|
|
80532b6d1b | ||
|
|
e4030868b4 | ||
|
|
7d1fe83f72 | ||
|
|
b24ddc5490 | ||
|
|
74e18d2638 | ||
|
|
9ba9ec94ad | ||
|
|
8291a52ba9 | ||
|
|
6968bebbc2 | ||
|
|
ac88c95b46 | ||
|
|
d4066a8f61 | ||
|
|
042dfb2d92 | ||
|
|
18e98380d9 | ||
|
|
cd151aace4 | ||
|
|
55bcb689e7 | ||
|
|
8930ac03ae | ||
|
|
f55ec950aa | ||
|
|
967c68d27d | ||
|
|
b07c4808a6 | ||
|
|
e9f37056b6 | ||
|
|
5965793f11 | ||
|
|
10d5066be8 | ||
|
|
c5e23c77cd | ||
|
|
8e01b21a50 | ||
|
|
7d8999255f | ||
|
|
815cf0f673 | ||
|
|
8684ffa35b | ||
|
|
14286d6cb1 | ||
|
|
f4d0c85c55 | ||
|
|
472685f0d7 | ||
|
|
dda20612e9 | ||
|
|
881148357e | ||
|
|
7b6ae265db | ||
|
|
ca8945ea5b | ||
|
|
dc628644b6 | ||
|
|
3626cc0525 | ||
|
|
25cae2fea9 | ||
| a38ad5d1e0 | |||
|
|
229246f7b6 | ||
| 70db8b5c2d | |||
|
|
66e0c275da | ||
|
|
c82e4fc3a1 | ||
| fd3b849c19 | |||
|
|
aeb197d5f4 | ||
| 9771928926 | |||
|
|
40eff976b4 | ||
| 33529dfb2b | |||
|
|
f838755991 | ||
|
|
8a8745fad2 | ||
| a5c18ccd0f | |||
| d07d15b2a7 |
48
.agents/skills/caveman/README.md
Normal file
48
.agents/skills/caveman/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# caveman
|
||||
|
||||
Talk like smart caveman. Same brain, fewer tokens.
|
||||
|
||||
## What it does
|
||||
|
||||
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped.
|
||||
|
||||
Six intensity levels:
|
||||
|
||||
| Level | What change |
|
||||
|-------|-------------|
|
||||
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
|
||||
| `full` | Default. Drop articles, fragments OK, short synonyms. |
|
||||
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
|
||||
| `wenyan-lite` | Classical Chinese register, light compression. |
|
||||
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
|
||||
| `wenyan-ultra` | Extreme classical compression. |
|
||||
|
||||
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
|
||||
|
||||
## How to invoke
|
||||
|
||||
```
|
||||
/caveman # full mode (default)
|
||||
/caveman lite # lighter compression
|
||||
/caveman ultra # extreme compression
|
||||
/caveman wenyan # classical Chinese
|
||||
stop caveman # back to normal prose
|
||||
```
|
||||
|
||||
## Example output
|
||||
|
||||
Question: "Why does my React component re-render?"
|
||||
|
||||
Normal prose:
|
||||
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
|
||||
|
||||
Caveman (full):
|
||||
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
|
||||
|
||||
Caveman (ultra):
|
||||
> Inline obj prop → new ref → re-render. `useMemo`.
|
||||
|
||||
## See also
|
||||
|
||||
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||
- [Caveman README](../../README.md) — repo overview, install, benchmarks
|
||||
74
.agents/skills/caveman/SKILL.md
Normal file
74
.agents/skills/caveman/SKILL.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: caveman
|
||||
description: >
|
||||
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
||||
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
||||
wenyan-lite, wenyan-full, wenyan-ultra.
|
||||
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
||||
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
||||
---
|
||||
|
||||
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
||||
|
||||
## Persistence
|
||||
|
||||
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
|
||||
|
||||
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
||||
|
||||
## Rules
|
||||
|
||||
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
||||
|
||||
Pattern: `[thing] [action] [reason]. [next step].`
|
||||
|
||||
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
||||
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
||||
|
||||
## Intensity
|
||||
|
||||
| Level | What change |
|
||||
|-------|------------|
|
||||
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
||||
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
||||
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
|
||||
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
||||
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
||||
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
||||
|
||||
Example — "Why React component re-render?"
|
||||
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
||||
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
||||
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
||||
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
||||
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
|
||||
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
|
||||
|
||||
Example — "Explain database connection pooling."
|
||||
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
||||
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
||||
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
||||
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
|
||||
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
|
||||
|
||||
## Auto-Clarity
|
||||
|
||||
Drop caveman when:
|
||||
- Security warnings
|
||||
- Irreversible action confirmations
|
||||
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
||||
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
|
||||
- User asks to clarify or repeats question
|
||||
|
||||
Resume caveman after clear part done.
|
||||
|
||||
Example — destructive op:
|
||||
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
||||
> ```sql
|
||||
> DROP TABLE users;
|
||||
> ```
|
||||
> Caveman resume. Verify backup exist first.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
||||
4
.github/workflows/deploy-dev.yml
vendored
4
.github/workflows/deploy-dev.yml
vendored
@@ -28,4 +28,8 @@ jobs:
|
||||
-i infra/development/hosts.ini \
|
||||
-e "ansible_ssh_private_key_file=~/.ssh/deploy_key" \
|
||||
-e "anthropic_api_key=${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
-e "entra_tenant_id=${{ secrets.ENTRA_TENANT_ID }}" \
|
||||
-e "entra_client_id=${{ secrets.ENTRA_CLIENT_ID }}" \
|
||||
-e "entra_client_secret=${{ secrets.ENTRA_CLIENT_SECRET }}" \
|
||||
-e "entra_admin_emails=${{ secrets.ENTRA_ADMIN_EMAILS }}" \
|
||||
infra/development/site/deploy-playbook.yml
|
||||
|
||||
4
.github/workflows/deploy-prod.yml
vendored
4
.github/workflows/deploy-prod.yml
vendored
@@ -30,4 +30,8 @@ jobs:
|
||||
-i infra/production/hosts.ini \
|
||||
-e "ansible_ssh_private_key_file=~/.ssh/deploy_key" \
|
||||
-e "anthropic_api_key=${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
-e "entra_tenant_id=${{ secrets.ENTRA_TENANT_ID }}" \
|
||||
-e "entra_client_id=${{ secrets.ENTRA_CLIENT_ID }}" \
|
||||
-e "entra_client_secret=${{ secrets.ENTRA_CLIENT_SECRET }}" \
|
||||
-e "entra_admin_emails=${{ secrets.ENTRA_ADMIN_EMAILS }}" \
|
||||
infra/production/site/deploy-playbook.yml
|
||||
|
||||
206
AI_AGENT.md
206
AI_AGENT.md
@@ -2,146 +2,150 @@
|
||||
|
||||
Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase.
|
||||
|
||||
> **Last updated:** 2026-05-18 — Adds 52-week annual curriculum system (§12). Reflects selective content generation (3 types), ISO week alignment, GitHub sync folder change, and AI extraction token limits.
|
||||
> **Last updated:** 2026-05-26 — Per-user curriculum start (employees enroll on first login; week/cycle derived from each user's `curriculum_started_at`, no longer from the ISO calendar week). Documents the real React/Vite + PocketBase stack, TF-IDF retrieval, 3 learning-content types, 3 micro-learning types, and the tiered Claude model setup.
|
||||
|
||||
## 1. Architectural Overview
|
||||
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer.
|
||||
* **Frontend:** React, React Router, Vanilla CSS (via CSS variables) + Tailwind utility classes mapped to those variables.
|
||||
* **Backend:** PocketBase (self-hosted). All data is stored in PocketBase collections, not localStorage.
|
||||
* **Animations:** Framer Motion (used extensively for page transitions, podium effects, and gamification feedback).
|
||||
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer. There is no separate backend server — the browser talks directly to PocketBase, and to the Anthropic API through a reverse proxy.
|
||||
* **Frontend:** React 19, React Router 7, Vanilla CSS (via CSS variables) + Tailwind v4 utilities mapped to those variables.
|
||||
* **Backend:** PocketBase (self-hosted, SQLite). All data is stored in PocketBase collections, not localStorage.
|
||||
* **Animations:** Framer Motion (page transitions, podium effects, gamification feedback).
|
||||
* **Icons:** Lucide React.
|
||||
* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph).
|
||||
* **Retrieval:** A dependency-free TF-IDF index over the knowledge graph (`src/lib/retrieval.js`). There is **no Qdrant and no embeddings API** — older specs that mention them describe a design that was never built.
|
||||
|
||||
> The top-level `app/` directory is abandoned Next.js scaffolding from that original design. It is not built or deployed. Ignore it; the real app is `src/`.
|
||||
|
||||
## 2. State Management & Storage (Critical)
|
||||
All persistent data lives in **PocketBase**. The data access layer is in `src/lib/db.js`, which wraps the PocketBase SDK client from `src/lib/pb.js`.
|
||||
|
||||
**PocketBase Collections:**
|
||||
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`).
|
||||
* `relations` — Knowledge graph edges (`source`, `target`, `type`).
|
||||
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types that have been generated for that topic (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged into the existing object by `learningService.js`; nothing is ever overwritten.
|
||||
* `quiz_banks` — Banks of AI-generated questions per topic (`topic_id`, `questions[]`).
|
||||
* `quiz_results` — User test scores (`user_id`, `week_number`, `score`, `total`, `percentage`, etc.).
|
||||
* `quiz_cache` — Cached weekly quiz for a user (`user_id`, `week_number`, `questions[]`, `meta`).
|
||||
* `learn_progress` — Whether a user completed the weekly learning session (`user_id`, `week_number`, `done`).
|
||||
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`).
|
||||
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`).
|
||||
* `sources` — Uploaded source documents and their extraction status (`name`, `status`, `error`).
|
||||
* `curriculum` — Annual learning schedule (`year`, `week_number`, `topic_id`, `theme`, `quarter`, `is_review_week`, `sort_order`). One entry per week per year. Managed via the admin Curriculum tab.
|
||||
* `settings` — Key/value store for app-wide settings (`key`, `value`).
|
||||
**PocketBase Collections (current):**
|
||||
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`, `learning_relevance`, `relevance_locked`, `theme`, `complexity_weight`, `difficulty`).
|
||||
* `relations` — Knowledge graph edges (`source`, `target`, `type` — `related_to` / `depends_on` / `part_of` / `executed_by`).
|
||||
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types generated so far (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged in by `learningService.js`; nothing is overwritten.
|
||||
* `micro_learnings` — Generated micro-learning artifacts (`topic_id`, `type`, `content`, `status`). One record per topic per type. `status='published'` items are visible to employees.
|
||||
* `micro_learning_completions` — Append-only completion events (`team_member_id`, `micro_learning_id`, `topic_id`, `type`, `session_week`).
|
||||
* `curriculum_versions` — Versioned 26-week schedules (`version_number`, `status`, `generation_reason`, `confirmed_by`, `confirmed_at`, `schedule` JSON, `coverage_stats` JSON). Exactly one `active` at a time.
|
||||
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`, `learnings_completed`).
|
||||
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`, `curriculum_started_at`, `enrollment_status`).
|
||||
* `sources` — Uploaded source documents and extraction status (`name`, `status`, `error`, `progress`).
|
||||
* `settings` — Key/value store (`key`, `value`).
|
||||
* `llm_calls` — Per-call telemetry (`task`, `model`, `tier`, `duration_ms`, token counts, `stop_reason`, `ok`, `error_msg`).
|
||||
|
||||
**Dropped collections:** `quiz_banks`, `quiz_results`, `quiz_cache`, `learn_progress`, and the legacy `curriculum` (v1) collection no longer exist. The matching `db.js` helpers are deprecated stubs — do not build on them.
|
||||
|
||||
**localStorage** is only used for **admin browser settings** (not user data):
|
||||
* `respellion:admin:anthropic_key` — Anthropic API key.
|
||||
* `respellion:admin:model` — Model override.
|
||||
* `respellion:admin:use_simulation` — Simulation mode toggle.
|
||||
* `kb:suggestions` — Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §8).
|
||||
* `quiz:active:{userId}` — Boolean flag set while the user is mid-quiz. R42's FAB is hidden when this is true (quiz-integrity rule).
|
||||
* `admin:model:fast` / `admin:model:standard` / `admin:model:reasoning` — per-tier model overrides (legacy `admin:model` still honored for `standard`).
|
||||
* `admin:use_simulation` — when true, `callLLM` returns stub data instead of calling Anthropic. Useful for UI work without spending tokens.
|
||||
* `kb:suggestions` — Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §9).
|
||||
* `quiz:active:{userId}` — Boolean flag set while a user is mid-quiz. R42's launcher is hidden when this is true (quiz-integrity rule).
|
||||
* `chat:thread:{userId}` — Persisted R42 conversation, capped at 50 messages.
|
||||
|
||||
**Session:** User login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (see `src/store/AppContext.jsx`).
|
||||
**Session:** Login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (`src/store/AppContext.jsx`).
|
||||
|
||||
**Week Number:** The current ISO-8601 week number is calculated dynamically on app load via `getWeekNumber(new Date())` in `src/store/AppContext.jsx`. It is **not** stored in the database. The `ADVANCE_WEEK` action still exists for admin use, but initial state always reflects the real calendar week.
|
||||
**Per-user curriculum position (important — changed):** Each employee starts the curriculum when *they* choose. On first login a blocking onboarding screen (`/onboarding`) records `curriculum_started_at` and flips `enrollment_status` to `active`. `AppContext` derives `state.weekNumber` — an absolute counter starting at 1 — from `getPersonalWeekNumber(curriculum_started_at)` (= `floor(days_since_start / 7) + 1`). The 26-week slot and cycle come from `getCurriculumWeek(n)` (`((n-1) % 26) + 1`) and `getCurriculumCycle(n)` (`floor((n-1)/26)+1`) in `curriculumService.js`. The cycle is **detached from the ISO calendar** — week 1 is simply the first 7 days after the user's start. After week 26 the cycle restarts at week 1 with the same content. `state.weekNumber` is `0` until the user enrolls.
|
||||
|
||||
**Curriculum Year:** The curriculum year is derived from `new Date().getFullYear()` via `getCurriculumYear()` in `src/lib/curriculumService.js`. It is not stored — always computed.
|
||||
> Do **not** reintroduce ISO-week-based scheduling or a shared `admin:current_week`. There is no global "current week" anymore — every employee has their own.
|
||||
|
||||
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` will silently pass a Promise where data is expected.
|
||||
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default, which aborts concurrent identical requests (common under React StrictMode and `Promise.all`) with `ClientResponseError 0`. It is **globally disabled** via `pb.autoCancellation(false)` in `src/lib/pb.js`. Never re-enable it.
|
||||
|
||||
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default. This causes concurrent identical requests (like `db.getTopics()` during React StrictMode renders or concurrent Promise.all) to abort with `ClientResponseError 0`. This feature is **globally disabled** in `src/lib/pb.js` via `pb.autoCancellation(false)` to prevent UI crashes during concurrent fetching.
|
||||
**PocketBase URL:** Resolved from `VITE_PB_URL`, else `window.location.origin` (`src/lib/pb.js`). In production Caddy proxies `/api/*` and `/_/*` to the PocketBase container.
|
||||
|
||||
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` silently passes a Promise where data is expected.
|
||||
|
||||
## 3. The AI Integration (Anthropic)
|
||||
The application calls the Anthropic API via a proxy to avoid CORS issues.
|
||||
* **Location:** `src/lib/api.js`.
|
||||
* **Proxy:** In Docker, `/api/anthropic` is proxied via Caddy to the Anthropic API endpoint. In local dev, configure `vite.config.js` to proxy the same path. The API key is injected server-side by Caddy; there is **no client-side API key**.
|
||||
* **Token limit:** `generateContent` uses `max_tokens: 8192`. Do not lower this — large knowledge-base files need the headroom.
|
||||
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks — a regex strip via `.match(/\{[\s\S]*\}/)` is already applied in all service files.
|
||||
All Anthropic calls go through one wrapper.
|
||||
* **Location:** `src/lib/llm.js`, function `callLLM(...)`. Callers must never reach `/api/anthropic` directly.
|
||||
* **Proxy:** Requests hit `/api/anthropic/v1/messages`. In Docker, Caddy proxies this to `https://api.anthropic.com` and injects the `x-api-key` header server-side. In local dev, `vite.config.js` does the same using `process.env.ANTHROPIC_API_KEY`. There is **no client-side API key**.
|
||||
* **Model tiers:** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`. Choose a tier per task; admins can override per tier from Settings.
|
||||
* **Structured output:** Prefer Anthropic **tool use** with a forced `toolChoice`. Tool inputs are validated against Zod schemas in `src/lib/llmSchemas.js` (auto-looked-up via `toolSchemaRegistry`). For text responses, `parseStructuredText` strips code fences and extracts the outermost balanced JSON.
|
||||
* **Prompt caching:** Wrap stable system text with `cachedSystem(text)` to attach `cache_control: ephemeral`.
|
||||
* **Retry/limits:** `src/lib/llmRetry.js` handles exponential backoff with jitter on retryable statuses (408/425/429/5xx/529), honors `Retry-After`, and provides rate limiters (e.g. the extraction limiter caps ~20 req/min, burst 2). Default `maxTokens` is 4096; extraction and long-form content use 8192.
|
||||
* **Telemetry:** Every call is logged (best-effort, non-blocking) to the `llm_calls` collection.
|
||||
|
||||
## 4. Design System & Aesthetics
|
||||
Respellion relies on a premium, modern aesthetic.
|
||||
* **CSS Variables:** Rely heavily on the variables defined in `src/index.css`.
|
||||
* Colors: `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`.
|
||||
* Radii: `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)` (an organic, pill-like shape used for badges and podiums).
|
||||
* **Tailwind:** Tailwind is mapped to these CSS variables. Avoid hardcoding random hex codes. Use the existing Tailwind classes like `bg-teal`, `text-fg-muted`, and `border-bg-warm`.
|
||||
* **Components:** Always reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
|
||||
* **CSS Variables:** Rely on the variables in `src/index.css` — `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`, radii `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)`.
|
||||
* **Tailwind:** Tailwind v4 utilities are mapped to these variables. Use classes like `bg-teal`, `text-fg-muted`, `border-bg-warm`. Avoid raw hex codes.
|
||||
* **`stylesheet.css`** (repo root) is the authoritative visual reference and is frozen — do not edit it.
|
||||
* **Components:** Reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
|
||||
|
||||
## 5. Learning Content Types
|
||||
`src/lib/learningService.js` supports **three** content types for selective generation:
|
||||
## 5. Learning Content Types (on-demand, per topic)
|
||||
`src/lib/learningService.js` generates **three** long-form content types into the `content` collection, on demand:
|
||||
| Type | Schema key | Description |
|
||||
|---|---|---|
|
||||
| `article` | `content.article` | Title, intro, sections, key takeaways |
|
||||
| `slides` | `content.slides` | Array of slides with bullets and speaker notes |
|
||||
| `slides` | `content.slides` | Slides with bullets and speaker notes |
|
||||
| `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote |
|
||||
|
||||
**There is no podcast type.** It was removed. Do not re-add it.
|
||||
`generateLearningContent(topic, force, selectedType)` accepts one of the three types, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly; new data is shallow-merged so other types are preserved. **There is no podcast type.**
|
||||
|
||||
`generateLearningContent(topic, force, selectedType)` accepts one of the three types above, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly. On generation, the new data is shallow-merged into the existing cached object so other types are preserved.
|
||||
Article refinement uses targeted patch tools (`set_intro`, `set_section`, `add_section`, `remove_section`, `replace_takeaways`) so the model edits only what changed.
|
||||
|
||||
The `LearningContentViewer` tab bar reflects exactly these three modes. The empty-state for an un-generated tab shows a "Generate [Type]" button that calls `onGenerate(activeMode)` passed from the parent.
|
||||
## 6. Micro-Learnings (the weekly session)
|
||||
`src/lib/microLearningService.js` generates short, single-topic interactions into the `micro_learnings` collection. **Three** types are currently active (a former `reflection_prompt` type was dropped):
|
||||
| Type | Tier | Shape |
|
||||
|---|---|---|
|
||||
| `concept_explainer` | standard | `{ sections: [{ title, content (HTML) }] }` (≥3 sections) |
|
||||
| `scenario_quiz` | standard | `{ scenario, options: [{ text, isCorrect, explanation }] }` (3–4 options, 1 correct) |
|
||||
| `flashcard_set` | fast (Haiku) | `{ cards: [{ front, back }] }` (5–10 cards) |
|
||||
|
||||
## 6. Gamification Rules
|
||||
If you are extending the Gamification system (`src/pages/Leaderboard.jsx`):
|
||||
* Tests grant **+2 points** per correct answer (handled in `testService.js → saveTestResult`).
|
||||
* A 100% score grants the **Perfectionist** badge (computed at render time in `Leaderboard.jsx`).
|
||||
* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**.
|
||||
* Points are accumulated in the `leaderboard` collection via `db.upsertLeaderboardEntry`.
|
||||
`getOrGenerateMicroLearning(topicId, type)` returns a cached published record if present, else generates and stores one. Completions are recorded via `useMicroLearningCompletions` into `micro_learning_completions` (append-only). A weekly session is "done" when every required topic has at least one completed micro-learning.
|
||||
|
||||
## 7. Docker & Deployment
|
||||
The app is fully containerized. PocketBase runs as a sidecar service.
|
||||
* **Build:** `docker build -t respellion-app:latest .`
|
||||
* **Run:** `docker compose up -d` (see `docker-compose.yml`).
|
||||
* **Caddy (reverse proxy):** Handles SPA fallback, injects the Anthropic API key via a `Authorization` header on `/api/anthropic/*` requests, and proxies `/pb/*` to the PocketBase service. Config lives in `Caddyfile`.
|
||||
* **PocketBase URL:** Resolved from `VITE_PB_URL` env var, or falls back to `window.location.origin + '/pb'` (see `src/lib/pb.js`).
|
||||
## 7. Weekly Test
|
||||
`src/lib/testService.js` builds a 5-question multiple-choice quiz for the user's current week.
|
||||
* Primary topic comes from the active curriculum week (else a deterministic hash fallback), plus a few review topics for breadth.
|
||||
* Generated in **one** `fast`-tier batch call (`emit_quiz_questions`), with quality gates: no duplicate options, no banned fillers ("all of the above"), explanations ≥20 chars, and a check that `correctIndex` isn't dominated by one position.
|
||||
* Scoring: **+2 points per correct answer** (`saveTestResult` → `score * 2`), written to the `leaderboard` collection.
|
||||
|
||||
## 8. GitHub Knowledge-Base Sync
|
||||
The Admin upload panel (`src/components/admin/UploadZone.jsx`) can sync markdown/text files directly from a GitHub repository.
|
||||
|
||||
* **Default folder:** `docs/knowledge-base/` of the `respellion/employee-handbook` repo. This is persisted in the `settings` collection under the key `github:url` so admins can change it from the UI.
|
||||
* **Change detection:** Each file's SHA is stored as `github:sha:<filename>` in `settings`. Files whose SHA differs are marked *Updated*; new files are *New*. Up-to-date files are skipped.
|
||||
* **Extraction pipeline (`src/lib/extractionPipeline.js`):** Calls `anthropicApi.generateContent` with a strict JSON-only system prompt. To prevent truncated responses on large files, the prompt limits extraction to **max 15 topics** and their most important relations. If the AI returns non-JSON, the file is marked `failed` in the `sources` collection.
|
||||
* **Deduplication:** A file already in `sources` with `status: completed` will throw and not be re-processed. Delete the source record first to force a re-analysis.
|
||||
* **Do not increase topic cap beyond 15** without also verifying the `max_tokens: 8192` budget is sufficient for the expected file size.
|
||||
## 8. Gamification
|
||||
If you are extending gamification (`src/pages/Leaderboard.jsx`):
|
||||
* Tests grant **+2 points** per correct answer (`testService.js → saveTestResult`).
|
||||
* Badges are computed at render time: **First Steps** (1 test), **Veteran** (5 tests), **Perfectionist** (a 100% score).
|
||||
* Points accumulate in `leaderboard` via `db.upsertLeaderboardEntry`. Admins are excluded from the public board.
|
||||
|
||||
## 9. R42 Chatbot
|
||||
The platform ships a global chatbot avatar called **R42**, rendered as the Respellion `{ r }` brand mark in three states (idle / typing / error).
|
||||
|
||||
* **Mark component:** `src/components/ui/Mark.jsx`. Pure SVG; renders the brand mark with `state`, `size`, `theme`, `showFrame`, `brace`, `letter` props. Requires the `BallPill` font (loaded via `@font-face` in `src/index.css`, served from `public/fonts/BallPill-light.otf`). Falls back to JetBrains Mono (already loaded).
|
||||
* **Mark component:** `src/components/ui/Mark.jsx` — pure SVG with `state`, `size`, `theme`, `brace`, `letter` props. Uses the `BallPill` font (`@font-face` in `src/index.css`), falling back to JetBrains Mono.
|
||||
* **Chat module:** `src/components/chat/`.
|
||||
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set. Listens to the `respellion:quiz-state` window event for fast updates.
|
||||
* `ChatWindow.jsx` — 380×480 chat panel; Esc closes; renders messages from `useChat`.
|
||||
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}`, calls `anthropicApi.chat()`. `buildKbContext` is async (PocketBase), so `send()` is fully async.
|
||||
* `prompts.js` — system prompt, greeting, and the `propose_graph_delta` tool spec.
|
||||
* `rag.js` — fetches `kb:topics` + `kb:relations` from PocketBase; lazy-loads `kb:content:{id}` only when a topic is mentioned. Returns `{ context, topics }` so `validateDelta` can reuse the fetched topics without a second round-trip.
|
||||
* **Multi-turn API:** `anthropicApi.chat(systemPrompt, messages, { tools })` in `src/lib/api.js`. Returns the raw Anthropic response (`{ content: [...], stop_reason }`) so callers can read both text blocks and `tool_use` blocks. No API key header — Caddy proxy injects it server-side, matching the existing `generateContent` pattern.
|
||||
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, plus dispatches a `respellion:quiz-state` event. Never bypass this — letting users ask R42 mid-quiz would break scoring.
|
||||
* **Graph refinement:** when R42's `tool_use` block proposes a `propose_graph_delta`, `rag.js` validates (no duplicate ids, no orphan relations, caps 3 topics / 5 relations) and surfaces a confirmation chip inline.
|
||||
* **Admin user clicks Ja** — `kbStore.applyDelta` writes to PocketBase via `db.upsertTopic` / `db.addRelation` immediately.
|
||||
* **Non-admin clicks Ja** — `kbStore.appendSuggestion` queues an entry in `kb:suggestions` localStorage (status `pending`).
|
||||
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx`, mounted at the top of the Knowledge Graph admin panel. Approve calls `kbStore.approveSuggestion(id)` which runs the same `applyDelta` merge (async, PocketBase) and flips status to `approved`. Reject flips to `rejected` for audit.
|
||||
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for KB mutations from the chatbot path. Topics/relations go to PocketBase; suggestions queue goes to localStorage. Dispatches `respellion:kb-updated` after any write so the D3 graph and the queue panel refresh without a reload.
|
||||
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set; listens to the `respellion:quiz-state` window event.
|
||||
* `ChatWindow.jsx` — chat panel; renders messages from `useChat`; surfaces graph-delta confirmation chips.
|
||||
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}` (cap 50; only the last ~12 turns are sent to the API), calls `callLLM`.
|
||||
* `prompts.js` — the cacheable system prompt blocks, greeting, and the `propose_graph_delta` tool spec (max 3 topics / 5 relations).
|
||||
* `rag.js` — builds KB context using the TF-IDF index from `src/lib/retrieval.js` (top-K topics + verbatim mentions), filters relations to retrieved topics, and validates proposed deltas (dedupe by id/label, no orphan/self relations, hard caps).
|
||||
* **Model:** R42 runs on the `fast`/standard Claude tier (Haiku/Sonnet); low latency matters for chat.
|
||||
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, dispatching `respellion:quiz-state`. Never bypass this — letting users ask R42 mid-quiz would break scoring.
|
||||
* **Graph refinement:** when R42 proposes a `propose_graph_delta`, `rag.js` validates it and a confirmation chip appears inline. **Admin clicks Yes** → `kbStore.applyDelta` writes to PocketBase immediately. **Non-admin clicks Yes** → `kbStore.appendSuggestion` queues a `pending` entry in `kb:suggestions`.
|
||||
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx` lets admins approve (re-runs `applyDelta`) or reject queued suggestions.
|
||||
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for chatbot-path KB mutations. It dispatches `respellion:kb-updated` after writes so the D3 graph and queue refresh.
|
||||
|
||||
## 10. How to Add New Features
|
||||
1. **Define Schema:** Add a new PocketBase collection via the Admin UI or the init script (`scripts/setup-pb-collections.mjs`).
|
||||
2. **Add DB Helpers:** Add async CRUD functions in `src/lib/db.js`.
|
||||
3. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`).
|
||||
4. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage state locally.
|
||||
5. **Test:** Run the Docker stack (`docker compose up`) to ensure no build errors exist.
|
||||
## 10. Admin Panel
|
||||
`src/pages/Admin/index.jsx` is tabbed: **Sources** (upload + extraction), **Content** (review/refine generated content), **Quizzes**, **Curriculum** (generate/preview/activate a 26-week schedule), **Graph** (D3 knowledge graph + suggestions queue), **Team** (manage members), **Settings** (model overrides, simulation toggle, smoke-test reset). Source upload lives in `src/components/admin/UploadZone.jsx` (`.txt` / `.md`, ≤5 MB).
|
||||
|
||||
## 11. Known Gotchas & Decisions
|
||||
* **No podcast type.** The podcast learning type was deliberately removed. The three supported types are `article`, `slides`, and `infographic`.
|
||||
* **Week number is computed, not stored.** Do not add a `admin:current_week` setting — the ISO week is always derived from `new Date()`.
|
||||
* **Caddy, not Nginx.** Older docs/comments may reference Nginx. The reverse proxy is Caddy. Update any references you encounter.
|
||||
* **AI token budget.** If you see `[Pipeline] AI returned non-JSON response` in the logs, the response was truncated. Increase the topic cap prompt constraint before raising `max_tokens`.
|
||||
* **PocketBase auto-cancellation is OFF.** `pb.autoCancellation(false)` is set globally in `src/lib/pb.js`. Never re-enable it — it causes abort errors during concurrent fetches in React StrictMode.
|
||||
## 11. How to Add New Features
|
||||
1. **Schema:** add a PocketBase collection via the PB Admin UI or a migration in `pb_migrations/` (and mirror it in `scripts/setup-pb-collections.mjs`).
|
||||
2. **DB helpers:** add async CRUD in `src/lib/db.js`.
|
||||
3. **UI:** build with `Card`, `Button`, `Tag`, and Framer Motion entry animations.
|
||||
4. **Logic:** connect to `src/store/AppContext.jsx` for global user/week context; otherwise keep state local.
|
||||
5. **Verify:** `npm test`, `npm run lint`, `npm run build`.
|
||||
|
||||
## 12. Annual Curriculum System
|
||||
The platform uses a **52-week annual curriculum** so every employee covers all knowledge-base topics in one calendar year.
|
||||
## 12. Known Gotchas & Decisions
|
||||
* **Per-user curriculum start.** Week/cycle derive from each user's `curriculum_started_at`, not the calendar. There is no shared current week. New users are gated through `/onboarding` until enrolled.
|
||||
* **No podcast type.** Three content types only: `article`, `slides`, `infographic`.
|
||||
* **Three micro-learning types.** `concept_explainer`, `scenario_quiz`, `flashcard_set`. `reflection_prompt` was dropped.
|
||||
* **No Qdrant / no embeddings.** Retrieval is local TF-IDF (`src/lib/retrieval.js`).
|
||||
* **Caddy, not Nginx.** The reverse proxy is Caddy (`Caddyfile`).
|
||||
* **PocketBase auto-cancellation is OFF.** Set in `src/lib/pb.js`; never re-enable.
|
||||
* **Go through `callLLM`.** Never call the Anthropic proxy directly; you lose retry, schema validation, and telemetry.
|
||||
* **AI token budget.** Truncation surfaces as `LLMTruncatedError` (`stop_reason: max_tokens`). For extraction, tighten the prompt's topic cap before raising `max_tokens`.
|
||||
|
||||
* **Service:** `src/lib/curriculumService.js` — curriculum engine with topic lookup, progress tracking, and auto-generation.
|
||||
* **DB functions:** `db.getCurriculum(year)`, `db.getCurriculumWeek(year, week)`, `db.setCurriculumWeek(...)`, `db.bulkSetCurriculum(year, weeks[])`.
|
||||
* **Admin UI:** `src/components/admin/CurriculumManager.jsx` — accessed via the "Curriculum" tab in the admin panel. Admins can auto-generate a schedule from KB topics or manually assign topics per week.
|
||||
* **Same topic for everyone:** All employees study the same topic each week — this is by design to enable team discussion and shared quizzes.
|
||||
* **Quarterly structure:** Weeks 1–13 (Q1), 14–26 (Q2), 27–39 (Q3), 40–52 (Q4). Review/recap weeks at 13, 26, 39, 52.
|
||||
* **Fallback:** If no curriculum exists for the current year, `getAssignedTopic()` falls back to the legacy hash-based assignment for backward compatibility.
|
||||
* **Progress tracking:** `getYearProgress(userId)` and `getQuarterProgress(userId, quarter)` compute completion from the `learn_progress` collection against the curriculum.
|
||||
* **Auto-generate:** `autoGenerateCurriculum(year)` distributes all non-fact topics across 48 content weeks + 4 review weeks. If there are fewer topics than weeks, they cycle. If more, excess topics remain in the self-service library.
|
||||
* **Do not remove the hash fallback** — it ensures the platform works even without a configured curriculum.
|
||||
## 13. 26-Week Per-User Curriculum System
|
||||
The platform uses a **26-week perpetual curriculum cycle**. Every employee covers the knowledge base in focused, thematic weekly blocks, **starting whenever they enroll**.
|
||||
* **Service:** `src/lib/curriculumService.js` — week/cycle math, AI schedule generation, version lifecycle, progress.
|
||||
* **DB:** `curriculum_versions` holds generated JSON schedules; `topics` carry `theme`, `complexity_weight`, `difficulty` as generation input.
|
||||
* **Version lifecycle:** `draft` → `active` → `superseded`. Only one active version at a time (`CurriculumManager.jsx`).
|
||||
* **Per-user weeks:** `getPersonalWeekNumber(startedAt)` yields an absolute week counter; `getCurriculumWeek`/`getCurriculumCycle` map it to the 1–26 slot and cycle. Same content each cycle.
|
||||
* **Topic enrichment:** a one-off AI step assigns `theme`/`complexity_weight`/`difficulty` to topics missing them before generation (`enrichTopicsForCurriculum`, batches of 20).
|
||||
* **Progress:** `getYearProgress(userId, personalWeekNumber)` computes completion for the current cycle.
|
||||
* **Fallback:** if no active version exists, `getAssignedTopic()` falls back to deterministic hash-based assignment. Keep the fallback.
|
||||
|
||||
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
# AI Pipeline Hardening — Implementation Plan
|
||||
|
||||
> **Audience:** an AI agent executing this plan against the Respellion Learning Platform.
|
||||
> **Owner before this work:** Raymond Verhoef (rve@respellion.nl).
|
||||
> **Source of truth for repo conventions:** [`AI_AGENT.md`](AI_AGENT.md). Read it before starting.
|
||||
|
||||
This plan upgrades the platform's interaction with the Anthropic API: how prompts are built, how responses are parsed, how the model is retried, and how outputs are validated. It is broken into six phases that can be implemented and shipped independently. Each phase ends with verifiable acceptance criteria.
|
||||
|
||||
---
|
||||
|
||||
## 0. Operating principles
|
||||
|
||||
These rules govern every phase. Re-read them before you commit.
|
||||
|
||||
1. **PocketBase is the source of truth.** No persistent state in localStorage (see [`AI_AGENT.md`](AI_AGENT.md) §2). The Anthropic API is proxied via Caddy; **never** add `x-api-key` headers in frontend code.
|
||||
2. **No behaviour regressions.** Existing UI flows (extraction, weekly learning, weekly quiz, R42 chat, handbook sync, analyze-graph) must keep working after every phase. Phases are additive.
|
||||
3. **Schema-first.** Where the model produces structured output, define a JSON Schema (Zod) and validate every response. Reject (don't paper over) malformed output.
|
||||
4. **Single LLM entry point.** After Phase 1 there is exactly one module that talks to `/api/anthropic/v1/messages`. All callers go through it.
|
||||
5. **No silent truncation.** If `stop_reason === 'max_tokens'` on a structured-output call, throw — never persist a partial parse.
|
||||
6. **Cache what is stable, vary what is dynamic.** Use prompt caching for system prompts and KB context. User messages are never cached.
|
||||
7. **Comments and docs:** follow the repo's terse style. Don't add explanatory comments unless the *why* is non-obvious.
|
||||
8. **Migrations:** when changing a PocketBase schema, add a migration in `pb_migrations/` following the existing timestamp prefix convention. Never edit shipped migrations.
|
||||
9. **Stop and ask** if you encounter a decision the plan doesn't cover (e.g. a model deprecation, a missing collection, a failing test that looks pre-existing).
|
||||
|
||||
### Files you will touch (or create) across all phases
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `src/lib/llm.js` *(new, Phase 1)* | Single Anthropic client wrapper |
|
||||
| `src/lib/llmSchemas.js` *(new, Phase 1)* | Zod schemas for every structured task |
|
||||
| `src/lib/llmRetry.js` *(new, Phase 1)* | Retry + backoff + abort policy |
|
||||
| `src/lib/random.js` *(new, Phase 4)* | Fisher–Yates shuffle + RNG helpers |
|
||||
| `src/lib/api.js` | Becomes a thin re-export from `llm.js` (back-compat) |
|
||||
| `src/lib/extractionPipeline.js` | Migrated to `llm.js` + tool use + overlap chunking |
|
||||
| `src/lib/learningService.js` | Migrated to `llm.js` + tool use + patch-refine |
|
||||
| `src/lib/testService.js` | Migrated to `llm.js` + tool use + dedup + shuffle fix |
|
||||
| `src/components/admin/KnowledgeGraph.jsx` | `analyzeGraph` → tool use + dry-run preview |
|
||||
| `src/components/chat/rag.js` | Retrieval (TF-IDF) + `lookup_topic` tool |
|
||||
| `src/components/chat/prompts.js` | Split system prompt into cacheable + dynamic |
|
||||
| `src/components/chat/useChat.js` | Wire retrieval + truncation |
|
||||
| `pb_migrations/*` | Schema additions for `llm_calls`, question `difficulty`, topic `relevance_locked` |
|
||||
| `evals/` *(new, Phase 6)* | Golden-set eval harness |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Foundation (single LLM client + robust parsing)
|
||||
|
||||
**Goal:** every LLM call goes through one module that handles retry, timeout, abort, JSON extraction, and schema validation. No behaviour change visible to the user.
|
||||
|
||||
### 1.1 Create `src/lib/llmRetry.js`
|
||||
|
||||
Implements the retry policy used by `llm.js`.
|
||||
|
||||
**Behaviour:**
|
||||
- Exponential backoff with full jitter, base 1000ms, cap 16000ms.
|
||||
- Retries only on these HTTP statuses: `408, 425, 429, 500, 502, 503, 504, 529`.
|
||||
- Honours `Retry-After` header (seconds or HTTP date). If present and ≤ 60s, use it; if > 60s, fail fast.
|
||||
- Default `maxRetries = 4`.
|
||||
- Does **not** retry on `AbortError`.
|
||||
|
||||
**Exported interface:**
|
||||
|
||||
```js
|
||||
// withRetry: (fn: (attempt:number) => Promise<T>, opts?) => Promise<T>
|
||||
// RetryableError(status, retryAfterMs)
|
||||
export async function withRetry(fn, { maxRetries = 4, signal } = {}) { ... }
|
||||
export class RetryableError extends Error { constructor(status, retryAfterMs) { ... } }
|
||||
```
|
||||
|
||||
### 1.2 Create `src/lib/llmSchemas.js`
|
||||
|
||||
One Zod schema per structured task. Install Zod (`npm i zod`).
|
||||
|
||||
Required schemas (names + shape match what callers already produce — do not change field names):
|
||||
|
||||
- `extractionResultSchema` — `{ topics: Topic[], relations: Relation[] }` matching the existing `SYSTEM_PROMPT` in [`extractionPipeline.js`](src/lib/extractionPipeline.js).
|
||||
- `handbookResultSchema` — same shape, but `relation.type` enum unified to `related_to | depends_on | part_of | executed_by` (see Phase 3 task 3.5 — for now the schema accepts both `executes` and `executed_by`, normalize `executes → executed_by` post-validation).
|
||||
- `learningArticleSchema`, `learningSlidesSchema`, `learningInfographicSchema`, `learningAllSchema` matching [`learningService.js`](src/lib/learningService.js).
|
||||
- `quizQuestionsSchema` — `{ questions: Question[] }` with `options.length === 4` and `correctIndex ∈ [0,3]`.
|
||||
- `customTopicSchema` — `{ label, type: 'concept'|'role'|'process', description }`.
|
||||
- `graphActionsSchema` — `{ merges, deletions, newRelations, relevanceUpdates }` matching [`KnowledgeGraph.jsx:329`](src/components/admin/KnowledgeGraph.jsx).
|
||||
- `proposeGraphDeltaSchema` — matches `PROPOSE_GRAPH_DELTA_TOOL.input_schema` in [`prompts.js`](src/components/chat/prompts.js).
|
||||
|
||||
**Acceptance:** every schema has at least one happy-path Vitest test in `src/lib/__tests__/llmSchemas.test.js` (add `vitest` if not present).
|
||||
|
||||
### 1.3 Create `src/lib/llm.js`
|
||||
|
||||
The single Anthropic client. All other modules must call only this one.
|
||||
|
||||
**Public interface:**
|
||||
|
||||
```js
|
||||
// Task tier — used to pick a model from settings.
|
||||
// 'fast' → admin:model:fast (default: claude-haiku-4-5-20251001)
|
||||
// 'standard' → admin:model:standard (default: claude-sonnet-4-6)
|
||||
// 'reasoning' → admin:model:reasoning (default: claude-opus-4-7)
|
||||
export async function callLLM({
|
||||
task, // string, e.g. 'extract.source' — used for logging only
|
||||
tier = 'standard',
|
||||
system, // string OR Array<{ type:'text', text:string, cache_control?:{type:'ephemeral'} }>
|
||||
messages, // [{ role, content }] OR omitted (use `user`)
|
||||
user, // shorthand for [{role:'user', content: user}]
|
||||
tools, // optional Anthropic tool definitions
|
||||
toolChoice, // optional, e.g. { type:'tool', name:'emit_knowledge_graph' }
|
||||
schema, // optional Zod schema for text→JSON path (used only when no tool)
|
||||
maxTokens = 4096,
|
||||
temperature = 0,
|
||||
signal, // AbortSignal
|
||||
}): Promise<{
|
||||
text: string,
|
||||
toolUses: Array<{ name, input }>,
|
||||
stopReason: string,
|
||||
usage: { input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens },
|
||||
requestId: string | null,
|
||||
model: string,
|
||||
durationMs: number,
|
||||
}>
|
||||
```
|
||||
|
||||
**Key requirements:**
|
||||
|
||||
1. **Simulation mode** preserved: if `storage.get('admin:use_simulation') === true`, return a deterministic stub (use existing `simulateResponse` payload for backward compatibility — branch on `task` prefix to return a matching stub).
|
||||
2. **Fetch** with `AbortController`; default **60-second timeout** if caller didn't pass a signal.
|
||||
3. **Retry** through `withRetry` (Phase 1.1).
|
||||
4. **Auth-portal detection** preserved: if response is not `application/json`, throw `Your session has expired. Please refresh the page and log in again.` exactly as today.
|
||||
5. **No truncation acceptance**: if `stop_reason === 'max_tokens'` AND caller passed `schema` OR `toolChoice` requested a tool, throw `LLMTruncatedError`.
|
||||
6. **Robust JSON extraction** when caller passed `schema` (and no tool was used): use `parseStructuredText(text)` that
|
||||
- strips ```` ```json ```` and ```` ``` ```` fences,
|
||||
- finds the outermost balanced JSON value (object **or** array) via a tiny brace-matching scan, not regex,
|
||||
- throws `LLMOutputError` if no balanced JSON found,
|
||||
- runs Zod `schema.parse` on the result.
|
||||
7. **Tool path:** when `tools` is provided and the model emits `tool_use`, return them under `toolUses`. Validate each tool's input against the corresponding Zod schema if the caller wired one in (via `toolSchemas: { [toolName]: ZodSchema }`).
|
||||
8. **Logging:** after every call, append a row to a new PocketBase collection `llm_calls` (best-effort — never block on this; catch and console.debug failures). Fields: `task, model, tier, duration_ms, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, stop_reason, ok, error_msg`. See Phase 5 task 5.6 for the migration.
|
||||
9. **Custom errors**: `LLMHttpError`, `LLMTruncatedError`, `LLMOutputError`, `LLMValidationError`. All extend `Error` and set `name` for `instanceof` checks.
|
||||
|
||||
### 1.4 Make `src/lib/api.js` a thin shim
|
||||
|
||||
Replace the existing `anthropicApi.generateContent` and `anthropicApi.chat` implementations with calls into `llm.js`. Preserve the exact exported names and return shapes so no caller breaks.
|
||||
|
||||
```js
|
||||
// api.js after Phase 1
|
||||
export const anthropicApi = {
|
||||
async generateContent(systemPrompt, userMessage, maxRetries = 1) {
|
||||
const { text } = await callLLM({
|
||||
task: 'legacy.generateContent',
|
||||
tier: 'standard',
|
||||
system: systemPrompt,
|
||||
user: userMessage,
|
||||
maxTokens: 8192,
|
||||
temperature: 0,
|
||||
});
|
||||
return text;
|
||||
},
|
||||
async chat(systemPrompt, messages, opts = {}) {
|
||||
const r = await callLLM({
|
||||
task: 'legacy.chat',
|
||||
tier: 'standard',
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
tools: opts.tools,
|
||||
maxTokens: 1024,
|
||||
temperature: 0.3, // chat default — see Phase 5
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
...(r.text ? [{ type:'text', text: r.text }] : []),
|
||||
...r.toolUses.map(tu => ({ type:'tool_use', name: tu.name, input: tu.input })),
|
||||
],
|
||||
stop_reason: r.stopReason,
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 1.5 Update default model + tiered settings
|
||||
|
||||
- Replace `DEFAULT_MODEL = 'claude-sonnet-4-20250514'` with the three tier defaults above.
|
||||
- In **Admin → Settings**, add three model selects (`fast`, `standard`, `reasoning`). Read existing `admin:model` as a legacy fallback for `standard` (so existing users don't lose their override).
|
||||
|
||||
### Phase 1 acceptance criteria
|
||||
|
||||
- [ ] `npm run lint` passes; `npm run test` passes (Vitest).
|
||||
- [ ] Every existing user flow (extraction, weekly content, weekly quiz, R42, handbook sync, analyze graph) still works against the live API.
|
||||
- [ ] `grep -r "fetch.*anthropic" src/` returns only `src/lib/llm.js`.
|
||||
- [ ] Simulation mode toggle still returns stubbed responses for all flows.
|
||||
- [ ] Manually verify: kill the network mid-call → request aborts within 60s and surfaces a clear error message.
|
||||
- [ ] Manually verify: rate-limit the proxy (429 + `Retry-After: 5`) → call retries once after ~5s and then succeeds.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Prompt caching & tool-based structured outputs
|
||||
|
||||
**Goal:** structured-output tasks no longer parse JSON out of prose. Large stable prompts are cached.
|
||||
|
||||
### 2.1 Migrate extraction to tool use
|
||||
|
||||
In [`extractionPipeline.js`](src/lib/extractionPipeline.js):
|
||||
|
||||
- Replace the "Return JSON only" instruction with a tool: `emit_knowledge_graph` whose `input_schema` mirrors `extractionResultSchema`.
|
||||
- Replace `anthropicApi.generateContent(...)` with `callLLM({ ..., tools:[emitKnowledgeGraphTool], toolChoice:{ type:'tool', name:'emit_knowledge_graph' } })`.
|
||||
- Read the validated object from `toolUses[0].input`.
|
||||
- Same migration for `analyzeHandbookDelta` (tool `emit_handbook_delta`).
|
||||
- Delete every `responseText.match(/\{[\s\S]*\}/)` site.
|
||||
|
||||
### 2.2 Migrate learning, quiz, custom-topic, graph-actions to tool use
|
||||
|
||||
Same pattern, in:
|
||||
|
||||
- [`learningService.js`](src/lib/learningService.js): tools `emit_learning_article`, `emit_learning_slides`, `emit_learning_infographic`, `emit_learning_all`, `emit_custom_topic`.
|
||||
- [`testService.js`](src/lib/testService.js): tool `emit_quiz_questions`.
|
||||
- [`KnowledgeGraph.jsx:297`](src/components/admin/KnowledgeGraph.jsx): tool `emit_graph_actions`.
|
||||
|
||||
### 2.3 Prompt caching
|
||||
|
||||
Pass `system` as an array of blocks so the stable parts can be cached:
|
||||
|
||||
```js
|
||||
system: [
|
||||
{ type:'text', text: STABLE_SYSTEM_HEADER, cache_control: { type:'ephemeral' } },
|
||||
{ type:'text', text: dynamicPart }, // not cached
|
||||
],
|
||||
```
|
||||
|
||||
Apply caching to:
|
||||
- Extraction `SYSTEM_PROMPT` and `HANDBOOK_SYSTEM_PROMPT` (both fully stable → cache the whole block).
|
||||
- R42 system prompt — split into three blocks: stable preamble (cached), KB context (cached *only* while the graph hasn't changed; bust by appending a short hash of the topic IDs+labels — Phase 5 details), and per-turn role line (not cached).
|
||||
|
||||
### 2.4 Patch-based learning refinement
|
||||
|
||||
Refactor `refineLearningContent` ([`learningService.js:147`](src/lib/learningService.js:147)) from "return the full updated JSON" to **patch operations** via tools:
|
||||
|
||||
- `set_section(heading: string, body: string)` — replace one section by heading match.
|
||||
- `add_section(heading: string, body: string, position: 'start'|'end')`.
|
||||
- `remove_section(heading: string)`.
|
||||
- `replace_takeaways(items: string[])`.
|
||||
- `set_intro(intro: string)`.
|
||||
|
||||
Apply patches client-side to the cached object. Re-validate against `learningArticleSchema` after patching; reject the whole turn if invalid.
|
||||
|
||||
### Phase 2 acceptance criteria
|
||||
|
||||
- [ ] No regex JSON extraction left in `src/`: `grep -rn "match(/\\\\{\\[\\\\s\\\\S\\]\\*\\\\}/)" src/` returns nothing.
|
||||
- [ ] Token usage telemetry shows `cache_read_input_tokens > 0` on the second extraction call within 5 minutes (cache hit).
|
||||
- [ ] Re-running extraction on a known source produces the same topic count ±10% as before this phase.
|
||||
- [ ] `refineLearningContent` round-trip ("make the intro shorter") produces only the changed section in the diff against the prior cached content.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Extraction quality
|
||||
|
||||
**Goal:** fewer near-duplicate topics, no silent truncation, adaptive throttling, unified vocabulary.
|
||||
|
||||
### 3.1 Sentence-aware chunking with overlap
|
||||
|
||||
Replace `chunkText` in [`extractionPipeline.js:87`](src/lib/extractionPipeline.js:87):
|
||||
|
||||
- Target **~2000 input tokens per chunk**. Approximate as `chars / 4`. Configurable via `MAX_CHUNK_CHARS = 8000`.
|
||||
- **200-token overlap** between chunks (`OVERLAP_CHARS = 800`).
|
||||
- Split on sentence boundaries (`/(?<=[.!?])\s+/`) first; fall back to paragraph boundary if a sentence is too long; never produce a chunk larger than `MAX_CHUNK_CHARS`.
|
||||
- Add a guard: if a single sentence exceeds `MAX_CHUNK_CHARS`, hard-split at character boundary and log a warning.
|
||||
|
||||
### 3.2 Stateful extraction
|
||||
|
||||
Before each chunk after the first, prepend to the user message:
|
||||
|
||||
```text
|
||||
Already-extracted topic IDs (do NOT create new IDs for these — reuse them if the same concept appears here):
|
||||
- software-engineer
|
||||
- onboarding-buddy
|
||||
...
|
||||
```
|
||||
|
||||
Cap the list at 200 IDs by recency to keep token cost bounded. The model will then reuse IDs instead of inventing variants like `software-developer`.
|
||||
|
||||
### 3.3 Adaptive throttling
|
||||
|
||||
Replace the hard `setTimeout(r, 12000)` in [`extractionPipeline.js:127`](src/lib/extractionPipeline.js:127) and the 15s sleep in [`KnowledgeGraph.jsx:274`](src/components/admin/KnowledgeGraph.jsx:274) with a shared token-bucket limiter in `src/lib/llmRetry.js`:
|
||||
|
||||
```js
|
||||
export const extractionLimiter = createLimiter({ rps: 5/60, burst: 1 }); // 5 req/min
|
||||
// usage: await extractionLimiter.acquire();
|
||||
```
|
||||
|
||||
`callLLM` accepts an optional `limiter` param that it `await`s before fetch. On 429 with `Retry-After`, the limiter is paused for that duration.
|
||||
|
||||
### 3.4 Preserve admin-edited relevance
|
||||
|
||||
Add a migration introducing `relevance_locked: bool` on `topics`. Set it to `true` whenever an admin edits `learning_relevance` via the UI ([`KnowledgeGraph.jsx`](src/components/admin/KnowledgeGraph.jsx) edit handler — locate by searching for `setLearningRelevance` or the relevance form field).
|
||||
|
||||
In `mergeKnowledgeGraph` ([`extractionPipeline.js:167`](src/lib/extractionPipeline.js:167)), when `relevance_locked`, never overwrite `learning_relevance`.
|
||||
|
||||
### 3.5 Unify relation vocabulary
|
||||
|
||||
Pick **one** set: `related_to | depends_on | part_of | executed_by`. Migrate:
|
||||
|
||||
- `HANDBOOK_SYSTEM_PROMPT` ([`extractionPipeline.js:42`](src/lib/extractionPipeline.js:42)) — change `executes` to `executed_by` and swap the source/target in the prompt example.
|
||||
- Write a one-shot migration script `pb_migrations/<timestamp>_normalize_relation_types.js` that rewrites any existing `executes` relation to `executed_by` and swaps `source ↔ target`.
|
||||
- Verify R42's `validateDelta` ([`rag.js:108`](src/components/chat/rag.js:108)) already enforces this set (it does) — no change needed there.
|
||||
|
||||
### 3.6 Cancellation
|
||||
|
||||
Add a "Cancel" button to the source-processing UI in `ContentManager.jsx` / `UploadZone.jsx` (locate the one that displays extraction progress). Wire it to abort the in-flight `callLLM` via the `signal` it receives. On cancel, set source status to `cancelled` (add to status enum migration).
|
||||
|
||||
### Phase 3 acceptance criteria
|
||||
|
||||
- [ ] Running extraction twice on the same `sources/ROLES.md` produces zero new topics on the second run (idempotency through reused IDs).
|
||||
- [ ] Locked-relevance topics survive re-extraction.
|
||||
- [ ] No fixed `setTimeout` ≥ 5s anywhere in `src/` (`grep -rn "setTimeout" src/`).
|
||||
- [ ] Cancelling an extraction mid-run leaves the source in `cancelled` state, not `processing`.
|
||||
- [ ] `pb_migrations` includes the relation-vocabulary normalization and the `relevance_locked` column.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Quiz & content quality
|
||||
|
||||
**Goal:** quiz questions are positionally unbiased, deduped, and difficulty-tagged. Random helpers are correct.
|
||||
|
||||
### 4.1 Random helpers
|
||||
|
||||
Create `src/lib/random.js`:
|
||||
|
||||
```js
|
||||
export function shuffle(arr) { /* Fisher–Yates, returns NEW array */ }
|
||||
export function sample(arr, n) { /* unbiased sample without replacement */ }
|
||||
export function pickInt(min, maxInclusive) { /* uniform integer */ }
|
||||
```
|
||||
|
||||
Replace every `.sort(() => 0.5 - Math.random())` with `shuffle(arr)`:
|
||||
|
||||
- [`testService.js:122`](src/lib/testService.js:122) and [`testService.js:163`](src/lib/testService.js:163).
|
||||
- Any other site found by `grep -rn "0.5 - Math.random()" src/`.
|
||||
|
||||
### 4.2 Debias `correctIndex` in quiz prompt
|
||||
|
||||
In [`testService.js:81`](src/lib/testService.js:81):
|
||||
|
||||
- Change the example in the prompt to use `"correctIndex": 2` (not 0).
|
||||
- Add to the prompt: *"Distribute correctIndex roughly evenly across 0, 1, 2, and 3. Do not place the correct answer at the same position more than 4 out of 10 times."*
|
||||
- After parsing, run a check: if more than 50% of the batch share the same `correctIndex`, log a warning and re-roll up to 2 times.
|
||||
|
||||
### 4.3 Difficulty field
|
||||
|
||||
- Add to `quizQuestionsSchema`: `difficulty: 'easy'|'medium'|'hard'`.
|
||||
- Update the prompt to require difficulty on every question (current prompt says "4 easy, 4 medium, 2 hard" but never tagged — now tag it).
|
||||
- Migration: add `difficulty` to the `quiz_banks.questions[]` element. PocketBase stores `questions` as JSON, so the migration is a no-op at the column level; older records get `difficulty: 'medium'` on read (add a normalizer in `db.js`).
|
||||
|
||||
### 4.4 Question dedup
|
||||
|
||||
In `forceGenerateTopicQuestions` ([`testService.js:65`](src/lib/testService.js:65)):
|
||||
|
||||
- Normalize question text (lowercase, strip punctuation, collapse whitespace) → `normKey`.
|
||||
- Before persisting, drop any new question whose `normKey` matches an existing bank question.
|
||||
- Log dropped duplicates with `console.debug('[quiz] dropped duplicate:', text)`.
|
||||
|
||||
### 4.5 Quality gate
|
||||
|
||||
In the same function, after schema validation:
|
||||
|
||||
- Reject the whole batch if any question has fewer than 4 distinct options.
|
||||
- Reject if any option contains `"all of the above"`, `"none of the above"`, `"both A and B"` (case-insensitive).
|
||||
- Reject if `explanation.trim().length < 20`.
|
||||
- Surface the rejection to the admin UI with a "Retry" button.
|
||||
|
||||
### 4.6 Custom topic ID hygiene
|
||||
|
||||
In `generateCustomTopic` ([`learningService.js:177`](src/lib/learningService.js:177)):
|
||||
|
||||
- Generate kebab-case ID from the polished label, not `Date.now()`.
|
||||
- Collision check against existing topics (append `-2`, `-3`, … if needed).
|
||||
- Default `learning_relevance: 'standard'` when the model omits it.
|
||||
|
||||
### Phase 4 acceptance criteria
|
||||
|
||||
- [ ] No `.sort(() => 0.5 - Math.random())` anywhere in `src/`.
|
||||
- [ ] Sample of 50 fresh quiz questions across 5 topics: no position holds >40% of correct answers.
|
||||
- [ ] Re-running quiz generation for the same topic does not grow the bank with semantic duplicates.
|
||||
- [ ] Custom topics created via R42 use kebab-case IDs and pass schema validation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — R42 retrieval & telemetry
|
||||
|
||||
**Goal:** R42 stops shipping the entire KG every turn; conversations are bounded; every call is logged.
|
||||
|
||||
### 5.1 TF-IDF retrieval in the browser
|
||||
|
||||
Create `src/lib/retrieval.js`:
|
||||
|
||||
```js
|
||||
export function buildIndex(topics) { /* TF-IDF over label + description */ }
|
||||
export function retrieveTopK(index, query, k = 10) { /* returns Topic[] */ }
|
||||
```
|
||||
|
||||
Implementation: a small dependency-free TF-IDF — tokenize on `/[a-zA-Z0-9-]+/`, lowercase, drop stopwords (Dutch + English short list). Cache the index on the `topics` array reference. About 100 lines.
|
||||
|
||||
### 5.2 Rewrite `buildKbContext`
|
||||
|
||||
In [`rag.js:11`](src/components/chat/rag.js:11):
|
||||
|
||||
- Use `retrieveTopK(index, userMessage, 10)` to pick which topics go into the system prompt.
|
||||
- Always include any topic whose ID or label is mentioned verbatim (existing behaviour).
|
||||
- Drop the full "every topic" dump.
|
||||
- Return `{ context, retrievedTopics, allTopics }` — `validateDelta` continues to use `allTopics`.
|
||||
|
||||
### 5.3 `lookup_topic` tool
|
||||
|
||||
Add a second R42 tool in [`prompts.js`](src/components/chat/prompts.js):
|
||||
|
||||
```js
|
||||
export const LOOKUP_TOPIC_TOOL = {
|
||||
name: 'lookup_topic',
|
||||
description: 'Fetch the full description and any deeper learning content for a topic. Use when the retrieved context does not contain enough to answer.',
|
||||
input_schema: { type:'object', properties: { id:{type:'string'} }, required:['id'] },
|
||||
};
|
||||
```
|
||||
|
||||
In `useChat.js`, when the model emits a `lookup_topic`, fetch via `db.getTopics()` + `db.getContent(id)` and append a `tool_result` block, then call `callLLM` again with the extended messages. Cap to **3 lookup hops per turn** to avoid loops.
|
||||
|
||||
### 5.4 R42 prompt cache busting
|
||||
|
||||
KB-context block is cached as ephemeral. The cache key is automatic per text, so any change busts it. **Append a stable suffix** to the KB block: `"\n[kb_hash: <first-8-chars-of-sha256-of-sorted-topic-ids>]"`. This guarantees the block content changes the moment a topic is added/removed, even if the included topic list looks similar.
|
||||
|
||||
### 5.5 Conversation truncation
|
||||
|
||||
In `useChat.js`:
|
||||
|
||||
- Keep last **12 turns** verbatim in `apiMessages`.
|
||||
- Older messages: if more than 12 turns exist, summarize the older ones with a cheap (`tier: 'fast'`) call and prepend a single `{ role:'system'-equivalent inside the user history is not allowed by Anthropic — instead prepend a user/assistant pair }` block, OR simply drop older turns. **Default: drop older turns and prepend a one-line `assistant` message saying "(earlier conversation truncated)".** Summarization is an optional follow-up.
|
||||
|
||||
### 5.6 `llm_calls` collection
|
||||
|
||||
Add a migration `pb_migrations/<timestamp>_created_llm_calls.js` for collection `llm_calls`:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `task` | text | e.g. `extract.source` |
|
||||
| `model` | text | resolved model ID |
|
||||
| `tier` | text | `fast` / `standard` / `reasoning` |
|
||||
| `duration_ms` | number | |
|
||||
| `input_tokens` | number | |
|
||||
| `output_tokens` | number | |
|
||||
| `cache_read_tokens` | number | |
|
||||
| `cache_create_tokens` | number | |
|
||||
| `stop_reason` | text | |
|
||||
| `ok` | bool | |
|
||||
| `error_msg` | text | nullable |
|
||||
|
||||
Wire `callLLM` to write to it (best-effort, never throws). Add a minimal `Admin → Diagnostics` view that shows the last 100 calls and aggregate cost (using public Anthropic prices in a constant; refresh manually).
|
||||
|
||||
### 5.7 R42 defaults
|
||||
|
||||
- `temperature: 0.3` for R42 chat in `callLLM`.
|
||||
- `maxTokens: 2048` for R42 (text + tool budget).
|
||||
|
||||
### Phase 5 acceptance criteria
|
||||
|
||||
- [ ] R42 system prompt is ≤ 4000 tokens regardless of KG size (verify on a graph with 200+ topics).
|
||||
- [ ] Adding a topic in the admin UI causes the **next** R42 call to show `cache_read_tokens === 0` for the KB block, then subsequent calls to show non-zero.
|
||||
- [ ] R42 successfully answers a question about a topic whose ID was not in the user's message by emitting `lookup_topic` (manual verification).
|
||||
- [ ] `Admin → Diagnostics` shows recent LLM calls with model, tokens, duration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Eval harness (optional, high-leverage)
|
||||
|
||||
**Goal:** prompt or model changes can be measured before they ship.
|
||||
|
||||
### 6.1 Golden sets
|
||||
|
||||
Create `evals/` at the repo root:
|
||||
|
||||
```
|
||||
evals/
|
||||
extraction/
|
||||
cases/
|
||||
roles-handbook.txt
|
||||
governance-handbook.txt
|
||||
expected/
|
||||
roles-handbook.json # { mustContain: ['software-engineer', ...], minTopics: 20 }
|
||||
governance-handbook.json
|
||||
quiz/
|
||||
cases/
|
||||
software-engineer.json # topic to generate quiz for
|
||||
rubric.md # human-readable quality rubric
|
||||
chat/
|
||||
scripts/
|
||||
ask-about-known-topic.json
|
||||
ask-about-unknown-topic.json
|
||||
propose-new-role.json
|
||||
```
|
||||
|
||||
10 extraction cases, 5 quiz topics, 10 chat scripts is enough to start.
|
||||
|
||||
### 6.2 Runner
|
||||
|
||||
`evals/run.mjs` — Node script that:
|
||||
|
||||
1. Loads each case.
|
||||
2. Invokes the same code path the app uses (import from `src/lib/*` directly; reuse the simulation toggle off).
|
||||
3. Compares against expectations:
|
||||
- **Extraction:** `mustContain` IDs present? `minTopics` met? No `stop_reason: 'max_tokens'`?
|
||||
- **Quiz:** distribution of `correctIndex`, mean explanation length, banned phrases.
|
||||
- **Chat:** for each script, did the expected tool fire? Did the reply contain expected anchors?
|
||||
4. Writes `evals/results/<ISO-timestamp>.json` and a Markdown diff against the previous baseline.
|
||||
|
||||
Add `npm run eval` to `package.json`.
|
||||
|
||||
### 6.3 Prompt versioning
|
||||
|
||||
In each prompt module, export `PROMPT_VERSION = '2026-05-20-001'`. Persist it on the artifact (`content.data.prompt_version`, `quiz_banks.questions[i].prompt_version`, `topics.metadata.prompt_version`). Add an admin button "Mark stale content for regeneration" that lists artifacts whose version is older than the current.
|
||||
|
||||
### Phase 6 acceptance criteria
|
||||
|
||||
- [ ] `npm run eval` runs end-to-end against the live API and produces a result file.
|
||||
- [ ] CI (or a manual check) runs evals on every change to `src/lib/llm.js`, prompts, or schemas.
|
||||
- [ ] Each AI-generated artifact in PocketBase carries a `prompt_version` and is filterable by it.
|
||||
|
||||
---
|
||||
|
||||
## Cross-phase verification checklist
|
||||
|
||||
After every phase, run this short checklist before merging:
|
||||
|
||||
1. **Build:** `npm run build` succeeds.
|
||||
2. **Lint:** `npm run lint` clean.
|
||||
3. **Tests:** `npm run test` green.
|
||||
4. **Smoke flows** in dev (simulation off, real API key):
|
||||
- Add a source via `Admin → Sources`, extract, verify topics appear.
|
||||
- Visit `Leren` for the current week, generate article. Then slides.
|
||||
- Visit `Testen`, generate weekly quiz. Submit. Score lands on leaderboard.
|
||||
- Open R42, ask a known and an unknown question; propose a new topic; admin accepts it.
|
||||
- Run `Admin → Knowledge Graph → Analyze & Optimize Graph`.
|
||||
- Run `Admin → Knowledge Graph → Sync Handbook` (small repo or mock).
|
||||
5. **Simulation toggle:** flip simulation mode on and confirm no real API calls happen (Network tab).
|
||||
|
||||
---
|
||||
|
||||
## Rollback strategy
|
||||
|
||||
Every phase is shippable on its own. If a phase introduces a regression:
|
||||
|
||||
- **Phase 1–2:** `git revert` the merge commit. `api.js` retains the legacy interface, so callers that haven't migrated still work.
|
||||
- **Phase 3:** revert + the relation-vocabulary migration is reversible (rerun the reverse swap).
|
||||
- **Phase 4:** revert; quiz schema additions are forward-compatible (older readers ignore `difficulty`).
|
||||
- **Phase 5:** revert + drop the `llm_calls` collection if undesired.
|
||||
- **Phase 6:** purely additive; remove the `evals/` folder if abandoned.
|
||||
|
||||
Never `git push --force` to `main`. PR-per-phase.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (do not do as part of this plan)
|
||||
|
||||
- Replacing PocketBase. Stays as-is.
|
||||
- Server-side embeddings or a vector store. Phase 5 deliberately uses in-browser TF-IDF.
|
||||
- Streaming responses. Mentioned as a future improvement; not in this plan.
|
||||
- Multi-tenant changes. The platform serves one company.
|
||||
- UI redesign of the Admin pages beyond what each phase requires.
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Tier** — coarse model class (`fast`/`standard`/`reasoning`) mapped to a concrete Anthropic model ID via admin settings.
|
||||
- **Tool use** — Anthropic's structured-output mechanism. The model emits a `tool_use` content block whose `input` is schema-valid JSON.
|
||||
- **Prompt caching** — Anthropic feature where blocks marked `cache_control: { type:'ephemeral' }` are reused across requests at lower input cost. 5-minute TTL.
|
||||
- **TF-IDF** — Term-frequency / inverse-document-frequency. A classic IR scoring function used here as a cheap retrieval signal.
|
||||
- **KB context** — The block in R42's system prompt that lists topics and relations from the knowledge graph.
|
||||
- **Delta** — A proposed addition to the knowledge graph emitted by R42 via the `propose_graph_delta` tool.
|
||||
142
CLAUDE.md
Normal file
142
CLAUDE.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## What this is
|
||||
|
||||
The **Respellion Learning Platform** — an internal AI-powered learning app that
|
||||
keeps employees current with the company's evolving knowledge base. Employees
|
||||
follow a perpetual **26-week curriculum**, each working through weekly learning
|
||||
sessions and tests. An AI assistant called **R42** is available on every screen.
|
||||
Admins upload source documents that Claude extracts into a knowledge graph.
|
||||
|
||||
This is a **single-page React application** (Vite) backed by **PocketBase**.
|
||||
There is **no separate backend** — all logic runs in the browser and talks
|
||||
directly to PocketBase collections and (via a proxy) the Anthropic API.
|
||||
|
||||
---
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Frontend | React 19 + Vite 8, React Router 7 |
|
||||
| Styling | Vanilla CSS (CSS custom properties) + Tailwind v4 utilities mapped to those variables |
|
||||
| Animation | Framer Motion |
|
||||
| Icons | Lucide React |
|
||||
| Graph viz | D3.js (admin knowledge graph only) |
|
||||
| Backend / DB / auth | PocketBase (self-hosted, SQLite) |
|
||||
| AI | Anthropic Claude via a reverse-proxy (Caddy in prod, Vite proxy in dev) |
|
||||
| Retrieval (RAG) | Local TF-IDF over the knowledge graph — **no Qdrant, no embeddings API** |
|
||||
| Validation | Zod on all AI tool output |
|
||||
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
|
||||
|
||||
**Claude models** (`src/lib/llm.js`, by tier):
|
||||
- `fast` → `claude-haiku-4-5-20251001` (R42 chat, quiz batches, flashcards)
|
||||
- `standard` → `claude-sonnet-4-6` (extraction, article/slides/infographic, curriculum)
|
||||
- `reasoning` → `claude-opus-4-7`
|
||||
|
||||
Admins can override any tier's model string from the Settings tab (persisted in
|
||||
localStorage as `admin:model:{tier}`).
|
||||
|
||||
---
|
||||
|
||||
## Repository structure
|
||||
|
||||
```
|
||||
repo/
|
||||
├── CLAUDE.md ← you are here
|
||||
├── AI_AGENT.md ← detailed architecture/patterns guide — read this
|
||||
├── README.md ← quickstart
|
||||
├── PROTECTED.md ← files you must not modify
|
||||
├── docs/ ← spec files (describe the system as built)
|
||||
├── src/ ← THE APPLICATION (React/Vite)
|
||||
│ ├── pages/ ← route screens (Dashboard, Leren, Testen, Leaderboard, Login, Onboarding, Admin/)
|
||||
│ ├── components/ ← ui primitives, admin panels, chat (R42), micro_learning
|
||||
│ ├── lib/ ← services: llm, db, extractionPipeline, learningService,
|
||||
│ │ microLearningService, testService, curriculumService, retrieval, pb
|
||||
│ ├── hooks/ ← React hooks
|
||||
│ └── store/ ← AppContext (global state)
|
||||
├── pb_migrations/ ← PocketBase schema migrations (JS, applied by the PB binary)
|
||||
├── scripts/ ← setup-pb-collections.mjs (local collection bootstrap)
|
||||
├── public/ ← static assets, fonts
|
||||
├── infra/ ← Ansible deploy playbooks (development / production)
|
||||
├── Caddyfile, Dockerfile, docker-compose.yml ← deployment (frozen)
|
||||
└── stylesheet.css ← authoritative visual reference (frozen)
|
||||
```
|
||||
|
||||
> **Note on `/app`:** the top-level `app/` directory is **abandoned scaffolding**
|
||||
> from the original Next.js + Qdrant design that was never shipped. It is not
|
||||
> wired into `package.json`, Vite, or Docker. **Ignore it.** The real app is `/src`.
|
||||
|
||||
---
|
||||
|
||||
## Absolute constraints
|
||||
|
||||
1. Never modify any file listed in `PROTECTED.md`.
|
||||
2. Never modify `stylesheet.css` — it is the authoritative visual reference. Use the
|
||||
CSS variables in `src/index.css` and the Tailwind classes mapped to them.
|
||||
3. Treat the deployment files as frozen unless explicitly asked: `Dockerfile`,
|
||||
`Caddyfile`, `docker-compose.yml`, `infra/`, `.github/workflows/`.
|
||||
4. Never delete files without explicit confirmation.
|
||||
5. Never re-enable PocketBase auto-cancellation — `pb.autoCancellation(false)` in
|
||||
`src/lib/pb.js` is deliberate (see AI_AGENT.md §2).
|
||||
6. There is **no podcast** learning type. It was removed. Do not re-add it.
|
||||
7. Ask before acting when scope is unclear.
|
||||
|
||||
---
|
||||
|
||||
## Code conventions
|
||||
|
||||
- All `src/lib/db.js` functions are `async` — always `await` them.
|
||||
- All Claude tool output is validated through Zod (`src/lib/llmSchemas.js`) before use.
|
||||
Never reach `/api/anthropic` directly — go through `callLLM` in `src/lib/llm.js`.
|
||||
- No client-side API key. The Anthropic key is injected server-side by the proxy.
|
||||
- Reuse the UI primitives in `src/components/ui/` (`Card`, `Button`, `Tag`, `Input`).
|
||||
- Mobile-first — the employee app targets 375px width and scales up.
|
||||
- No hardcoded hex colors; use the design-system CSS variables / Tailwind tokens.
|
||||
|
||||
---
|
||||
|
||||
## Running locally
|
||||
|
||||
```bash
|
||||
npm install
|
||||
./pocketbase.exe serve # or the muchobien/pocketbase Docker image
|
||||
node scripts/setup-pb-collections.mjs # first run only — bootstraps collections
|
||||
npm run dev # Vite dev server (proxies /api/anthropic)
|
||||
```
|
||||
|
||||
Set `ANTHROPIC_API_KEY` in the environment so the Vite proxy can inject it
|
||||
(`vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically
|
||||
when the PB binary starts.
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `npm run dev` | Vite dev server |
|
||||
| `npm run build` | Production build to `dist/` |
|
||||
| `npm test` | Vitest unit tests |
|
||||
| `npm run lint` | ESLint |
|
||||
|
||||
---
|
||||
|
||||
## Documentation map
|
||||
|
||||
- `AI_AGENT.md` — detailed patterns, gotchas, and subsystem guide. **Start here for any real work.**
|
||||
- `docs/architecture.md` — system design and data flows
|
||||
- `docs/data-model.md` — every PocketBase collection and field
|
||||
- `docs/ingestion-spec.md` — upload → knowledge-graph extraction
|
||||
- `docs/generation-spec.md` — learning content + micro-learning generation
|
||||
- `docs/curriculum-spec.md` — 26-week per-user curriculum engine
|
||||
- `docs/r42-spec.md` — the R42 chatbot
|
||||
- `docs/auth-spec.md` — Azure (Entra ID) login: OIDC via PocketBase, provisioning, roles
|
||||
- `docs/frontend-spec.md` — screens, routing, onboarding
|
||||
- `docs/gamification-spec.md` — points, badges, leaderboard
|
||||
- `micro-learning-spec.md` — micro-learning learner experience
|
||||
|
||||
---
|
||||
|
||||
## When you are uncertain
|
||||
|
||||
Check the spec files in `docs/` and `AI_AGENT.md` first. If the spec does not
|
||||
cover it, read the actual code in `src/` — it is the source of truth. Flag gaps
|
||||
explicitly rather than assuming. Do not treat the abandoned `/app` scaffolding or
|
||||
`legacy`-style references in old comments as guidance.
|
||||
30
PROTECTED.md
Normal file
30
PROTECTED.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Protected files — do not modify
|
||||
|
||||
These files and directories must never be modified by Claude Code without an
|
||||
explicit request. They belong to the deployment pipeline and the authoritative
|
||||
visual reference, and are otherwise frozen.
|
||||
|
||||
## Deployment & CI/CD
|
||||
.github/workflows/
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
Caddyfile
|
||||
Caddyfile.test
|
||||
|
||||
## Infrastructure & provisioning
|
||||
infra/ (Ansible playbooks: development / production)
|
||||
|
||||
## Stylesheet
|
||||
stylesheet.css (repo root — authoritative visual reference, use as-is)
|
||||
|
||||
## Abandoned scaffolding (do not edit or rely on)
|
||||
app/ (original Next.js + Qdrant design, never shipped — the real app is /src)
|
||||
|
||||
---
|
||||
|
||||
Notes:
|
||||
- The active application lives entirely in `/src`. Editing the app means editing `/src`.
|
||||
- PocketBase schema changes go through `pb_migrations/` (and should be mirrored in
|
||||
`scripts/setup-pb-collections.mjs`) — those are NOT frozen.
|
||||
- If a task genuinely requires touching a frozen file (e.g. a deliberate deployment
|
||||
change), confirm with the human first.
|
||||
59
README.md
59
README.md
@@ -4,24 +4,28 @@ An internal AI-powered learning platform that keeps Respellion employees up to d
|
||||
|
||||
## Features
|
||||
|
||||
- **Weekly Learning Station** — Each employee is assigned a topic each week (via deterministic hash of user ID + week number). They choose their preferred format: Article, Slides, or Infographic. Content is generated on-demand by Claude and cached per topic.
|
||||
- **Weekly Test** — AI-generated quiz based on the knowledge graph. Results are stored and feed the leaderboard.
|
||||
- **Leaderboard & Gamification** — Points for correct answers, badges for streaks and perfect scores.
|
||||
- **R42 Chatbot** — An always-available AI assistant (backed by Claude) with access to the full knowledge graph. Can propose graph updates that admins approve or reject.
|
||||
- **Admin Panel** — Manage the knowledge graph, sync from GitHub, review generated content, refine it with AI, and monitor team progress.
|
||||
- **Onboarding** — On first login each employee enrolls into the curriculum with one tap. Their 26-week cycle starts then and there.
|
||||
- **Weekly Learning Station** — Each employee gets a topic for their current curriculum week, drawn from the active 26-week schedule (with a deterministic hash fallback when no curriculum is active). They engage via micro-learnings (concept explainer, scenario quiz, flashcard set) and can also generate long-form formats (Article, Slides, Infographic) on demand.
|
||||
- **Weekly Test** — A 5-question AI-generated quiz for the week's topic. Results feed the leaderboard.
|
||||
- **Leaderboard & Gamification** — Points for correct answers, badges for milestones (First Steps, Veteran, Perfectionist).
|
||||
- **R42 Chatbot** — An always-available AI assistant (Claude) grounded in the knowledge graph via local TF-IDF retrieval. It can propose graph updates that admins approve or reject.
|
||||
- **Admin Panel** — Manage the knowledge graph, upload source files, review/refine generated content, generate the curriculum, and manage the team.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Frontend | React 18 + Vite |
|
||||
| Styling | Vanilla CSS (custom properties) + Tailwind utility classes |
|
||||
| Frontend | React 19 + Vite 8, React Router 7 |
|
||||
| Styling | Vanilla CSS (custom properties) + Tailwind v4 utilities |
|
||||
| Animations | Framer Motion |
|
||||
| Icons | Lucide React |
|
||||
| Graph viz | D3.js (admin knowledge graph only) |
|
||||
| Backend / DB | PocketBase (self-hosted) |
|
||||
| Backend / DB / auth | PocketBase (self-hosted, SQLite) |
|
||||
| Retrieval | Local TF-IDF (no Qdrant, no embeddings API) |
|
||||
| AI | Anthropic Claude (via Caddy reverse proxy) |
|
||||
| Infra | Docker + Caddy |
|
||||
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
|
||||
|
||||
**Claude models (by tier):** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`.
|
||||
|
||||
## Getting Started (local dev)
|
||||
|
||||
@@ -29,14 +33,17 @@ An internal AI-powered learning platform that keeps Respellion employees up to d
|
||||
# 1. Install dependencies
|
||||
npm install
|
||||
|
||||
# 2. Start PocketBase (Windows)
|
||||
# 2. Start PocketBase
|
||||
./pocketbase.exe serve
|
||||
|
||||
# 3. Start the dev server
|
||||
# 3. Bootstrap collections (first run only)
|
||||
node scripts/setup-pb-collections.mjs
|
||||
|
||||
# 4. Start the dev server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The Vite dev server proxies `/api/anthropic` and `/pb` — see `vite.config.js`.
|
||||
Set `ANTHROPIC_API_KEY` in your environment — the Vite dev server proxies `/api/anthropic` and injects the key server-side (see `vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically when the binary starts.
|
||||
|
||||
## Deployment (Docker)
|
||||
|
||||
@@ -46,35 +53,39 @@ docker compose up -d
|
||||
|
||||
The `Caddyfile` handles:
|
||||
- SPA fallback routing
|
||||
- `/pb/*` → PocketBase sidecar
|
||||
- `/api/anthropic/*` → Anthropic API (with server-side API key injection)
|
||||
- `/api/anthropic/*` → Anthropic API (server-side API key injection)
|
||||
- `/api/*` and `/_/*` → the PocketBase sidecar
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/lib/learningService.js` | Selective content generation (article / slides / infographic) |
|
||||
| `src/lib/extractionPipeline.js` | GitHub file → knowledge graph extraction |
|
||||
| `src/lib/api.js` | Anthropic API wrapper (`generateContent` + `chat`) |
|
||||
| `src/lib/llm.js` | Core Anthropic wrapper (`callLLM`): tiers, retry, schema validation, telemetry |
|
||||
| `src/lib/db.js` | All PocketBase data access |
|
||||
| `src/lib/giteaService.js` | GitHub API client (folder listing + raw file fetch) |
|
||||
| `src/store/AppContext.jsx` | Global state; computes ISO week number on load |
|
||||
| `src/components/admin/UploadZone.jsx` | GitHub sync UI (default folder: `docs/knowledge-base/`) |
|
||||
| `src/lib/extractionPipeline.js` | Uploaded file → knowledge-graph extraction |
|
||||
| `src/lib/learningService.js` | On-demand content generation (article / slides / infographic) |
|
||||
| `src/lib/microLearningService.js` | Micro-learning generation (3 types) |
|
||||
| `src/lib/curriculumService.js` | 26-week per-user curriculum engine |
|
||||
| `src/lib/retrieval.js` | TF-IDF retrieval for R42 |
|
||||
| `src/store/AppContext.jsx` | Global state; derives the user's curriculum week from their start date |
|
||||
| `AI_AGENT.md` | Detailed context guide for AI coding agents |
|
||||
|
||||
## Content Types
|
||||
|
||||
Learning content is generated **on demand per type** and merged into the cached object:
|
||||
On-demand long-form content (the `content` collection), generated per type and merged into the cached object:
|
||||
|
||||
| Type | Key in DB | Description |
|
||||
|---|---|---|
|
||||
|---|---|---|
|
||||
| Article | `content.article` | Long-form reading |
|
||||
| Slides | `content.slides` | Slide deck with speaker notes |
|
||||
| Infographic | `content.infographic` | Visual summary with stats and steps |
|
||||
|
||||
> The podcast type was removed. Do not re-add it.
|
||||
|
||||
Micro-learnings (the `micro_learnings` collection): `concept_explainer`, `scenario_quiz`, `flashcard_set`.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **`AI_AGENT.md`** — Full architectural guide for AI coding agents (patterns, gotchas, decisions).
|
||||
- **`CHANGELOG.md`** — PocketBase upstream changelog (not application changelog).
|
||||
- **`CLAUDE.md`** — project overview and constraints for AI coding agents.
|
||||
- **`AI_AGENT.md`** — full architectural guide (patterns, gotchas, decisions).
|
||||
- **`docs/`** — subsystem specs (architecture, data model, ingestion, generation, curriculum, R42, frontend, gamification).
|
||||
|
||||
0
app/frontend/.gitkeep
Normal file
0
app/frontend/.gitkeep
Normal file
80
app/frontend/.next/app-build-manifest.json
Normal file
80
app/frontend/.next/app-build-manifest.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"pages": {
|
||||
"/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/page.js"
|
||||
],
|
||||
"/layout": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/css/app/layout.css",
|
||||
"static/chunks/app/layout.js"
|
||||
],
|
||||
"/auth/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/auth/page.js"
|
||||
],
|
||||
"/admin/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/page.js"
|
||||
],
|
||||
"/admin/layout": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/layout.js"
|
||||
],
|
||||
"/admin/documents/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/documents/page.js"
|
||||
],
|
||||
"/admin/knowledge/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/knowledge/page.js"
|
||||
],
|
||||
"/admin/curriculum/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/curriculum/page.js"
|
||||
],
|
||||
"/app/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/app/page.js"
|
||||
],
|
||||
"/app/layout": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/app/layout.js"
|
||||
],
|
||||
"/app/session/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/app/session/page.js"
|
||||
],
|
||||
"/app/library/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/app/library/page.js"
|
||||
],
|
||||
"/app/profile/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/app/profile/page.js"
|
||||
],
|
||||
"/app/library/[topicId]/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/app/library/[topicId]/page.js"
|
||||
],
|
||||
"/_not-found/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/_not-found/page.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
19
app/frontend/.next/build-manifest.json
Normal file
19
app/frontend/.next/build-manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"polyfillFiles": [
|
||||
"static/chunks/polyfills.js"
|
||||
],
|
||||
"devFiles": [],
|
||||
"ampDevFiles": [],
|
||||
"lowPriorityFiles": [
|
||||
"static/development/_buildManifest.js",
|
||||
"static/development/_ssgManifest.js"
|
||||
],
|
||||
"rootMainFiles": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"pages": {
|
||||
"/_app": []
|
||||
},
|
||||
"ampFirstPages": []
|
||||
}
|
||||
BIN
app/frontend/.next/cache/webpack/client-development/0.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/0.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/1.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/1.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/10.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/10.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/11.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/11.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/12.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/12.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/13.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/13.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/2.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/2.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/3.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/3.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/4.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/4.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/5.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/5.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/6.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/6.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/7.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/7.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/8.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/8.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/9.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/9.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz.old
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz.old
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/0.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/0.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/1.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/1.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/10.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/10.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/11.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/11.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/12.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/12.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/13.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/13.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/14.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/14.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/2.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/2.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/3.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/3.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/4.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/4.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/5.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/5.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/6.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/6.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/7.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/7.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/8.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/8.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/9.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/9.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz.old
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz.old
vendored
Normal file
Binary file not shown.
1
app/frontend/.next/package.json
Normal file
1
app/frontend/.next/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{"type": "commonjs"}
|
||||
1
app/frontend/.next/react-loadable-manifest.json
Normal file
1
app/frontend/.next/react-loadable-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
8
app/frontend/.next/server/app-paths-manifest.json
Normal file
8
app/frontend/.next/server/app-paths-manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"/_not-found/page": "app/_not-found/page.js",
|
||||
"/auth/page": "app/auth/page.js",
|
||||
"/admin/documents/page": "app/admin/documents/page.js",
|
||||
"/app/session/page": "app/app/session/page.js",
|
||||
"/admin/curriculum/page": "app/admin/curriculum/page.js",
|
||||
"/app/library/page": "app/app/library/page.js"
|
||||
}
|
||||
133
app/frontend/.next/server/app/_not-found/page.js
Normal file
133
app/frontend/.next/server/app/_not-found/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
277
app/frontend/.next/server/app/admin/curriculum/page.js
Normal file
277
app/frontend/.next/server/app/admin/curriculum/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
321
app/frontend/.next/server/app/admin/documents/page.js
Normal file
321
app/frontend/.next/server/app/admin/documents/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
288
app/frontend/.next/server/app/admin/knowledge/page.js
Normal file
288
app/frontend/.next/server/app/admin/knowledge/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
226
app/frontend/.next/server/app/admin/page.js
Normal file
226
app/frontend/.next/server/app/admin/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
420
app/frontend/.next/server/app/app/library/[topicId]/page.js
Normal file
420
app/frontend/.next/server/app/app/library/[topicId]/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
299
app/frontend/.next/server/app/app/library/page.js
Normal file
299
app/frontend/.next/server/app/app/library/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
259
app/frontend/.next/server/app/app/page.js
Normal file
259
app/frontend/.next/server/app/app/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
332
app/frontend/.next/server/app/app/profile/page.js
Normal file
332
app/frontend/.next/server/app/app/profile/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
453
app/frontend/.next/server/app/app/session/page.js
Normal file
453
app/frontend/.next/server/app/app/session/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
205
app/frontend/.next/server/app/auth/page.js
Normal file
205
app/frontend/.next/server/app/auth/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
154
app/frontend/.next/server/app/page.js
Normal file
154
app/frontend/.next/server/app/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"
|
||||
21
app/frontend/.next/server/middleware-build-manifest.js
Normal file
21
app/frontend/.next/server/middleware-build-manifest.js
Normal file
@@ -0,0 +1,21 @@
|
||||
self.__BUILD_MANIFEST = {
|
||||
"polyfillFiles": [
|
||||
"static/chunks/polyfills.js"
|
||||
],
|
||||
"devFiles": [],
|
||||
"ampDevFiles": [],
|
||||
"lowPriorityFiles": [],
|
||||
"rootMainFiles": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"pages": {
|
||||
"/_app": []
|
||||
},
|
||||
"ampFirstPages": []
|
||||
};
|
||||
self.__BUILD_MANIFEST.lowPriorityFiles = [
|
||||
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
|
||||
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
|
||||
|
||||
];
|
||||
6
app/frontend/.next/server/middleware-manifest.json
Normal file
6
app/frontend/.next/server/middleware-manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 3,
|
||||
"middleware": {},
|
||||
"functions": {},
|
||||
"sortedMiddleware": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
self.__REACT_LOADABLE_MANIFEST="{}"
|
||||
1
app/frontend/.next/server/next-font-manifest.js
Normal file
1
app/frontend/.next/server/next-font-manifest.js
Normal file
@@ -0,0 +1 @@
|
||||
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
||||
1
app/frontend/.next/server/next-font-manifest.json
Normal file
1
app/frontend/.next/server/next-font-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
||||
1
app/frontend/.next/server/pages-manifest.json
Normal file
1
app/frontend/.next/server/pages-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
app/frontend/.next/server/server-reference-manifest.js
Normal file
1
app/frontend/.next/server/server-reference-manifest.js
Normal file
@@ -0,0 +1 @@
|
||||
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"
|
||||
5
app/frontend/.next/server/server-reference-manifest.json
Normal file
5
app/frontend/.next/server/server-reference-manifest.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"node": {},
|
||||
"edge": {},
|
||||
"encryptionKey": "QnzKIJRR8GrjaHk7QhiM928LOJzfdW5Af6gmYU5xeZs="
|
||||
}
|
||||
75
app/frontend/.next/server/vendor-chunks/@swc.js
Normal file
75
app/frontend/.next/server/vendor-chunks/@swc.js
Normal file
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
exports.id = "vendor-chunks/@swc";
|
||||
exports.ids = ["vendor-chunks/@swc"];
|
||||
exports.modules = {
|
||||
|
||||
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js":
|
||||
/*!**************************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js ***!
|
||||
\**************************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_base),\n/* harmony export */ _class_private_field_loose_base: () => (/* binding */ _class_private_field_loose_base)\n/* harmony export */ });\nfunction _class_private_field_loose_base(receiver, privateKey) {\n if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {\n throw new TypeError(\"attempted to use private field on non-instance\");\n }\n\n return receiver;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQU87QUFDUDtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNnRCIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2VfYmFzZS5qcz9kY2RiIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlKHJlY2VpdmVyLCBwcml2YXRlS2V5KSB7XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwocmVjZWl2ZXIsIHByaXZhdGVLZXkpKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJhdHRlbXB0ZWQgdG8gdXNlIHByaXZhdGUgZmllbGQgb24gbm9uLWluc3RhbmNlXCIpO1xuICAgIH1cblxuICAgIHJldHVybiByZWNlaXZlcjtcbn1cbmV4cG9ydCB7IF9jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2Jhc2UgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js":
|
||||
/*!*************************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js ***!
|
||||
\*************************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_key),\n/* harmony export */ _class_private_field_loose_key: () => (/* binding */ _class_private_field_loose_key)\n/* harmony export */ });\nvar id = 0;\n\nfunction _class_private_field_loose_key(name) {\n return \"__private_\" + id++ + \"_\" + name;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQTs7QUFFTztBQUNQO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9sZWFybmluZy1wbGF0Zm9ybS1mcm9udGVuZC8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2tleS5qcz9hZjBiIl0sInNvdXJjZXNDb250ZW50IjpbInZhciBpZCA9IDA7XG5cbmV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkobmFtZSkge1xuICAgIHJldHVybiBcIl9fcHJpdmF0ZV9cIiArIGlkKysgKyBcIl9cIiArIG5hbWU7XG59XG5leHBvcnQgeyBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
|
||||
/*!*******************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
|
||||
\*******************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzY4ZDkiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js":
|
||||
/*!********************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_interop_require_wildcard.js ***!
|
||||
\********************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_wildcard),\n/* harmony export */ _interop_require_wildcard: () => (/* binding */ _interop_require_wildcard)\n/* harmony export */ });\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n\n return (_getRequireWildcardCache = function(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nfunction _interop_require_wildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) return obj;\n if (obj === null || typeof obj !== \"object\" && typeof obj !== \"function\") return { default: obj };\n\n var cache = _getRequireWildcardCache(nodeInterop);\n\n if (cache && cache.has(obj)) return cache.get(obj);\n\n var newObj = { __proto__: null };\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);\n else newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n\n if (cache) cache.set(obj, newObj);\n\n return newObj;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxLQUFLO0FBQ0w7QUFDTztBQUNQO0FBQ0EsdUZBQXVGOztBQUV2Rjs7QUFFQTs7QUFFQSxtQkFBbUI7QUFDbkI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7O0FBRUE7QUFDQTtBQUMwQyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZC5qcz9hYjM4Il0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZShub2RlSW50ZXJvcCkge1xuICAgIGlmICh0eXBlb2YgV2Vha01hcCAhPT0gXCJmdW5jdGlvblwiKSByZXR1cm4gbnVsbDtcblxuICAgIHZhciBjYWNoZUJhYmVsSW50ZXJvcCA9IG5ldyBXZWFrTWFwKCk7XG4gICAgdmFyIGNhY2hlTm9kZUludGVyb3AgPSBuZXcgV2Vha01hcCgpO1xuXG4gICAgcmV0dXJuIChfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUgPSBmdW5jdGlvbihub2RlSW50ZXJvcCkge1xuICAgICAgICByZXR1cm4gbm9kZUludGVyb3AgPyBjYWNoZU5vZGVJbnRlcm9wIDogY2FjaGVCYWJlbEludGVyb3A7XG4gICAgfSkobm9kZUludGVyb3ApO1xufVxuZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQob2JqLCBub2RlSW50ZXJvcCkge1xuICAgIGlmICghbm9kZUludGVyb3AgJiYgb2JqICYmIG9iai5fX2VzTW9kdWxlKSByZXR1cm4gb2JqO1xuICAgIGlmIChvYmogPT09IG51bGwgfHwgdHlwZW9mIG9iaiAhPT0gXCJvYmplY3RcIiAmJiB0eXBlb2Ygb2JqICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiB7IGRlZmF1bHQ6IG9iaiB9O1xuXG4gICAgdmFyIGNhY2hlID0gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKTtcblxuICAgIGlmIChjYWNoZSAmJiBjYWNoZS5oYXMob2JqKSkgcmV0dXJuIGNhY2hlLmdldChvYmopO1xuXG4gICAgdmFyIG5ld09iaiA9IHsgX19wcm90b19fOiBudWxsIH07XG4gICAgdmFyIGhhc1Byb3BlcnR5RGVzY3JpcHRvciA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xuXG4gICAgZm9yICh2YXIga2V5IGluIG9iaikge1xuICAgICAgICBpZiAoa2V5ICE9PSBcImRlZmF1bHRcIiAmJiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBrZXkpKSB7XG4gICAgICAgICAgICB2YXIgZGVzYyA9IGhhc1Byb3BlcnR5RGVzY3JpcHRvciA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqLCBrZXkpIDogbnVsbDtcbiAgICAgICAgICAgIGlmIChkZXNjICYmIChkZXNjLmdldCB8fCBkZXNjLnNldCkpIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShuZXdPYmosIGtleSwgZGVzYyk7XG4gICAgICAgICAgICBlbHNlIG5ld09ialtrZXldID0gb2JqW2tleV07XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBuZXdPYmouZGVmYXVsdCA9IG9iajtcblxuICAgIGlmIChjYWNoZSkgY2FjaGUuc2V0KG9iaiwgbmV3T2JqKTtcblxuICAgIHJldHVybiBuZXdPYmo7XG59XG5leHBvcnQgeyBfaW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js":
|
||||
/*!*************************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js ***!
|
||||
\*************************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _tagged_template_literal_loose),\n/* harmony export */ _tagged_template_literal_loose: () => (/* binding */ _tagged_template_literal_loose)\n/* harmony export */ });\nfunction _tagged_template_literal_loose(strings, raw) {\n if (!raw) raw = strings.slice(0);\n\n strings.raw = raw;\n\n return strings;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2UuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQOztBQUVBOztBQUVBO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9sZWFybmluZy1wbGF0Zm9ybS1mcm9udGVuZC8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL190YWdnZWRfdGVtcGxhdGVfbGl0ZXJhbF9sb29zZS5qcz83YjYyIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2Uoc3RyaW5ncywgcmF3KSB7XG4gICAgaWYgKCFyYXcpIHJhdyA9IHN0cmluZ3Muc2xpY2UoMCk7XG5cbiAgICBzdHJpbmdzLnJhdyA9IHJhdztcblxuICAgIHJldHVybiBzdHJpbmdzO1xufVxuZXhwb3J0IHsgX3RhZ2dlZF90ZW1wbGF0ZV9saXRlcmFsX2xvb3NlIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
|
||||
/*!*******************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
|
||||
\*******************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzFhYjAiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
2398
app/frontend/.next/server/vendor-chunks/next.js
Normal file
2398
app/frontend/.next/server/vendor-chunks/next.js
Normal file
File diff suppressed because one or more lines are too long
25
app/frontend/.next/server/vendor-chunks/pocketbase.js
Normal file
25
app/frontend/.next/server/vendor-chunks/pocketbase.js
Normal file
File diff suppressed because one or more lines are too long
215
app/frontend/.next/server/webpack-runtime.js
Normal file
215
app/frontend/.next/server/webpack-runtime.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({});
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ var threw = true;
|
||||
/******/ try {
|
||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
||||
/******/ threw = false;
|
||||
/******/ } finally {
|
||||
/******/ if(threw) delete __webpack_module_cache__[moduleId];
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = __webpack_modules__;
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ (() => {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = (module) => {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ () => (module['default']) :
|
||||
/******/ () => (module);
|
||||
/******/ __webpack_require__.d(getter, { a: getter });
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/create fake namespace object */
|
||||
/******/ (() => {
|
||||
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
|
||||
/******/ var leafPrototypes;
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 16: return value when it's Promise-like
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = this(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if(typeof value === 'object' && value) {
|
||||
/******/ if((mode & 4) && value.__esModule) return value;
|
||||
/******/ if((mode & 16) && typeof value.then === 'function') return value;
|
||||
/******/ }
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ var def = {};
|
||||
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
|
||||
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
|
||||
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
|
||||
/******/ }
|
||||
/******/ def['default'] = () => (value);
|
||||
/******/ __webpack_require__.d(ns, def);
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/define property getters */
|
||||
/******/ (() => {
|
||||
/******/ // define getter functions for harmony exports
|
||||
/******/ __webpack_require__.d = (exports, definition) => {
|
||||
/******/ for(var key in definition) {
|
||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/ensure chunk */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.f = {};
|
||||
/******/ // This file contains only the entry chunk.
|
||||
/******/ // The chunk loading function for additional chunks
|
||||
/******/ __webpack_require__.e = (chunkId) => {
|
||||
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
|
||||
/******/ __webpack_require__.f[key](chunkId, promises);
|
||||
/******/ return promises;
|
||||
/******/ }, []));
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/get javascript chunk filename */
|
||||
/******/ (() => {
|
||||
/******/ // This function allow to reference async chunks and sibling chunks for the entrypoint
|
||||
/******/ __webpack_require__.u = (chunkId) => {
|
||||
/******/ // return url for filenames based on template
|
||||
/******/ return "" + chunkId + ".js";
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("89440127ad6e3f87")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/make namespace object */
|
||||
/******/ (() => {
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = (exports) => {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/node module decorator */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.nmd = (module) => {
|
||||
/******/ module.paths = [];
|
||||
/******/ if (!module.children) module.children = [];
|
||||
/******/ return module;
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/startup entrypoint */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.X = (result, chunkIds, fn) => {
|
||||
/******/ // arguments: chunkIds, moduleId are deprecated
|
||||
/******/ var moduleId = chunkIds;
|
||||
/******/ if(!fn) chunkIds = result, fn = () => (__webpack_require__(__webpack_require__.s = moduleId));
|
||||
/******/ chunkIds.map(__webpack_require__.e, __webpack_require__)
|
||||
/******/ var r = fn();
|
||||
/******/ return r === undefined ? result : r;
|
||||
/******/ }
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/require chunk loading */
|
||||
/******/ (() => {
|
||||
/******/ // no baseURI
|
||||
/******/
|
||||
/******/ // object to store loaded chunks
|
||||
/******/ // "1" means "loaded", otherwise not loaded yet
|
||||
/******/ var installedChunks = {
|
||||
/******/ "webpack-runtime": 1
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // no on chunks loaded
|
||||
/******/
|
||||
/******/ var installChunk = (chunk) => {
|
||||
/******/ var moreModules = chunk.modules, chunkIds = chunk.ids, runtime = chunk.runtime;
|
||||
/******/ for(var moduleId in moreModules) {
|
||||
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
||||
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ if(runtime) runtime(__webpack_require__);
|
||||
/******/ for(var i = 0; i < chunkIds.length; i++)
|
||||
/******/ installedChunks[chunkIds[i]] = 1;
|
||||
/******/
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // require() chunk loading for javascript
|
||||
/******/ __webpack_require__.f.require = (chunkId, promises) => {
|
||||
/******/ // "1" is the signal for "already loaded"
|
||||
/******/ if(!installedChunks[chunkId]) {
|
||||
/******/ if("webpack-runtime" != chunkId) {
|
||||
/******/ installChunk(require("./" + __webpack_require__.u(chunkId)));
|
||||
/******/ } else installedChunks[chunkId] = 1;
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ module.exports = __webpack_require__;
|
||||
/******/ __webpack_require__.C = installChunk;
|
||||
/******/
|
||||
/******/ // no HMR
|
||||
/******/
|
||||
/******/ // no HMR manifest
|
||||
/******/ })();
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
||||
138
app/frontend/.next/static/chunks/app-pages-internals.js
Normal file
138
app/frontend/.next/static/chunks/app-pages-internals.js
Normal file
File diff suppressed because one or more lines are too long
39
app/frontend/.next/static/chunks/app/_not-found/page.js
Normal file
39
app/frontend/.next/static/chunks/app/_not-found/page.js
Normal file
File diff suppressed because one or more lines are too long
127
app/frontend/.next/static/chunks/app/admin/curriculum/page.js
Normal file
127
app/frontend/.next/static/chunks/app/admin/curriculum/page.js
Normal file
File diff suppressed because one or more lines are too long
171
app/frontend/.next/static/chunks/app/admin/documents/page.js
Normal file
171
app/frontend/.next/static/chunks/app/admin/documents/page.js
Normal file
File diff suppressed because one or more lines are too long
369
app/frontend/.next/static/chunks/app/admin/knowledge/page.js
Normal file
369
app/frontend/.next/static/chunks/app/admin/knowledge/page.js
Normal file
File diff suppressed because one or more lines are too long
347
app/frontend/.next/static/chunks/app/admin/layout.js
Normal file
347
app/frontend/.next/static/chunks/app/admin/layout.js
Normal file
File diff suppressed because one or more lines are too long
28
app/frontend/.next/static/chunks/app/admin/page.js
Normal file
28
app/frontend/.next/static/chunks/app/admin/page.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([["app/admin/page"],{
|
||||
|
||||
/***/ "(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false!":
|
||||
/*!*******************************************************************************************************!*\
|
||||
!*** ./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false! ***!
|
||||
\*******************************************************************************************************/
|
||||
/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
|
||||
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
|
||||
/******/ __webpack_require__.O(0, ["main-app"], function() { return __webpack_exec__("(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false!"); });
|
||||
/******/ var __webpack_exports__ = __webpack_require__.O();
|
||||
/******/ _N_E = __webpack_exports__;
|
||||
/******/ }
|
||||
]);
|
||||
380
app/frontend/.next/static/chunks/app/app/layout.js
Normal file
380
app/frontend/.next/static/chunks/app/app/layout.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
336
app/frontend/.next/static/chunks/app/app/library/page.js
Normal file
336
app/frontend/.next/static/chunks/app/app/library/page.js
Normal file
File diff suppressed because one or more lines are too long
28
app/frontend/.next/static/chunks/app/app/page.js
Normal file
28
app/frontend/.next/static/chunks/app/app/page.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([["app/app/page"],{
|
||||
|
||||
/***/ "(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false!":
|
||||
/*!*******************************************************************************************************!*\
|
||||
!*** ./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false! ***!
|
||||
\*******************************************************************************************************/
|
||||
/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
|
||||
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
|
||||
/******/ __webpack_require__.O(0, ["main-app"], function() { return __webpack_exec__("(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false!"); });
|
||||
/******/ var __webpack_exports__ = __webpack_require__.O();
|
||||
/******/ _N_E = __webpack_exports__;
|
||||
/******/ }
|
||||
]);
|
||||
160
app/frontend/.next/static/chunks/app/app/profile/page.js
Normal file
160
app/frontend/.next/static/chunks/app/app/profile/page.js
Normal file
File diff suppressed because one or more lines are too long
281
app/frontend/.next/static/chunks/app/app/session/page.js
Normal file
281
app/frontend/.next/static/chunks/app/app/session/page.js
Normal file
File diff suppressed because one or more lines are too long
116
app/frontend/.next/static/chunks/app/auth/page.js
Normal file
116
app/frontend/.next/static/chunks/app/auth/page.js
Normal file
File diff suppressed because one or more lines are too long
50
app/frontend/.next/static/chunks/app/layout.js
Normal file
50
app/frontend/.next/static/chunks/app/layout.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([["app/layout"],{
|
||||
|
||||
/***/ "(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Cstylesheet.css%22%2C%22ids%22%3A%5B%5D%7D&modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Capp%5C%5Cfrontend%5C%5Csrc%5C%5Capp%5C%5Cglobals.css%22%2C%22ids%22%3A%5B%5D%7D&server=false!":
|
||||
/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
|
||||
!*** ./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Cstylesheet.css%22%2C%22ids%22%3A%5B%5D%7D&modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Capp%5C%5Cfrontend%5C%5Csrc%5C%5Capp%5C%5Cglobals.css%22%2C%22ids%22%3A%5B%5D%7D&server=false! ***!
|
||||
\***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
|
||||
/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
|
||||
|
||||
eval(__webpack_require__.ts("Promise.resolve(/*! import() eager */).then(__webpack_require__.bind(__webpack_require__, /*! ../../stylesheet.css */ \"(app-pages-browser)/../../stylesheet.css\"));\n;\nPromise.resolve(/*! import() eager */).then(__webpack_require__.bind(__webpack_require__, /*! ./src/app/globals.css */ \"(app-pages-browser)/./src/app/globals.css\"));\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL25vZGVfbW9kdWxlcy9uZXh0L2Rpc3QvYnVpbGQvd2VicGFjay9sb2FkZXJzL25leHQtZmxpZ2h0LWNsaWVudC1lbnRyeS1sb2FkZXIuanM/bW9kdWxlcz0lN0IlMjJyZXF1ZXN0JTIyJTNBJTIyQyUzQSU1QyU1Q1VzZXJzJTVDJTVDUmF5bW9uZFZlcmhvZWYlNUMlNUNHaXRlYSU1QyU1Q2xlYXJuaW5nLXBsYXRmb3JtJTVDJTVDc3R5bGVzaGVldC5jc3MlMjIlMkMlMjJpZHMlMjIlM0ElNUIlNUQlN0QmbW9kdWxlcz0lN0IlMjJyZXF1ZXN0JTIyJTNBJTIyQyUzQSU1QyU1Q1VzZXJzJTVDJTVDUmF5bW9uZFZlcmhvZWYlNUMlNUNHaXRlYSU1QyU1Q2xlYXJuaW5nLXBsYXRmb3JtJTVDJTVDYXBwJTVDJTVDZnJvbnRlbmQlNUMlNUNzcmMlNUMlNUNhcHAlNUMlNUNnbG9iYWxzLmNzcyUyMiUyQyUyMmlkcyUyMiUzQSU1QiU1RCU3RCZzZXJ2ZXI9ZmFsc2UhIiwibWFwcGluZ3MiOiJBQUFBLGtLQUF3RztBQUN4RztBQUNBLG9LQUE4SCIsInNvdXJjZXMiOlsid2VicGFjazovL19OX0UvPzM5N2MiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0KC8qIHdlYnBhY2tNb2RlOiBcImVhZ2VyXCIgKi8gXCJDOlxcXFxVc2Vyc1xcXFxSYXltb25kVmVyaG9lZlxcXFxHaXRlYVxcXFxsZWFybmluZy1wbGF0Zm9ybVxcXFxzdHlsZXNoZWV0LmNzc1wiKTtcbjtcbmltcG9ydCgvKiB3ZWJwYWNrTW9kZTogXCJlYWdlclwiICovIFwiQzpcXFxcVXNlcnNcXFxcUmF5bW9uZFZlcmhvZWZcXFxcR2l0ZWFcXFxcbGVhcm5pbmctcGxhdGZvcm1cXFxcYXBwXFxcXGZyb250ZW5kXFxcXHNyY1xcXFxhcHBcXFxcZ2xvYmFscy5jc3NcIik7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Cstylesheet.css%22%2C%22ids%22%3A%5B%5D%7D&modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Capp%5C%5Cfrontend%5C%5Csrc%5C%5Capp%5C%5Cglobals.css%22%2C%22ids%22%3A%5B%5D%7D&server=false!\n"));
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(app-pages-browser)/./src/app/globals.css":
|
||||
/*!*****************************!*\
|
||||
!*** ./src/app/globals.css ***!
|
||||
\*****************************/
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"44e586abe902\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6IjtBQUFBLCtEQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vX05fRS8uL3NyYy9hcHAvZ2xvYmFscy5jc3M/ZjYwMSJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjQ0ZTU4NmFiZTkwMlwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(app-pages-browser)/../../stylesheet.css":
|
||||
/*!****************************!*\
|
||||
!*** ../../stylesheet.css ***!
|
||||
\****************************/
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"2fe938d18e15\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uLi8uLi9zdHlsZXNoZWV0LmNzcyIsIm1hcHBpbmdzIjoiO0FBQUEsK0RBQWUsY0FBYztBQUM3QixJQUFJLElBQVUsSUFBSSxpQkFBaUIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9fTl9FLy4uLy4uL3N0eWxlc2hlZXQuY3NzP2E3MmMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCIyZmU5MzhkMThlMTVcIlxuaWYgKG1vZHVsZS5ob3QpIHsgbW9kdWxlLmhvdC5hY2NlcHQoKSB9XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/../../stylesheet.css\n"));
|
||||
|
||||
/***/ })
|
||||
|
||||
},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
|
||||
/******/ __webpack_require__.O(0, ["main-app"], function() { return __webpack_exec__("(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Cstylesheet.css%22%2C%22ids%22%3A%5B%5D%7D&modules=%7B%22request%22%3A%22C%3A%5C%5CUsers%5C%5CRaymondVerhoef%5C%5CGitea%5C%5Clearning-platform%5C%5Capp%5C%5Cfrontend%5C%5Csrc%5C%5Capp%5C%5Cglobals.css%22%2C%22ids%22%3A%5B%5D%7D&server=false!"); });
|
||||
/******/ var __webpack_exports__ = __webpack_require__.O();
|
||||
/******/ _N_E = __webpack_exports__;
|
||||
/******/ }
|
||||
]);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user