- {TYPES.map(({ key, label, description, icon: Icon }) => {
- const exists = existingTypes[key];
- return (
-
handleSelection(key)}
- >
- {exists && (
-
-
- Generated
-
- )}
-
-
-
-
-
{label}
+ {TYPES.map(({ key, label, description, icon: Icon }) => (
+
handleSelection(key)}
+ >
+
+
+
-
{description}
+
{label}
- );
- })}
+
{description}
+
+ ))}
);
diff --git a/src/hooks/useMicroLearnings.js b/src/hooks/useMicroLearnings.js
index c0f4ff7..c55dfd6 100644
--- a/src/hooks/useMicroLearnings.js
+++ b/src/hooks/useMicroLearnings.js
@@ -1,4 +1,4 @@
-import { getOrGenerateMicroLearning, regenerateMicroLearning, getExistingTypes } from '../lib/microLearningService';
+import { getOrGenerateMicroLearning, regenerateMicroLearning } from '../lib/microLearningService';
export function useMicroLearnings() {
/**
@@ -16,13 +16,5 @@ export function useMicroLearnings() {
return regenerateMicroLearning(topicId, type);
};
- /**
- * Check which micro learning types already exist for a given topic.
- * Returns { concept_explainer: true, scenario_quiz: false, ... }
- */
- const checkExistingTypes = async (topicId) => {
- return getExistingTypes(topicId);
- };
-
- return { getOrGenerate, regenerate, checkExistingTypes };
+ return { getOrGenerate, regenerate };
}
diff --git a/src/lib/__tests__/testService.test.js b/src/lib/__tests__/testService.test.js
index fa791d4..d79b6b0 100644
--- a/src/lib/__tests__/testService.test.js
+++ b/src/lib/__tests__/testService.test.js
@@ -24,7 +24,8 @@ vi.mock('../llm', () => ({
cachedSystem: (text) => [{ type: 'text', text }],
}));
vi.mock('../curriculumService', () => ({
- getCurrentWeekContent: vi.fn(async () => null),
+ getCurriculumTopic: vi.fn(async () => ({ topic: null })),
+ getQuarterForWeek: vi.fn(() => 1),
}));
import { forceGenerateTopicQuestions } from '../testService';
@@ -53,8 +54,8 @@ describe('forceGenerateTopicQuestions', () => {
beforeEach(() => {
bankStore.clear();
callLLMMock.mockReset();
- debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => { });
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
});
afterEach(() => { debugSpy.mockRestore(); warnSpy.mockRestore(); });
@@ -66,6 +67,17 @@ describe('forceGenerateTopicQuestions', () => {
expect(bankStore.get('onboarding')).toHaveLength(5);
});
+ it('re-rolls when one correctIndex dominates the batch, then accepts on the third try', async () => {
+ const allZero = [0, 1, 2, 3, 4].map((i) => makeQuestion(i, { correctIndex: 0 }));
+ llmEmits(allZero);
+ llmEmits(allZero);
+ llmEmits(allZero);
+ const out = await forceGenerateTopicQuestions(topic, 5);
+ expect(callLLMMock).toHaveBeenCalledTimes(3);
+ expect(out).toHaveLength(5);
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('correctIndex dominated'));
+ });
+
it('rejects a batch containing a banned "all of the above" option', async () => {
const bad = [0, 1, 2, 3, 4].map((i) =>
makeQuestion(i, { options: ['A) x', 'B) y', 'C) z', 'D) All of the above'] }),
@@ -73,7 +85,7 @@ describe('forceGenerateTopicQuestions', () => {
llmEmits(bad);
llmEmits(bad);
llmEmits(bad);
- await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/banned filler|failed/i);
+ await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/banned filler|rejected/i);
});
it('rejects a batch where an explanation is too short', async () => {
@@ -81,7 +93,7 @@ describe('forceGenerateTopicQuestions', () => {
llmEmits(bad);
llmEmits(bad);
llmEmits(bad);
- await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/too short|failed/i);
+ await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/too short|rejected/i);
});
it('drops duplicates whose normalized text matches an existing bank entry', async () => {
@@ -100,19 +112,4 @@ describe('forceGenerateTopicQuestions', () => {
expect(out.find((q) => q.question.toLowerCase().includes('buddy'))).toBeUndefined();
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('dropped duplicate'), expect.any(String));
});
-
- it('retries on LLM failure and succeeds on a later attempt', async () => {
- callLLMMock.mockRejectedValueOnce(new Error('API timeout'));
- llmEmits([0, 1, 2, 3, 4].map((i) => makeQuestion(i)));
- const out = await forceGenerateTopicQuestions(topic, 5);
- expect(callLLMMock).toHaveBeenCalledTimes(2);
- expect(out).toHaveLength(5);
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('LLM call failed'), expect.any(String));
- });
-
- it('throws after 3 consecutive LLM failures', async () => {
- callLLMMock.mockRejectedValue(new Error('schema validation'));
- await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/failed.*schema validation/i);
- expect(callLLMMock).toHaveBeenCalledTimes(3);
- });
});
diff --git a/src/lib/db.js b/src/lib/db.js
index e2f1d23..9e7f745 100644
--- a/src/lib/db.js
+++ b/src/lib/db.js
@@ -41,10 +41,10 @@ export async function upsertTopic(topic) {
await pb.collection('topics').getOne(topic.id);
return await pb.collection('topics').update(topic.id, topic);
} catch {
- return await pb.collection('topics').create({
- id: topic.id,
+ return await pb.collection('topics').create({
+ id: topic.id,
learning_relevance: 'standard',
- ...topic
+ ...topic
});
}
}
diff --git a/src/lib/llmSchemas.js b/src/lib/llmSchemas.js
index 76ff716..63ae60f 100644
--- a/src/lib/llmSchemas.js
+++ b/src/lib/llmSchemas.js
@@ -115,7 +115,7 @@ export const learningAllSchema = z.object({
const quizDifficultyEnum = z.enum(['easy', 'medium', 'hard']).catch('medium');
const quizQuestionSchema = z.object({
- id: z.string().min(1).catch(() => `gen-${Math.random().toString(36).slice(2, 8)}`),
+ id: z.string().min(1).catch(`gen-${Math.random().toString(36).slice(2, 8)}`),
question: z.string().min(1),
topicLabel: z.string().min(1).catch('General'),
options: z.array(z.string().min(1)).length(4),
diff --git a/src/lib/microLearningService.js b/src/lib/microLearningService.js
index 1633e2f..d76e522 100644
--- a/src/lib/microLearningService.js
+++ b/src/lib/microLearningService.js
@@ -15,6 +15,7 @@ import {
EMIT_CONCEPT_EXPLAINER_TOOL,
EMIT_SCENARIO_QUIZ_TOOL,
EMIT_FLASHCARD_SET_TOOL,
+ EMIT_REFLECTION_PROMPT_TOOL,
} from './llmTools';
import * as db from './db';
@@ -52,6 +53,15 @@ Mix three question types:
- Relationships: "How does X relate to Y?"
Keep answers concise — one or two sentences maximum.`,
},
+ reflection_prompt: {
+ tool: EMIT_REFLECTION_PROMPT_TOOL,
+ tier: 'fast',
+ maxTokens: 1024,
+ instructions: `Generate a reflection prompt.
+The question must be open-ended and cannot be answered with a fact.
+It must require the employee to think about their own professional context — their team, their role, their past experience.
+The model answer should show depth and specificity (3–5 sentences). It is not a rubric — it is an example of thoughtful reflection.`,
+ },
};
const SYSTEM_PROMPT = `You are an expert learning content writer for Respellion, an internal IT company.
@@ -135,21 +145,6 @@ export async function deleteAllForTopic(topicId) {
}
}
-// ── Public helpers ────────────────────────────────────────────────────────────
-
-/**
- * Check which micro learning types already exist for a given topic.
- * Returns an object like { concept_explainer: true, scenario_quiz: false, ... }
- */
-export async function getExistingTypes(topicId) {
- const types = Object.keys(MICRO_LEARNING_TYPES);
- const result = {};
- for (const type of types) {
- result[type] = !!(await findExisting(topicId, type));
- }
- return result;
-}
-
// ── Internal helpers ──────────────────────────────────────────────────────────
async function findExisting(topicId, type) {
diff --git a/src/lib/testService.js b/src/lib/testService.js
index 096a4ad..38fbadf 100644
--- a/src/lib/testService.js
+++ b/src/lib/testService.js
@@ -4,24 +4,25 @@ import { EMIT_QUIZ_QUESTIONS_TOOL } from './llmTools';
import { getCurrentWeekContent } from './curriculumService';
import { shuffle, sample } from './random';
-// ── System prompt ────────────────────────────────────────────
-
const QUIZ_SYSTEM = `You are a quiz generator for Respellion, an internal IT company learning platform.
You generate multiple-choice questions to test employee knowledge on specific topics.
Always write in clear, professional English.
Emit questions through the emit_quiz_questions tool. Each question has exactly four options; correctIndex is 0-based. Tag every question with difficulty ('easy', 'medium' or 'hard'). For a typical 5-question batch, mix difficulty roughly 2 easy / 2 medium / 1 hard; scale the ratio proportionally for larger batches.
-Distribute correctIndex roughly evenly across 0, 1, 2, and 3.
+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.
Never use filler options such as "all of the above", "none of the above", or "both A and B". Every explanation must be a substantive sentence (≥ 20 characters) describing why the correct answer is correct.`;
-// ── Quality helpers ──────────────────────────────────────────
-
const BANNED_OPTION_PATTERNS = [
/all of the above/i,
/none of the above/i,
- /both [a-d] and [a-d]/i,
+ /both a and b/i,
+ /both b and c/i,
+ /both c and d/i,
+ /both a and c/i,
+ /both b and d/i,
+ /both a and d/i,
];
function normalizeQuestionText(text) {
@@ -32,15 +33,16 @@ function normalizeQuestionText(text) {
.trim();
}
-/**
- * Validate a batch of questions for quality. Returns an error string
- * if the batch should be rejected, or null if acceptable.
- */
+function dominantCorrectIndex(questions) {
+ if (!questions.length) return null;
+ const counts = [0, 0, 0, 0];
+ for (const q of questions) counts[q.correctIndex] = (counts[q.correctIndex] || 0) + 1;
+ const max = Math.max(...counts);
+ return max / questions.length > 0.8 ? { index: counts.indexOf(max), ratio: max / questions.length } : null;
+}
+
function validateBatchQuality(questions) {
for (const q of questions) {
- if (!q.question || !Array.isArray(q.options) || q.options.length !== 4) {
- return `Question is malformed (missing text or wrong option count).`;
- }
const distinct = new Set(q.options.map((o) => o.trim().toLowerCase()));
if (distinct.size < 4) {
return `Question "${q.question}" has duplicate options.`;
@@ -57,13 +59,6 @@ function validateBatchQuality(questions) {
return null;
}
-// ── LLM call ─────────────────────────────────────────────────
-
-/**
- * Call the LLM to generate quiz questions for a topic.
- * The Zod schema in llmSchemas.js handles coercion and defaults,
- * so we just extract the validated `questions` array here.
- */
async function callQuizModel(topic, count) {
const prompt = `Generate exactly ${count} multiple-choice quiz questions for this knowledge topic and emit them via the emit_quiz_questions tool:
@@ -71,7 +66,7 @@ Topic: ${topic.label}
Type: ${topic.type}
Description: ${topic.description}
-Each option must start with "A) ", "B) ", "C) ", "D) ". Make questions specific and practical, not trivial.`;
+Options must be prefixed "A) ", "B) ", "C) ", "D) ". Make questions specific and practical, not trivial. Ensure correct answers are evenly distributed.`;
const result = await callLLM({
task: 'quiz.generate',
@@ -83,38 +78,66 @@ Each option must start with "A) ", "B) ", "C) ", "D) ". Make questions specific
maxTokens: 4096,
});
- // After Zod validation in callLLM, emitted.questions is a valid array
- const questions = result.toolUses[0]?.input?.questions;
- if (!questions?.length) {
- throw new Error(`LLM did not produce questions for "${topic.label}".`);
+ const emitted = result.toolUses[0]?.input;
+ if (!emitted) {
+ throw new Error(`LLM did not emit tool output for ${topic.label}`);
}
- return questions;
-}
-// ── Topic selection ──────────────────────────────────────────
+ // Handle different shapes the LLM might return:
+ // 1. { questions: [...] } — expected
+ // 2. [...] — model returned array directly
+ // 3. { questions: { ... } } — model wrapped single question in object
+ let questions;
+ if (Array.isArray(emitted.questions)) {
+ questions = emitted.questions;
+ } else if (Array.isArray(emitted)) {
+ questions = emitted;
+ } else if (emitted.questions && typeof emitted.questions === 'object') {
+ // Single question wrapped as object instead of array
+ questions = [emitted.questions];
+ } else {
+ throw new Error(`Unexpected quiz output shape for ${topic.label}`);
+ }
+
+ if (!questions.length) {
+ throw new Error(`No questions generated for ${topic.label}`);
+ }
+
+ // Ensure each question has required fields (fill in defaults for missing ones)
+ return questions.map((q, i) => ({
+ id: q.id || `gen-${i}-${Math.random().toString(36).slice(2, 8)}`,
+ question: q.question || '',
+ topicLabel: q.topicLabel || topic.label,
+ options: Array.isArray(q.options) ? q.options.slice(0, 4) : [],
+ correctIndex: typeof q.correctIndex === 'number' ? q.correctIndex : 0,
+ explanation: q.explanation || 'No explanation provided.',
+ difficulty: q.difficulty || 'medium',
+ })).filter(q => q.question && q.options.length === 4);
+}
async function selectTestTopics(userId, isoWeekNumber) {
const allTopics = await db.getTopics();
const topics = allTopics.filter(t => t.type !== 'fact' && t.learning_relevance !== 'exclude');
- if (!topics.length) return { primaryTopic: null, reviewTopics: [] };
+ if (!topics || topics.length === 0) return { primaryTopic: null, reviewTopics: [], isReviewWeek: false };
- // Try curriculum-based selection first
try {
const weekContent = await getCurrentWeekContent(isoWeekNumber);
- if (weekContent?.topics?.length > 0) {
+
+ if (weekContent && weekContent.topics && weekContent.topics.length > 0) {
const primaryTopic = weekContent.topics[0];
- if (primaryTopic?.id) {
+ if (!primaryTopic?.id) {
+ console.warn('[Test] First curriculum topic has no id — falling back to hash');
+ } else {
const others = topics.filter(t => t.id !== primaryTopic.id);
const reviewTopics = sample(others, Math.min(5, others.length));
- return { primaryTopic, reviewTopics };
+ return { primaryTopic, reviewTopics, isReviewWeek: false };
}
- console.warn('[Test] First curriculum topic has no id — falling back to hash');
}
} catch (e) {
console.warn('[Test] Curriculum lookup failed, falling back to hash:', e.message);
}
- // Fallback: deterministic hash-based assignment
+ // Fallback hash-based assignment
const str = `${userId}:${isoWeekNumber}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
@@ -123,18 +146,93 @@ async function selectTestTopics(userId, isoWeekNumber) {
}
const primaryIndex = Math.abs(hash) % topics.length;
const primaryTopic = topics[primaryIndex];
+
const others = topics.filter((_, i) => i !== primaryIndex);
const reviewTopics = sample(others, Math.min(5, others.length));
- return { primaryTopic, reviewTopics };
+ return { primaryTopic, reviewTopics, isReviewWeek: false };
}
-// ── Question bank management ────────────────────────────────
-
export async function getCachedQuiz(userId, weekNumber) {
return db.getCachedQuiz(userId, weekNumber);
}
+export async function forceGenerateTopicQuestions(topic, count = 5) {
+ const existingBank = await db.getQuizBank(topic.id);
+ const existingKeys = new Set(existingBank.map((q) => normalizeQuestionText(q.question)));
+
+ let lastQualityError = null;
+ let candidates = null;
+
+ for (let attempt = 0; attempt < 3; attempt++) {
+ let questions;
+ try {
+ questions = await callQuizModel(topic, count);
+ } catch (err) {
+ console.warn(`[quiz] LLM failed (attempt ${attempt + 1}):`, err.message);
+ lastQualityError = err.message;
+ continue;
+ }
+
+ const qualityError = validateBatchQuality(questions);
+ if (qualityError) {
+ lastQualityError = qualityError;
+ console.warn(`[quiz] batch rejected (attempt ${attempt + 1}): ${qualityError}`);
+ continue;
+ }
+
+ const dominant = dominantCorrectIndex(questions);
+ if (dominant && attempt < 2) {
+ console.warn(`[quiz] correctIndex dominated by ${dominant.index} (${Math.round(dominant.ratio * 100)}%) — re-rolling`);
+ continue;
+ }
+
+ candidates = questions;
+ break;
+ }
+
+ if (!candidates) {
+ throw new Error(`Quality gate rejected the generated batch for ${topic.label}: ${lastQualityError || 'unbalanced answer distribution'}. Click "Generate" to try again.`);
+ }
+
+ const accepted = [];
+ for (const q of candidates) {
+ const key = normalizeQuestionText(q.question);
+ if (existingKeys.has(key)) {
+ console.debug('[quiz] dropped duplicate:', q.question);
+ continue;
+ }
+ existingKeys.add(key);
+ accepted.push({
+ ...q,
+ id: `${topic.id}-${Math.random().toString(36).slice(2, 11)}`,
+ });
+ }
+
+ if (!accepted.length) {
+ throw new Error(`All generated questions for ${topic.label} were duplicates of existing ones.`);
+ }
+
+ const merged = [...existingBank, ...accepted];
+ await db.setQuizBank(topic.id, merged);
+ return accepted;
+}
+
+async function getOrGenerateTopicQuestions(topic, count) {
+ let bank = await db.getQuizBank(topic.id);
+
+ if (bank.length < count) {
+ try {
+ await forceGenerateTopicQuestions(topic, 5);
+ bank = await db.getQuizBank(topic.id);
+ } catch (err) {
+ console.warn(`[quiz] Failed to generate questions for ${topic.label}:`, err.message);
+ }
+ }
+
+ return sample(bank, Math.min(count, bank.length));
+}
+
export async function getTopicQuestionBank(topicId) {
return db.getQuizBank(topicId);
}
@@ -143,84 +241,7 @@ export async function deleteQuestion(topicId, questionId) {
return db.deleteQuestionFromBank(topicId, questionId);
}
-/**
- * Generate new questions for a topic and merge them into the bank.
- * Retries up to 3 times on LLM or quality failures.
- */
-export async function forceGenerateTopicQuestions(topic, count = 5) {
- const existingBank = await db.getQuizBank(topic.id);
- const existingKeys = new Set(existingBank.map((q) => normalizeQuestionText(q.question)));
-
- let lastError = null;
-
- for (let attempt = 0; attempt < 3; attempt++) {
- let questions;
- try {
- questions = await callQuizModel(topic, count);
- } catch (err) {
- console.warn(`[quiz] LLM call failed (attempt ${attempt + 1}):`, err.message);
- lastError = err.message;
- continue;
- }
-
- const qualityError = validateBatchQuality(questions);
- if (qualityError) {
- console.warn(`[quiz] Batch rejected (attempt ${attempt + 1}): ${qualityError}`);
- lastError = qualityError;
- continue;
- }
-
- // Batch passed quality — deduplicate against existing bank
- const accepted = [];
- for (const q of questions) {
- const key = normalizeQuestionText(q.question);
- if (existingKeys.has(key)) {
- console.debug('[quiz] dropped duplicate:', q.question);
- continue;
- }
- existingKeys.add(key);
- accepted.push({
- ...q,
- id: `${topic.id}-${Math.random().toString(36).slice(2, 11)}`,
- });
- }
-
- if (!accepted.length) {
- lastError = `All generated questions for "${topic.label}" were duplicates.`;
- continue;
- }
-
- const merged = [...existingBank, ...accepted];
- await db.setQuizBank(topic.id, merged);
- return accepted;
- }
-
- throw new Error(`Quiz generation failed for "${topic.label}": ${lastError || 'unknown error'}.`);
-}
-
-/**
- * Get questions from the bank for a topic, generating if needed.
- * Never throws — returns whatever is available (possibly []).
- */
-async function getOrGenerateTopicQuestions(topic, count) {
- let bank = await db.getQuizBank(topic.id);
-
- if (bank.length < count) {
- try {
- await forceGenerateTopicQuestions(topic, 5);
- bank = await db.getQuizBank(topic.id);
- } catch (err) {
- console.warn(`[quiz] Could not populate bank for "${topic.label}":`, err.message);
- }
- }
-
- return sample(bank, Math.min(count, bank.length));
-}
-
-// ── Weekly quiz assembly ─────────────────────────────────────
-
export async function generateWeeklyQuiz(userId, weekNumber, force = false) {
- // Return cached quiz if it has valid questions
if (!force) {
const cached = await db.getCachedQuiz(userId, weekNumber);
if (cached?.questions?.length > 0) return cached;
@@ -231,48 +252,32 @@ export async function generateWeeklyQuiz(userId, weekNumber, force = false) {
const questions = [];
- // 1. Pull up to 3 from primary topic
const primaryQs = await getOrGenerateTopicQuestions(primaryTopic, 3);
questions.push(...primaryQs);
- // 2. Fill remaining slots from review topics
for (const rt of reviewTopics) {
if (questions.length >= 5) break;
const rtQs = await getOrGenerateTopicQuestions(rt, 1);
questions.push(...rtQs);
}
- // 3. If still short, pull more from primary
if (questions.length < 5) {
+ const needed = 5 - questions.length;
+ const extraQs = await getOrGenerateTopicQuestions(primaryTopic, needed + 5);
const existingIds = new Set(questions.map(q => q.id));
- const extraQs = await getOrGenerateTopicQuestions(primaryTopic, 5);
for (const eq of extraQs) {
if (!existingIds.has(eq.id) && questions.length < 5) {
questions.push(eq);
- existingIds.add(eq.id);
}
}
}
- // 4. Last resort: try a fresh generation if we still have 0 questions
- if (questions.length === 0) {
- try {
- const freshQs = await callQuizModel(primaryTopic, 5);
- const qualityError = validateBatchQuality(freshQs);
- if (!qualityError) {
- questions.push(...freshQs.slice(0, 5));
- }
- } catch (err) {
- console.warn('[quiz] Last-resort generation also failed:', err.message);
- }
- }
-
- if (questions.length === 0) {
- throw new Error('Could not generate any questions for this week\'s test. Please try again.');
- }
-
const shuffled = shuffle(questions).slice(0, 5);
+ if (shuffled.length === 0) {
+ throw new Error('Could not assemble enough questions for this week\'s test. Please try again.');
+ }
+
const quiz = {
questions: shuffled,
meta: {
@@ -287,8 +292,6 @@ export async function generateWeeklyQuiz(userId, weekNumber, force = false) {
return quiz;
}
-// ── Result management ────────────────────────────────────────
-
export async function saveTestResult(userId, weekNumber, result) {
await db.saveQuizResult(userId, weekNumber, result);
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index e929e70..1db4420 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -67,15 +67,15 @@ const Dashboard = () => {
console.warn('[Dashboard] Could not load curriculum data:', e.message);
}
- setDashData({
- topic,
- learnDone,
- testResult,
- top3,
- myRank,
- myPoints,
- activity,
- yearProgress,
+ setDashData({
+ topic,
+ learnDone,
+ testResult,
+ top3,
+ myRank,
+ myPoints,
+ activity,
+ yearProgress,
hasCurriculum: curriculumExists,
theme: topic?.theme || '',
});
@@ -178,7 +178,7 @@ const Dashboard = () => {
Testing
-
Weekly knowledge test
+
Weekly test — 5 questions
{testResult ?
Score: {testResult.percentage}% :
To Do }
@@ -207,9 +207,8 @@ const Dashboard = () => {
) : (
activity.slice(0, 5).map((act, i) => (
-
+
{act.type === 'test' ? 'T' : 'L'}
diff --git a/src/pages/Testen.jsx b/src/pages/Testen.jsx
index e425c51..dd06e73 100644
--- a/src/pages/Testen.jsx
+++ b/src/pages/Testen.jsx
@@ -182,7 +182,7 @@ const Testen = () => {
Weekly Test
Week {weekNumber}
- Test your knowledge with AI-generated questions. You have 5 minutes to complete the test. Good luck!
+ Test your knowledge with 5 AI-generated questions. You have 5 minutes to complete the test. Good luck!
{error && (
@@ -196,9 +196,9 @@ const Testen = () => {
- Up to 5 questions
+ 5 questions
5 minutes
- 2 pts each
+ 20 pts max
Start Test
@@ -215,7 +215,7 @@ const Testen = () => {
AI is generating your test...
-
Preparing questions based on your learning topics.
+
Preparing 5 questions based on your learning topics.
);
}
@@ -233,9 +233,8 @@ const Testen = () => {
animate={{ scale: 1, opacity: 1 }}
className="text-center mb-10"
>
-
+
{result.percentage}%
@@ -281,9 +280,8 @@ const Testen = () => {
{result.breakdown.map((item, i) => (
-
+
{i + 1}
@@ -299,13 +297,12 @@ const Testen = () => {
return (
{isCorrect && }
{wasSelected && !isCorrect && }