From 0b0881074a6751c3b391abfa0c746e573098a3ca Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 9 Apr 2026 21:33:47 -0500 Subject: [PATCH] Toast notifications for memory mutations, slide-in animation --- app/globals.css | 7 +++++ app/memory/page.tsx | 23 ++++++++++++++--- components/Providers.tsx | 7 ++++- components/Toast.tsx | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 components/Toast.tsx diff --git a/app/globals.css b/app/globals.css index da05eae..d398dd3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -103,6 +103,13 @@ body { .prose-hermes hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; } .prose-hermes strong { color: var(--text-1); } +/* Toast animation */ +@keyframes slide-in { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} +.animate-slide-in { animation: slide-in 0.2s ease-out; } + /* Divider */ .divider { height: 1px; diff --git a/app/memory/page.tsx b/app/memory/page.tsx index 9266b95..6107c9f 100644 --- a/app/memory/page.tsx +++ b/app/memory/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Plus } from "lucide-react"; import { useMemory, useAddMemory, usePatchMemory, useDeleteMemory } from "@/lib/hooks"; import { MemoryEntry } from "@/components/MemoryEntry"; +import { useToast } from "@/components/Toast"; const TABS = [ { id: "memory", label: "Agent" }, @@ -18,6 +19,7 @@ export default function MemoryPage() { const addMutation = useAddMemory(); const patchMutation = usePatchMemory(); const deleteMutation = useDeleteMemory(); + const { toast } = useToast(); const target = data?.targets?.find((t) => t.target === tab); const entries = target?.entries || []; @@ -28,7 +30,22 @@ export default function MemoryPage() { const handleAdd = () => { if (!newText.trim()) return; addMutation.mutate({ target: tab, content: newText.trim() }, { - onSuccess: () => { setNewText(""); setAdding(false); }, + onSuccess: () => { setNewText(""); setAdding(false); toast("Entry added"); }, + onError: () => toast("Failed to add entry", "error"), + }); + }; + + const handlePatch = (oldText: string, newContent: string) => { + patchMutation.mutate({ target: tab, old_text: oldText, content: newContent }, { + onSuccess: () => toast("Entry updated"), + onError: () => toast("Failed to update", "error"), + }); + }; + + const handleDelete = (oldText: string) => { + deleteMutation.mutate({ target: tab, old_text: oldText }, { + onSuccess: () => toast("Entry deleted"), + onError: () => toast("Failed to delete", "error"), }); }; @@ -115,8 +132,8 @@ export default function MemoryPage() { patchMutation.mutate({ target: tab, old_text: entry.slice(0, 40), content: nc })} - onDelete={() => deleteMutation.mutate({ target: tab, old_text: entry.slice(0, 40) })} + onPatch={(nc) => handlePatch(entry.slice(0, 40), nc)} + onDelete={() => handleDelete(entry.slice(0, 40))} /> ))} diff --git a/components/Providers.tsx b/components/Providers.tsx index 43351ae..9385182 100644 --- a/components/Providers.tsx +++ b/components/Providers.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; +import { ToastProvider } from "@/components/Toast"; export function Providers({ children }: { children: React.ReactNode }) { const [client] = useState( @@ -12,5 +13,9 @@ export function Providers({ children }: { children: React.ReactNode }) { }, }) ); - return {children}; + return ( + + {children} + + ); } diff --git a/components/Toast.tsx b/components/Toast.tsx new file mode 100644 index 0000000..26dd461 --- /dev/null +++ b/components/Toast.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { createContext, useContext, useState, useCallback, type ReactNode } from "react"; +import { Check, X, AlertTriangle } from "lucide-react"; + +interface Toast { + id: number; + message: string; + type: "success" | "error"; +} + +const ToastContext = createContext<{ + toast: (message: string, type?: "success" | "error") => void; +}>({ toast: () => {} }); + +export function useToast() { + return useContext(ToastContext); +} + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + let nextId = 0; + + const toast = useCallback((message: string, type: "success" | "error" = "success") => { + const id = ++nextId; + setToasts((t) => [...t, { id, message, type }]); + setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2500); + }, []); + + return ( + + {children} +
+ {toasts.map((t) => ( +
+ {t.type === "error" ? ( + + ) : ( + + )} + + {t.message} + +
+ ))} +
+
+ ); +}