Download src/lib/api/learn-anything-roadmap.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 6.05 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/learn-anything-roadmap.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/lib/api/learn-anything-roadmap.ts
-
curl -L -o learn-anything-roadmap.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/learn-anything-roadmap.ts
6.05 kB
| import { apiGet, apiPost } from "@/lib/api/client"; | |
| import { hasLiveSession } from "@/lib/api/mode"; | |
| import type { LearnAnythingRoadmap } from "@/lib/phase3-learning"; | |
| /** | |
| * Backend persistence for Learn Anything roadmaps. localStorage is an offline | |
| * cache/draft; the authenticated backend owns saved roadmap progress and lets | |
| * /learn-anything/[roadmapId] resume it on another device. No-op when logged | |
| * out, so an anonymous draft never becomes account state by accident. | |
| */ | |
| type RoadmapRemote = { roadmap_id: string; data: LearnAnythingRoadmap; updated_at: string }; | |
| type RoadmapCommand = { | |
| client_command_id: string; | |
| command: | |
| | "set_topic_status" | |
| | "record_diagnostic" | |
| | "complete_lesson" | |
| | "record_quiz" | |
| | "review_flashcard"; | |
| topic_key?: string | null; | |
| status?: "covered" | null; | |
| known_topic_keys?: string[]; | |
| title?: string | null; | |
| correct?: number | null; | |
| total?: number | null; | |
| missed_questions?: string[]; | |
| card_front?: string | null; | |
| remembered?: boolean | null; | |
| }; | |
| function commandId(roadmapId: string): string { | |
| const random = | |
| typeof crypto !== "undefined" && "randomUUID" in crypto | |
| ? crypto.randomUUID() | |
| : `${Date.now()}-${Math.random().toString(36).slice(2)}`; | |
| return `roadmap:${roadmapId}:${random}`.slice(0, 160); | |
| } | |
| async function sendCommand( | |
| roadmapId: string, | |
| command: Omit<RoadmapCommand, "client_command_id">, | |
| ): Promise<void> { | |
| await apiPost<RoadmapRemote, RoadmapCommand>( | |
| `/learn-anything-roadmaps/${encodeURIComponent(roadmapId)}/commands`, | |
| { client_command_id: commandId(roadmapId), ...command }, | |
| ); | |
| } | |
| function same(left: unknown, right: unknown): boolean { | |
| return JSON.stringify(left) === JSON.stringify(right); | |
| } | |
| async function syncProgressCommands( | |
| previous: LearnAnythingRoadmap, | |
| next: LearnAnythingRoadmap, | |
| ): Promise<void> { | |
| const previousStatuses = previous.topicStatus ?? {}; | |
| const nextStatuses = next.topicStatus ?? {}; | |
| const newLessonKeys = new Set( | |
| (next.completedLessons ?? []) | |
| .filter( | |
| (lesson) => | |
| lesson.lessonId !== "today" && | |
| !(previous.completedLessons ?? []).some( | |
| (previousLesson) => previousLesson.lessonId === lesson.lessonId, | |
| ), | |
| ) | |
| .map((lesson) => lesson.lessonId), | |
| ); | |
| const diagnosticKeys = !previous.diagnosticDone && next.diagnosticDone | |
| ? Object.keys(nextStatuses).filter( | |
| (key) => !previousStatuses[key] && Boolean(nextStatuses[key]), | |
| ) | |
| : []; | |
| const statusOwnedByExplicitAction = new Set([...newLessonKeys, ...diagnosticKeys]); | |
| for (const key of new Set([ | |
| ...Object.keys(previousStatuses), | |
| ...Object.keys(nextStatuses), | |
| ])) { | |
| if ( | |
| previousStatuses[key] === nextStatuses[key] || | |
| statusOwnedByExplicitAction.has(key) | |
| ) { | |
| continue; | |
| } | |
| await sendCommand(next.id, { | |
| command: "set_topic_status", | |
| topic_key: key, | |
| // Legacy self-marked "mastered" never becomes authoritative mastery. | |
| status: nextStatuses[key] ? "covered" : null, | |
| }); | |
| } | |
| if (!previous.diagnosticDone && next.diagnosticDone) { | |
| await sendCommand(next.id, { | |
| command: "record_diagnostic", | |
| known_topic_keys: diagnosticKeys, | |
| }); | |
| } | |
| for (const lesson of next.completedLessons ?? []) { | |
| if (!newLessonKeys.has(lesson.lessonId)) continue; | |
| await sendCommand(next.id, { | |
| command: "complete_lesson", | |
| topic_key: lesson.lessonId, | |
| title: lesson.title, | |
| }); | |
| } | |
| if (!same(previous.quizResult, next.quizResult) && next.quizResult) { | |
| await sendCommand(next.id, { | |
| command: "record_quiz", | |
| correct: next.quizResult.correct, | |
| total: next.quizResult.total, | |
| missed_questions: next.quizResult.missedQuestions, | |
| }); | |
| } | |
| for (const card of next.flashcards ?? []) { | |
| const before = previous.srState?.[card.front]; | |
| const after = next.srState?.[card.front]; | |
| if (same(before, after) || !after) continue; | |
| await sendCommand(next.id, { | |
| command: "review_flashcard", | |
| card_front: card.front, | |
| remembered: after.box > 0, | |
| }); | |
| } | |
| } | |
| function cleanInitialRoadmap( | |
| roadmap: LearnAnythingRoadmap, | |
| ): LearnAnythingRoadmap { | |
| const clean = { | |
| ...roadmap, | |
| completedWeeks: [], | |
| topicStatus: {}, | |
| diagnosticDone: false, | |
| srState: {}, | |
| completedLessons: [], | |
| progress: { ...roadmap.progress, completed: 0 }, | |
| }; | |
| delete clean.quizResult; | |
| return clean; | |
| } | |
| export async function saveRoadmapRemote(roadmap: LearnAnythingRoadmap): Promise<boolean> { | |
| if (!hasLiveSession()) return false; | |
| try { | |
| // A newly generated roadmap has no detail resource yet. Resolve its | |
| // existence from the already-owned collection so first save does not | |
| // intentionally hit a 404 (which browsers surface as a console error), | |
| // and so the UI cannot race a detail-page load against the initial POST. | |
| const remoteRows = await apiGet<RoadmapRemote[]>("/learn-anything-roadmaps"); | |
| let remote = remoteRows.find((row) => row.roadmap_id === roadmap.id)?.data ?? null; | |
| if (!remote) { | |
| const initial = cleanInitialRoadmap(roadmap); | |
| await apiPost<RoadmapRemote, { roadmap_id: string; data: LearnAnythingRoadmap }>( | |
| "/learn-anything-roadmaps", | |
| { roadmap_id: roadmap.id, data: initial }, | |
| ); | |
| remote = initial; | |
| } | |
| await syncProgressCommands(remote, roadmap); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| export async function getRoadmapRemote(id: string): Promise<LearnAnythingRoadmap | null> { | |
| if (!hasLiveSession()) return null; | |
| try { | |
| const remote = await apiGet<RoadmapRemote>(`/learn-anything-roadmaps/${encodeURIComponent(id)}`); | |
| return remote?.data ?? null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export async function listRoadmapsRemote(): Promise<LearnAnythingRoadmap[]> { | |
| if (!hasLiveSession()) return []; | |
| try { | |
| const rows = await apiGet<RoadmapRemote[]>("/learn-anything-roadmaps"); | |
| return (rows ?? []).map((row) => row.data).filter(Boolean); | |
| } catch { | |
| return []; | |
| } | |
| } | |