Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Stivenz3/Nexu/llms.txt

Use this file to discover all available pages before exploring further.

Nexu uses a single questions subcollection under each lesson to store every question — both the inline quizzes embedded in theory blocks and the pool of questions drawn for the final exam. The questionType field distinguishes between them at query time. All questions live at lessons/{lessonId}/questions/{questionId}.

Two question types

questionTypeWhere usedHow referenced
intercaladaEmbedded inside a theory blockBlock’s questionId or questionIds[] field
evaluacion_finalFinal exam question poolFetched by fetchExamQuestions
Lesson 1 has 8 inline questions (one per theory block, with block b04_modulo_c having three and b05_modulo_d having two) and a pool of 15 exam questions (q_e01 through q_e15).

LessonQuestion schema

Every question document — inline or exam — conforms to this shape:
FieldTypeDescription
questionIdstringUnique ID, e.g. q_p1i or q_e01
blockRefstringblockId of the block this question belongs to
questionType'intercalada' | 'evaluacion_final'Determines where the question is used
ordernumberDisplay order within the question pool
textstringFull question text shown to the learner
optionsQuestionOption[]Answer choices (see below)
explanationstringExplanation shown after answering (inline) or after the exam
normativeRefstringRegulation reference for the correct answer
difficulty'basica' | 'intermedia' | 'avanzada'Informational label — not used for filtering in the current implementation

QuestionOption shape

interface QuestionOption {
  label: string        // Single letter, e.g. "A", "B", "C", "D"
  text: string         // Answer text displayed to the learner
  isCorrect: boolean   // True for exactly one option per question
  feedback?: string    // Per-option incorrect feedback (falls back to `explanation` if absent)
}

Example question (q_p1i — inline, intercalada)

{
  "questionId": "q_p1i",
  "blockRef": "b02_modulo_a",
  "questionType": "intercalada",
  "order": 1,
  "text": "Un manipulador llega a trabajar y refiere que tuvo diarrea durante la noche anterior, pero actualmente no presenta síntomas. ¿Cuál es la conducta correcta según la Resolución 2674 de 2013?",
  "options": [
    { "label": "A", "text": "Puede manipular alimentos normalmente, ya que no presenta síntomas activos.", "isCorrect": false },
    { "label": "B", "text": "Debe ser retirado temporalmente del área de manipulación hasta obtener coprocultivo negativo.", "isCorrect": true },
    { "label": "C", "text": "Puede trabajar si usa guantes.", "isCorrect": false },
    { "label": "D", "text": "Puede manipular alimentos solo en áreas de almacenamiento, no de producción.", "isCorrect": false }
  ],
  "explanation": "La norma restringe la manipulación ante cualquier evidencia de enfermedad gastrointestinal reciente, incluyendo la condición de portador asintomático.",
  "normativeRef": "Art. 11, Res. 2674 de 2013",
  "difficulty": "intermedia"
}

Example question (q_e01 — exam pool, evaluacion_final)

{
  "questionId": "q_e01",
  "blockRef": "b08_exam",
  "questionType": "evaluacion_final",
  "order": 1,
  "text": "¿Con qué frecuencia mínima deben realizarse los exámenes médicos periódicos a los manipuladores de alimentos según las buenas prácticas del sector en Colombia?",
  "options": [
    { "label": "A", "text": "Cada seis meses", "isCorrect": false },
    { "label": "B", "text": "Anualmente", "isCorrect": true },
    { "label": "C", "text": "Cada dos años", "isCorrect": false },
    { "label": "D", "text": "Solo al ingreso", "isCorrect": false }
  ],
  "explanation": "Aunque la Resolución 2674 no fija la periodicidad exacta, la práctica estándar avalada por INVIMA es la realización de exámenes médicos con frecuencia mínima anual.",
  "normativeRef": "Art. 11, Res. 2674 de 2013",
  "difficulty": "basica"
}

Fetching exam questions — fetchExamQuestions

fetchExamQuestions filters the lesson’s questions subcollection to only evaluacion_final questions, optionally shuffles them, and returns the requested count:
export async function fetchExamQuestions(
  lessonId: string,
  count = 15,
  randomize = true
): Promise<LessonQuestion[]>
It calls fetchLessonQuestions (which fetches the entire questions subcollection) and then filters client-side:
const all = (await fetchLessonQuestions(lessonId)).filter(
  (q) => q.questionType === 'evaluacion_final'
)
const pool = randomize
  ? shuffle([...all])
  : [...all].sort((a, b) => a.order - b.order)
return pool.slice(0, count)
LessonExamPage calls fetchExamQuestions(lessonId, 15, true) so each attempt presents a freshly shuffled selection from the pool.

Inline questions

Inline questions are referenced by the theory block that contains them — either via questionId (a single question ID string) or questionIds (an array of IDs). The helper getTheoryQuestionIds normalises both into string[]:
export function getTheoryQuestionIds(content: TheoryBlockContent): string[] {
  if (content.questionIds?.length) return content.questionIds
  if (content.questionId) return [content.questionId]
  return []
}
After the learner reads a theory block and clicks “Continuar”, LessonFlowPage fetches each question by ID using fetchQuestion(lessonId, questionId) and presents them sequentially via InlineQuestionView. The learner must answer every inline question correctly to mark the theory block as completed and unlock the next block.

Exam flow

The full exam lifecycle from gate-check to certificate issuance:
1

Gate check — canAccessFinalExam

When the learner navigates to /leccion/:id/evaluacion, LessonExamPage first calls canAccessFinalExam(userId, lessonId). This returns { allowed: boolean, reason? } with the following possible reasons if access is denied:
reasonMeaning
not_completedNot all non-exam blocks are completed
already_passedLearner has already passed this exam
no_progressNo progress document exists yet
no_examNo exam block found in the lesson
If allowed is false, the page renders a blocked state and redirects the learner back to the lesson.
2

Load questions

If access is allowed, LessonExamPage calls fetchExamQuestions(lessonId, 15, true) to draw 15 randomized questions from the evaluacion_final pool. It also loads the exam block’s ExamBlockContent to read passingPercent and other configuration.
3

Learner answers all questions

The exam renders one question at a time. The learner selects an option and clicks “Siguiente” to advance. No feedback is shown during the exam — all answers are revealed only after submission.
4

Score calculation

On the final question, clicking “Finalizar evaluación” calculates the score client-side: the number of correct answers divided by total questions, multiplied by 100 and rounded to the nearest integer.
5

Persist result — saveExamResult

saveExamResult writes the result to the learner’s lessonProgress document:
export async function saveExamResult(
  userId: string,
  lessonId: string,
  score: number,
  passingPercent: number,
  examBlockId?: string
): Promise<void>
It finds the exam block by looking for type === 'exam' in the lesson’s blocks — it does not assume a fixed block ID like b08_exam. The function updates:
  • blocksProgress[examBlockId]{ completed: passed, lastScore: score, attempts: n, passed }
  • status'passed' if score >= passingPercent, otherwise 'failed'
  • bestEvalScore → maximum of previous best and current score
  • evalAttempts → appended with the current score
  • completedAt → set to server timestamp if passed
6

Outcome

  • Score ≥ passingPercentstatus: 'passed'. LessonExamPage calls tryIssueCourseCertificateAfterLesson() and navigates to /certificado.
  • Score < passingPercentstatus: 'failed'. The learner can click “Intentar de nuevo” to retry immediately — unlimitedAttempts: true in the current lesson seed means there is no retry limit.

Using fetchExamQuestions and saveExamResult together

The following pattern mirrors what LessonExamPage does end-to-end:
import { fetchExamQuestions } from '@/services/lessonService'
import { saveExamResult, canAccessFinalExam } from '@/services/progressService'

// 1. Gate check
const { allowed, reason } = await canAccessFinalExam(userId, lessonId)
if (!allowed) {
  console.warn('Exam blocked:', reason)
  return
}

// 2. Fetch a randomized set of 15 questions
const questions = await fetchExamQuestions(lessonId, 15, true)

// 3. ... (collect learner answers) ...

// 4. Persist result (passingPercent = 70 for Lesson 1)
await saveExamResult(userId, lessonId, score, 70)

Lesson 1 question stats

CategoryCountIDs
Inline (intercalada)8q_p1iq_p8i
Exam pool (evaluacion_final)15q_e01q_e15
Total23
The difficulty field values — basica, intermedia, avanzada — are stored on each question for informational purposes. The current implementation does not use difficulty for filtering or adaptive selection; fetchExamQuestions draws uniformly from the entire pool.

Build docs developers (and LLMs) love