| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182 |
- import type { ReviewLesson } from './lesson-types';
- export type ReviewRating = 'revisit' | 'ready';
- export type ReviewState = {
- answers: Record<string, ReviewRating>;
- notes: Record<string, string>;
- evidence: Record<string, boolean>;
- contentChanged: boolean;
- };
- export function emptyReviewState(): ReviewState {
- return { answers: {}, notes: {}, evidence: {}, contentChanged: false };
- }
- // Invalidate old self-ratings whenever their questions or mastery criteria change.
- export function reviewSignature(lesson: ReviewLesson) {
- const assessable = (item: ReviewLesson['questions'][number]) => ({
- id: item.id,
- prompt: item.prompt,
- checkpoints: item.checkpoints,
- misconception: item.misconception,
- });
- const text = JSON.stringify({
- sourceModule: lesson.sourceModule,
- questions: lesson.questions.map(assessable),
- challenges: lesson.challenges.map(assessable),
- mastery: lesson.mastery,
- });
- let hash = 2166136261;
- for (let i = 0; i < text.length; i++) {
- hash ^= text.charCodeAt(i);
- hash = Math.imul(hash, 16777619);
- }
- return (hash >>> 0).toString(16);
- }
- export function restoreReviewState(
- raw: string | null,
- lesson: ReviewLesson,
- ): ReviewState {
- const state = emptyReviewState();
- if (!raw) return state;
- try {
- const saved = JSON.parse(raw);
- if (saved?.sourceModule !== lesson.sourceModule || !saved.state)
- return state;
- const sameContent = saved.signature === reviewSignature(lesson);
- state.contentChanged = !sameContent || saved.state.contentChanged === true;
- for (const item of [...lesson.questions, ...lesson.challenges]) {
- const value = saved.state?.answers?.[item.id];
- if (sameContent && (value === 'revisit' || value === 'ready'))
- state.answers[item.id] = value;
- const note = saved.state?.notes?.[item.id];
- if (typeof note === 'string') state.notes[item.id] = note.slice(0, 4000);
- }
- for (const item of lesson.mastery) {
- if (sameContent && saved.state?.evidence?.[item.id] === true)
- state.evidence[item.id] = true;
- }
- } catch {
- // Corrupt or unavailable browser storage must not prevent studying.
- }
- return state;
- }
- export function reviewProgress(lesson: ReviewLesson, state: ReviewState) {
- const items = [...lesson.questions, ...lesson.challenges];
- const ready = items.filter(
- (item) => state.answers[item.id] === 'ready',
- ).length;
- const evidence = lesson.mastery.filter(
- (item) => state.evidence[item.id] === true,
- ).length;
- return {
- ready,
- total: items.length,
- evidence,
- evidenceTotal: lesson.mastery.length,
- mastered:
- items.length > 0 &&
- lesson.mastery.length > 0 &&
- ready === items.length &&
- evidence === lesson.mastery.length,
- revisit: items.filter((item) => state.answers[item.id] !== 'ready'),
- };
- }
|