@swarmclawai/swarmclaw 1.3.4 → 1.3.6
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 +20 -76
- package/package.json +3 -2
- package/skills/swarmclaw.md +17 -0
- package/src/app/api/agents/[id]/dream/route.ts +45 -0
- package/src/app/api/knowledge/[id]/route.ts +48 -49
- package/src/app/api/knowledge/hygiene/route.ts +13 -0
- package/src/app/api/knowledge/route.ts +70 -42
- package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
- package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
- package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
- package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
- package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
- package/src/app/api/knowledge/sources/route.ts +1 -0
- package/src/app/api/knowledge/upload/route.ts +3 -51
- package/src/app/api/memory/dream/[id]/route.ts +19 -0
- package/src/app/api/memory/dream/route.ts +34 -0
- package/src/app/knowledge/layout.tsx +1 -1
- package/src/app/knowledge/page.tsx +2 -22
- package/src/app/protocols/page.tsx +21 -2
- package/src/cli/index.js +16 -0
- package/src/cli/spec.js +5 -0
- package/src/components/agents/agent-sheet.tsx +65 -0
- package/src/components/chat/message-bubble.tsx +10 -0
- package/src/components/knowledge/grounding-panel.tsx +99 -0
- package/src/components/knowledge/knowledge-detail.tsx +402 -0
- package/src/components/knowledge/knowledge-list.tsx +351 -126
- package/src/components/knowledge/knowledge-sheet.tsx +208 -119
- package/src/components/memory/dream-history.tsx +155 -0
- package/src/components/memory/memory-card.tsx +7 -0
- package/src/components/memory/memory-detail.tsx +46 -0
- package/src/components/runs/run-list.tsx +23 -0
- package/src/lib/server/api-routes.test.ts +43 -2
- package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
- package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
- package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +108 -0
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
- package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
- package/src/lib/server/chat-execution/chat-execution.ts +1 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
- package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
- package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
- package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
- package/src/lib/server/execution-engine/task-attempt.ts +8 -2
- package/src/lib/server/knowledge-import.ts +159 -0
- package/src/lib/server/knowledge-sources.test.ts +261 -0
- package/src/lib/server/knowledge-sources.ts +1284 -0
- package/src/lib/server/memory/dream-cycles.ts +49 -0
- package/src/lib/server/memory/dream-idle-callback.ts +38 -0
- package/src/lib/server/memory/dream-service.ts +315 -0
- package/src/lib/server/memory/memory-db.ts +37 -2
- package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
- package/src/lib/server/protocols/protocol-service.test.ts +99 -0
- package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
- package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
- package/src/lib/server/protocols/protocol-types.ts +4 -0
- package/src/lib/server/runtime/daemon-state/core.ts +6 -1
- package/src/lib/server/runtime/run-ledger.test.ts +120 -0
- package/src/lib/server/runtime/run-ledger.ts +27 -1
- package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
- package/src/lib/server/storage-normalization.ts +5 -0
- package/src/lib/server/storage.ts +15 -0
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
- package/src/stores/slices/ui-slice.ts +4 -0
- package/src/types/agent.ts +7 -0
- package/src/types/dream.ts +45 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +3 -0
- package/src/types/misc.ts +131 -0
- package/src/types/protocol.ts +4 -0
- package/src/types/run.ts +4 -1
|
@@ -5,7 +5,7 @@ import { api } from '@/lib/app/api-client'
|
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
7
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
|
-
import type {
|
|
8
|
+
import type { KnowledgeSourceDetail, KnowledgeSourceKind } from '@/types'
|
|
9
9
|
import { toast } from 'sonner'
|
|
10
10
|
|
|
11
11
|
const ACCEPTED_TYPES = '.txt,.md,.csv,.json,.jsonl,.html,.xml,.yaml,.yml,.toml,.py,.js,.ts,.tsx,.jsx,.go,.rs,.java,.c,.cpp,.h,.rb,.php,.sh,.sql,.log,.pdf'
|
|
@@ -20,195 +20,263 @@ interface UploadResult {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function KnowledgeSheet() {
|
|
23
|
-
const open = useAppStore((
|
|
24
|
-
const setOpen = useAppStore((
|
|
25
|
-
const editingId = useAppStore((
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
23
|
+
const open = useAppStore((state) => state.knowledgeSheetOpen)
|
|
24
|
+
const setOpen = useAppStore((state) => state.setKnowledgeSheetOpen)
|
|
25
|
+
const editingId = useAppStore((state) => state.editingKnowledgeId)
|
|
26
|
+
const setEditingKnowledgeId = useAppStore((state) => state.setEditingKnowledgeId)
|
|
27
|
+
const setSelectedKnowledgeSourceId = useAppStore((state) => state.setSelectedKnowledgeSourceId)
|
|
28
|
+
const triggerKnowledgeRefresh = useAppStore((state) => state.triggerKnowledgeRefresh)
|
|
29
|
+
const agents = useAppStore((state) => state.agents)
|
|
30
|
+
const loadAgents = useAppStore((state) => state.loadAgents)
|
|
31
|
+
|
|
32
|
+
const [kind, setKind] = useState<KnowledgeSourceKind>('manual')
|
|
29
33
|
const [title, setTitle] = useState('')
|
|
30
34
|
const [content, setContent] = useState('')
|
|
31
35
|
const [tags, setTags] = useState('')
|
|
32
36
|
const [scope, setScope] = useState<'global' | 'agent'>('global')
|
|
33
37
|
const [agentIds, setAgentIds] = useState<string[]>([])
|
|
38
|
+
const [sourceUrl, setSourceUrl] = useState('')
|
|
39
|
+
const [sourcePath, setSourcePath] = useState('')
|
|
40
|
+
const [sourceLabel, setSourceLabel] = useState('')
|
|
34
41
|
const [saving, setSaving] = useState(false)
|
|
35
|
-
const [uploadedFile, setUploadedFile] = useState<{ name: string; url: string; size: number } | null>(null)
|
|
36
42
|
const [uploading, setUploading] = useState(false)
|
|
37
43
|
const [isDragging, setIsDragging] = useState(false)
|
|
44
|
+
const [uploadedFile, setUploadedFile] = useState<{ name: string; url: string; size: number | null } | null>(null)
|
|
45
|
+
|
|
38
46
|
const dragCounter = useRef(0)
|
|
39
47
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
40
48
|
const agentList = Object.values(agents)
|
|
41
49
|
|
|
42
50
|
useEffect(() => {
|
|
43
51
|
if (open) loadAgents()
|
|
44
|
-
|
|
45
|
-
}, [open])
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (!open) return
|
|
49
|
-
if (editingId) {
|
|
50
|
-
void api<MemoryEntry>('GET', `/knowledge/${editingId}`).then((entry) => {
|
|
51
|
-
setTitle(entry.title)
|
|
52
|
-
setContent(entry.content)
|
|
53
|
-
const meta = entry.metadata as { tags?: string[]; scope?: 'global' | 'agent'; agentIds?: string[] } | undefined
|
|
54
|
-
setTags(meta?.tags?.join(', ') || '')
|
|
55
|
-
setScope(meta?.scope || 'global')
|
|
56
|
-
setAgentIds(meta?.agentIds || [])
|
|
57
|
-
}).catch(() => {
|
|
58
|
-
setOpen(false)
|
|
59
|
-
})
|
|
60
|
-
} else {
|
|
61
|
-
setTitle('')
|
|
62
|
-
setContent('')
|
|
63
|
-
setTags('')
|
|
64
|
-
setScope('global')
|
|
65
|
-
setAgentIds([])
|
|
66
|
-
setUploadedFile(null)
|
|
67
|
-
}
|
|
68
|
-
}, [open, editingId, setOpen])
|
|
52
|
+
}, [loadAgents, open])
|
|
69
53
|
|
|
70
|
-
const
|
|
71
|
-
|
|
54
|
+
const resetForm = useCallback(() => {
|
|
55
|
+
setKind('manual')
|
|
72
56
|
setTitle('')
|
|
73
57
|
setContent('')
|
|
74
58
|
setTags('')
|
|
75
59
|
setScope('global')
|
|
76
60
|
setAgentIds([])
|
|
61
|
+
setSourceUrl('')
|
|
62
|
+
setSourcePath('')
|
|
63
|
+
setSourceLabel('')
|
|
77
64
|
setUploadedFile(null)
|
|
78
65
|
setIsDragging(false)
|
|
79
66
|
dragCounter.current = 0
|
|
80
|
-
}
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!open) return
|
|
71
|
+
if (!editingId) {
|
|
72
|
+
resetForm()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resetForm()
|
|
77
|
+
void api<KnowledgeSourceDetail>('GET', `/knowledge/sources/${editingId}`).then((detail) => {
|
|
78
|
+
const { source } = detail
|
|
79
|
+
setKind(source.kind)
|
|
80
|
+
setTitle(source.title)
|
|
81
|
+
setContent(source.content || '')
|
|
82
|
+
setTags(source.tags.join(', '))
|
|
83
|
+
setScope(source.scope)
|
|
84
|
+
setAgentIds(source.agentIds)
|
|
85
|
+
setSourceUrl(source.sourceUrl || '')
|
|
86
|
+
setSourcePath(source.sourcePath || '')
|
|
87
|
+
setSourceLabel(source.sourceLabel || '')
|
|
88
|
+
setUploadedFile(source.kind === 'file'
|
|
89
|
+
? { name: source.sourceLabel || source.title, url: source.sourceUrl || '', size: null }
|
|
90
|
+
: null)
|
|
91
|
+
}).catch(() => {
|
|
92
|
+
toast.error('Unable to load this knowledge source')
|
|
93
|
+
setOpen(false)
|
|
94
|
+
})
|
|
95
|
+
}, [editingId, open, resetForm, setOpen])
|
|
96
|
+
|
|
97
|
+
const onClose = useCallback(() => {
|
|
98
|
+
setOpen(false)
|
|
99
|
+
setEditingKnowledgeId(null)
|
|
100
|
+
resetForm()
|
|
101
|
+
}, [resetForm, setEditingKnowledgeId, setOpen])
|
|
81
102
|
|
|
82
103
|
const parseTags = (raw: string): string[] =>
|
|
83
|
-
raw.split(',').map((
|
|
104
|
+
raw.split(',').map((tag) => tag.trim()).filter(Boolean)
|
|
105
|
+
|
|
106
|
+
const toggleAgent = (id: string) => {
|
|
107
|
+
setAgentIds((current) => current.includes(id) ? current.filter((agentId) => agentId !== id) : [...current, id])
|
|
108
|
+
}
|
|
84
109
|
|
|
85
110
|
const handleUpload = useCallback(async (file: File) => {
|
|
86
111
|
setUploading(true)
|
|
87
112
|
try {
|
|
88
|
-
const
|
|
113
|
+
const response = await fetch('/api/knowledge/upload', {
|
|
89
114
|
method: 'POST',
|
|
90
115
|
headers: { 'X-Filename': file.name },
|
|
91
116
|
body: file,
|
|
92
117
|
})
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const payload = await response.json().catch(() => ({ error: 'Upload failed' }))
|
|
121
|
+
toast.error(payload.error || 'Upload failed')
|
|
96
122
|
return
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
|
|
125
|
+
const result: UploadResult = await response.json()
|
|
126
|
+
setKind('file')
|
|
127
|
+
setTitle((current) => current.trim() || result.title)
|
|
100
128
|
setContent(result.content)
|
|
129
|
+
setSourcePath(result.filePath)
|
|
130
|
+
setSourceUrl(result.url)
|
|
131
|
+
setSourceLabel(result.filename)
|
|
101
132
|
setUploadedFile({ name: result.filename, url: result.url, size: result.size })
|
|
102
133
|
toast.success('Document content extracted')
|
|
103
134
|
|
|
104
|
-
// Auto-tag based on file extension
|
|
105
135
|
const ext = file.name.split('.').pop()?.toLowerCase() || ''
|
|
106
|
-
if (ext
|
|
107
|
-
setTags((
|
|
136
|
+
if (ext) {
|
|
137
|
+
setTags((current) => current.includes(ext) ? current : current ? `${current}, ${ext}` : ext)
|
|
108
138
|
}
|
|
109
|
-
} catch (
|
|
110
|
-
toast.error(
|
|
139
|
+
} catch (error) {
|
|
140
|
+
toast.error(error instanceof Error ? error.message : 'Upload failed')
|
|
111
141
|
} finally {
|
|
112
142
|
setUploading(false)
|
|
113
143
|
}
|
|
114
|
-
}, [
|
|
144
|
+
}, [])
|
|
115
145
|
|
|
116
|
-
const handleFileChange = useCallback((
|
|
117
|
-
const file =
|
|
146
|
+
const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
147
|
+
const file = event.target.files?.[0]
|
|
118
148
|
if (file) void handleUpload(file)
|
|
119
|
-
|
|
149
|
+
event.target.value = ''
|
|
120
150
|
}, [handleUpload])
|
|
121
151
|
|
|
122
|
-
const handleDragOver = useCallback((
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
const handleDragOver = useCallback((event: React.DragEvent) => {
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
event.stopPropagation()
|
|
125
155
|
}, [])
|
|
126
156
|
|
|
127
|
-
const handleDragEnter = useCallback((
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
dragCounter.current
|
|
131
|
-
if (
|
|
157
|
+
const handleDragEnter = useCallback((event: React.DragEvent) => {
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
event.stopPropagation()
|
|
160
|
+
dragCounter.current += 1
|
|
161
|
+
if (event.dataTransfer.types.includes('Files')) setIsDragging(true)
|
|
132
162
|
}, [])
|
|
133
163
|
|
|
134
|
-
const handleDragLeave = useCallback((
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
dragCounter.current
|
|
164
|
+
const handleDragLeave = useCallback((event: React.DragEvent) => {
|
|
165
|
+
event.preventDefault()
|
|
166
|
+
event.stopPropagation()
|
|
167
|
+
dragCounter.current -= 1
|
|
138
168
|
if (dragCounter.current === 0) setIsDragging(false)
|
|
139
169
|
}, [])
|
|
140
170
|
|
|
141
|
-
const handleDrop = useCallback((
|
|
142
|
-
|
|
143
|
-
|
|
171
|
+
const handleDrop = useCallback((event: React.DragEvent) => {
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
event.stopPropagation()
|
|
144
174
|
dragCounter.current = 0
|
|
145
175
|
setIsDragging(false)
|
|
146
|
-
const file =
|
|
176
|
+
const file = event.dataTransfer.files?.[0]
|
|
147
177
|
if (file) void handleUpload(file)
|
|
148
178
|
}, [handleUpload])
|
|
149
179
|
|
|
150
|
-
const toggleAgent = (id: string) => {
|
|
151
|
-
setAgentIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const scopeHelperText = scope === 'global'
|
|
155
|
-
? 'This knowledge will be accessible to all agents'
|
|
156
|
-
: agentIds.length === 0
|
|
157
|
-
? 'Select which agents can access this knowledge'
|
|
158
|
-
: `${agentIds.length} agent(s) selected`
|
|
159
|
-
|
|
160
180
|
const handleSave = async () => {
|
|
181
|
+
if (kind === 'manual' && !content.trim()) {
|
|
182
|
+
toast.error('Manual knowledge needs content')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if (kind === 'file' && !sourcePath && !content.trim()) {
|
|
186
|
+
toast.error('Upload a file or provide extracted content')
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (kind === 'url' && !sourceUrl.trim()) {
|
|
190
|
+
toast.error('A URL is required for URL knowledge')
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
161
194
|
setSaving(true)
|
|
162
195
|
try {
|
|
163
196
|
const payload = {
|
|
164
|
-
|
|
197
|
+
kind,
|
|
198
|
+
title: title.trim(),
|
|
165
199
|
content,
|
|
166
200
|
tags: parseTags(tags),
|
|
167
201
|
scope,
|
|
168
202
|
agentIds: scope === 'agent' ? agentIds : [],
|
|
203
|
+
sourceUrl: sourceUrl.trim() || undefined,
|
|
204
|
+
sourcePath: sourcePath.trim() || undefined,
|
|
205
|
+
sourceLabel: sourceLabel.trim() || undefined,
|
|
206
|
+
metadata: uploadedFile?.size != null
|
|
207
|
+
? { fileSize: uploadedFile.size }
|
|
208
|
+
: undefined,
|
|
169
209
|
}
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
await api('PUT', `/knowledge/${editingId}`, payload)
|
|
173
|
-
|
|
174
|
-
} else {
|
|
175
|
-
await api('POST', '/knowledge', payload)
|
|
176
|
-
toast.success('Knowledge entry created')
|
|
177
|
-
}
|
|
211
|
+
const detail = editingId
|
|
212
|
+
? await api<KnowledgeSourceDetail>('PUT', `/knowledge/sources/${editingId}`, payload)
|
|
213
|
+
: await api<KnowledgeSourceDetail>('POST', '/knowledge/sources', payload)
|
|
178
214
|
|
|
215
|
+
setSelectedKnowledgeSourceId(detail.source.id)
|
|
216
|
+
triggerKnowledgeRefresh()
|
|
217
|
+
toast.success(editingId ? 'Knowledge source updated' : 'Knowledge source created')
|
|
179
218
|
onClose()
|
|
180
|
-
} catch (
|
|
181
|
-
toast.error(
|
|
219
|
+
} catch (error) {
|
|
220
|
+
toast.error(error instanceof Error ? error.message : 'Failed to save knowledge')
|
|
182
221
|
} finally {
|
|
183
222
|
setSaving(false)
|
|
184
223
|
}
|
|
185
224
|
}
|
|
186
225
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
const formatSize = (bytes: number) => {
|
|
226
|
+
const formatSize = (bytes: number | null) => {
|
|
227
|
+
if (bytes == null) return null
|
|
190
228
|
if (bytes < 1024) return `${bytes} B`
|
|
191
229
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
192
230
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
193
231
|
}
|
|
194
232
|
|
|
233
|
+
const inputClass = 'w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow'
|
|
234
|
+
const scopeHelperText = scope === 'global'
|
|
235
|
+
? 'This source will be searchable across the whole fleet'
|
|
236
|
+
: agentIds.length === 0
|
|
237
|
+
? 'Select which agents should receive this source during retrieval'
|
|
238
|
+
: `${agentIds.length} agent(s) selected`
|
|
239
|
+
|
|
240
|
+
const canSave = kind === 'manual'
|
|
241
|
+
? Boolean(content.trim())
|
|
242
|
+
: kind === 'file'
|
|
243
|
+
? Boolean(sourcePath || content.trim())
|
|
244
|
+
: Boolean(sourceUrl.trim())
|
|
245
|
+
|
|
195
246
|
return (
|
|
196
247
|
<BottomSheet open={open} onClose={onClose}>
|
|
197
248
|
<div className="mb-10">
|
|
198
249
|
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
199
|
-
{editingId ? 'Edit Knowledge' : 'New Knowledge'}
|
|
250
|
+
{editingId ? 'Edit Knowledge Source' : 'New Knowledge Source'}
|
|
200
251
|
</h2>
|
|
201
252
|
<p className="text-[14px] text-text-3">
|
|
202
|
-
|
|
253
|
+
Manual notes, uploaded files, and imported URLs all index into the same knowledge library.
|
|
203
254
|
</p>
|
|
204
255
|
</div>
|
|
205
256
|
|
|
206
|
-
|
|
207
|
-
|
|
257
|
+
<div className="mb-8">
|
|
258
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Source Type</label>
|
|
259
|
+
<div className="grid grid-cols-3 gap-2">
|
|
260
|
+
{(['manual', 'file', 'url'] as const).map((sourceKind) => (
|
|
261
|
+
<button
|
|
262
|
+
key={sourceKind}
|
|
263
|
+
onClick={() => setKind(sourceKind)}
|
|
264
|
+
className={`py-3 rounded-[14px] text-[13px] font-600 border transition-all cursor-pointer ${
|
|
265
|
+
kind === sourceKind
|
|
266
|
+
? 'border-accent-bright/25 bg-accent-soft text-accent-bright'
|
|
267
|
+
: 'border-white/[0.08] bg-white/[0.02] text-text-3 hover:text-text-2'
|
|
268
|
+
}`}
|
|
269
|
+
style={{ fontFamily: 'inherit' }}
|
|
270
|
+
>
|
|
271
|
+
{sourceKind === 'manual' ? 'Manual' : sourceKind === 'file' ? 'File' : 'URL'}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{kind === 'file' && (
|
|
208
278
|
<div className="mb-8">
|
|
209
|
-
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
210
|
-
Upload Document
|
|
211
|
-
</label>
|
|
279
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Upload Document</label>
|
|
212
280
|
|
|
213
281
|
{uploadedFile ? (
|
|
214
282
|
<div className="flex items-center gap-3 px-4 py-3 rounded-[14px] border border-emerald-500/20 bg-emerald-500/[0.04]">
|
|
@@ -219,19 +287,24 @@ export function KnowledgeSheet() {
|
|
|
219
287
|
</svg>
|
|
220
288
|
<div className="flex-1 min-w-0">
|
|
221
289
|
<p className="text-[13px] text-text font-500 truncate">{uploadedFile.name}</p>
|
|
222
|
-
<p className="text-[11px] text-text-3/60">
|
|
290
|
+
<p className="text-[11px] text-text-3/60">
|
|
291
|
+
{formatSize(uploadedFile.size) ? `${formatSize(uploadedFile.size)} • ` : ''}content extracted
|
|
292
|
+
</p>
|
|
223
293
|
</div>
|
|
224
294
|
<button
|
|
225
295
|
onClick={() => {
|
|
226
296
|
setUploadedFile(null)
|
|
297
|
+
setSourcePath('')
|
|
298
|
+
setSourceUrl('')
|
|
299
|
+
setSourceLabel('')
|
|
227
300
|
setContent('')
|
|
228
|
-
setTitle('')
|
|
229
301
|
}}
|
|
230
302
|
className="p-1.5 rounded-[8px] text-text-3 hover:text-red-400 hover:bg-red-400/10 border-none bg-transparent cursor-pointer transition-colors"
|
|
231
303
|
aria-label="Remove uploaded file"
|
|
232
304
|
>
|
|
233
305
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
234
|
-
<line x1="18" y1="6" x2="6" y2="18"
|
|
306
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
307
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
235
308
|
</svg>
|
|
236
309
|
</button>
|
|
237
310
|
</div>
|
|
@@ -242,12 +315,11 @@ export function KnowledgeSheet() {
|
|
|
242
315
|
onDragLeave={handleDragLeave}
|
|
243
316
|
onDrop={handleDrop}
|
|
244
317
|
onClick={() => fileInputRef.current?.click()}
|
|
245
|
-
className={`flex flex-col items-center gap-3 px-6 py-8 rounded-[14px] border-2 border-dashed cursor-pointer transition-all duration-200
|
|
246
|
-
|
|
318
|
+
className={`flex flex-col items-center gap-3 px-6 py-8 rounded-[14px] border-2 border-dashed cursor-pointer transition-all duration-200 ${
|
|
319
|
+
isDragging
|
|
247
320
|
? 'border-accent-bright/50 bg-accent-soft/20'
|
|
248
321
|
: 'border-white/[0.08] bg-white/[0.02] hover:border-white/[0.15] hover:bg-white/[0.03]'
|
|
249
|
-
|
|
250
|
-
${uploading ? 'opacity-60 pointer-events-none' : ''}`}
|
|
322
|
+
} ${uploading ? 'opacity-60 pointer-events-none' : ''}`}
|
|
251
323
|
>
|
|
252
324
|
{uploading ? (
|
|
253
325
|
<>
|
|
@@ -266,7 +338,7 @@ export function KnowledgeSheet() {
|
|
|
266
338
|
{isDragging ? 'Drop document here' : 'Drop a document or click to browse'}
|
|
267
339
|
</p>
|
|
268
340
|
<p className="text-[11px] text-text-3/50 mt-1">
|
|
269
|
-
Supports
|
|
341
|
+
Supports text, code, structured files, and PDFs
|
|
270
342
|
</p>
|
|
271
343
|
</div>
|
|
272
344
|
</>
|
|
@@ -284,12 +356,29 @@ export function KnowledgeSheet() {
|
|
|
284
356
|
</div>
|
|
285
357
|
)}
|
|
286
358
|
|
|
359
|
+
{kind === 'url' && (
|
|
360
|
+
<div className="mb-8">
|
|
361
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Source URL</label>
|
|
362
|
+
<input
|
|
363
|
+
type="url"
|
|
364
|
+
value={sourceUrl}
|
|
365
|
+
onChange={(event) => setSourceUrl(event.target.value)}
|
|
366
|
+
placeholder="https://example.com/docs/article"
|
|
367
|
+
className={inputClass}
|
|
368
|
+
style={{ fontFamily: 'inherit' }}
|
|
369
|
+
/>
|
|
370
|
+
<p className="text-[11px] text-text-3/55 mt-1.5 pl-1">
|
|
371
|
+
Save to fetch, clean, and index the page. You can also edit the extracted text below before saving again.
|
|
372
|
+
</p>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
287
376
|
<div className="mb-8">
|
|
288
377
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label>
|
|
289
378
|
<input
|
|
290
379
|
type="text"
|
|
291
380
|
value={title}
|
|
292
|
-
onChange={(
|
|
381
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
293
382
|
placeholder="Knowledge title"
|
|
294
383
|
className={inputClass}
|
|
295
384
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -298,7 +387,7 @@ export function KnowledgeSheet() {
|
|
|
298
387
|
|
|
299
388
|
<div className="mb-8">
|
|
300
389
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
301
|
-
Content
|
|
390
|
+
Indexed Content
|
|
302
391
|
{content.length > 0 && (
|
|
303
392
|
<span className="ml-2 text-text-3/40 font-mono text-[10px] normal-case tracking-normal">
|
|
304
393
|
{content.length.toLocaleString()} chars
|
|
@@ -307,10 +396,10 @@ export function KnowledgeSheet() {
|
|
|
307
396
|
</label>
|
|
308
397
|
<textarea
|
|
309
398
|
value={content}
|
|
310
|
-
onChange={(
|
|
311
|
-
placeholder=
|
|
312
|
-
rows={
|
|
313
|
-
className={`${inputClass} resize-y min-h-[
|
|
399
|
+
onChange={(event) => setContent(event.target.value)}
|
|
400
|
+
placeholder={kind === 'manual' ? 'Knowledge content...' : 'Extracted content appears here after import'}
|
|
401
|
+
rows={8}
|
|
402
|
+
className={`${inputClass} resize-y min-h-[180px]`}
|
|
314
403
|
style={{ fontFamily: 'inherit' }}
|
|
315
404
|
/>
|
|
316
405
|
</div>
|
|
@@ -320,7 +409,7 @@ export function KnowledgeSheet() {
|
|
|
320
409
|
<input
|
|
321
410
|
type="text"
|
|
322
411
|
value={tags}
|
|
323
|
-
onChange={(
|
|
412
|
+
onChange={(event) => setTags(event.target.value)}
|
|
324
413
|
placeholder="api, docs, internal (comma-separated)"
|
|
325
414
|
className={inputClass}
|
|
326
415
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -330,16 +419,16 @@ export function KnowledgeSheet() {
|
|
|
330
419
|
<div className="mb-8">
|
|
331
420
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Scope</label>
|
|
332
421
|
<div className="flex p-1 rounded-[12px] bg-bg border border-white/[0.06]">
|
|
333
|
-
{(['global', 'agent'] as const).map((
|
|
422
|
+
{(['global', 'agent'] as const).map((nextScope) => (
|
|
334
423
|
<button
|
|
335
|
-
key={
|
|
336
|
-
onClick={() => setScope(
|
|
424
|
+
key={nextScope}
|
|
425
|
+
onClick={() => setScope(nextScope)}
|
|
337
426
|
className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
|
|
338
|
-
scope ===
|
|
427
|
+
scope === nextScope ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
|
|
339
428
|
}`}
|
|
340
429
|
style={{ fontFamily: 'inherit' }}
|
|
341
430
|
>
|
|
342
|
-
{
|
|
431
|
+
{nextScope === 'global' ? 'Global' : 'Specific'}
|
|
343
432
|
</button>
|
|
344
433
|
))}
|
|
345
434
|
</div>
|
|
@@ -389,7 +478,7 @@ export function KnowledgeSheet() {
|
|
|
389
478
|
</button>
|
|
390
479
|
<button
|
|
391
480
|
onClick={() => { void handleSave() }}
|
|
392
|
-
disabled={!
|
|
481
|
+
disabled={!canSave || saving}
|
|
393
482
|
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"
|
|
394
483
|
style={{ fontFamily: 'inherit' }}
|
|
395
484
|
>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
|
+
import type { DreamCycle } from '@/types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
agentId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatDuration(ms: number): string {
|
|
11
|
+
if (ms < 1000) return `${ms}ms`
|
|
12
|
+
const sec = Math.round(ms / 1000)
|
|
13
|
+
if (sec < 60) return `${sec}s`
|
|
14
|
+
const min = Math.floor(sec / 60)
|
|
15
|
+
const remSec = sec % 60
|
|
16
|
+
return remSec > 0 ? `${min}m ${remSec}s` : `${min}m`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function timeAgo(ts: number, now: number): string {
|
|
20
|
+
const diff = now - ts
|
|
21
|
+
if (diff < 60_000) return 'just now'
|
|
22
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
23
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
24
|
+
return `${Math.floor(diff / 86_400_000)}d ago`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const statusColors: Record<string, string> = {
|
|
28
|
+
completed: 'bg-emerald-400/10 text-emerald-300',
|
|
29
|
+
running: 'bg-amber-400/10 text-amber-300',
|
|
30
|
+
pending: 'bg-amber-400/10 text-amber-300',
|
|
31
|
+
failed: 'bg-red-400/10 text-red-300',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function DreamHistory({ agentId }: Props) {
|
|
35
|
+
const [cycles, setCycles] = useState<DreamCycle[]>([])
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
const [triggering, setTriggering] = useState(false)
|
|
38
|
+
const [error, setError] = useState<string | null>(null)
|
|
39
|
+
const [now] = useState(() => Date.now())
|
|
40
|
+
|
|
41
|
+
const load = useCallback(async () => {
|
|
42
|
+
try {
|
|
43
|
+
setLoading(true)
|
|
44
|
+
setError(null)
|
|
45
|
+
const res = await fetch(`/api/memory/dream?agentId=${encodeURIComponent(agentId)}&limit=10`)
|
|
46
|
+
const data = await res.json() as { ok: boolean; cycles?: DreamCycle[]; error?: string }
|
|
47
|
+
if (data.ok && Array.isArray(data.cycles)) {
|
|
48
|
+
setCycles(data.cycles)
|
|
49
|
+
} else {
|
|
50
|
+
setError(data.error || 'Failed to load dream cycles')
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
setError('Unable to reach the server')
|
|
54
|
+
} finally {
|
|
55
|
+
setLoading(false)
|
|
56
|
+
}
|
|
57
|
+
}, [agentId])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
void load()
|
|
61
|
+
}, [load])
|
|
62
|
+
|
|
63
|
+
const handleTrigger = async () => {
|
|
64
|
+
try {
|
|
65
|
+
setTriggering(true)
|
|
66
|
+
setError(null)
|
|
67
|
+
const res = await fetch('/api/memory/dream', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ agentId }),
|
|
71
|
+
})
|
|
72
|
+
const data = await res.json() as { ok: boolean; error?: string }
|
|
73
|
+
if (!data.ok) {
|
|
74
|
+
setError(data.error || 'Dream trigger failed')
|
|
75
|
+
}
|
|
76
|
+
await load()
|
|
77
|
+
} catch {
|
|
78
|
+
setError('Unable to trigger dream cycle')
|
|
79
|
+
} finally {
|
|
80
|
+
setTriggering(false)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-3">
|
|
86
|
+
<div className="flex items-center justify-between gap-3">
|
|
87
|
+
<h4 className="font-display text-[14px] font-600 text-text">Dream History</h4>
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={() => { void load() }}
|
|
92
|
+
disabled={loading}
|
|
93
|
+
className="px-2.5 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-600 text-text-3 hover:bg-white/[0.04] hover:text-text-2 transition-all cursor-pointer disabled:opacity-50"
|
|
94
|
+
style={{ fontFamily: 'inherit' }}
|
|
95
|
+
>
|
|
96
|
+
Refresh
|
|
97
|
+
</button>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={() => { void handleTrigger() }}
|
|
101
|
+
disabled={triggering}
|
|
102
|
+
className="px-2.5 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[11px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-50"
|
|
103
|
+
style={{ fontFamily: 'inherit' }}
|
|
104
|
+
>
|
|
105
|
+
{triggering ? 'Running...' : 'Trigger Dream'}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{error && (
|
|
111
|
+
<p className="text-[12px] text-red-400/80 leading-[1.5]">{error}</p>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{loading && cycles.length === 0 ? (
|
|
115
|
+
<p className="text-[12px] text-text-3/60 py-4 text-center">Loading dream cycles...</p>
|
|
116
|
+
) : cycles.length === 0 ? (
|
|
117
|
+
<p className="text-[12px] text-text-3/60 py-4 text-center">No dream cycles yet. Trigger one manually or enable dreaming in agent settings.</p>
|
|
118
|
+
) : (
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
{cycles.map((cycle) => (
|
|
121
|
+
<div
|
|
122
|
+
key={cycle.id}
|
|
123
|
+
className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3.5 py-3"
|
|
124
|
+
>
|
|
125
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
126
|
+
<span className={`px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-[0.08em] ${statusColors[cycle.status] || 'bg-white/[0.04] text-text-3/75'}`}>
|
|
127
|
+
{cycle.status}
|
|
128
|
+
</span>
|
|
129
|
+
<span className="px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-[0.08em] bg-white/[0.04] text-text-3/75">
|
|
130
|
+
{cycle.trigger}
|
|
131
|
+
</span>
|
|
132
|
+
{cycle.status === 'completed' && cycle.startedAt && cycle.completedAt && (
|
|
133
|
+
<span className="text-[10px] text-text-3/50 font-mono tabular-nums">
|
|
134
|
+
{formatDuration(cycle.completedAt - cycle.startedAt)}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
<span className="ml-auto text-[10px] text-text-3/50 tabular-nums font-mono">
|
|
138
|
+
{timeAgo(cycle.startedAt, now)}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
{cycle.status === 'completed' && cycle.result && (
|
|
142
|
+
<p className="mt-1.5 text-[11px] text-text-3/70 leading-[1.5]">
|
|
143
|
+
{cycle.result.decayed} decayed, {cycle.result.pruned} pruned, {cycle.result.promoted} promoted, {cycle.result.consolidated} consolidated
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
{cycle.status === 'failed' && cycle.error && (
|
|
147
|
+
<p className="mt-1.5 text-[11px] text-red-400/70 leading-[1.5] line-clamp-2">{cycle.error}</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|