DocDoeAI / src /lib /api /flashcards.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
5.65 kB
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<FlashcardSetResponse, FlashcardGenerateRequest>("/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<string, ReviewState> = {}; // 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<DueCardsResponse> {
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<DueCardsResponse>("/flashcards/due"),
() => ({ due_count: 0, cards: [], also_from_notes: 0 }),
);
}
export async function recordCardReview(setId: string, cardIndex: number, quality: number): Promise<ReviewResponse> {
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<ReviewResponse, { set_id: string; card_index: number; quality: number }>("/flashcards/review", { set_id: setId, card_index: cardIndex, quality }),
() => ({ ok: true, set_id: setId, card_index: cardIndex, new_due: "demo", quality }),
);
}