Toast notifications for memory mutations, slide-in animation
This commit is contained in:
@@ -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;
|
||||
|
||||
+20
-3
@@ -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() {
|
||||
<MemoryEntry
|
||||
key={tab + "-" + i}
|
||||
content={entry}
|
||||
onPatch={(nc) => 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))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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 <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Toast[]>([]);
|
||||
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 (
|
||||
<ToastContext.Provider value={{ toast }}>
|
||||
{children}
|
||||
<div className="fixed top-4 left-4 right-4 z-[100] flex flex-col items-center gap-2 pointer-events-none max-w-lg mx-auto">
|
||||
{toasts.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className="card px-4 py-2.5 flex items-center gap-2 pointer-events-auto animate-slide-in"
|
||||
style={{
|
||||
background: t.type === "error" ? "rgba(255,59,48,0.15)" : "rgba(52,199,89,0.15)",
|
||||
border: `1px solid ${t.type === "error" ? "rgba(255,59,48,0.3)" : "rgba(52,199,89,0.3)"}`,
|
||||
}}
|
||||
>
|
||||
{t.type === "error" ? (
|
||||
<AlertTriangle size={14} style={{ color: "#ff3b30" }} />
|
||||
) : (
|
||||
<Check size={14} style={{ color: "#34c759" }} />
|
||||
)}
|
||||
<span className="text-[13px]" style={{ color: t.type === "error" ? "#ff3b30" : "#34c759" }}>
|
||||
{t.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user