feat: implement quiz results tracking and caching for user tests
All checks were successful
On Push to Main / test (push) Successful in 47s
On Push to Main / publish (push) Successful in 1m27s
On Push to Main / deploy-dev (push) Successful in 2m7s

This commit is contained in:
RaymondVerhoef
2026-05-27 10:05:56 +02:00
parent 07af2783dc
commit 3aa32c383e
3 changed files with 199 additions and 10 deletions

View File

@@ -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');

View File

@@ -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 ──────────────────────────────────────────