Download src/components/tuition/chapter-dashboard/studyQueue.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/tuition/chapter-dashboard/studyQueue.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/tuition/chapter-dashboard/studyQueue.ts
-
curl -L -o studyQueue.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/tuition/chapter-dashboard/studyQueue.ts
11.4 kB
| import type { SourceRef } from "./sessionData"; | |
| import type { MissionCatalog, MissionCatalogChapter, MissionSelection } from "./missionAdapter"; | |
| import { | |
| getChapterProgress, | |
| getMissionProgress, | |
| isMissionComplete, | |
| } from "./missionProgress"; | |
| import { getDueRevisionItems } from "./revisionScheduler"; | |
| /** | |
| * Local Today Study Queue. | |
| * | |
| * Decides what DocDoe should suggest first when /tuition opens, using only the | |
| * existing local data (revision scheduler, mission progress, chapter progress) | |
| * plus the mission catalog from the metadata API. Pure — no React, no writes. | |
| * source_refs are carried through unchanged when available. | |
| */ | |
| export type StudyQueueType = "revision" | "continue_mission" | "next_mission" | "chapter_complete"; | |
| export type StudyQueueItem = { | |
| type: StudyQueueType; | |
| priority: number; | |
| title: string; | |
| subtitle: string; | |
| discipline: string; | |
| chapterId: string; | |
| chapterTitle: string; | |
| chapterNo: number; | |
| missionId: string; | |
| missionTitle: string; | |
| source_refs?: SourceRef[]; | |
| reason: string; | |
| /** Selection to switch to (mission actions). */ | |
| selection?: MissionSelection; | |
| /** Revision item id (revision actions). */ | |
| revisionItemId?: string; | |
| }; | |
| export const STUDY_QUEUE_PRIORITY = { | |
| overdue_revision: 100, | |
| due_revision: 80, | |
| continue_mission: 70, | |
| next_mission: 50, | |
| chapter_complete: 10, | |
| } as const; | |
| type MissionDescriptor = { | |
| discipline: string; | |
| chapterId: string; | |
| chapterTitle: string; | |
| chapterNo: number; | |
| missionId: string; | |
| missionTitle: string; | |
| }; | |
| function missionShortTitle(title: string): string { | |
| return title.replace(/^M\d+:\s*/i, "").trim() || title; | |
| } | |
| function startOfDay(now: number): number { | |
| const date = new Date(now); | |
| date.setHours(0, 0, 0, 0); | |
| return date.getTime(); | |
| } | |
| function chapterMetaById( | |
| catalog: MissionCatalog | null, | |
| chapterId: string, | |
| ): { discipline: string; chapterNo: number; chapterTitle: string } | null { | |
| if (!catalog) return null; | |
| for (const subject of catalog.subjects) { | |
| const chapter = subject.chapters.find((item) => item.chapter_id === chapterId); | |
| if (chapter) { | |
| return { discipline: chapter.discipline || "", chapterNo: chapter.chapter_no, chapterTitle: chapter.chapter_title }; | |
| } | |
| } | |
| return null; | |
| } | |
| export function chapterForSelection( | |
| catalog: MissionCatalog | null, | |
| selection: MissionSelection, | |
| ): MissionCatalogChapter | null { | |
| if (!catalog) return null; | |
| const subject = catalog.subjects.find((s) => s.subject === selection.subject); | |
| if (!subject) return null; | |
| return ( | |
| subject.chapters.find( | |
| (chapter) => chapter.chapter_id === selection.chapterId || (chapter.discipline === selection.discipline && chapter.chapter_no === selection.chapterNo), | |
| ) ?? null | |
| ); | |
| } | |
| function flatMissions(catalog: MissionCatalog | null): MissionDescriptor[] { | |
| const out: MissionDescriptor[] = []; | |
| for (const subject of catalog?.subjects ?? []) { | |
| for (const chapter of subject.chapters) { | |
| for (const mission of chapter.missions) { | |
| out.push({ | |
| discipline: chapter.discipline || "", | |
| chapterId: chapter.chapter_id, | |
| chapterTitle: chapter.chapter_title, | |
| chapterNo: chapter.chapter_no, | |
| missionId: mission.mission_id, | |
| missionTitle: mission.mission_title, | |
| }); | |
| } | |
| } | |
| } | |
| return out; | |
| } | |
| function toSelection(descriptor: MissionDescriptor, subjectName: "Social Science" | "Physics" = "Social Science"): MissionSelection { | |
| return { board: "Kerala SSLC", classLevel: "10", subject: subjectName, discipline: descriptor.discipline, chapterNo: descriptor.chapterNo, chapterId: descriptor.chapterId, missionId: descriptor.missionId }; | |
| } | |
| /** The mission the student currently has selected, as a descriptor. */ | |
| export function resolveCurrentMission( | |
| catalog: MissionCatalog | null, | |
| selection: MissionSelection, | |
| ): MissionDescriptor | null { | |
| const chapter = chapterForSelection(catalog, selection); | |
| if (!chapter) return null; | |
| const mission = chapter.missions.find((item) => item.mission_id === selection.missionId); | |
| return { | |
| discipline: chapter.discipline || "", | |
| chapterId: chapter.chapter_id, | |
| chapterTitle: chapter.chapter_title, | |
| chapterNo: chapter.chapter_no, | |
| missionId: selection.missionId, | |
| missionTitle: mission?.mission_title ?? selection.missionId, | |
| }; | |
| } | |
| /** The saved progress for the currently selected mission (or null). */ | |
| export function resolveCurrentMissionProgress( | |
| studentId: string, | |
| catalog: MissionCatalog | null, | |
| selection: MissionSelection, | |
| ) { | |
| const current = resolveCurrentMission(catalog, selection); | |
| if (!current) return null; | |
| return getMissionProgress(studentId, current.chapterId, current.missionId); | |
| } | |
| export type NextMissionResolution = MissionDescriptor & { unlocked: boolean; reason: string }; | |
| /** | |
| * The next mission the student should do. If the current mission is in progress | |
| * it returns the mission that comes after it ("up next"); otherwise it returns | |
| * the first not-completed mission at or after the current one (the current | |
| * mission itself when it is fresh, or the next chapter's first mission when the | |
| * current chapter is complete). | |
| */ | |
| export function resolveNextUnlockedMission( | |
| studentId: string, | |
| catalog: MissionCatalog | null, | |
| selection: MissionSelection, | |
| ): NextMissionResolution | null { | |
| const flat = flatMissions(catalog); | |
| if (flat.length === 0) return null; | |
| const current = resolveCurrentMission(catalog, selection); | |
| const currentIndex = current | |
| ? flat.findIndex((item) => item.chapterId === current.chapterId && item.missionId === current.missionId) | |
| : -1; | |
| const start = currentIndex >= 0 ? currentIndex : 0; | |
| const currentProgress = current ? getMissionProgress(studentId, current.chapterId, current.missionId) : null; | |
| const currentInProgress = Boolean( | |
| currentProgress && currentProgress.missionStarted && !isMissionComplete(currentProgress), | |
| ); | |
| const from = currentInProgress ? start + 1 : start; | |
| for (let index = from; index < flat.length; index += 1) { | |
| const descriptor = flat[index]; | |
| const chapterProgress = getChapterProgress(studentId, descriptor.chapterId); | |
| if (chapterProgress.completedMissionIds.includes(descriptor.missionId)) continue; | |
| const unlocked = descriptor.missionId === selection.missionId || chapterProgress.unlockedMissionIds.includes(descriptor.missionId) || isFirstMissionOfChapter(catalog, descriptor); | |
| const sameChapter = current && descriptor.chapterId === current.chapterId; | |
| const reason = currentInProgress | |
| ? "Up next after this mission" | |
| : sameChapter | |
| ? "Your current mission is ready" | |
| : "Next chapter unlocked"; | |
| return { ...descriptor, unlocked, reason }; | |
| } | |
| return null; | |
| } | |
| function isFirstMissionOfChapter(catalog: MissionCatalog | null, descriptor: MissionDescriptor): boolean { | |
| if (!catalog) return false; | |
| for (const subject of catalog.subjects) { | |
| const chapter = subject.chapters.find((item) => item.chapter_id === descriptor.chapterId); | |
| if (chapter) { | |
| return chapter.missions[0]?.mission_id === descriptor.missionId; | |
| } | |
| } | |
| return false; | |
| } | |
| function isChapterComplete(studentId: string, chapter: MissionCatalogChapter): boolean { | |
| if (chapter.missions.length === 0) return false; | |
| const chapterProgress = getChapterProgress(studentId, chapter.chapter_id); | |
| const completed = new Set(chapterProgress.completedMissionIds); | |
| return chapter.missions.every((mission) => completed.has(mission.mission_id)); | |
| } | |
| /** Build the ordered Today study queue. */ | |
| export function buildTodayStudyQueue( | |
| studentId: string, | |
| catalog: MissionCatalog | null, | |
| selection: MissionSelection, | |
| now: number = Date.now(), | |
| ): StudyQueueItem[] { | |
| const items: StudyQueueItem[] = []; | |
| const dayStart = startOfDay(now); | |
| // 1. Due revision items (overdue first). | |
| for (const revision of getDueRevisionItems(studentId, now)) { | |
| const overdue = new Date(revision.dueAt).getTime() < dayStart; | |
| const meta = chapterMetaById(catalog, revision.chapterId); | |
| items.push({ | |
| type: "revision", | |
| priority: overdue ? STUDY_QUEUE_PRIORITY.overdue_revision : STUDY_QUEUE_PRIORITY.due_revision, | |
| title: `Revise: ${revision.weakAreaTag}`, | |
| subtitle: revision.chapterTitle, | |
| discipline: meta?.discipline ?? "", | |
| chapterId: revision.chapterId, | |
| chapterTitle: revision.chapterTitle, | |
| chapterNo: meta?.chapterNo ?? 0, | |
| missionId: revision.missionId, | |
| missionTitle: "", | |
| source_refs: revision.source_refs, | |
| reason: overdue ? "Overdue revision" : "Revision due today", | |
| revisionItemId: revision.id, | |
| }); | |
| } | |
| // 2. Unfinished current mission. | |
| const current = resolveCurrentMission(catalog, selection); | |
| if (current) { | |
| const progress = getMissionProgress(studentId, current.chapterId, current.missionId); | |
| if (progress && progress.missionStarted && !isMissionComplete(progress)) { | |
| items.push({ | |
| type: "continue_mission", | |
| priority: STUDY_QUEUE_PRIORITY.continue_mission, | |
| title: `Continue: ${missionShortTitle(current.missionTitle)}`, | |
| subtitle: current.chapterTitle, | |
| discipline: current.discipline, | |
| chapterId: current.chapterId, | |
| chapterTitle: current.chapterTitle, | |
| chapterNo: current.chapterNo, | |
| missionId: current.missionId, | |
| missionTitle: current.missionTitle, | |
| source_refs: progress.source_refs, | |
| reason: `Resume from the ${progress.lastStage} stage`, | |
| selection: toSelection(current, selection.subject), | |
| }); | |
| } | |
| } | |
| // 3. Next unlocked / up-next mission. | |
| const next = resolveNextUnlockedMission(studentId, catalog, selection); | |
| if (next) { | |
| items.push({ | |
| type: "next_mission", | |
| priority: STUDY_QUEUE_PRIORITY.next_mission, | |
| title: `Start: ${missionShortTitle(next.missionTitle)}`, | |
| subtitle: next.chapterTitle, | |
| discipline: next.discipline, | |
| chapterId: next.chapterId, | |
| chapterTitle: next.chapterTitle, | |
| chapterNo: next.chapterNo, | |
| missionId: next.missionId, | |
| missionTitle: next.missionTitle, | |
| reason: next.reason, | |
| selection: toSelection(next, selection.subject), | |
| }); | |
| } | |
| // 4. Completed chapter (info). | |
| const currentChapter = chapterForSelection(catalog, selection); | |
| if (currentChapter && isChapterComplete(studentId, currentChapter)) { | |
| items.push({ | |
| type: "chapter_complete", | |
| priority: STUDY_QUEUE_PRIORITY.chapter_complete, | |
| title: `Chapter complete: ${currentChapter.chapter_title}`, | |
| subtitle: `${currentChapter.missions.length}/${currentChapter.missions.length} missions`, | |
| discipline: currentChapter.discipline || "", | |
| chapterId: currentChapter.chapter_id, | |
| chapterTitle: currentChapter.chapter_title, | |
| chapterNo: currentChapter.chapter_no, | |
| missionId: "", | |
| missionTitle: "", | |
| reason: "All missions in this chapter are completed", | |
| }); | |
| } | |
| return items.sort((a, b) => b.priority - a.priority); | |
| } | |
| /** The single highest-priority action (or null when nothing is queued). */ | |
| export function getTopStudyAction( | |
| studentId: string, | |
| catalog: MissionCatalog | null, | |
| selection: MissionSelection, | |
| now: number = Date.now(), | |
| ): StudyQueueItem | null { | |
| return buildTodayStudyQueue(studentId, catalog, selection, now)[0] ?? null; | |
| } | |