feat(cards): multiple-choice Card-Type mit dynamischen Distractors

- CardTypeSchema: 'multiple-choice' (Felder: front + answer, distractor_pool optional)
- subIndexCount: 'multiple-choice' → 1
- GET /api/v1/decks/:deckId/distractors: N zufällige Feldwerte anderer Karten
  im Deck; field-Allowlist (front/back/answer/question); RANDOM() ORDER; Fallback
  auf distractor_pool wenn Deck < 4 Karten
- fetchDistractors(): Frontend-Client-Funktion
- MultipleChoiceView.svelte: lädt Distractors on mount, shuffelt 4 Optionen,
  zeigt Sofort-Feedback (correct/wrong/neutral), Keyboard 1–4 + Space;
  auto-grade correct→good, wrong→again
- Study-Page: isMultipleChoice + multipleChoiceData derived, Action-Bar
  ausgeblendet, onKey delegiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-10 15:28:37 +02:00
parent 0791436107
commit 2b36990e43
6 changed files with 351 additions and 4 deletions

View file

@ -153,6 +153,8 @@ export function subIndexCount(type: string): number {
return 1;
case 'multiple-choice':
return 1;
case 'multiple-choice':
return 1;
case 'cloze':
throw new Error(
'subIndexCount("cloze") not supported — use subIndexCountForCloze(text) from @cards/domain'

View file

@ -12,6 +12,7 @@ export const CardTypeSchema = z.enum([
'image-occlusion',
'audio-front',
'typing',
'multiple-choice',
]);
export type CardType = z.infer<typeof CardTypeSchema>;
@ -50,7 +51,7 @@ export function validateFieldsForType(
'image-occlusion': ['image_ref', 'mask_regions'],
'audio-front': ['audio_ref', 'back'],
typing: ['front', 'answer'],
'multiple-choice': ['question', 'options', 'correct_index'],
'multiple-choice': ['front', 'answer'],
};
const need = required[type] ?? [];
const missing = need.filter((k) => !(k in fields));