feat(web): audio-front Upload-Widget + typing Aliases-Feld + Edit-Fixes
audio-front: - AudioUploadField.svelte: Datei-Upload statt rohem media_ref-Textfeld; ruft uploadMedia() auf, zeigt Dateiname nach Upload + Replace-Button - Karten-Erstellungsseite: AudioUploadField ersetzt das unbrauchbare Textfeld - Edit-Seite: audio-front wird jetzt korrekt geladen (audio_ref + back statt dem falschen basic-else-Zweig) und gespeichert typing: - Aliases-Feld in Erstellungs- und Edit-Seite; kommagetrennte Alternativ- antworten werden in fields.aliases gespeichert und von checkTypingAnswer ausgewertet - Edit-Seite: typing wird jetzt korrekt geladen (front + answer + aliases) i18n: alle 5 Sprachen mit audio_upload_btn/uploading/failed/replace, typing_aliases_label/hint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
926ff685c7
commit
3669a86599
8 changed files with 254 additions and 21 deletions
143
apps/web/src/lib/components/AudioUploadField.svelte
Normal file
143
apps/web/src/lib/components/AudioUploadField.svelte
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { uploadMedia } from '$lib/api/media.ts';
|
||||||
|
import { t } from '$lib/i18n/index.svelte.ts';
|
||||||
|
|
||||||
|
let { mediaRef = $bindable('') }: { mediaRef: string } = $props();
|
||||||
|
|
||||||
|
let fileInput = $state<HTMLInputElement | null>(null);
|
||||||
|
let uploading = $state(false);
|
||||||
|
let uploadError = $state<string | null>(null);
|
||||||
|
let uploadedName = $state('');
|
||||||
|
|
||||||
|
async function handleFile(file: File) {
|
||||||
|
uploading = true;
|
||||||
|
uploadError = null;
|
||||||
|
try {
|
||||||
|
const result = await uploadMedia(file);
|
||||||
|
mediaRef = result.id;
|
||||||
|
uploadedName = file.name;
|
||||||
|
} catch (e) {
|
||||||
|
uploadError = e instanceof Error ? e.message : '?';
|
||||||
|
} finally {
|
||||||
|
uploading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPick(e: Event) {
|
||||||
|
const f = (e.currentTarget as HTMLInputElement).files?.[0];
|
||||||
|
if (f) handleFile(f);
|
||||||
|
(e.currentTarget as HTMLInputElement).value = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="upload-field">
|
||||||
|
{#if uploading}
|
||||||
|
<div class="status uploading">{t('card_new.audio_uploading')}</div>
|
||||||
|
{:else if mediaRef}
|
||||||
|
<div class="status done">
|
||||||
|
<span class="filename">{uploadedName || mediaRef}</span>
|
||||||
|
<button type="button" class="replace-btn" onclick={() => fileInput?.click()}>
|
||||||
|
{t('card_new.audio_replace')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button type="button" class="upload-btn" onclick={() => fileInput?.click()}>
|
||||||
|
{t('card_new.audio_upload_btn')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if uploadError}
|
||||||
|
<p class="error">{t('card_new.audio_upload_failed', { msg: uploadError })}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={fileInput}
|
||||||
|
type="file"
|
||||||
|
accept="audio/*,.mp3,.ogg,.wav,.m4a,.webm,.aac"
|
||||||
|
class="hidden"
|
||||||
|
onchange={onPick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border: 2px dashed hsl(var(--color-border));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.upload-btn:hover {
|
||||||
|
border-color: hsl(var(--color-primary));
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.625rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploading {
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
background: hsl(var(--color-border) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.done {
|
||||||
|
background: hsl(var(--color-success) / 0.08);
|
||||||
|
border: 1px solid hsl(var(--color-success) / 0.3);
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replace-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.replace-btn:hover {
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: hsl(var(--color-error));
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -120,6 +120,12 @@ export const de: TranslationNode = {
|
||||||
distractor_pool_placeholder: 'Ein Eintrag pro Zeile — Fallback wenn das Deck zu klein für KI-Distractors ist',
|
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_label: 'Audio-Referenz (media_ref)',
|
||||||
audio_ref_placeholder: 'z. B. abc123.mp3',
|
audio_ref_placeholder: 'z. B. abc123.mp3',
|
||||||
|
audio_upload_btn: '🎵 Audio-Datei hochladen',
|
||||||
|
audio_uploading: 'Lade hoch…',
|
||||||
|
audio_upload_failed: 'Upload fehlgeschlagen: {msg}',
|
||||||
|
audio_replace: 'Austauschen',
|
||||||
|
typing_aliases_label: 'Aliase (optional)',
|
||||||
|
typing_aliases_hint: 'Kommagetrennte Alternativantworten — z. B. Paris, Paris (Frankreich)',
|
||||||
back_audio_label: 'Antworttext (Markdown)',
|
back_audio_label: 'Antworttext (Markdown)',
|
||||||
toast_typing: 'Typing-Karte angelegt',
|
toast_typing: 'Typing-Karte angelegt',
|
||||||
toast_multiple_choice: 'Multiple-Choice-Karte angelegt',
|
toast_multiple_choice: 'Multiple-Choice-Karte angelegt',
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ export const en: TranslationNode = {
|
||||||
distractor_pool_placeholder: 'One entry per line — fallback when the deck is too small for AI distractors',
|
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_label: 'Audio reference (media_ref)',
|
||||||
audio_ref_placeholder: 'e.g. abc123.mp3',
|
audio_ref_placeholder: 'e.g. abc123.mp3',
|
||||||
|
audio_upload_btn: '🎵 Upload audio file',
|
||||||
|
audio_uploading: 'Uploading…',
|
||||||
|
audio_upload_failed: 'Upload failed: {msg}',
|
||||||
|
audio_replace: 'Replace',
|
||||||
|
typing_aliases_label: 'Aliases (optional)',
|
||||||
|
typing_aliases_hint: 'Comma-separated alternative answers — e.g. Paris, Paris (France)',
|
||||||
back_audio_label: 'Answer text (Markdown)',
|
back_audio_label: 'Answer text (Markdown)',
|
||||||
toast_typing: 'Typing card created',
|
toast_typing: 'Typing card created',
|
||||||
toast_multiple_choice: 'Multiple-choice card created',
|
toast_multiple_choice: 'Multiple-choice card created',
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ export const es: TranslationNode = {
|
||||||
distractor_pool_placeholder: 'Un elemento por línea — usado si el mazo es demasiado pequeño para distractores IA',
|
distractor_pool_placeholder: 'Un elemento por línea — usado si el mazo es demasiado pequeño para distractores IA',
|
||||||
audio_ref_label: 'Referencia de audio (media_ref)',
|
audio_ref_label: 'Referencia de audio (media_ref)',
|
||||||
audio_ref_placeholder: 'ej. abc123.mp3',
|
audio_ref_placeholder: 'ej. abc123.mp3',
|
||||||
|
audio_upload_btn: '🎵 Subir archivo de audio',
|
||||||
|
audio_uploading: 'Subiendo…',
|
||||||
|
audio_upload_failed: 'Error al subir: {msg}',
|
||||||
|
audio_replace: 'Reemplazar',
|
||||||
|
typing_aliases_label: 'Alias (opcional)',
|
||||||
|
typing_aliases_hint: 'Respuestas alternativas separadas por comas — ej. París, París (Francia)',
|
||||||
back_audio_label: 'Texto de respuesta (Markdown)',
|
back_audio_label: 'Texto de respuesta (Markdown)',
|
||||||
toast_typing: 'Tarjeta de escritura creada',
|
toast_typing: 'Tarjeta de escritura creada',
|
||||||
toast_multiple_choice: 'Tarjeta de opción múltiple creada',
|
toast_multiple_choice: 'Tarjeta de opción múltiple creada',
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ export const fr: TranslationNode = {
|
||||||
distractor_pool_placeholder: 'Un élément par ligne — utilisé si la collection est trop petite pour les distracteurs IA',
|
distractor_pool_placeholder: 'Un élément par ligne — utilisé si la collection est trop petite pour les distracteurs IA',
|
||||||
audio_ref_label: 'Référence audio (media_ref)',
|
audio_ref_label: 'Référence audio (media_ref)',
|
||||||
audio_ref_placeholder: 'ex. abc123.mp3',
|
audio_ref_placeholder: 'ex. abc123.mp3',
|
||||||
|
audio_upload_btn: '🎵 Téléverser un fichier audio',
|
||||||
|
audio_uploading: 'Téléversement…',
|
||||||
|
audio_upload_failed: 'Échec du téléversement : {msg}',
|
||||||
|
audio_replace: 'Remplacer',
|
||||||
|
typing_aliases_label: 'Alias (facultatif)',
|
||||||
|
typing_aliases_hint: 'Réponses alternatives séparées par des virgules — ex. Paris, Paris (France)',
|
||||||
back_audio_label: 'Texte de réponse (Markdown)',
|
back_audio_label: 'Texte de réponse (Markdown)',
|
||||||
toast_typing: 'Carte de saisie créée',
|
toast_typing: 'Carte de saisie créée',
|
||||||
toast_multiple_choice: 'Carte à choix multiple créée',
|
toast_multiple_choice: 'Carte à choix multiple créée',
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ export const it: TranslationNode = {
|
||||||
distractor_pool_placeholder: 'Un elemento per riga — usato se il mazzo è troppo piccolo per i distrattori IA',
|
distractor_pool_placeholder: 'Un elemento per riga — usato se il mazzo è troppo piccolo per i distrattori IA',
|
||||||
audio_ref_label: 'Riferimento audio (media_ref)',
|
audio_ref_label: 'Riferimento audio (media_ref)',
|
||||||
audio_ref_placeholder: 'es. abc123.mp3',
|
audio_ref_placeholder: 'es. abc123.mp3',
|
||||||
|
audio_upload_btn: '🎵 Carica file audio',
|
||||||
|
audio_uploading: 'Caricamento…',
|
||||||
|
audio_upload_failed: 'Caricamento fallito: {msg}',
|
||||||
|
audio_replace: 'Sostituisci',
|
||||||
|
typing_aliases_label: 'Alias (facoltativo)',
|
||||||
|
typing_aliases_hint: 'Risposte alternative separate da virgola — es. Parigi, Parigi (Francia)',
|
||||||
back_audio_label: 'Testo risposta (Markdown)',
|
back_audio_label: 'Testo risposta (Markdown)',
|
||||||
toast_typing: 'Carta di digitazione creata',
|
toast_typing: 'Carta di digitazione creata',
|
||||||
toast_multiple_choice: 'Carta a scelta multipla creata',
|
toast_multiple_choice: 'Carta a scelta multipla creata',
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import { t } from '$lib/i18n/index.svelte.ts';
|
import { t } from '$lib/i18n/index.svelte.ts';
|
||||||
import ImageOcclusionEditor from '$lib/components/ImageOcclusionEditor.svelte';
|
import ImageOcclusionEditor from '$lib/components/ImageOcclusionEditor.svelte';
|
||||||
import MultipleChoiceCardForm from '$lib/components/MultipleChoiceCardForm.svelte';
|
import MultipleChoiceCardForm from '$lib/components/MultipleChoiceCardForm.svelte';
|
||||||
|
import AudioUploadField from '$lib/components/AudioUploadField.svelte';
|
||||||
|
|
||||||
let card = $state<Card | null>(null);
|
let card = $state<Card | null>(null);
|
||||||
let cardType = $state<CardType>('basic');
|
let cardType = $state<CardType>('basic');
|
||||||
|
|
@ -26,6 +27,9 @@
|
||||||
let extra = $state('');
|
let extra = $state('');
|
||||||
let imageRef = $state('');
|
let imageRef = $state('');
|
||||||
let maskRegionsJson = $state('[]');
|
let maskRegionsJson = $state('[]');
|
||||||
|
let answer = $state('');
|
||||||
|
let aliases = $state('');
|
||||||
|
let audioFileRef = $state('');
|
||||||
let mcOptions = $state(['', '', '', '']);
|
let mcOptions = $state(['', '', '', '']);
|
||||||
let mcCorrectIdx = $state(0);
|
let mcCorrectIdx = $state(0);
|
||||||
let mcExplanation = $state('');
|
let mcExplanation = $state('');
|
||||||
|
|
@ -68,6 +72,13 @@
|
||||||
mcOptions = [answer, ...distractors, '', '', ''].slice(0, 4);
|
mcOptions = [answer, ...distractors, '', '', ''].slice(0, 4);
|
||||||
mcCorrectIdx = 0;
|
mcCorrectIdx = 0;
|
||||||
mcExplanation = fields.explanation ?? '';
|
mcExplanation = fields.explanation ?? '';
|
||||||
|
} else if (c.type === 'typing') {
|
||||||
|
front = fields.front ?? '';
|
||||||
|
answer = fields.answer ?? '';
|
||||||
|
aliases = fields.aliases ?? '';
|
||||||
|
} else if (c.type === 'audio-front') {
|
||||||
|
audioFileRef = fields.audio_ref ?? '';
|
||||||
|
back = fields.back ?? '';
|
||||||
} else {
|
} else {
|
||||||
front = fields.front ?? '';
|
front = fields.front ?? '';
|
||||||
back = fields.back ?? '';
|
back = fields.back ?? '';
|
||||||
|
|
@ -89,6 +100,8 @@
|
||||||
const filledCount = mcOptions.filter((o) => o.trim()).length;
|
const filledCount = mcOptions.filter((o) => o.trim()).length;
|
||||||
return front.trim().length > 0 && mcOptions[mcCorrectIdx].trim().length > 0 && filledCount >= 2;
|
return front.trim().length > 0 && mcOptions[mcCorrectIdx].trim().length > 0 && filledCount >= 2;
|
||||||
}
|
}
|
||||||
|
if (cardType === 'typing') 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;
|
return front.trim().length > 0 && back.trim().length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -112,6 +125,11 @@
|
||||||
fields = { front: front.trim(), answer: mcOptions[mcCorrectIdx].trim() };
|
fields = { front: front.trim(), answer: mcOptions[mcCorrectIdx].trim() };
|
||||||
if (distractors) fields.distractor_pool = distractors;
|
if (distractors) fields.distractor_pool = distractors;
|
||||||
if (mcExplanation.trim()) fields.explanation = mcExplanation.trim();
|
if (mcExplanation.trim()) fields.explanation = mcExplanation.trim();
|
||||||
|
} else if (cardType === 'typing') {
|
||||||
|
fields = { front: front.trim(), answer: answer.trim() };
|
||||||
|
if (aliases.trim()) fields.aliases = aliases.trim();
|
||||||
|
} else if (cardType === 'audio-front') {
|
||||||
|
fields = { audio_ref: audioFileRef.trim(), back: back.trim() };
|
||||||
} else {
|
} else {
|
||||||
fields = { front: front.trim(), back: back.trim() };
|
fields = { front: front.trim(), back: back.trim() };
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +225,55 @@
|
||||||
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"
|
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>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
{:else if cardType === 'typing'}
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm font-medium">{t('card_new.front_label')}</span>
|
||||||
|
<textarea
|
||||||
|
bind:value={front}
|
||||||
|
required
|
||||||
|
rows="8"
|
||||||
|
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>
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm font-medium">{t('card_new.answer_label')}</span>
|
||||||
|
<textarea
|
||||||
|
bind:value={answer}
|
||||||
|
required
|
||||||
|
rows="8"
|
||||||
|
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>
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm font-medium">{t('card_new.typing_aliases_label')}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={aliases}
|
||||||
|
placeholder="Paris, Paris (Frankreich)"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">{t('card_new.typing_aliases_hint')}</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{:else if cardType === 'audio-front'}
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm font-medium">{t('card_new.audio_ref_label')}</span>
|
||||||
|
<div class="mt-1">
|
||||||
|
<AudioUploadField bind:mediaRef={audioFileRef} />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm font-medium">{t('card_new.back_audio_label')}</span>
|
||||||
|
<textarea
|
||||||
|
bind:value={back}
|
||||||
|
required
|
||||||
|
rows="6"
|
||||||
|
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}
|
{:else}
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
import ImageOcclusionEditor from '$lib/components/ImageOcclusionEditor.svelte';
|
import ImageOcclusionEditor from '$lib/components/ImageOcclusionEditor.svelte';
|
||||||
import ClozeCardForm from '$lib/components/ClozeCardForm.svelte';
|
import ClozeCardForm from '$lib/components/ClozeCardForm.svelte';
|
||||||
import MultipleChoiceCardForm from '$lib/components/MultipleChoiceCardForm.svelte';
|
import MultipleChoiceCardForm from '$lib/components/MultipleChoiceCardForm.svelte';
|
||||||
|
import AudioUploadField from '$lib/components/AudioUploadField.svelte';
|
||||||
import CardSurface from '$lib/components/CardSurface.svelte';
|
import CardSurface from '$lib/components/CardSurface.svelte';
|
||||||
|
|
||||||
type DeckLite = { id: string; name: string };
|
type DeckLite = { id: string; name: string };
|
||||||
|
|
@ -33,6 +34,7 @@
|
||||||
let imageRef = $state('');
|
let imageRef = $state('');
|
||||||
let maskRegionsJson = $state('[]');
|
let maskRegionsJson = $state('[]');
|
||||||
let answer = $state('');
|
let answer = $state('');
|
||||||
|
let aliases = $state('');
|
||||||
let audioFileRef = $state('');
|
let audioFileRef = $state('');
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let previewFlipped = $state(false);
|
let previewFlipped = $state(false);
|
||||||
|
|
@ -119,6 +121,7 @@
|
||||||
fields = { image_ref: imageRef, mask_regions: maskRegionsJson };
|
fields = { image_ref: imageRef, mask_regions: maskRegionsJson };
|
||||||
} else if (cardType === 'typing') {
|
} else if (cardType === 'typing') {
|
||||||
fields = { front: front.trim(), answer: answer.trim() };
|
fields = { front: front.trim(), answer: answer.trim() };
|
||||||
|
if (aliases.trim()) fields.aliases = aliases.trim();
|
||||||
} else if (cardType === 'multiple-choice') {
|
} else if (cardType === 'multiple-choice') {
|
||||||
const distractors = mcOptions
|
const distractors = mcOptions
|
||||||
.filter((_, i) => i !== mcCorrectIdx)
|
.filter((_, i) => i !== mcCorrectIdx)
|
||||||
|
|
@ -223,21 +226,23 @@
|
||||||
placeholder={t('card_new.answer_placeholder')}
|
placeholder={t('card_new.answer_placeholder')}
|
||||||
class="input mono"
|
class="input mono"
|
||||||
></textarea>
|
></textarea>
|
||||||
<span class="field-hint">Kommagetrennte Aliase erlaubt: <code>Paris, Paris (Frankreich)</code></span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">{t('card_new.typing_aliases_label')}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={aliases}
|
||||||
|
placeholder="Paris, Paris (Frankreich)"
|
||||||
|
class="input mono"
|
||||||
|
/>
|
||||||
|
<span class="field-hint">{t('card_new.typing_aliases_hint')}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
{:else if cardType === 'audio-front'}
|
{:else if cardType === 'audio-front'}
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field-label">{t('card_new.audio_ref_label')}</span>
|
<span class="field-label">{t('card_new.audio_ref_label')}</span>
|
||||||
<input
|
<AudioUploadField bind:mediaRef={audioFileRef} />
|
||||||
type="text"
|
|
||||||
bind:value={audioFileRef}
|
|
||||||
required
|
|
||||||
placeholder={t('card_new.audio_ref_placeholder')}
|
|
||||||
class="input mono"
|
|
||||||
/>
|
|
||||||
<span class="field-hint">Die media_ref-ID aus dem Media-Upload-Endpoint.</span>
|
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field-label">{t('card_new.back_audio_label')}</span>
|
<span class="field-label">{t('card_new.back_audio_label')}</span>
|
||||||
|
|
@ -524,18 +529,6 @@
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: hsl(var(--color-muted-foreground));
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.field-hint code {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
background: hsl(var(--color-muted) / 0.5);
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-error {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: hsl(var(--color-error));
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue