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>
This commit is contained in:
86
src/lib/components/ToastContainer.svelte
Normal file
86
src/lib/components/ToastContainer.svelte
Normal file
@@ -0,0 +1,86 @@
|
||||
<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}
|
||||
Reference in New Issue
Block a user