diff --git a/apps/web/src/lib/api/error.ts b/apps/web/src/lib/api/error.ts new file mode 100644 index 0000000..2f153d8 --- /dev/null +++ b/apps/web/src/lib/api/error.ts @@ -0,0 +1,21 @@ +import { ApiError } from './client.ts'; + +/** + * Extrahiert die beste verfügbare Fehlermeldung aus einem unbekannten Fehler. + * Bei ApiError wird `body.detail` oder `body.error` bevorzugt — das sind die + * vom Server gesetzten, lesbaren Fehlertexte. Fallback auf Error.message bzw. + * String-Konvertierung. + */ +export function apiErrorMessage(err: unknown): string { + if (err instanceof ApiError) { + const b = err.body; + if (b && typeof b === 'object') { + const body = b as Record; + if (typeof body.detail === 'string' && body.detail) return body.detail; + if (typeof body.error === 'string' && body.error) return body.error; + } + return `Fehler ${err.status}`; + } + if (err instanceof Error) return err.message; + return String(err); +} diff --git a/apps/web/src/lib/components/ImageOcclusionEditor.svelte b/apps/web/src/lib/components/ImageOcclusionEditor.svelte index 0c2dbe0..7b0c146 100644 --- a/apps/web/src/lib/components/ImageOcclusionEditor.svelte +++ b/apps/web/src/lib/components/ImageOcclusionEditor.svelte @@ -12,6 +12,7 @@ import { API_BASE } from '$lib/api/client.ts'; import { toasts } from '$lib/stores/toasts.svelte.ts'; import { t } from '$lib/i18n/index.svelte.ts'; + import { apiErrorMessage } from '$lib/api/error.ts'; let { imageRef = $bindable(''), @@ -44,7 +45,7 @@ imageRef = r.id; maskRegionsJson = '[]'; } catch (err) { - toasts.error(`${(err as Error).message}`); + toasts.error(`${apiErrorMessage(err)}`); } finally { uploading = false; input.value = ''; diff --git a/apps/web/src/lib/components/MultipleChoiceView.svelte b/apps/web/src/lib/components/MultipleChoiceView.svelte index 03a0233..cfa493c 100644 --- a/apps/web/src/lib/components/MultipleChoiceView.svelte +++ b/apps/web/src/lib/components/MultipleChoiceView.svelte @@ -22,6 +22,7 @@ let options = $state([]); let selected = $state(null); let loading = $state(true); + let tooFewOptions = $state(false); const correct = $derived(selected === answer); @@ -51,6 +52,13 @@ distractors = [...distractors, ...poolItems].slice(0, 3); } + // Zu wenig Distractors: Fallback auf Antwort-Anzeige mit manueller Bewertung + if (distractors.length < 1) { + tooFewOptions = true; + loading = false; + return; + } + options = shuffle([answer, ...distractors.slice(0, 3)]); loading = false; }); @@ -100,6 +108,15 @@ {#if loading}
Optionen werden geladen …
+ {:else if tooFewOptions} +
+

Deck hat zu wenig Karten für Multiple Choice.

+
{answer}
+
+ + +
+
{:else}
{#each options as opt, i (opt)} @@ -168,6 +185,47 @@ padding: 0.5rem 0; } + .few-options { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + .few-hint { + margin: 0; + font-size: 0.8125rem; + color: hsl(var(--color-muted-foreground)); + } + .few-answer { + padding: 0.75rem 1rem; + border-radius: 0.5rem; + background: hsl(var(--color-surface)); + border: 1px solid hsl(var(--color-border)); + font-size: 1rem; + } + .few-actions { + display: flex; + gap: 0.5rem; + } + .few-btn { + flex: 1; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + border: 1px solid hsl(var(--color-border)); + font: inherit; + font-size: 0.9375rem; + cursor: pointer; + } + .few-btn-again { + background: hsl(var(--color-error) / 0.1); + border-color: hsl(var(--color-error) / 0.4); + color: hsl(var(--color-foreground)); + } + .few-btn-good { + background: hsl(var(--color-success) / 0.1); + border-color: hsl(var(--color-success) / 0.4); + color: hsl(var(--color-foreground)); + } + .options { display: flex; flex-direction: column; diff --git a/apps/web/src/lib/components/NewDeckCard.svelte b/apps/web/src/lib/components/NewDeckCard.svelte index b8d9fa7..3b5f882 100644 --- a/apps/web/src/lib/components/NewDeckCard.svelte +++ b/apps/web/src/lib/components/NewDeckCard.svelte @@ -7,6 +7,7 @@ import { i18n, t } from '$lib/i18n/index.svelte.ts'; import CardSurface from './CardSurface.svelte'; import DeckCategoryIcon from './DeckCategoryIcon.svelte'; + import { apiErrorMessage } from '$lib/api/error.ts'; let open = $state(false); let catOpen = $state(false); @@ -95,7 +96,7 @@ toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`); goto(`/decks/${result.deck.id}`); } catch (err) { - imageError = (err as Error).message; + imageError = apiErrorMessage(err); imageGenerating = false; } } @@ -119,7 +120,7 @@ toasts.success(`${deck.name} ✓`); goto(`/decks/${deck.id}`); } catch (err) { - toasts.error(t('deck_new.create_failed', { msg: (err as Error).message })); + toasts.error(t('deck_new.create_failed', { msg: apiErrorMessage(err) })); saving = false; } } @@ -133,7 +134,7 @@ toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`); goto(`/decks/${result.deck.id}`); } catch (err) { - aiError = (err as Error).message; + aiError = apiErrorMessage(err); generating = false; } } diff --git a/apps/web/src/lib/components/marketplace/DiscussionThread.svelte b/apps/web/src/lib/components/marketplace/DiscussionThread.svelte index fd1196c..fd88518 100644 --- a/apps/web/src/lib/components/marketplace/DiscussionThread.svelte +++ b/apps/web/src/lib/components/marketplace/DiscussionThread.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; hideDiscussion, listDiscussions, postDiscussion, @@ -37,7 +38,7 @@ const result = await listDiscussions(cardContentHash); comments = result.discussions; } catch (e) { - toasts.error(`Discussions laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Discussions laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loading = false; } @@ -53,7 +54,7 @@ replyToId = null; await refresh(); } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { posting = false; } @@ -65,7 +66,7 @@ await hideDiscussion(id); await refresh(); } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } } diff --git a/apps/web/src/lib/components/marketplace/PublishDeckModal.svelte b/apps/web/src/lib/components/marketplace/PublishDeckModal.svelte index 801c7e8..482ef2d 100644 --- a/apps/web/src/lib/components/marketplace/PublishDeckModal.svelte +++ b/apps/web/src/lib/components/marketplace/PublishDeckModal.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import type { Card, Deck } from '@cards/domain'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; getMyAuthorProfile, getMarketplaceDeck, initMarketplaceDeck, @@ -125,7 +126,7 @@ ); onPublished(slug.trim()); } catch (e) { - error = (e as Error).message; + error = apiErrorMessage(e); busy = false; } } diff --git a/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte b/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte index 86ac716..7180a29 100644 --- a/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte +++ b/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte @@ -1,6 +1,7 @@ diff --git a/apps/web/src/routes/cards/new/+page.svelte b/apps/web/src/routes/cards/new/+page.svelte index e0b5d1a..41d8e30 100644 --- a/apps/web/src/routes/cards/new/+page.svelte +++ b/apps/web/src/routes/cards/new/+page.svelte @@ -3,6 +3,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; extractClusterIds, maskRegionCount, renderClozePrompt, @@ -82,7 +83,7 @@ } } } catch (e) { - toasts.error(t('card_new.decks_load_failed', { msg: (e as Error).message })); + toasts.error(t('card_new.decks_load_failed', { msg: apiErrorMessage(e) })); } }); @@ -145,7 +146,7 @@ toasts.success(msg); goto(`/decks/${card.deck_id}`); } catch (e) { - toasts.error(t('card_new.create_failed', { msg: (e as Error).message })); + toasts.error(t('card_new.create_failed', { msg: apiErrorMessage(e) })); saving = false; } } diff --git a/apps/web/src/routes/d/[slug]/+page.svelte b/apps/web/src/routes/d/[slug]/+page.svelte index 40545e3..5a83a1f 100644 --- a/apps/web/src/routes/d/[slug]/+page.svelte +++ b/apps/web/src/routes/d/[slug]/+page.svelte @@ -5,6 +5,7 @@ import { goto } from '$app/navigation'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; forkDeck, getMarketplaceDeck, getMarketplaceVersion, @@ -80,7 +81,7 @@ cards = version; discussionCounts = counts; } catch (e) { - toasts.error(`Deck laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Deck laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loading = false; } @@ -101,7 +102,7 @@ starred = true; } } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { busy = false; } @@ -124,7 +125,7 @@ toasts.success('Abonniert. Update-Benachrichtigung an.'); } } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { busy = false; } @@ -141,7 +142,7 @@ toasts.success(`Deck geforkt — ${result.cards_created} Karten kopiert.`); await goto(`/decks/${result.deck.id}`); } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { busy = false; } diff --git a/apps/web/src/routes/decks/[id]/+page.svelte b/apps/web/src/routes/decks/[id]/+page.svelte index 22cc2db..e4867a2 100644 --- a/apps/web/src/routes/decks/[id]/+page.svelte +++ b/apps/web/src/routes/decks/[id]/+page.svelte @@ -15,6 +15,7 @@ import PublishDeckModal from '$lib/components/marketplace/PublishDeckModal.svelte'; import { Image, CaretRight, DotsThree, PencilSimple, Trash } from '@mana/shared-icons'; import { marked } from 'marked'; + import { apiErrorMessage } from '$lib/api/error.ts'; function md(text: string): string { return marked.parse(text, { async: false }) as string; @@ -62,7 +63,7 @@ dueCount = due.total; error = null; } catch (e) { - error = (e as Error).message; + error = apiErrorMessage(e); } finally { loading = false; } @@ -75,7 +76,7 @@ deck = await updateDeck(deck.id, { category: next ?? undefined }); categoryOpen = false; } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } } @@ -86,7 +87,7 @@ toasts.success(t('card_edit.deleted')); await refresh(); } catch (e) { - toasts.error(t('card_edit.delete_failed', { msg: (e as Error).message })); + toasts.error(t('card_edit.delete_failed', { msg: apiErrorMessage(e) })); } } diff --git a/apps/web/src/routes/decks/[id]/edit/+page.svelte b/apps/web/src/routes/decks/[id]/edit/+page.svelte index ba1ee50..953a26a 100644 --- a/apps/web/src/routes/decks/[id]/edit/+page.svelte +++ b/apps/web/src/routes/decks/[id]/edit/+page.svelte @@ -6,6 +6,7 @@ import { devUser } from '$lib/auth/dev-stub.svelte.ts'; import { toasts } from '$lib/stores/toasts.svelte.ts'; import { t } from '$lib/i18n/index.svelte.ts'; + import { apiErrorMessage } from '$lib/api/error.ts'; const deckId = $derived(page.params.id ?? ''); @@ -23,7 +24,7 @@ description = deck.description ?? ''; color = deck.color ?? '#6366f1'; } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); goto(`/decks/${deckId}`); } finally { loading = false; @@ -45,7 +46,7 @@ toasts.success(t('deck_edit.saved')); goto(`/decks/${deckId}`); } catch (e) { - toasts.error(t('deck_edit.save_failed', { msg: (e as Error).message })); + toasts.error(t('deck_edit.save_failed', { msg: apiErrorMessage(e) })); saving = false; } } diff --git a/apps/web/src/routes/decks/new/+page.svelte b/apps/web/src/routes/decks/new/+page.svelte index 05b6008..c348c1c 100644 --- a/apps/web/src/routes/decks/new/+page.svelte +++ b/apps/web/src/routes/decks/new/+page.svelte @@ -7,6 +7,7 @@ import { toasts } from '$lib/stores/toasts.svelte.ts'; import { i18n, t } from '$lib/i18n/index.svelte.ts'; import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte'; + import { apiErrorMessage } from '$lib/api/error.ts'; let name = $state(''); let description = $state(''); @@ -68,7 +69,7 @@ toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`); goto(`/decks/${result.deck.id}`); } catch (err) { - imageError = (err as Error).message; + imageError = apiErrorMessage(err); imageGenerating = false; } } @@ -95,7 +96,7 @@ toasts.success(`${deck.name} ✓`); goto(`/decks/${deck.id}`); } catch (err) { - toasts.error(t('deck_new.create_failed', { msg: (err as Error).message })); + toasts.error(t('deck_new.create_failed', { msg: apiErrorMessage(err) })); saving = false; } } @@ -109,7 +110,7 @@ toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`); goto(`/decks/${result.deck.id}`); } catch (err) { - aiError = (err as Error).message; + aiError = apiErrorMessage(err); generating = false; } } diff --git a/apps/web/src/routes/explore/+page.svelte b/apps/web/src/routes/explore/+page.svelte index 6140b36..a2cab8a 100644 --- a/apps/web/src/routes/explore/+page.svelte +++ b/apps/web/src/routes/explore/+page.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; browseDecks, getExplore, type DeckListEntry, @@ -31,7 +32,7 @@ featured = explore.featured; trending = explore.trending; } catch (e) { - toasts.error(`Explore-Liste laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Explore-Liste laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loadingExplore = false; } @@ -59,7 +60,7 @@ } browseTotal = result.total; } catch (e) { - toasts.error(`Browse fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Browse fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loadingBrowse = false; } diff --git a/apps/web/src/routes/me/forks/+page.svelte b/apps/web/src/routes/me/forks/+page.svelte index 5e0e547..df1e148 100644 --- a/apps/web/src/routes/me/forks/+page.svelte +++ b/apps/web/src/routes/me/forks/+page.svelte @@ -10,6 +10,7 @@ import EmptyState from '$lib/components/marketplace/EmptyState.svelte'; import { toasts } from '$lib/stores/toasts.svelte.ts'; import { GitFork } from '@mana/shared-icons'; + import { apiErrorMessage } from '$lib/api/error.ts'; let forks = $state([]); let loading = $state(true); @@ -29,7 +30,7 @@ const result = await listDecks({ forkedFromMarketplace: true }); forks = result.decks; } catch (e) { - toasts.error(`Forks laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Forks laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loading = false; } @@ -48,7 +49,7 @@ await refresh(); } } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { busyId = null; } diff --git a/apps/web/src/routes/me/published/+page.svelte b/apps/web/src/routes/me/published/+page.svelte index d85f65b..67469ac 100644 --- a/apps/web/src/routes/me/published/+page.svelte +++ b/apps/web/src/routes/me/published/+page.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; browseDecks, getMyAuthorProfile, upsertMyAuthorProfile, @@ -42,7 +43,7 @@ decks = result.items; } } catch (e) { - toasts.error(`Profil laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Profil laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loading = false; } @@ -85,7 +86,7 @@ decks = result.items; } } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { saving = false; } diff --git a/apps/web/src/routes/me/subscribed/+page.svelte b/apps/web/src/routes/me/subscribed/+page.svelte index e43e236..6070914 100644 --- a/apps/web/src/routes/me/subscribed/+page.svelte +++ b/apps/web/src/routes/me/subscribed/+page.svelte @@ -8,6 +8,7 @@ import EmptyState from '$lib/components/marketplace/EmptyState.svelte'; import { toasts } from '$lib/stores/toasts.svelte.ts'; import { ArrowCounterClockwise } from '@mana/shared-icons'; + import { apiErrorMessage } from '$lib/api/error.ts'; let items = $state([]); let loading = $state(true); @@ -21,7 +22,7 @@ const result = await getMySubscriptions(); items = result.subscriptions; } catch (e) { - toasts.error(`Subs laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Subs laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loading = false; } diff --git a/apps/web/src/routes/stats/+page.svelte b/apps/web/src/routes/stats/+page.svelte index 710b36b..2e92db6 100644 --- a/apps/web/src/routes/stats/+page.svelte +++ b/apps/web/src/routes/stats/+page.svelte @@ -4,6 +4,7 @@ import { devUser } from '$lib/auth/dev-stub.svelte.ts'; import { loadStats, type UserStats } from '$lib/api/me.ts'; import { i18n, t, tn } from '$lib/i18n/index.svelte.ts'; + import { apiErrorMessage } from '$lib/api/error.ts'; let stats = $state(null); let loading = $state(true); @@ -27,7 +28,7 @@ try { stats = await loadStats(); } catch (e) { - error = (e as Error).message; + error = apiErrorMessage(e); } finally { loading = false; } diff --git a/apps/web/src/routes/study/[deckId]/+page.svelte b/apps/web/src/routes/study/[deckId]/+page.svelte index 6a3fe11..318a6ab 100644 --- a/apps/web/src/routes/study/[deckId]/+page.svelte +++ b/apps/web/src/routes/study/[deckId]/+page.svelte @@ -3,6 +3,7 @@ import { page } from '$app/state'; import { goto } from '$app/navigation'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; clusterIdForSubIndex, maskForSubIndex, renderClozePrompt, @@ -143,7 +144,7 @@ deckColor = d.color ?? null; queue = due.reviews; } catch (e) { - toasts.error(`Sitzung konnte nicht geladen werden: ${(e as Error).message}`); + toasts.error(`Sitzung konnte nicht geladen werden: ${apiErrorMessage(e)}`); goto('/study'); return; } @@ -189,7 +190,7 @@ queueIndex += 1; revealed = false; } catch (e) { - toasts.error(t('card_edit.save_failed', { msg: (e as Error).message })); + toasts.error(t('card_edit.save_failed', { msg: apiErrorMessage(e) })); } finally { busy = false; } diff --git a/apps/web/src/routes/u/[slug]/+page.svelte b/apps/web/src/routes/u/[slug]/+page.svelte index 3221022..c1f5844 100644 --- a/apps/web/src/routes/u/[slug]/+page.svelte +++ b/apps/web/src/routes/u/[slug]/+page.svelte @@ -4,6 +4,7 @@ import { page } from '$app/state'; import { + import { apiErrorMessage } from '$lib/api/error.ts'; browseDecks, followAuthor, getAuthor, @@ -44,7 +45,7 @@ decks = d.items; following = follow.following; } catch (e) { - toasts.error(`Author laden fehlgeschlagen: ${(e as Error).message}`); + toasts.error(`Author laden fehlgeschlagen: ${apiErrorMessage(e)}`); } finally { loading = false; } @@ -65,7 +66,7 @@ following = true; } } catch (e) { - toasts.error((e as Error).message); + toasts.error(apiErrorMessage(e)); } finally { busy = false; }