feat(chat): add toast notification system and docker AI config

- Add global Toast component with success/error/warning/info types
- Create toastStore using Svelte 5 runes for centralized notifications
- Add GOOGLE_GENAI_API_KEY to mana-core-auth docker-compose config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-29 23:02:52 +01:00
parent 4f06301fe3
commit 0893ed7daa
5 changed files with 175 additions and 2 deletions

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { toastStore, type Toast } from '$lib/stores/toast.svelte';
import { X, CheckCircle, XCircle, Warning, Info } from '@manacore/shared-icons';
let toasts = $derived(toastStore.toasts);
const icons = {
success: CheckCircle,
error: XCircle,
warning: Warning,
info: Info,
};
const colors = {
success: 'bg-green-500/90 text-white',
error: 'bg-destructive/90 text-destructive-foreground',
warning: 'bg-amber-500/90 text-white',
info: 'bg-primary/90 text-primary-foreground',
};
function handleDismiss(id: string) {
toastStore.dismiss(id);
}
</script>
{#if toasts.length > 0}
<div class="fixed bottom-6 right-6 z-[100] flex flex-col gap-3 max-w-sm">
{#each toasts as toast (toast.id)}
{@const Icon = icons[toast.type]}
<div
class="flex items-start gap-3 px-4 py-3 rounded-xl shadow-lg backdrop-blur-xl border border-white/20 animate-slide-in {colors[toast.type]}"
role="alert"
>
<Icon size={20} weight="fill" class="flex-shrink-0 mt-0.5" />
<p class="flex-1 text-sm font-medium">{toast.message}</p>
<button
onclick={() => handleDismiss(toast.id)}
class="flex-shrink-0 p-1 rounded-lg hover:bg-white/20 transition-colors"
aria-label="Schließen"
>
<X size={16} weight="bold" />
</button>
</div>
{/each}
</div>
{/if}
<style>
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-slide-in {
animation: slide-in 0.3s ease-out;
}
</style>

View file

@ -3,6 +3,7 @@
*/
import { conversationService } from '$lib/services/conversation';
import { toastStore } from './toast.svelte';
import type { Conversation } from '@chat/types';
// State
@ -36,7 +37,9 @@ export const conversationsStore = {
try {
conversations = await conversationService.getConversations(spaceId);
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load conversations';
const message = e instanceof Error ? e.message : 'Konversationen konnten nicht geladen werden';
error = message;
toastStore.error(message);
conversations = [];
} finally {
isLoading = false;
@ -53,7 +56,9 @@ export const conversationsStore = {
try {
archivedConversations = await conversationService.getArchivedConversations();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load archived conversations';
const message = e instanceof Error ? e.message : 'Archivierte Konversationen konnten nicht geladen werden';
error = message;
toastStore.error(message);
archivedConversations = [];
} finally {
isLoading = false;

View file

@ -0,0 +1,100 @@
/**
* Toast Store - Centralized notification system using Svelte 5 runes
*/
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration: number;
}
// State
let toasts = $state<Toast[]>([]);
// Auto-incrementing ID
let nextId = 0;
function generateId(): string {
return `toast-${++nextId}-${Date.now()}`;
}
export const toastStore = {
// Getter for reading toasts
get toasts() {
return toasts;
},
/**
* Show a toast notification
*/
show(message: string, type: ToastType = 'info', duration: number = 4000) {
const id = generateId();
const toast: Toast = { id, type, message, duration };
toasts = [...toasts, toast];
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => {
this.dismiss(id);
}, duration);
}
return id;
},
/**
* Show success toast
*/
success(message: string, duration?: number) {
return this.show(message, 'success', duration);
},
/**
* Show error toast
*/
error(message: string, duration: number = 6000) {
return this.show(message, 'error', duration);
},
/**
* Show warning toast
*/
warning(message: string, duration?: number) {
return this.show(message, 'warning', duration);
},
/**
* Show info toast
*/
info(message: string, duration?: number) {
return this.show(message, 'info', duration);
},
/**
* Dismiss a specific toast
*/
dismiss(id: string) {
toasts = toasts.filter((t) => t.id !== id);
},
/**
* Dismiss all toasts
*/
dismissAll() {
toasts = [];
},
};
/**
* Helper function for API error handling
* Use this in services/stores to show user-friendly error messages
*/
export function handleApiError(error: unknown, fallbackMessage: string = 'Ein Fehler ist aufgetreten'): string {
const message = error instanceof Error ? error.message : fallbackMessage;
toastStore.error(message);
return message;
}

View file

@ -2,6 +2,7 @@
import '../app.css';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import Toast from '$lib/components/Toast.svelte';
let { children } = $props();
@ -14,3 +15,6 @@
<div class="min-h-screen bg-background text-foreground">
{@render children()}
</div>
<!-- Global Toast notifications -->
<Toast />

View file

@ -68,6 +68,7 @@ services:
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173,http://localhost:8081}
CREDITS_SIGNUP_BONUS: ${CREDITS_SIGNUP_BONUS:-150}
CREDITS_DAILY_FREE: ${CREDITS_DAILY_FREE:-5}
GOOGLE_GENAI_API_KEY: ${GOOGLE_GENAI_API_KEY}
depends_on:
postgres:
condition: service_healthy