diff --git a/pb_migrations/1780900001_created_test_results.js b/pb_migrations/1780900001_created_test_results.js new file mode 100644 index 0000000..bf85d9b --- /dev/null +++ b/pb_migrations/1780900001_created_test_results.js @@ -0,0 +1,133 @@ +/// +// Recreates the test_results collection (previously called quiz_results, which +// was dropped in 1780800001_deleted_legacy_collections.js). Stores one record +// per user per week containing their quiz score, answer breakdown, and points +// earned. Used by testService.saveTestResult / getTestResult. +migrate((app) => { + const collection = new Collection({ + "createRule": "", + "deleteRule": "", + "listRule": "", + "updateRule": "", + "viewRule": "", + "name": "test_results", + "type": "base", + "fields": [ + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_user_id", + "max": 0, + "min": 0, + "name": "user_id", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number_week_number", + "max": null, + "min": null, + "name": "week_number", + "onlyInt": true, + "presentable": false, + "required": true, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number_score", + "max": null, + "min": null, + "name": "score", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number_total", + "max": null, + "min": null, + "name": "total", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number_percentage", + "max": null, + "min": null, + "name": "percentage", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number_time_used", + "max": null, + "min": null, + "name": "time_used", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_completed_at", + "max": 0, + "min": 0, + "name": "completed_at", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json_breakdown", + "maxSize": 0, + "name": "breakdown", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "number_points_earned", + "max": null, + "min": null, + "name": "points_earned", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + } + ] + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("test_results"); + return app.delete(collection); +}); diff --git a/src/lib/db.js b/src/lib/db.js index 9e7f745..0fe4451 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -1,4 +1,5 @@ import { pb } from './pb'; +import { storage } from './storage'; // Upsert helper: update existing record if found, otherwise create. async function pbUpsert(collection, filter, updateData, createData) { @@ -159,19 +160,65 @@ export async function deleteTeamMember(id) { return pb.collection('team_members').delete(id); } -// ── Quiz Results (DEPRECATED — collection dropped) ────────────────────────── +// ── Quiz Results ────────────────────────────────────────────────────────────── +// Persists one record per user per week in the `test_results` PocketBase +// collection (recreated in migration 1780900001_created_test_results.js). -/** @deprecated quiz_results collection has been dropped. */ -export async function getQuizResult() { return null; } -/** @deprecated quiz_results collection has been dropped. */ -export async function saveQuizResult() { return null; } +export async function saveQuizResult(userId, weekNumber, result) { + const data = { + user_id: userId, + week_number: weekNumber, + score: result.score, + total: result.total, + percentage: result.percentage, + time_used: result.timeUsed, + completed_at: result.completedAt, + breakdown: result.breakdown, + points_earned: result.score * 2, + }; + return pbUpsert( + 'test_results', + `user_id="${userId}" && week_number=${weekNumber}`, + data, + data, + ); +} -// ── Quiz Cache (DEPRECATED — collection dropped) ──────────────────────────── +export async function getQuizResult(userId, weekNumber) { + try { + const r = await pb.collection('test_results').getFirstListItem( + `user_id="${userId}" && week_number=${weekNumber}`, + ); + // Map snake_case PB fields back to the camelCase shape Testen.jsx expects. + return { + score: r.score, + total: r.total, + percentage: r.percentage, + timeUsed: r.time_used, + completedAt: r.completed_at, + breakdown: r.breakdown, + pointsEarned: r.points_earned, + }; + } catch { + return null; + } +} -/** @deprecated quiz_cache collection has been dropped. */ -export async function getCachedQuiz() { return null; } -/** @deprecated quiz_cache collection has been dropped. */ -export async function setCachedQuiz() { return null; } +// ── Quiz Cache (localStorage — ephemeral pre-generation) ───────────────────── +// Stores a pre-generated quiz object in localStorage so the Testen page can +// skip the LLM call when the user arrives after completing their learning +// session. The key includes weekNumber so stale entries are silently ignored +// when the week rolls over. + +export function getCachedQuiz(userId, weekNumber) { + const cached = storage.get(`quiz_cache:${userId}:${weekNumber}`); + return Promise.resolve(cached || null); +} + +export function setCachedQuiz(userId, weekNumber, quiz) { + storage.set(`quiz_cache:${userId}:${weekNumber}`, quiz); + return Promise.resolve(quiz); +} // ── Learn Progress (DEPRECATED — collection dropped) ──────────────────────── @@ -347,6 +394,7 @@ export async function resetForSmokeTest({ 'topics', 'micro_learnings', 'micro_learning_completions', + 'test_results', ]; if (includeProgress) collections.push('leaderboard'); diff --git a/src/pages/Leren.jsx b/src/pages/Leren.jsx index d89ceb8..e5ad63f 100644 --- a/src/pages/Leren.jsx +++ b/src/pages/Leren.jsx @@ -7,6 +7,7 @@ import Button from '../components/ui/Button'; import Tag from '../components/ui/Tag'; import { useApp } from '../store/AppContext'; import { getAssignedTopic } from '../lib/learningService'; +import { generateWeeklyQuiz } from '../lib/testService'; import { getYearProgress, getCurriculumCycle, getCurriculumWeek, getActiveVersion, getCurrentWeekContent } from '../lib/curriculumService'; import * as db from '../lib/db'; import MicroLearningSelector from '../components/micro_learning/MicroLearningSelector'; @@ -86,6 +87,13 @@ const Leren = () => { const handleTopicCompleted = () => { setSessionDone(true); + // Pre-generate this week's test questions in the background so they are + // ready in localStorage by the time the user navigates to /test. The call + // is fire-and-forget — a failure here is harmless; the test page will just + // generate on demand as before. + if (state.currentUser) { + generateWeeklyQuiz(state.currentUser.id, state.weekNumber).catch(() => {}); + } }; // ── Detail View ──────────────────────────────────────────