Spaces:
Configuration error
Configuration error
| 'use client' | |
| import { useEffect, useState } from 'react' | |
| import { useAgentStore } from '@/hooks/useAgentStore' | |
| import { getMemory } from '@/lib/api' | |
| const searchMemory = (q: string) => fetch('/api/v1/memory/search?q=' + encodeURIComponent(q)).then(r => r.json()) | |
| import { Brain, Search, RefreshCw, MessageSquare, Settings, Code2, User } from 'lucide-react' | |
| import { formatDistanceToNow } from 'date-fns' | |
| const TYPE_META: Record<string, { icon: React.ElementType; color: string; label: string }> = { | |
| conversation: { icon: MessageSquare, color: '#22d3ee', label: 'Conversation' }, | |
| user_preference: { icon: User, color: '#fbbf24', label: 'Preference' }, | |
| user_directive: { icon: User, color: '#fbbf24', label: 'Directive' }, | |
| project_context: { icon: Code2, color: '#34d399', label: 'Project' }, | |
| general: { icon: Brain, color: '#818cf8', label: 'General' }, | |
| } | |
| export default function MemoryPanel() { | |
| const { sessionId, locale } = useAgentStore() | |
| const [memories, setMemories] = useState<any[]>([]) | |
| const [loading, setLoading] = useState(true) | |
| const [query, setQuery] = useState('') | |
| const [searching, setSearching] = useState(false) | |
| const load = async () => { | |
| setLoading(true) | |
| try { | |
| const data = await getMemory() | |
| setMemories(Array.isArray(data) ? data : data.memories || []) | |
| } catch {} | |
| setLoading(false) | |
| } | |
| const search = async () => { | |
| if (!query.trim()) { load(); return } | |
| setSearching(true) | |
| try { | |
| const data = await searchMemory(query) | |
| setMemories(Array.isArray(data) ? data : data.results || []) | |
| } catch {} | |
| setSearching(false) | |
| } | |
| useEffect(() => { load() }, [sessionId]) | |
| return ( | |
| <div className="flex flex-col h-full" style={{ background: 'var(--bg-2)' }}> | |
| <div className="flex items-center justify-between px-4 py-2.5 border-b shrink-0" | |
| style={{ borderColor: 'var(--border)', background: 'var(--bg-3)' }}> | |
| <div className="flex items-center gap-2"> | |
| <Brain size={14} className="text-yellow-400" /> | |
| <span className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| {locale === 'my' ? 'ααΎααΊαα¬ααΊ' : 'Memory'} | |
| </span> | |
| {memories.length > 0 && ( | |
| <span className="text-[10px] px-1.5 py-0.5 rounded-full" | |
| style={{ background: 'rgba(251,191,36,0.15)', color: '#fbbf24', border: '1px solid rgba(251,191,36,0.3)' }}> | |
| {memories.length} | |
| </span> | |
| )} | |
| </div> | |
| <button onClick={load} className="p-1.5 rounded-lg hover:bg-white/5 transition-colors"> | |
| <RefreshCw size={12} style={{ color: 'var(--text-muted)' }} className={loading ? 'animate-spin' : ''} /> | |
| </button> | |
| </div> | |
| {/* Search */} | |
| <div className="px-3 py-2 border-b" style={{ borderColor: 'var(--border)' }}> | |
| <div className="flex gap-1.5"> | |
| <div className="flex-1 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg" | |
| style={{ background: 'var(--bg-3)', border: '1px solid var(--border)' }}> | |
| <Search size={11} style={{ color: 'var(--text-muted)' }} /> | |
| <input | |
| type="text" | |
| value={query} | |
| onChange={e => setQuery(e.target.value)} | |
| onKeyDown={e => e.key === 'Enter' && search()} | |
| placeholder={locale === 'my' ? 'ααΎααΊαα¬ααΊααΎα¬αα½α±αααΊ...' : 'Search memory...'} | |
| className="flex-1 bg-transparent text-[11px] outline-none" | |
| style={{ color: 'var(--text-primary)' }} | |
| /> | |
| </div> | |
| <button onClick={search} disabled={searching} | |
| className="px-2.5 py-1.5 rounded-lg text-[11px] disabled:opacity-40 transition-all" | |
| style={{ background: 'var(--brand)', color: '#fff' }}> | |
| <Search size={11} /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-3 space-y-2"> | |
| {loading ? ( | |
| <div className="space-y-2"> | |
| {[...Array(4)].map((_, i) => ( | |
| <div key={i} className="h-16 rounded-xl shimmer" /> | |
| ))} | |
| </div> | |
| ) : memories.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center h-full gap-3 py-8"> | |
| <div className="w-12 h-12 rounded-xl flex items-center justify-center" | |
| style={{ background: 'var(--bg-3)', border: '1px solid var(--border)' }}> | |
| <Brain size={20} style={{ color: 'var(--text-muted)' }} /> | |
| </div> | |
| <p className="text-sm" style={{ color: 'var(--text-secondary)' }}> | |
| {locale === 'my' ? 'ααΎααΊαα¬ααΊαααΎααα±αΈαα«' : 'No memories yet'} | |
| </p> | |
| <p className="text-[11px] text-center max-w-xs" style={{ color: 'var(--text-muted)' }}> | |
| {locale === 'my' | |
| ? 'Conversation αα»α¬αΈ ααΎααΊαα¬ααΊαα½ααΊ ααααΊαΈαααΊαΈαααΊ' | |
| : 'Conversations and context will be saved here automatically'} | |
| </p> | |
| </div> | |
| ) : ( | |
| memories.map((mem: any, i: number) => { | |
| const tm = TYPE_META[mem.memory_type] || TYPE_META.general | |
| const Icon = tm.icon | |
| return ( | |
| <div key={mem.id || i} className="rounded-xl p-3 transition-all" | |
| style={{ background: 'var(--bg-3)', border: '1px solid var(--border)' }}> | |
| <div className="flex items-center gap-2 mb-1.5"> | |
| <Icon size={11} style={{ color: tm.color }} /> | |
| <span className="text-[9px] px-1.5 py-0.5 rounded-full font-medium" | |
| style={{ background: `${tm.color}15`, color: tm.color, border: `1px solid ${tm.color}30` }}> | |
| {tm.label} | |
| </span> | |
| {mem.key && ( | |
| <span className="text-[9px] font-mono" style={{ color: 'var(--text-muted)' }}> | |
| {mem.key} | |
| </span> | |
| )} | |
| <span className="text-[9px] ml-auto" style={{ color: 'var(--text-muted)' }}> | |
| {mem.created_at ? formatDistanceToNow(new Date(mem.created_at * 1000), { addSuffix: true }) : ''} | |
| </span> | |
| </div> | |
| <p className="text-[11px] leading-relaxed line-clamp-3" style={{ color: 'var(--text-secondary)' }}> | |
| {mem.content} | |
| </p> | |
| </div> | |
| ) | |
| }) | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |