Download src/components/tuition/chapter-dashboard/classStepFlow.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 5.57 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/tuition/chapter-dashboard/classStepFlow.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/tuition/chapter-dashboard/classStepFlow.ts
-
curl -L -o classStepFlow.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/tuition/chapter-dashboard/classStepFlow.ts
5.57 kB
| /** | |
| * Pure helpers for the continuous class step flow. | |
| * Step ids stay stable for localStorage resume; labels match product language. | |
| */ | |
| import type { TuitionLessonStepId } from "./classSessionData"; | |
| import { tuitionLessonSteps } from "./classSessionData"; | |
| export type ClassStepGateState = { | |
| practiceAnswer: string; | |
| practiceSubmitted: boolean; | |
| quizAnswers: Record<string, string>; | |
| quizSubmitted: boolean; | |
| }; | |
| /** Marks lost must never exceed maxMarks. */ | |
| export function clampMarksLost(score: number, maxMarks: number): number { | |
| if (!Number.isFinite(maxMarks) || maxMarks <= 0) return 0; | |
| if (!Number.isFinite(score)) return maxMarks; | |
| const safeScore = Math.max(0, Math.min(maxMarks, score)); | |
| return Math.max(0, Math.min(maxMarks, maxMarks - safeScore)); | |
| } | |
| /** Score displayed to the student is always within [0, maxMarks]. */ | |
| export function clampScoreEstimate(score: number, maxMarks: number): number { | |
| if (!Number.isFinite(maxMarks) || maxMarks <= 0) return 0; | |
| if (!Number.isFinite(score)) return 0; | |
| return Math.max(0, Math.min(maxMarks, Math.round(score))); | |
| } | |
| /** | |
| * Required-answer gate: Your Turn cannot be treated complete without a real answer. | |
| * Empty or whitespace-only answers never count as submitted. | |
| */ | |
| export function isPracticeAnswerComplete(answer: string, submitted: boolean): boolean { | |
| return submitted && answer.trim().length > 0; | |
| } | |
| /** | |
| * Whether the student may leave the current step for the next one. | |
| * Session Plan / Teach / Video / Visual / Methods / Notes / Exam are free to continue. | |
| * Your Turn requires a submitted non-empty practice answer. | |
| * Quiz requires submitted, non-empty answers before it can be completed. | |
| */ | |
| export function canAdvanceFromStep(stepId: TuitionLessonStepId, state: ClassStepGateState): boolean { | |
| if (stepId === "your-turn") { | |
| return isPracticeAnswerComplete(state.practiceAnswer, state.practiceSubmitted); | |
| } | |
| if (stepId === "quiz") { | |
| const answers = Object.values(state.quizAnswers); | |
| return state.quizSubmitted && answers.length > 0 && answers.every((answer) => answer.trim().length > 0); | |
| } | |
| return true; | |
| } | |
| export function advanceBlockedReason(stepId: TuitionLessonStepId, state: ClassStepGateState): string | null { | |
| if (stepId === "your-turn" && !isPracticeAnswerComplete(state.practiceAnswer, state.practiceSubmitted)) { | |
| return "Write and submit your answer before continuing. Required answers cannot be skipped."; | |
| } | |
| if (stepId === "quiz" && !canAdvanceFromStep(stepId, state)) { | |
| return "Answer and submit every quiz question before continuing."; | |
| } | |
| return null; | |
| } | |
| /** Weak-area tags must come only from real mistakes (missing keywords / failed points). */ | |
| export function weakAreasFromMistakes(missingKeywords: string[], isCorrect: boolean): string[] { | |
| if (isCorrect) return []; | |
| return missingKeywords.map((tag) => tag.trim()).filter(Boolean); | |
| } | |
| export function stepIndex(stepId: TuitionLessonStepId): number { | |
| return tuitionLessonSteps.findIndex((step) => step.id === stepId); | |
| } | |
| export function productStepLabels(): string[] { | |
| return tuitionLessonSteps.map((step) => step.label); | |
| } | |
| /** True when every selected plan day subject is in the student's selected subjects. */ | |
| export function weekPlanSubjectsAreBound( | |
| daySubjects: string[], | |
| selectedSubjects: string[], | |
| ): boolean { | |
| if (selectedSubjects.length === 0) return daySubjects.length === 0; | |
| const allowed = new Set(selectedSubjects.map((s) => s.trim().toLowerCase())); | |
| return daySubjects.every((subject) => allowed.has(subject.trim().toLowerCase())); | |
| } | |
| /** Chapter context lock: practice/mission selection must not silently switch chapter. */ | |
| export function isSameChapterContext( | |
| expected: { chapterId?: string; chapter?: string }, | |
| actual: { chapterId?: string; chapter?: string }, | |
| ): boolean { | |
| if (expected.chapterId && actual.chapterId) { | |
| return expected.chapterId === actual.chapterId; | |
| } | |
| if (expected.chapter && actual.chapter) { | |
| return expected.chapter.trim().toLowerCase() === actual.chapter.trim().toLowerCase(); | |
| } | |
| return true; | |
| } | |
| /** Normalize video pipeline status for student UI — never invent progress. */ | |
| export type HonestVideoUiStatus = | |
| | "queued" | |
| | "rendering" | |
| | "ready" | |
| | "failed" | |
| | "unavailable" | |
| | "plan_only"; | |
| export function mapVideoJobStatus(status: string | null | undefined): HonestVideoUiStatus { | |
| const value = (status ?? "").toLowerCase().trim(); | |
| if (!value || value === "unavailable" || value === "not_available" || value === "not_generated") return "unavailable"; | |
| if (value === "plan_only" || value === "renderer_coming_later") return "plan_only"; | |
| if (value === "ready" || value === "complete" || value === "completed") return "ready"; | |
| if (value === "failed" || value === "error" || value === "qa_failed") return "failed"; | |
| if (value === "package_ready") return "queued"; | |
| if ( | |
| value === "rendering" || | |
| value === "running" || | |
| value === "audio_generating" || | |
| value === "visuals_generating" || | |
| value === "uploading" || | |
| value === "planning" || | |
| value === "script_ready" | |
| ) { | |
| return "rendering"; | |
| } | |
| if (value === "queued") return "queued"; | |
| return "unavailable"; | |
| } | |
| export function videoStatusLabel(status: HonestVideoUiStatus): string { | |
| switch (status) { | |
| case "queued": | |
| return "Queued"; | |
| case "rendering": | |
| return "Rendering"; | |
| case "ready": | |
| return "Ready"; | |
| case "failed": | |
| return "Failed"; | |
| case "plan_only": | |
| return "Plan only"; | |
| case "unavailable": | |
| default: | |
| return "Unavailable"; | |
| } | |
| } | |