import { delay, demoFlashcardSet } from "@/lib/frontend-demo-data"; import { apiGet, apiPost } from "@/lib/api/client"; import { shouldUseDemoData, withDemoFallback } from "@/lib/api/mode"; import type { FlashcardGenerateRequest, FlashcardResponseItem, FlashcardSetResponse } from "@/lib/api/types"; type DemoCardWithReview = FlashcardResponseItem & { review: ReviewState; }; type ReviewResponse = { ok: boolean; set_id: string; card_index: number; new_due: string; new_ease?: number; new_interval?: number; quality: number; }; function getFlashcardItems(payload: FlashcardSetResponse["cards_json"]): FlashcardResponseItem[] { return Array.isArray(payload) ? payload : payload.cards; } export async function generateFlashcards(payload: FlashcardGenerateRequest) { if (shouldUseDemoData(payload.document_id)) { return delay({ ...demoFlashcardSet, document_id: payload.document_id, cards: getFlashcardItems(demoFlashcardSet.cards_json), }); } return withDemoFallback( () => apiPost("/flashcards/generate", payload), () => delay({ ...demoFlashcardSet, document_id: payload.document_id, }), ); } // ─── Spaced Repetition client (SM-2 due + record) ────────────────────────────── // Works in live + local demo (augments demo cards with review state on the fly). export type ReviewState = { ease: number; interval: number; reps: number; due: string; last_reviewed?: string | null; }; export type DueCard = { set_id: string; card_index: number; front: string; back: string; hint?: string | null; due: string; ease: number; interval_days: number; source_title?: string | null; }; export type DueCardsResponse = { due_count: number; cards: DueCard[]; also_from_notes?: number; }; const demoReviewStore: Record = {}; // key = `${setId}:${idx}` , session only for demo magic function _demoCardKey(setId: string, idx: number) { return `${setId}:${idx}`; } function _ensureDemoReview(card: FlashcardResponseItem, setId: string, idx: number): DemoCardWithReview { const key = _demoCardKey(setId, idx); if (!demoReviewStore[key]) { demoReviewStore[key] = { ease: 2.5, interval: 0, reps: 0, due: new Date().toISOString().slice(0, 10) }; } const rev = demoReviewStore[key]; return { ...card, review: rev, }; } export async function getDueFlashcards(): Promise { if (shouldUseDemoData("demo")) { // Build magical demo due set from the demoFlashcardSet + some note points const base = getFlashcardItems(demoFlashcardSet.cards_json); const today = new Date().toISOString().slice(0, 10); const demoDue: DueCard[] = base.map((c, i) => { const c2 = _ensureDemoReview(c, "flashcards-demo-1", i); const r = c2.review; return { set_id: "flashcards-demo-1", card_index: i, front: c.front || c2.front, back: c.back || c2.back, hint: c.hint || c.memory_hook, due: r.due || today, ease: r.ease, interval_days: r.interval || 0, source_title: "Demo Physics Notes", }; }).filter((d: DueCard) => d.due <= today); // Add 2 virtual "note points" always due for cramming demo demoDue.push({ set_id: "notes-demo", card_index: 0, front: "Key: Lenz's law opposes change in flux", back: "State direction of induced current opposes the change.", due: today, ease: 2.5, interval_days: 0, source_title: "Notes: Electromagnetic Induction", }); demoDue.push({ set_id: "notes-demo", card_index: 1, front: "Faraday's law: EMF = -dΦ/dt", back: "Magnitude of induced EMF equals rate of change of flux.", due: today, ease: 2.5, interval_days: 0, source_title: "Notes: Electromagnetic Induction", }); return delay({ due_count: demoDue.length, cards: demoDue, also_from_notes: 2 }); } return withDemoFallback( () => apiGet("/flashcards/due"), () => ({ due_count: 0, cards: [], also_from_notes: 0 }), ); } export async function recordCardReview(setId: string, cardIndex: number, quality: number): Promise { if (shouldUseDemoData("demo")) { const key = _demoCardKey(setId, cardIndex); const prev = demoReviewStore[key] || { ease: 2.5, interval: 0, reps: 0, due: new Date().toISOString().slice(0, 10) }; // simple local SM-2 mirror of backend let { ease, interval, reps } = prev; if (quality < 3) { reps = 0; interval = quality === 2 ? 1 : 0; } else { reps += 1; if (reps === 1) interval = 1; else if (reps === 2) interval = 6; else interval = Math.max(1, Math.round(interval * ease)); } ease = Math.max(1.3, Math.min(2.5, ease + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)))); const nextDue = new Date(Date.now() + interval * 86400000).toISOString().slice(0, 10); demoReviewStore[key] = { ease: Math.round(ease * 100) / 100, interval, reps, due: nextDue, last_reviewed: new Date().toISOString() }; return delay({ ok: true, set_id: setId, card_index: cardIndex, new_due: nextDue, new_ease: ease, new_interval: interval, quality }); } return withDemoFallback( () => apiPost("/flashcards/review", { set_id: setId, card_index: cardIndex, quality }), () => ({ ok: true, set_id: setId, card_index: cardIndex, new_due: "demo", quality }), ); }