Files
wisp/src/lib/learn/flashcards.ts
T
NilsBriggen 40858e0025
CI / test (push) Successful in 16s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 30s
feat(phase3): learning helpers — summary, glossary, flashcards (SM-2), quizzes
Deterministic, on-device, no model:
- src/lib/learn pure modules (tokenize, summary [TextRank-ish], glossary
  [definition-pattern + frequency], flashcards [cloze/Q-A], srs [SM-2], quiz
  [MCQ with distractors]) — 37 unit tests.
- Flashcard persistence: Dexie v4 + native v4 `flashcards` table; create/list/
  listDue/updateSrs/delete/counts; cascades (transcript delete, course->Unsorted).
- UI: transcript "Study aids" (generate summary+glossary, click-to-seek; create
  flashcards), Study screen (SM-2 review + Anki CSV export), per-lecture Quiz,
  library Study link with due-count badge.

215 tests green, 0 tsc errors, web export builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:37:42 +02:00

52 lines
1.7 KiB
TypeScript

// Turn glossary entries into flashcard seeds. Deterministic, no model.
// A seed is a UI-friendly front/back pair carrying its source provenance; the
// repo later wraps it into a FlashcardDraft + SRS state.
//
// IMPORTANT: relative imports only inside src/lib (vitest has no '@/*' alias).
import type { GlossaryEntry } from './glossary';
/** A front/back pair for a flashcard, with optional source anchor. */
export interface CardSeed {
front: string;
back: string;
/** Source segment id, for click-to-seek. */
segmentId?: string;
/** Source start time (seconds). */
start?: number;
}
/**
* Build one {@link CardSeed} per glossary entry.
*
* Front: if the term appears verbatim (case-insensitively) inside the
* definition, we make a cloze deletion — blanking the term in the definition so
* the learner recalls it in context. Otherwise we fall back to the plain
* "What is {term}?" prompt. Back is always the full definition.
*/
export function cardsFromGlossary(entries: GlossaryEntry[]): CardSeed[] {
return entries.map((entry) => {
const front = makeFront(entry.term, entry.definition);
return {
front,
back: entry.definition,
segmentId: entry.segmentId,
start: entry.start,
};
});
}
/** "_____" cloze if the term occurs in the definition, else a Q prompt. */
function makeFront(term: string, definition: string): string {
const re = new RegExp(escapeRegExp(term), 'i');
if (re.test(definition)) {
return definition.replace(re, '_____');
}
return `What is ${term}?`;
}
/** Escape a string for safe use inside a RegExp. */
function escapeRegExp(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}