Sealearn doesn’t deliver the same lesson twice. Every time a student starts a session the backend runsDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/DerBasilisk/SEA-ServicioEvaluaconAsistida/llms.txt
Use this file to discover all available pages before exploring further.
getAdaptiveConfig() — a scoring function that reads three live signals from the learner’s history and returns a tailored configuration that controls how many questions appear, how hard they are, and how much XP the session is worth. Students who are on a roll get more questions at a lower difficulty to keep them in flow; students who are struggling get fewer, harder questions to re-engage their focus without overwhelming them.
How the Adaptive Engine Works
The engine lives inadaptive.service.js and exposes three exported functions: getAdaptiveConfig, selectQuestions, and getAdaptiveXPReward. They run in sequence every time POST /api/lessons/:id/start is called.
The Three Input Signals
getAdaptiveConfig(userId, lesson) fetches three data points in parallel:
-
Weekly lessons completed (40% weight) — the count of
UserProgressdocuments withstatus: "completed"andlastCompletedAtwithin the last 7 days. Normalised against a cap of 14 (2 lessons per day), so a student completing 14 lessons scores a perfect 100 on this component. -
Streak days (30% weight) — the
currentfield from theStreakdocument for that user. Normalised against a cap of 30 days. -
Average recent score (30% weight) — the mean of
lastScoreacross the 10 most recently completed lessons. Defaults to50(neutral) when no history exists.
Activity Score Tiers
The activity score is then mapped to one of three configuration tiers:| Tier | Activity Score | Question Count | Difficulty | Easy Ratio | Hard Ratio |
|---|---|---|---|---|---|
| Low | ≤ 30 | 5 – 8 | Escalated +1 level | 0.2 | 0.5 |
| Medium | 31 – 60 | 9 – 13 | Base lesson difficulty | 0.33 | 0.33 |
| High | > 60 | 14 – 18 | Reduced −1 level | 0.5 | 0.2 |
escalateDifficulty(current, delta) which moves through the ordered array ["easy", "medium", "hard"], clamped at both ends. The final question count is always clamped to the range [5, 18].
Question Selection: selectQuestions()
Once the configuration is known, selectQuestions(allQuestions, count, easyRatio, hardRatio) builds the actual set delivered to the student:
- All active questions for the lesson are split into three buckets:
easy,medium, andhard. - Target counts are calculated —
easyCount = round(count × easyRatio),hardCount = round(count × hardRatio),mediumCount = count − easyCount − hardCount. - Each bucket is randomly shuffled and sliced to its target count. If a bucket has fewer questions than requested, it contributes what it can.
- A fill-up pass picks random questions from the unused remainder until the total reaches
count. - The combined list is shuffled a final time and returned — ensuring students never see the same question order twice.
Adaptive XP: getAdaptiveXPReward()
The XP awarded at lesson completion is not a flat value. getAdaptiveXPReward(baseXP, config) applies two multipliers to the lesson’s base XP reward:
baseXP before the adaptive multipliers are applied, so high activity, hard difficulty, and perfect accuracy compound into the maximum possible XP reward.
The 9 Question Types
Every question document carries atype field drawn from the following enum. Answer validation logic lives partly in the Question model’s instance methods and partly in the answerQuestion controller.
multiple_choice — Pick the correct option
multiple_choice — Pick the correct option
The question presents a
prompt and an options array. Each option has a text string and an isCorrect boolean. When the lesson is served to the client, isCorrect is stripped and the options are shuffled. Validation calls the model method checkMultipleChoice(selectedOptionId), which looks up the option by its _id subdocument and returns option.isCorrect. At least 2 options are required, exactly one must be correct, and duplicate option text is rejected at the pre('validate') hook.true_false — True or false statement
true_false — True or false statement
A single
prompt is presented alongside two choices. The correct answer is stored as correctBoolean (a MongoDB Boolean). Client answers may arrive as a boolean or as the strings "true" / "false". Validation uses checkTrueFalse(userAnswer), which coerces the incoming value to a boolean before comparing. The correctBoolean field is stripped from the payload sent to the client.fill_blank — Complete the blank
fill_blank — Complete the blank
The
prompt contains a gap for the student to fill in. One or more accepted answers are stored in correctAnswers. A pre('save') hook automatically expands the array to include lowercase, accent-stripped (NFD normalisation), and capitalised variants so that "Árbol", "arbol", "Arbol", and "árbol" all evaluate as correct. The checkFillBlank(userAnswer) method normalises the user’s answer with the same pipeline before comparing. If the exact check fails, the controller also calls an AI-powered secondary evaluation via evaluateFillBlankAnswer.order_items — Drag items into the correct sequence
order_items — Drag items into the correct sequence
The question stores an ordered array of strings in
items (the canonical order). The client receives a shuffledItems array with the same strings in random order. The student drags them into position. Validation compares JSON.stringify(userOrder) with JSON.stringify(question.items). At least 2 items are required.match_pairs — Connect left column to right column
match_pairs — Connect left column to right column
Two columns are defined as an array of
pairs, each with a left and right string. On delivery, the pairs are split into leftItems and rightItems, each shuffled independently. The pairs array itself is deleted from the payload. Validation checks that every user-submitted { leftId, rightId } pair corresponds to a matching subdocument _id in the stored pairs. Duplicate values in the right column are rejected at validation.sentence_builder — Assemble a sentence from a word bank
sentence_builder — Assemble a sentence from a word bank
A
wordBank array of strings (minimum 2) is provided; the prompt marks where blanks appear using ___. On delivery, the word bank is shuffled. The student drags words into the blank positions. Validation compares JSON.stringify(userWords) with JSON.stringify(question.correctAnswers). The pre('validate') hook normalises spacing around ___ markers before saving.free_text — Open-ended written response
free_text — Open-ended written response
Students type a free-form answer. Because no deterministic correct answer exists, validation is handled exclusively by AI:
evaluateOpenResponse() receives the prompt, the student’s answer, an optional evaluationCriteria field, and the question’s maxScore (default 10). The AI returns { approved, score, feedback, strengths, improvements }. The approved boolean determines isCorrect, and the full evaluation object is saved to the session’s attempts array. The isCodeExercise flag tells the AI evaluator to apply programming-specific rubrics.typing — Reproduce text exactly
typing — Reproduce text exactly
A passage is stored in
typingText. The student must reproduce it character-for-character. Validation is a strict equality check: userTyped === target. Typing statistics (typingStats) can be submitted alongside the answer and are persisted on the session record. A wrong answer costs one heart.code_python — Write and run Python code
code_python — Write and run Python code
The question carries a
testCases array (minimum 1). Each test case specifies a description, a testType ("stdout" or "return"), an expectedOutput, and optional callCode. Code execution and test evaluation happen client-side (or in a sandboxed runner); the client submits { passed, passedCount, totalCount, code }. The controller accepts passed: true as isCorrect. The full code and test-case counts are stored in the session for review. A wrong answer (not all tests passing) costs one heart.Lesson Lifecycle
Every lesson session follows four states tracked inUserProgress:
-
Start —
POST /api/lessons/:id/startruns the adaptive engine, selects questions, generates optional AI theory slides, savesstatus: "in_progress", and returns sanitised questions to the client. Requires at least 1 heart. -
Answer —
POST /api/lessons/:id/answeraccepts one answer at a time, evaluates correctness, deducts a heart on a wrong answer, and returns immediate feedback includingisCorrect,correctAnswer,explanation,heartsRemaining, andxpEarned. -
Complete —
POST /api/lessons/:id/completecalculates the session score, applies adaptive XP multipliers, awards gems (with a bonus for perfect sessions), updates the user’s streak, contributes XP to the league room, checks for newly unlocked achievements, and potentially unlocks the next lesson in the unit. -
Abandon —
POST /api/lessons/:id/abandonresets the progress record back tostatus: "available"and clears the current session, so the student can restart without penalty.