diff --git a/apps/web/src/lib/i18n/de.ts b/apps/web/src/lib/i18n/de.ts index f182f38..da90080 100644 --- a/apps/web/src/lib/i18n/de.ts +++ b/apps/web/src/lib/i18n/de.ts @@ -97,6 +97,19 @@ export const de: TranslationNode = { toast_cloze: '{n} Reviews initialisiert (1 pro Cluster)', toast_image_occlusion: '{n} Reviews initialisiert (1 pro Maske)', type_image_occlusion: 'Image-Occlusion (Bild + N Masken)', + type_typing: 'Typing (Texteingabe, Fuzzy-Match)', + type_multiple_choice: 'Multiple-Choice (4 Optionen, KI-Distractors)', + type_audio_front: 'Audio-Front (Hören + Antworten)', + answer_label: 'Antwort (Markdown)', + answer_placeholder: 'Richtige Antwort', + distractor_pool_label: 'Distractor-Pool (optional)', + distractor_pool_placeholder: 'Ein Eintrag pro Zeile — Fallback wenn das Deck zu klein für KI-Distractors ist', + audio_ref_label: 'Audio-Referenz (media_ref)', + audio_ref_placeholder: 'z. B. abc123.mp3', + back_audio_label: 'Antworttext (Markdown)', + toast_typing: 'Typing-Karte angelegt', + toast_multiple_choice: 'Multiple-Choice-Karte angelegt', + toast_audio_front: 'Audio-Front-Karte angelegt', decks_load_failed: 'Decks konnten nicht geladen werden: {msg}', }, card_edit: { diff --git a/apps/web/src/lib/i18n/en.ts b/apps/web/src/lib/i18n/en.ts index 8860bb3..eb99780 100644 --- a/apps/web/src/lib/i18n/en.ts +++ b/apps/web/src/lib/i18n/en.ts @@ -94,6 +94,19 @@ export const en: TranslationNode = { toast_cloze: '{n} reviews initialized (1 per cluster)', toast_image_occlusion: '{n} reviews initialized (1 per mask)', type_image_occlusion: 'Image-Occlusion (image + N masks)', + type_typing: 'Typing (text input, fuzzy match)', + type_multiple_choice: 'Multiple-Choice (4 options, AI distractors)', + type_audio_front: 'Audio-Front (listen + answer)', + answer_label: 'Answer (Markdown)', + answer_placeholder: 'Correct answer', + distractor_pool_label: 'Distractor pool (optional)', + distractor_pool_placeholder: 'One entry per line — fallback when the deck is too small for AI distractors', + audio_ref_label: 'Audio reference (media_ref)', + audio_ref_placeholder: 'e.g. abc123.mp3', + back_audio_label: 'Answer text (Markdown)', + toast_typing: 'Typing card created', + toast_multiple_choice: 'Multiple-choice card created', + toast_audio_front: 'Audio-front card created', decks_load_failed: 'Could not load decks: {msg}', }, card_edit: { diff --git a/apps/web/src/routes/cards/new/+page.svelte b/apps/web/src/routes/cards/new/+page.svelte index 614f90e..fe4bfc1 100644 --- a/apps/web/src/routes/cards/new/+page.svelte +++ b/apps/web/src/routes/cards/new/+page.svelte @@ -27,6 +27,9 @@ let extra = $state(''); let imageRef = $state(''); let maskRegionsJson = $state('[]'); + let answer = $state(''); + let distractorPool = $state(''); + let audioFileRef = $state(''); let saving = $state(false); const frontHtml = $derived(renderMarkdown(front)); @@ -65,12 +68,10 @@ const canSave = $derived.by(() => { if (saving || !deckId) return false; - if (cardType === 'cloze') { - return text.trim().length > 0 && clusterIds.length > 0; - } - if (cardType === 'image-occlusion') { - return imageRef.length > 0 && maskCount > 0; - } + if (cardType === 'cloze') return text.trim().length > 0 && clusterIds.length > 0; + if (cardType === 'image-occlusion') return imageRef.length > 0 && maskCount > 0; + if (cardType === 'typing' || cardType === 'multiple-choice') return front.trim().length > 0 && answer.trim().length > 0; + if (cardType === 'audio-front') return audioFileRef.trim().length > 0 && back.trim().length > 0; return front.trim().length > 0 && back.trim().length > 0; }); @@ -86,6 +87,13 @@ : { text: text.trim() }; } else if (cardType === 'image-occlusion') { fields = { image_ref: imageRef, mask_regions: maskRegionsJson }; + } else if (cardType === 'typing') { + fields = { front: front.trim(), answer: answer.trim() }; + } else if (cardType === 'multiple-choice') { + fields = { front: front.trim(), answer: answer.trim() }; + if (distractorPool.trim()) fields.distractor_pool = distractorPool.trim(); + } else if (cardType === 'audio-front') { + fields = { audio_ref: audioFileRef.trim(), back: back.trim() }; } else { fields = { front: front.trim(), back: back.trim() }; } @@ -95,9 +103,15 @@ ? t('card_new.toast_cloze', { n: clusterIds.length }) : cardType === 'image-occlusion' ? t('card_new.toast_image_occlusion', { n: maskCount }) - : cardType === 'basic-reverse' - ? t('card_new.toast_basic_reverse') - : t('card_new.toast_basic'); + : cardType === 'typing' + ? t('card_new.toast_typing') + : cardType === 'multiple-choice' + ? t('card_new.toast_multiple_choice') + : cardType === 'audio-front' + ? t('card_new.toast_audio_front') + : cardType === 'basic-reverse' + ? t('card_new.toast_basic_reverse') + : t('card_new.toast_basic'); toasts.success(msg); goto(`/decks/${card.deck_id}`); } catch (e) { @@ -137,6 +151,9 @@ + + + @@ -186,6 +203,79 @@ class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm" > + {:else if cardType === 'typing' || cardType === 'multiple-choice'} +