NexusMemory / frontend /src /components /MemoryPanel.jsx
bharatverse11's picture
Nexus Memory backend on HF Spaces (Docker + Ollama)
044b758
Raw
History Blame Contribute Delete
7.12 kB
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Brain, X, Tag, Plus, Trash2 } from "lucide-react";
import { addMemory, deleteMemory } from "../services/api";
import { format } from "date-fns";
const CATEGORY_OPTIONS = [
"general",
"technical",
"preference",
"professional",
"goal",
];
const categoryStyles = {
technical: "text-blue-400 bg-blue-950/50 border-blue-800/40",
preference: "text-purple-400 bg-purple-950/50 border-purple-800/40",
professional: "text-emerald-400 bg-emerald-950/50 border-emerald-800/40",
goal: "text-amber-400 bg-amber-950/50 border-amber-800/40",
general: "text-slate-400 bg-slate-800/50 border-slate-700/40",
};
export default function MemoryPanel({
sessionId,
memories,
onClose,
onMemoryChange,
}) {
const [newFact, setNewFact] = useState("");
const [newCategory, setNewCategory] = useState("general");
const [isAdding, setIsAdding] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const handleAddMemory = async () => {
if (!newFact.trim() || !sessionId) return;
setIsAdding(true);
try {
await addMemory({
session_id: sessionId,
fact: newFact.trim(),
category: newCategory,
});
setNewFact("");
setShowAddForm(false);
onMemoryChange?.();
} catch (err) {
console.error("Failed to add memory:", err);
} finally {
setIsAdding(false);
}
};
const handleDeleteMemory = async (memoryId) => {
try {
await deleteMemory(memoryId);
onMemoryChange?.();
} catch (err) {
console.error("Failed to delete memory:", err);
}
};
return (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="h-full flex flex-col bg-nexus-surface border-l border-nexus-border"
>
{/* Header */}
<div
className="flex items-center justify-between px-4 py-3
border-b border-nexus-border flex-shrink-0"
>
<div className="flex items-center gap-2">
<Brain className="w-4 h-4 text-nexus-accent" />
<span className="text-sm font-semibold text-nexus-text">
Memory Store
</span>
<span
className="text-xs bg-nexus-accent/20 text-nexus-accent-light
px-1.5 py-0.5 rounded-full font-medium"
>
{memories.length}
</span>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-nexus-card text-nexus-muted
hover:text-nexus-text transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Add Memory Button */}
<div className="px-4 pt-3 pb-2 flex-shrink-0">
<button
onClick={() => setShowAddForm(!showAddForm)}
className="w-full flex items-center justify-center gap-2 px-3 py-2
rounded-lg border border-dashed border-nexus-border
text-xs text-nexus-muted hover:text-nexus-text
hover:border-nexus-accent/50 transition-all"
>
<Plus className="w-3.5 h-3.5" />
Add Memory Manually
</button>
{/* Add Form */}
<AnimatePresence>
{showAddForm && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="mt-2 space-y-2 overflow-hidden"
>
<textarea
value={newFact}
onChange={(e) => setNewFact(e.target.value)}
placeholder="Enter a fact to remember..."
rows={3}
className="nexus-input w-full text-xs resize-none"
/>
<div className="flex gap-2">
<select
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
className="nexus-input flex-1 text-xs"
>
{CATEGORY_OPTIONS.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<button
onClick={handleAddMemory}
disabled={!newFact.trim() || isAdding}
className="nexus-button-primary text-xs px-3"
>
{isAdding ? "..." : "Save"}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Memory List */}
<div className="flex-1 overflow-y-auto px-4 pb-4 space-y-2">
{memories.length === 0 ? (
<div className="text-center py-12">
<Brain className="w-10 h-10 text-nexus-border mx-auto mb-3" />
<p className="text-sm text-nexus-muted">No memories yet</p>
<p className="text-xs text-nexus-muted mt-1">
Chat naturally and memories will form automatically.
</p>
</div>
) : (
<AnimatePresence>
{memories.map((memory, index) => (
<motion.div
key={memory.id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ delay: index * 0.03 }}
className="nexus-card p-3 group space-y-2"
>
<div className="flex items-start justify-between gap-2">
<p className="text-xs text-nexus-text leading-relaxed flex-1">
{memory.fact}
</p>
<button
onClick={() => handleDeleteMemory(memory.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity
p-1 rounded hover:bg-nexus-surface text-nexus-muted
hover:text-nexus-error flex-shrink-0"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
<div className="flex items-center justify-between">
<span
className={`inline-flex items-center gap-1 text-xs
px-1.5 py-0.5 rounded-full border font-medium
${categoryStyles[memory.category] || categoryStyles.general}`}
>
<Tag className="w-2 h-2" />
{memory.category}
</span>
<span className="text-xs text-nexus-muted">
{format(new Date(memory.created_at), "MMM d, HH:mm")}
</span>
</div>
</motion.div>
))}
</AnimatePresence>
)}
</div>
</motion.div>
);
}