feat(cards/new): typing, multiple-choice, audio-front im Erstellungs-UI
- Dropdown um 3 neue Types erweitert - typing/multiple-choice: front + answer Felder (korrekte field-Namen) - multiple-choice: optionaler distractor_pool (Fallback für kleine Decks) - audio-front: audio_ref Text-Input + back Antworttext - canSave + onSubmit korrekt pro Type - i18n de + en vollständig Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2b36990e43
commit
a612ad05d6
3 changed files with 125 additions and 9 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,6 +103,12 @@
|
|||
? t('card_new.toast_cloze', { n: clusterIds.length })
|
||||
: cardType === 'image-occlusion'
|
||||
? t('card_new.toast_image_occlusion', { n: maskCount })
|
||||
: 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');
|
||||
|
|
@ -137,6 +151,9 @@
|
|||
<option value="basic-reverse">{t('card_new.type_basic_reverse')}</option>
|
||||
<option value="cloze">{t('card_new.type_cloze')}</option>
|
||||
<option value="image-occlusion">{t('card_new.type_image_occlusion')}</option>
|
||||
<option value="typing">{t('card_new.type_typing')}</option>
|
||||
<option value="multiple-choice">{t('card_new.type_multiple_choice')}</option>
|
||||
<option value="audio-front">{t('card_new.type_audio_front')}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -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"
|
||||
></textarea>
|
||||
</label>
|
||||
{:else if cardType === 'typing' || cardType === 'multiple-choice'}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{t('card_new.front_label')}</span>
|
||||
<textarea
|
||||
bind:value={front}
|
||||
required
|
||||
rows="8"
|
||||
placeholder={t('card_new.front_placeholder')}
|
||||
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"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if front.trim()}
|
||||
<div class="mt-2 rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">{t('card_new.preview_label')}</div>
|
||||
<div class="prose prose-sm max-w-none">{@html frontHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{t('card_new.answer_label')}</span>
|
||||
<textarea
|
||||
bind:value={answer}
|
||||
required
|
||||
rows="8"
|
||||
placeholder={t('card_new.answer_placeholder')}
|
||||
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"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if cardType === 'multiple-choice'}
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{t('card_new.distractor_pool_label')}</span>
|
||||
<textarea
|
||||
bind:value={distractorPool}
|
||||
rows="4"
|
||||
placeholder={t('card_new.distractor_pool_placeholder')}
|
||||
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"
|
||||
></textarea>
|
||||
</label>
|
||||
{/if}
|
||||
{:else if cardType === 'audio-front'}
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{t('card_new.audio_ref_label')}</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={audioFileRef}
|
||||
required
|
||||
placeholder={t('card_new.audio_ref_placeholder')}
|
||||
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"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{t('card_new.back_audio_label')}</span>
|
||||
<textarea
|
||||
bind:value={back}
|
||||
required
|
||||
rows="6"
|
||||
placeholder={t('card_new.back_placeholder')}
|
||||
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"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if back.trim()}
|
||||
<div class="rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">{t('card_new.preview_label')}</div>
|
||||
<div class="prose prose-sm max-w-none">{@html backHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue