export const lessonContentTypes = ["VIDEO", "LECTURE", "ACTIVITY", "QUIZ", "FINAL_EXAM"] as const; export type LessonContentType = (typeof lessonContentTypes)[number]; export const lessonQuestionKinds = ["TRUE_FALSE", "MULTIPLE_CHOICE"] as const; export type LessonQuestionKind = (typeof lessonQuestionKinds)[number]; export type LessonQuestionOption = { id: string; text: string; isCorrect: boolean; }; export type LessonQuestion = { id: string; kind: LessonQuestionKind; prompt: string; explanation: string; options: LessonQuestionOption[]; }; export type LessonActivityMeta = { instructions: string; passingScorePercent: number; questions: LessonQuestion[]; }; type LessonDescriptionMeta = { text: string; contentType: LessonContentType; materialUrl: string | null; lectureContent: string; activity: LessonActivityMeta | null; }; const lessonTypeAliases: Record = { VIDEO: "VIDEO", LECTURE: "LECTURE", LECTURA: "LECTURE", READING: "LECTURE", ACTIVITY: "ACTIVITY", ACTIVIDAD: "ACTIVITY", QUIZ: "QUIZ", EVALUACION: "QUIZ", EVALUACIÓN: "QUIZ", FINAL_EXAM: "FINAL_EXAM", EXAMEN_FINAL: "FINAL_EXAM", EXAMENFINAL: "FINAL_EXAM", EVALUACION_FINAL: "FINAL_EXAM", EVALUACIÓN_FINAL: "FINAL_EXAM", }; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function asString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } function asNumber(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { const parsed = Number(value); if (Number.isFinite(parsed)) return parsed; } return null; } function normalizeType(value: string): LessonContentType { const normalized = value.trim().toUpperCase(); return lessonTypeAliases[normalized] ?? "VIDEO"; } function getDescriptionText(input: unknown): string { if (typeof input === "string") return input.trim(); if (isRecord(input)) { const direct = asString(input.text); if (direct) return direct; const es = asString(input.es); if (es) return es; const en = asString(input.en); if (en) return en; const summary = asString(input.summary); if (summary) return summary; } return ""; } function normalizeQuestionKind(value: unknown): LessonQuestionKind { const raw = asString(value).toUpperCase(); return raw === "TRUE_FALSE" ? "TRUE_FALSE" : "MULTIPLE_CHOICE"; } function parseQuestionOptions(value: unknown, kind: LessonQuestionKind): LessonQuestionOption[] { if (!Array.isArray(value)) { if (kind === "TRUE_FALSE") { return [ { id: "true", text: "Verdadero", isCorrect: true }, { id: "false", text: "Falso", isCorrect: false }, ]; } return []; } const options = value .map((option, index) => { if (!isRecord(option)) return null; const id = asString(option.id) || `opt-${index + 1}`; const text = asString(option.text); const isCorrect = Boolean(option.isCorrect); if (!text) return null; return { id, text, isCorrect }; }) .filter((option): option is LessonQuestionOption => option !== null); if (kind === "TRUE_FALSE") { const hasTrue = options.find((option) => option.id === "true" || option.text.toLowerCase() === "verdadero"); const hasFalse = options.find((option) => option.id === "false" || option.text.toLowerCase() === "falso"); if (!hasTrue || !hasFalse) { const correctIndex = options.findIndex((option) => option.isCorrect); return [ { id: "true", text: "Verdadero", isCorrect: correctIndex !== 1 }, { id: "false", text: "Falso", isCorrect: correctIndex === 1 }, ]; } } return options; } function parseQuestion(value: unknown, index: number): LessonQuestion | null { if (!isRecord(value)) return null; const kind = normalizeQuestionKind(value.kind ?? value.type); const id = asString(value.id) || `q-${index + 1}`; const prompt = asString(value.prompt ?? value.question); const explanation = asString(value.explanation); const options = parseQuestionOptions(value.options, kind); if (!prompt) return null; if (options.length < 2) return null; const hasCorrect = options.some((option) => option.isCorrect); const normalizedOptions = hasCorrect ? options : options.map((option, optIndex) => ({ ...option, isCorrect: optIndex === 0 })); return { id, kind, prompt, explanation, options: normalizedOptions, }; } function normalizePassingScore(value: unknown, fallback: number): number { const parsed = asNumber(value); if (parsed == null) return fallback; return Math.max(0, Math.min(100, Math.round(parsed))); } function parseActivityMeta(description: Record): LessonActivityMeta | null { const activityRaw = (isRecord(description.activity) && description.activity) || (isRecord(description.quiz) && description.quiz) || (isRecord(description.exercise) && description.exercise) || null; if (!activityRaw) return null; const instructions = asString(activityRaw.instructions ?? activityRaw.intro ?? description.es ?? description.text); const passingScorePercent = normalizePassingScore(activityRaw.passingScorePercent, 70); const questionsRaw = Array.isArray(activityRaw.questions) ? activityRaw.questions : []; const questions = questionsRaw .map((question, index) => parseQuestion(question, index)) .filter((question): question is LessonQuestion => question !== null); if (questions.length === 0) return null; return { instructions, passingScorePercent, questions, }; } export function parseLessonDescriptionMeta(description: unknown): LessonDescriptionMeta { if (!isRecord(description)) { const fallbackText = getDescriptionText(description); return { text: fallbackText, contentType: "VIDEO", materialUrl: null, lectureContent: fallbackText, activity: null, }; } const contentTypeRaw = asString(description.contentType) || asString(description.kind) || asString(description.type); const materialUrl = asString(description.materialUrl) || asString(description.resourceUrl) || asString(description.pdfUrl) || asString(description.attachmentUrl) || ""; const text = getDescriptionText(description); const lectureContent = asString(description.lectureContent) || asString(description.content) || text; const activity = parseActivityMeta(description); return { text, contentType: normalizeType(contentTypeRaw || "VIDEO"), materialUrl: materialUrl || null, lectureContent, activity, }; } export function buildLessonDescriptionMeta(input: { text: string; contentType: LessonContentType; materialUrl?: string | null; lectureContent?: string | null; activity?: LessonActivityMeta | null; }): Record { const payload: Record = { contentType: input.contentType, }; const text = input.text.trim(); if (text) payload.es = text; const materialUrl = (input.materialUrl ?? "").trim(); if (materialUrl) payload.materialUrl = materialUrl; const lectureContent = (input.lectureContent ?? "").trim(); if (lectureContent) payload.lectureContent = lectureContent; const activity = input.activity; if (activity && activity.questions.length > 0) { payload.activity = { instructions: activity.instructions.trim(), passingScorePercent: Math.max(0, Math.min(100, Math.round(activity.passingScorePercent))), questions: activity.questions.map((question, qIndex) => { const questionId = question.id.trim() || `q-${qIndex + 1}`; const prompt = question.prompt.trim(); const explanation = question.explanation.trim(); const kind: LessonQuestionKind = question.kind === "TRUE_FALSE" ? "TRUE_FALSE" : "MULTIPLE_CHOICE"; const options = question.options .map((option, optionIndex) => { const optionId = option.id.trim() || `opt-${optionIndex + 1}`; const optionText = option.text.trim(); if (!optionText) return null; return { id: optionId, text: optionText, isCorrect: Boolean(option.isCorrect), }; }) .filter((option): option is LessonQuestionOption => option !== null); const hasCorrect = options.some((option) => option.isCorrect); const normalizedOptions = hasCorrect ? options : options.map((option, optionIndex) => ({ ...option, isCorrect: optionIndex === 0 })); return { id: questionId, kind, prompt, explanation, options: normalizedOptions, }; }), }; } return payload; } export function getLessonContentTypeLabel(contentType: LessonContentType): string { if (contentType === "LECTURE") return "Lectura"; if (contentType === "ACTIVITY") return "Actividad"; if (contentType === "QUIZ") return "Quiz"; if (contentType === "FINAL_EXAM") return "Evaluación final"; return "Video"; } export function isFinalExam(contentType: LessonContentType): boolean { return contentType === "FINAL_EXAM"; }