feat(web): apiErrorMessage-Utility + MultipleChoice-Fallback
- Neue Utility `apiErrorMessage()` in `$lib/api/error.ts`: liest `body.detail` / `body.error` aus ApiError-Responses statt generischer "(err as Error).message" — 22 Dateien auf die Utility umgestellt, keine rohen Type-Casts mehr - MultipleChoiceView: Fallback-UI wenn < 1 Distractor verfügbar — zeigt Antwort direkt + Nochmal/Gewusst-Buttons statt kaputter 1-Option-Auswahl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3a148171a
commit
f2f752e9ee
24 changed files with 381 additions and 284 deletions
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
let options = $state<string[]>([]);
|
||||
let selected = $state<string | null>(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}
|
||||
<div class="loading" aria-live="polite">Optionen werden geladen …</div>
|
||||
{:else if tooFewOptions}
|
||||
<div class="few-options">
|
||||
<p class="few-hint">Deck hat zu wenig Karten für Multiple Choice.</p>
|
||||
<div class="few-answer">{answer}</div>
|
||||
<div class="few-actions">
|
||||
<button class="few-btn few-btn-again" onclick={() => ongrade('again')}>Nochmal</button>
|
||||
<button class="few-btn few-btn-good" onclick={() => ongrade('good')}>Gewusst</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="options" role="group" aria-label="Antwortmöglichkeiten">
|
||||
{#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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { publishMarketplaceVersion } from '$lib/api/marketplace.ts';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
}
|
||||
cards = parsed;
|
||||
} catch (e) {
|
||||
error = `JSON-Parse-Fehler: ${(e as Error).message}`;
|
||||
error = `JSON-Parse-Fehler: ${apiErrorMessage(e)}`;
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
toasts.success(`Version ${result.version.semver} veröffentlicht (${result.version.card_count} Karten)`);
|
||||
onPublished?.();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
closePullRequest,
|
||||
listPullRequests,
|
||||
mergePullRequest,
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
const result = await listPullRequests(slug, statusFilter);
|
||||
prs = result.pull_requests;
|
||||
} catch (e) {
|
||||
toasts.error(`PRs laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`PRs laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
toasts.success('PR gemerged — neue Version live.');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
await rejectPullRequest(id);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
await closePullRequest(id);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { createPullRequest } from '$lib/api/marketplace.ts';
|
||||
import type { MarketplaceVersionCard } from '$lib/api/marketplace.ts';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
toasts.success('Pull-Request eingereicht — Author bekommt Bescheid.');
|
||||
onSubmitted?.();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue