@swarmclawai/swarmclaw 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -162,7 +162,7 @@ export function LogList() {
|
|
|
162
162
|
<button
|
|
163
163
|
key={i}
|
|
164
164
|
onClick={() => { setLevelFilter(f.levels); setSearch(f.search) }}
|
|
165
|
-
className="group flex items-center gap-1 px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-accent-soft text-accent-bright hover:bg-
|
|
165
|
+
className="group flex items-center gap-1 px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-accent-soft text-accent-bright hover:bg-accent-bright/15"
|
|
166
166
|
>
|
|
167
167
|
{f.name}
|
|
168
168
|
<span
|
|
@@ -337,7 +337,7 @@ export function LogList() {
|
|
|
337
337
|
<button
|
|
338
338
|
onClick={handleCreateTask}
|
|
339
339
|
disabled={creatingTask}
|
|
340
|
-
className="px-5 py-3 rounded-[14px] border-none bg-
|
|
340
|
+
className="px-5 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600
|
|
341
341
|
cursor-pointer active:scale-[0.97] disabled:opacity-40 transition-all
|
|
342
342
|
shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110 shrink-0"
|
|
343
343
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -207,7 +207,7 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
207
207
|
<button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
208
208
|
Cancel
|
|
209
209
|
</button>
|
|
210
|
-
<button onClick={handleSave} disabled={!canSave} className="flex-1 py-3.5 rounded-[14px] border-none bg-
|
|
210
|
+
<button onClick={handleSave} disabled={!canSave} className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110" style={{ fontFamily: 'inherit' }}>
|
|
211
211
|
{editing ? 'Save' : 'Create'}
|
|
212
212
|
</button>
|
|
213
213
|
</div>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
+
import { getMemoryCounts } from '@/lib/memory'
|
|
7
|
+
|
|
8
|
+
export function MemoryAgentList() {
|
|
9
|
+
const agents = useAppStore((s) => s.agents)
|
|
10
|
+
const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter)
|
|
11
|
+
const setMemoryAgentFilter = useAppStore((s) => s.setMemoryAgentFilter)
|
|
12
|
+
const setSelectedMemoryId = useAppStore((s) => s.setSelectedMemoryId)
|
|
13
|
+
const refreshKey = useAppStore((s) => s.memoryRefreshKey)
|
|
14
|
+
|
|
15
|
+
const [counts, setCounts] = useState<Record<string, number>>({})
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
getMemoryCounts()
|
|
19
|
+
.then((data) => setCounts(data))
|
|
20
|
+
.catch(() => {/* ignore */})
|
|
21
|
+
}, [refreshKey])
|
|
22
|
+
|
|
23
|
+
const totalCount = Object.values(counts).reduce((a, b) => a + b, 0)
|
|
24
|
+
const globalCount = counts['_global'] || 0
|
|
25
|
+
|
|
26
|
+
const agentList = Object.values(agents).sort((a, b) =>
|
|
27
|
+
a.name.localeCompare(b.name),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const handleSelect = (agentId: string | null) => {
|
|
31
|
+
setMemoryAgentFilter(agentId)
|
|
32
|
+
setSelectedMemoryId(null)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex-1 flex flex-col overflow-y-auto">
|
|
37
|
+
{/* Header */}
|
|
38
|
+
<div className="px-4 pt-4 pb-2 shrink-0">
|
|
39
|
+
<h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em]">Memory</h2>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* All Memories row */}
|
|
43
|
+
<div className="px-2 flex flex-col gap-0.5">
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => handleSelect(null)}
|
|
46
|
+
className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
|
|
47
|
+
${!memoryAgentFilter
|
|
48
|
+
? 'bg-accent-soft'
|
|
49
|
+
: 'bg-transparent hover:bg-white/[0.02]'}`}
|
|
50
|
+
style={{ fontFamily: 'inherit' }}
|
|
51
|
+
>
|
|
52
|
+
{!memoryAgentFilter && (
|
|
53
|
+
<div className="absolute left-0 top-2.5 bottom-2.5 w-[2.5px] rounded-full bg-accent-bright" />
|
|
54
|
+
)}
|
|
55
|
+
<div className="w-[28px] h-[28px] rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
|
|
56
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={!memoryAgentFilter ? 'text-accent-bright' : 'text-text-3'}>
|
|
57
|
+
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
|
58
|
+
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
|
|
59
|
+
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
|
60
|
+
</svg>
|
|
61
|
+
</div>
|
|
62
|
+
<span className={`text-[13px] font-600 flex-1 ${!memoryAgentFilter ? 'text-accent-bright' : 'text-text-2'}`}>
|
|
63
|
+
All Memories
|
|
64
|
+
</span>
|
|
65
|
+
{totalCount > 0 && (
|
|
66
|
+
<span className="text-[10px] font-mono tabular-nums text-text-3/60 bg-white/[0.04] px-1.5 py-0.5 rounded-[5px]">
|
|
67
|
+
{totalCount}
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
{/* Global row */}
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => handleSelect('_global')}
|
|
75
|
+
className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
|
|
76
|
+
${memoryAgentFilter === '_global'
|
|
77
|
+
? 'bg-accent-soft'
|
|
78
|
+
: 'bg-transparent hover:bg-white/[0.02]'}`}
|
|
79
|
+
style={{ fontFamily: 'inherit' }}
|
|
80
|
+
>
|
|
81
|
+
{memoryAgentFilter === '_global' && (
|
|
82
|
+
<div className="absolute left-0 top-2.5 bottom-2.5 w-[2.5px] rounded-full bg-accent-bright" />
|
|
83
|
+
)}
|
|
84
|
+
<div className="w-[28px] h-[28px] rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
|
|
85
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={memoryAgentFilter === '_global' ? 'text-accent-bright' : 'text-text-3'}>
|
|
86
|
+
<circle cx="12" cy="12" r="10" />
|
|
87
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
88
|
+
<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" />
|
|
89
|
+
</svg>
|
|
90
|
+
</div>
|
|
91
|
+
<span className={`text-[13px] font-600 flex-1 ${memoryAgentFilter === '_global' ? 'text-accent-bright' : 'text-text-2'}`}>
|
|
92
|
+
Global
|
|
93
|
+
</span>
|
|
94
|
+
{globalCount > 0 && (
|
|
95
|
+
<span className="text-[10px] font-mono tabular-nums text-text-3/60 bg-white/[0.04] px-1.5 py-0.5 rounded-[5px]">
|
|
96
|
+
{globalCount}
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Divider */}
|
|
103
|
+
<div className="mx-4 my-2 border-t border-white/[0.04]" />
|
|
104
|
+
|
|
105
|
+
{/* Agent list */}
|
|
106
|
+
<div className="px-2 flex flex-col gap-0.5 pb-4">
|
|
107
|
+
{agentList.map((agent) => {
|
|
108
|
+
const count = counts[agent.id] || 0
|
|
109
|
+
const isActive = memoryAgentFilter === agent.id
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
key={agent.id}
|
|
113
|
+
onClick={() => handleSelect(agent.id)}
|
|
114
|
+
className={`relative flex items-center gap-3 px-3 py-2 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
|
|
115
|
+
${isActive
|
|
116
|
+
? 'bg-accent-soft'
|
|
117
|
+
: 'bg-transparent hover:bg-white/[0.02]'}`}
|
|
118
|
+
style={{ fontFamily: 'inherit' }}
|
|
119
|
+
>
|
|
120
|
+
{isActive && (
|
|
121
|
+
<div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
|
|
122
|
+
)}
|
|
123
|
+
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={28} />
|
|
124
|
+
<span className={`text-[13px] font-600 flex-1 truncate ${isActive ? 'text-accent-bright' : 'text-text-2'}`}>
|
|
125
|
+
{agent.name}
|
|
126
|
+
</span>
|
|
127
|
+
{count > 0 && (
|
|
128
|
+
<span className="text-[10px] font-mono tabular-nums text-text-3/60 bg-white/[0.04] px-1.5 py-0.5 rounded-[5px]">
|
|
129
|
+
{count}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</button>
|
|
133
|
+
)
|
|
134
|
+
})}
|
|
135
|
+
{agentList.length === 0 && (
|
|
136
|
+
<div className="text-[12px] text-text-3/50 px-3 py-4 text-center">
|
|
137
|
+
No agents yet
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { searchMemory } from '@/lib/memory'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { MemoryCard } from './memory-card'
|
|
7
|
+
import { MemoryDetail } from './memory-detail'
|
|
8
|
+
import type { MemoryEntry } from '@/types'
|
|
9
|
+
|
|
10
|
+
export function MemoryBrowser() {
|
|
11
|
+
const selectedMemoryId = useAppStore((s) => s.selectedMemoryId)
|
|
12
|
+
const setSelectedMemoryId = useAppStore((s) => s.setSelectedMemoryId)
|
|
13
|
+
const refreshKey = useAppStore((s) => s.memoryRefreshKey)
|
|
14
|
+
const agents = useAppStore((s) => s.agents)
|
|
15
|
+
const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter)
|
|
16
|
+
|
|
17
|
+
const [search, setSearch] = useState('')
|
|
18
|
+
const [entries, setEntries] = useState<MemoryEntry[]>([])
|
|
19
|
+
const [loaded, setLoaded] = useState(false)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
const [categoryFilter, setCategoryFilter] = useState<string>('')
|
|
22
|
+
const searchRef = useRef(search)
|
|
23
|
+
|
|
24
|
+
// Derive the API agentId from the filter
|
|
25
|
+
const apiAgentId = useMemo(() => {
|
|
26
|
+
if (!memoryAgentFilter) return undefined // all
|
|
27
|
+
if (memoryAgentFilter === '_global') return undefined // we'll filter client-side
|
|
28
|
+
return memoryAgentFilter
|
|
29
|
+
}, [memoryAgentFilter])
|
|
30
|
+
|
|
31
|
+
const load = useCallback(async (query: string) => {
|
|
32
|
+
try {
|
|
33
|
+
const results = await searchMemory({ q: query || undefined, agentId: apiAgentId })
|
|
34
|
+
setEntries(Array.isArray(results) ? results : [])
|
|
35
|
+
setError(null)
|
|
36
|
+
} catch {
|
|
37
|
+
setError('Unable to load memories right now.')
|
|
38
|
+
}
|
|
39
|
+
setLoaded(true)
|
|
40
|
+
}, [apiAgentId])
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
searchRef.current = search
|
|
44
|
+
}, [search])
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const timer = setTimeout(() => { void load(searchRef.current) }, 0)
|
|
48
|
+
return () => clearTimeout(timer)
|
|
49
|
+
}, [refreshKey, load])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const timer = setTimeout(() => { void load(search) }, 300)
|
|
53
|
+
return () => clearTimeout(timer)
|
|
54
|
+
}, [search, load])
|
|
55
|
+
|
|
56
|
+
// Reset selection when filter changes
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setSelectedMemoryId(null)
|
|
59
|
+
setCategoryFilter('')
|
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
+
}, [memoryAgentFilter])
|
|
62
|
+
|
|
63
|
+
const uniqueCategories = useMemo(() => {
|
|
64
|
+
const cats = new Set<string>()
|
|
65
|
+
for (const e of entries) cats.add(e.category || 'note')
|
|
66
|
+
return Array.from(cats).sort()
|
|
67
|
+
}, [entries])
|
|
68
|
+
|
|
69
|
+
const filtered = useMemo(() => {
|
|
70
|
+
return entries.filter((e) => {
|
|
71
|
+
// Client-side global filter
|
|
72
|
+
if (memoryAgentFilter === '_global' && e.agentId) return false
|
|
73
|
+
if (categoryFilter && (e.category || 'note') !== categoryFilter) return false
|
|
74
|
+
return true
|
|
75
|
+
})
|
|
76
|
+
}, [entries, memoryAgentFilter, categoryFilter])
|
|
77
|
+
|
|
78
|
+
const filterLabel = useMemo(() => {
|
|
79
|
+
if (!memoryAgentFilter) return 'All Memories'
|
|
80
|
+
if (memoryAgentFilter === '_global') return 'Global Memories'
|
|
81
|
+
return agents[memoryAgentFilter]?.name || 'Agent'
|
|
82
|
+
}, [memoryAgentFilter, agents])
|
|
83
|
+
|
|
84
|
+
// No agent selected prompt
|
|
85
|
+
if (!memoryAgentFilter && !loaded && entries.length === 0) {
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex-1 flex items-center justify-center px-8">
|
|
88
|
+
<div className="text-center max-w-[380px]">
|
|
89
|
+
<div className="w-14 h-14 rounded-[16px] bg-white/[0.03] flex items-center justify-center mb-4 mx-auto">
|
|
90
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/60">
|
|
91
|
+
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
|
92
|
+
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
|
|
93
|
+
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
|
94
|
+
</svg>
|
|
95
|
+
</div>
|
|
96
|
+
<h2 className="font-display text-[15px] font-600 text-text-2 mb-2">Browse Memories</h2>
|
|
97
|
+
<p className="text-[13px] text-text-3/70">Select an agent from the sidebar to browse their memories, or view all.</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex-1 flex h-full min-w-0">
|
|
105
|
+
{/* Left: Memory card list */}
|
|
106
|
+
<div className="w-[360px] shrink-0 border-r border-white/[0.06] flex flex-col overflow-hidden">
|
|
107
|
+
{/* Header + search */}
|
|
108
|
+
<div className="px-3 pt-3 pb-1 shrink-0">
|
|
109
|
+
<div className="flex items-center gap-2 mb-2">
|
|
110
|
+
<h3 className="font-display text-[13px] font-600 text-text-2 tracking-[-0.01em] flex-1 truncate">{filterLabel}</h3>
|
|
111
|
+
<span className="text-[10px] font-mono tabular-nums text-text-3/50">{filtered.length}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<input
|
|
114
|
+
type="text"
|
|
115
|
+
value={search}
|
|
116
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
117
|
+
placeholder="Search memories..."
|
|
118
|
+
className="w-full px-4 py-2.5 rounded-[12px] border border-white/[0.04] bg-surface text-text
|
|
119
|
+
text-[13px] outline-none transition-all duration-200 placeholder:text-text-3/70 focus-glow"
|
|
120
|
+
style={{ fontFamily: 'inherit' }}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Category chips */}
|
|
125
|
+
{entries.length > 0 && uniqueCategories.length > 1 && (
|
|
126
|
+
<div className="px-3 py-1.5 shrink-0">
|
|
127
|
+
<div className="flex gap-1 flex-wrap">
|
|
128
|
+
<button
|
|
129
|
+
onClick={() => setCategoryFilter('')}
|
|
130
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border-none
|
|
131
|
+
${!categoryFilter ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'}`}
|
|
132
|
+
style={{ fontFamily: 'inherit' }}
|
|
133
|
+
>
|
|
134
|
+
all
|
|
135
|
+
</button>
|
|
136
|
+
{uniqueCategories.map((c) => (
|
|
137
|
+
<button
|
|
138
|
+
key={c}
|
|
139
|
+
onClick={() => setCategoryFilter(categoryFilter === c ? '' : c)}
|
|
140
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border-none
|
|
141
|
+
${categoryFilter === c ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'}`}
|
|
142
|
+
style={{ fontFamily: 'inherit' }}
|
|
143
|
+
>
|
|
144
|
+
{c}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Cards */}
|
|
152
|
+
<div className="flex-1 overflow-y-auto">
|
|
153
|
+
{filtered.length > 0 ? (
|
|
154
|
+
<div className="flex flex-col gap-0.5 px-2 pb-4">
|
|
155
|
+
{filtered.map((e) => {
|
|
156
|
+
// Show agent info on cards when in "All Memories" view
|
|
157
|
+
const showAgent = !memoryAgentFilter
|
|
158
|
+
const agent = showAgent && e.agentId ? agents[e.agentId] : null
|
|
159
|
+
return (
|
|
160
|
+
<MemoryCard
|
|
161
|
+
key={e.id}
|
|
162
|
+
entry={e}
|
|
163
|
+
active={e.id === selectedMemoryId}
|
|
164
|
+
agentName={showAgent ? (agent?.name || null) : undefined}
|
|
165
|
+
agentAvatarSeed={showAgent ? (agent?.avatarSeed || null) : undefined}
|
|
166
|
+
onClick={() => setSelectedMemoryId(e.id)}
|
|
167
|
+
/>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
) : error ? (
|
|
172
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
|
|
173
|
+
<p className="font-display text-[14px] font-600 text-text-2">Couldn't load memories</p>
|
|
174
|
+
<p className="text-[12px] text-text-3/60">{error}</p>
|
|
175
|
+
<button
|
|
176
|
+
onClick={() => { void load(search) }}
|
|
177
|
+
className="px-3 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[12px] font-600 cursor-pointer border-none"
|
|
178
|
+
style={{ fontFamily: 'inherit' }}
|
|
179
|
+
>
|
|
180
|
+
Retry
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
) : loaded ? (
|
|
184
|
+
<div className="flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
|
|
185
|
+
<div className="w-10 h-10 rounded-[12px] bg-accent-soft flex items-center justify-center">
|
|
186
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
|
|
187
|
+
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
|
188
|
+
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
|
|
189
|
+
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
|
190
|
+
</svg>
|
|
191
|
+
</div>
|
|
192
|
+
<p className="font-display text-[14px] font-600 text-text-2">No memories yet</p>
|
|
193
|
+
<p className="text-[12px] text-text-3/50">Agents store knowledge here as they learn</p>
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Right: Detail */}
|
|
200
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
201
|
+
<MemoryDetail />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { MemoryEntry } from '@/types'
|
|
4
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
4
5
|
|
|
5
6
|
function timeAgo(ts: number): string {
|
|
6
7
|
if (!ts) return ''
|
|
@@ -15,10 +16,11 @@ interface Props {
|
|
|
15
16
|
entry: MemoryEntry
|
|
16
17
|
active?: boolean
|
|
17
18
|
agentName?: string | null
|
|
19
|
+
agentAvatarSeed?: string | null
|
|
18
20
|
onClick: () => void
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
export function MemoryCard({ entry, active, agentName, onClick }: Props) {
|
|
23
|
+
export function MemoryCard({ entry, active, agentName, agentAvatarSeed, onClick }: Props) {
|
|
22
24
|
return (
|
|
23
25
|
<div
|
|
24
26
|
onClick={onClick}
|
|
@@ -35,14 +37,33 @@ export function MemoryCard({ entry, active, agentName, onClick }: Props) {
|
|
|
35
37
|
<span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright/70 bg-accent-soft px-1.5 py-0.5 rounded-[5px]">
|
|
36
38
|
{entry.category || 'note'}
|
|
37
39
|
</span>
|
|
40
|
+
{entry.pinned && (
|
|
41
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" className="shrink-0 text-amber-400/80">
|
|
42
|
+
<path d="M16 2l-4 4-4-4-2 2 4 4-5 5v1h1l5-5 4 4 2-2-4-4 4-4z" transform="rotate(45 12 12)" />
|
|
43
|
+
</svg>
|
|
44
|
+
)}
|
|
38
45
|
<span className="font-display text-[13px] font-600 truncate flex-1 tracking-[-0.01em]">{entry.title}</span>
|
|
39
46
|
<span className="text-[10px] text-text-3/60 shrink-0 tabular-nums font-mono">
|
|
40
47
|
{timeAgo(entry.updatedAt || entry.createdAt)}
|
|
41
48
|
</span>
|
|
42
49
|
</div>
|
|
43
|
-
<div className="text-[12px] text-text-2/40 mt-1
|
|
50
|
+
<div className="text-[12px] text-text-2/40 mt-1 line-clamp-3 leading-relaxed">
|
|
44
51
|
{entry.content || '(empty)'}
|
|
45
52
|
</div>
|
|
53
|
+
{(entry.image?.path || entry.imagePath) && (
|
|
54
|
+
<div className="mt-2 w-10 h-10 rounded-[6px] overflow-hidden bg-white/[0.04] shrink-0">
|
|
55
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
56
|
+
<img
|
|
57
|
+
src={
|
|
58
|
+
(entry.image?.path || entry.imagePath || '').startsWith('data/memory-images/')
|
|
59
|
+
? `/api/memory-images/${(entry.image?.path || entry.imagePath || '').split('/').pop()}`
|
|
60
|
+
: (entry.image?.path || entry.imagePath || '')
|
|
61
|
+
}
|
|
62
|
+
alt=""
|
|
63
|
+
className="w-full h-full object-cover"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
46
67
|
{(entry.references?.length || entry.linkedMemoryIds?.length || entry.image?.path || entry.imagePath) && (
|
|
47
68
|
<div className="flex items-center gap-2 mt-1.5 text-[10px] text-text-3/35">
|
|
48
69
|
{entry.references?.length ? <span>{entry.references.length} ref{entry.references.length === 1 ? '' : 's'}</span> : null}
|
|
@@ -50,14 +71,20 @@ export function MemoryCard({ entry, active, agentName, onClick }: Props) {
|
|
|
50
71
|
{(entry.image?.path || entry.imagePath) ? <span>image</span> : null}
|
|
51
72
|
</div>
|
|
52
73
|
)}
|
|
53
|
-
{agentName
|
|
74
|
+
{agentName ? (
|
|
75
|
+
<div className="flex items-center gap-1.5 mt-1.5">
|
|
76
|
+
<AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={16} />
|
|
77
|
+
<span className="text-[10px] text-text-3/60 truncate">{agentName}</span>
|
|
78
|
+
</div>
|
|
79
|
+
) : !entry.agentId ? (
|
|
54
80
|
<div className="flex items-center gap-1 mt-1.5">
|
|
55
|
-
<svg width="
|
|
56
|
-
<
|
|
81
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50">
|
|
82
|
+
<circle cx="12" cy="12" r="10" /><line x1="2" y1="12" x2="22" y2="12" />
|
|
83
|
+
<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" />
|
|
57
84
|
</svg>
|
|
58
|
-
<span className="text-[10px] text-text-3/
|
|
85
|
+
<span className="text-[10px] text-text-3/50">Global</span>
|
|
59
86
|
</div>
|
|
60
|
-
)}
|
|
87
|
+
) : null}
|
|
61
88
|
</div>
|
|
62
89
|
)
|
|
63
90
|
}
|