Files
llm-multiverse-ui/src/lib/components/ToastContainer.svelte
shahondin1624 14c83832f5 feat: add error handling, toast notifications, connection status, and retry logic (#20)
Add global error boundary, toast notification system for gRPC errors,
connection status indicator in chat header, and automatic retry with
exponential backoff for transient failures. Map gRPC status codes to
user-friendly messages and add a Retry button on failed requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:27:40 +01:00

87 lines
3.4 KiB
Svelte

<script lang="ts">
import { toastStore, type ToastType } from '$lib/stores/toast.svelte';
const typeStyles: Record<ToastType, { bg: string; border: string; icon: string }> = {
error: {
bg: 'bg-red-50 dark:bg-red-900/40',
border: 'border-red-200 dark:border-red-800',
icon: 'text-red-500 dark:text-red-400'
},
warning: {
bg: 'bg-amber-50 dark:bg-amber-900/40',
border: 'border-amber-200 dark:border-amber-800',
icon: 'text-amber-500 dark:text-amber-400'
},
success: {
bg: 'bg-green-50 dark:bg-green-900/40',
border: 'border-green-200 dark:border-green-800',
icon: 'text-green-500 dark:text-green-400'
},
info: {
bg: 'bg-blue-50 dark:bg-blue-900/40',
border: 'border-blue-200 dark:border-blue-800',
icon: 'text-blue-500 dark:text-blue-400'
}
};
const typeLabels: Record<ToastType, string> = {
error: 'Error',
warning: 'Warning',
success: 'Success',
info: 'Info'
};
</script>
{#if toastStore.toasts.length > 0}
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none">
{#each toastStore.toasts as toast (toast.id)}
{@const style = typeStyles[toast.type]}
<div
class="pointer-events-auto flex items-start gap-3 rounded-lg border px-4 py-3 shadow-lg {style.bg} {style.border}"
role="alert"
>
<!-- Icon -->
<div class="mt-0.5 shrink-0 {style.icon}">
{#if toast.type === 'error'}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
{:else if toast.type === 'warning'}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
{:else if toast.type === 'success'}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
{:else}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
{/if}
</div>
<!-- Content -->
<div class="min-w-0 flex-1">
<p class="text-xs font-medium text-gray-900 dark:text-gray-100">{typeLabels[toast.type]}</p>
<p class="mt-0.5 text-sm text-gray-700 dark:text-gray-300">{toast.message}</p>
</div>
<!-- Dismiss button -->
{#if toast.dismissable}
<button
type="button"
onclick={() => toastStore.removeToast(toast.id)}
class="shrink-0 rounded-md p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
aria-label="Dismiss"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
{/if}