memory-sheet.tsx
1 'use client' 2 3 import { useState } from 'react' 4 import { useAppStore } from '@/stores/use-app-store' 5 import { createMemory } from '@/lib/memory' 6 import { getMemoryTierForCategory } from '@/lib/memory-presentation' 7 import { BottomSheet } from '@/components/shared/bottom-sheet' 8 import { AgentAvatar } from '@/components/agents/agent-avatar' 9 import { SheetFooter } from '@/components/shared/sheet-footer' 10 import { inputClass } from '@/components/shared/form-styles' 11 12 const CATEGORIES = ['note', 'fact', 'preference', 'finding', 'learning', 'general'] 13 14 export function MemorySheet() { 15 const open = useAppStore((s) => s.memorySheetOpen) 16 const setOpen = useAppStore((s) => s.setMemorySheetOpen) 17 const triggerRefresh = useAppStore((s) => s.triggerMemoryRefresh) 18 const agents = useAppStore((s) => s.agents) 19 const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter) 20 21 // Track open transitions to reset form 22 const [prevOpen, setPrevOpen] = useState(false) 23 const defaultAgentId = memoryAgentFilter && memoryAgentFilter !== '_global' ? memoryAgentFilter : null 24 25 const [title, setTitle] = useState('') 26 const [content, setContent] = useState('') 27 const [category, setCategory] = useState('note') 28 const [tier, setTier] = useState<'working' | 'durable' | 'archive'>(getMemoryTierForCategory('note')) 29 const [agentId, setAgentId] = useState<string | null>(defaultAgentId) 30 const [sharedWith, setSharedWith] = useState<string[]>([]) 31 const [saving, setSaving] = useState(false) 32 33 // Reset form when sheet opens (getDerivedStateFromProps pattern) 34 if (open && !prevOpen) { 35 setPrevOpen(true) 36 setAgentId(defaultAgentId) 37 setSharedWith([]) 38 setTitle('') 39 setContent('') 40 setCategory('note') 41 setTier(getMemoryTierForCategory('note')) 42 setSaving(false) 43 } else if (!open && prevOpen) { 44 setPrevOpen(false) 45 } 46 47 const onClose = () => { 48 setOpen(false) 49 } 50 51 const handleSave = async () => { 52 if (!title.trim()) return 53 setSaving(true) 54 try { 55 await createMemory({ 56 title: title.trim(), 57 category, 58 content, 59 agentId, 60 sessionId: null, 61 sharedWith: sharedWith.length ? sharedWith : undefined, 62 metadata: { 63 tier, 64 scope: agentId ? 'agent' : 'global', 65 visibility: agentId ? (sharedWith.length ? 'shared' : 'private') : 'global', 66 }, 67 }) 68 triggerRefresh() 69 onClose() 70 } catch { 71 /* ignore */ 72 } 73 setSaving(false) 74 } 75 76 const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)) 77 const selectedAgent = agentId ? agents[agentId] : null 78 79 return ( 80 <BottomSheet open={open} onClose={onClose}> 81 <div className="mb-8"> 82 <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New Memory</h2> 83 <p className="text-[14px] text-text-3">Store a piece of knowledge for an agent or globally</p> 84 </div> 85 86 {/* Agent selector */} 87 <div className="mb-6"> 88 <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Visibility</label> 89 <div className="flex gap-2 flex-wrap"> 90 <button 91 onClick={() => setAgentId(null)} 92 className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border 93 ${!agentId 94 ? 'bg-accent-soft border-accent-bright/20 text-accent-bright' 95 : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`} 96 style={{ fontFamily: 'inherit' }} 97 > 98 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={!agentId ? 'text-accent-bright' : 'text-text-3/60'}> 99 <circle cx="12" cy="12" r="10" /> 100 <line x1="2" y1="12" x2="22" y2="12" /> 101 <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /> 102 </svg> 103 Global 104 </button> 105 {agentList.map((agent) => ( 106 <button 107 key={agent.id} 108 onClick={() => setAgentId(agent.id)} 109 className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border 110 ${agentId === agent.id 111 ? 'bg-accent-soft border-accent-bright/20 text-accent-bright' 112 : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`} 113 style={{ fontFamily: 'inherit' }} 114 > 115 <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={20} /> 116 <span className="truncate max-w-[120px]">{agent.name}</span> 117 </button> 118 ))} 119 </div> 120 {selectedAgent && ( 121 <p className="text-[11px] text-text-3/50 mt-2"> 122 Owned by <span className="text-text-2">{selectedAgent.name}</span>. Add collaborators below if other agents should be able to recall it too. 123 </p> 124 )} 125 {!agentId && ( 126 <p className="text-[11px] text-text-3/50 mt-2"> 127 Global memories are accessible to every agent in the workspace. 128 </p> 129 )} 130 </div> 131 132 {/* Share with (only when assigned to an agent) */} 133 {agentId && agentList.filter((a) => a.id !== agentId).length > 0 && ( 134 <div className="mb-6"> 135 <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Share with</label> 136 <div className="flex gap-2 flex-wrap"> 137 {agentList 138 .filter((a) => a.id !== agentId) 139 .map((agent) => { 140 const isShared = sharedWith.includes(agent.id) 141 return ( 142 <button 143 key={agent.id} 144 onClick={() => setSharedWith(isShared ? sharedWith.filter((id) => id !== agent.id) : [...sharedWith, agent.id])} 145 className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border 146 ${isShared 147 ? 'bg-accent-soft border-accent-bright/20 text-accent-bright' 148 : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`} 149 style={{ fontFamily: 'inherit' }} 150 > 151 <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={20} /> 152 <span className="truncate max-w-[120px]">{agent.name}</span> 153 </button> 154 ) 155 })} 156 </div> 157 {sharedWith.length > 0 && ( 158 <p className="text-[11px] text-text-3/50 mt-2"> 159 Shared with {sharedWith.length} agent{sharedWith.length === 1 ? '' : 's'} in addition to the owner 160 </p> 161 )} 162 </div> 163 )} 164 165 {/* Title */} 166 <div className="mb-6"> 167 <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label> 168 <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Memory title" className={inputClass} style={{ fontFamily: 'inherit' }} /> 169 </div> 170 171 {/* Category */} 172 <div className="mb-6"> 173 <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Category</label> 174 <div className="flex gap-1.5 flex-wrap"> 175 {CATEGORIES.map((c) => ( 176 <button 177 key={c} 178 onClick={() => { 179 setCategory(c) 180 setTier(getMemoryTierForCategory(c)) 181 }} 182 className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 capitalize cursor-pointer transition-all border-none 183 ${category === c 184 ? 'bg-accent-soft text-accent-bright' 185 : 'bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`} 186 style={{ fontFamily: 'inherit' }} 187 > 188 {c} 189 </button> 190 ))} 191 </div> 192 </div> 193 194 <div className="mb-6"> 195 <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Tier</label> 196 <select 197 value={tier} 198 onChange={(e) => setTier(e.target.value as typeof tier)} 199 className={inputClass} 200 style={{ fontFamily: 'inherit' }} 201 > 202 <option value="working">Working: short-horizon, active context</option> 203 <option value="durable">Durable: keep this around as reusable knowledge</option> 204 <option value="archive">Archive: preserve, but keep less salient</option> 205 </select> 206 <p className="text-[11px] text-text-3/50 mt-2"> 207 Tier controls how aggressively this memory should stay in active recall. 208 </p> 209 </div> 210 211 {/* Content */} 212 <div className="mb-8"> 213 <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Content</label> 214 <textarea 215 value={content} 216 onChange={(e) => setContent(e.target.value)} 217 placeholder="Memory content..." 218 rows={6} 219 className={`${inputClass} resize-y min-h-[150px]`} 220 style={{ fontFamily: 'inherit' }} 221 /> 222 </div> 223 224 <SheetFooter 225 onCancel={onClose} 226 onSave={handleSave} 227 saveLabel={saving ? 'Saving...' : 'Save'} 228 saveDisabled={!title.trim() || saving} 229 /> 230 </BottomSheet> 231 ) 232 }