@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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/app/api-client'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { EmptyState } from '@/components/shared/empty-state'
|
|
7
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
10
|
+
import type { KnowledgeSourceDetail } from '@/types'
|
|
11
|
+
import { toast } from 'sonner'
|
|
12
|
+
|
|
13
|
+
export function KnowledgeDetail() {
|
|
14
|
+
const selectedKnowledgeSourceId = useAppStore((state) => state.selectedKnowledgeSourceId)
|
|
15
|
+
const setSelectedKnowledgeSourceId = useAppStore((state) => state.setSelectedKnowledgeSourceId)
|
|
16
|
+
const setEditingKnowledgeId = useAppStore((state) => state.setEditingKnowledgeId)
|
|
17
|
+
const setKnowledgeSheetOpen = useAppStore((state) => state.setKnowledgeSheetOpen)
|
|
18
|
+
const refreshKey = useAppStore((state) => state.knowledgeRefreshKey)
|
|
19
|
+
const triggerKnowledgeRefresh = useAppStore((state) => state.triggerKnowledgeRefresh)
|
|
20
|
+
const agents = useAppStore((state) => state.agents)
|
|
21
|
+
const loadAgents = useAppStore((state) => state.loadAgents)
|
|
22
|
+
|
|
23
|
+
const [detail, setDetail] = useState<KnowledgeSourceDetail | null>(null)
|
|
24
|
+
const [loaded, setLoaded] = useState(false)
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
const [syncing, setSyncing] = useState(false)
|
|
27
|
+
const [deleting, setDeleting] = useState(false)
|
|
28
|
+
const [archiving, setArchiving] = useState(false)
|
|
29
|
+
const [restoring, setRestoring] = useState(false)
|
|
30
|
+
const [supersedeTargetId, setSupersedeTargetId] = useState('')
|
|
31
|
+
|
|
32
|
+
const loadDetail = useCallback(async (id: string) => {
|
|
33
|
+
try {
|
|
34
|
+
const nextDetail = await api<KnowledgeSourceDetail>('GET', `/knowledge/sources/${id}`)
|
|
35
|
+
setDetail(nextDetail)
|
|
36
|
+
setSupersedeTargetId('')
|
|
37
|
+
setError(null)
|
|
38
|
+
} catch {
|
|
39
|
+
setDetail(null)
|
|
40
|
+
setError('Unable to load this knowledge source.')
|
|
41
|
+
}
|
|
42
|
+
setLoaded(true)
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
loadAgents()
|
|
47
|
+
}, [loadAgents])
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!selectedKnowledgeSourceId) {
|
|
51
|
+
setDetail(null)
|
|
52
|
+
setError(null)
|
|
53
|
+
setSupersedeTargetId('')
|
|
54
|
+
setLoaded(true)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
setLoaded(false)
|
|
58
|
+
void loadDetail(selectedKnowledgeSourceId)
|
|
59
|
+
}, [loadDetail, refreshKey, selectedKnowledgeSourceId])
|
|
60
|
+
|
|
61
|
+
const openEdit = useCallback(() => {
|
|
62
|
+
if (!selectedKnowledgeSourceId) return
|
|
63
|
+
setEditingKnowledgeId(selectedKnowledgeSourceId)
|
|
64
|
+
setKnowledgeSheetOpen(true)
|
|
65
|
+
}, [selectedKnowledgeSourceId, setEditingKnowledgeId, setKnowledgeSheetOpen])
|
|
66
|
+
|
|
67
|
+
const handleSync = useCallback(async () => {
|
|
68
|
+
if (!selectedKnowledgeSourceId) return
|
|
69
|
+
setSyncing(true)
|
|
70
|
+
try {
|
|
71
|
+
const nextDetail = await api<KnowledgeSourceDetail>('POST', `/knowledge/sources/${selectedKnowledgeSourceId}/sync`)
|
|
72
|
+
setDetail(nextDetail)
|
|
73
|
+
triggerKnowledgeRefresh()
|
|
74
|
+
toast.success('Knowledge source synced')
|
|
75
|
+
} catch (syncError) {
|
|
76
|
+
toast.error(syncError instanceof Error ? syncError.message : 'Knowledge sync failed')
|
|
77
|
+
} finally {
|
|
78
|
+
setSyncing(false)
|
|
79
|
+
}
|
|
80
|
+
}, [selectedKnowledgeSourceId, triggerKnowledgeRefresh])
|
|
81
|
+
|
|
82
|
+
const handleDelete = useCallback(async () => {
|
|
83
|
+
if (!selectedKnowledgeSourceId) return
|
|
84
|
+
setDeleting(true)
|
|
85
|
+
try {
|
|
86
|
+
await api('DELETE', `/knowledge/sources/${selectedKnowledgeSourceId}`)
|
|
87
|
+
setSelectedKnowledgeSourceId(null)
|
|
88
|
+
triggerKnowledgeRefresh()
|
|
89
|
+
toast.success('Knowledge source deleted')
|
|
90
|
+
} catch (deleteError) {
|
|
91
|
+
toast.error(deleteError instanceof Error ? deleteError.message : 'Failed to delete knowledge source')
|
|
92
|
+
} finally {
|
|
93
|
+
setDeleting(false)
|
|
94
|
+
}
|
|
95
|
+
}, [selectedKnowledgeSourceId, setSelectedKnowledgeSourceId, triggerKnowledgeRefresh])
|
|
96
|
+
|
|
97
|
+
const handleArchive = useCallback(async () => {
|
|
98
|
+
if (!selectedKnowledgeSourceId) return
|
|
99
|
+
setArchiving(true)
|
|
100
|
+
try {
|
|
101
|
+
const nextDetail = await api<KnowledgeSourceDetail>('POST', `/knowledge/sources/${selectedKnowledgeSourceId}/archive`, {
|
|
102
|
+
reason: 'manual',
|
|
103
|
+
})
|
|
104
|
+
setDetail(nextDetail)
|
|
105
|
+
triggerKnowledgeRefresh()
|
|
106
|
+
toast.success('Knowledge source archived')
|
|
107
|
+
} catch (error) {
|
|
108
|
+
toast.error(error instanceof Error ? error.message : 'Failed to archive knowledge source')
|
|
109
|
+
} finally {
|
|
110
|
+
setArchiving(false)
|
|
111
|
+
}
|
|
112
|
+
}, [selectedKnowledgeSourceId, triggerKnowledgeRefresh])
|
|
113
|
+
|
|
114
|
+
const handleRestore = useCallback(async () => {
|
|
115
|
+
if (!selectedKnowledgeSourceId) return
|
|
116
|
+
setRestoring(true)
|
|
117
|
+
try {
|
|
118
|
+
const nextDetail = await api<KnowledgeSourceDetail>('POST', `/knowledge/sources/${selectedKnowledgeSourceId}/restore`)
|
|
119
|
+
setDetail(nextDetail)
|
|
120
|
+
triggerKnowledgeRefresh()
|
|
121
|
+
toast.success('Knowledge source restored')
|
|
122
|
+
} catch (error) {
|
|
123
|
+
toast.error(error instanceof Error ? error.message : 'Failed to restore knowledge source')
|
|
124
|
+
} finally {
|
|
125
|
+
setRestoring(false)
|
|
126
|
+
}
|
|
127
|
+
}, [selectedKnowledgeSourceId, triggerKnowledgeRefresh])
|
|
128
|
+
|
|
129
|
+
const handleSupersede = useCallback(async () => {
|
|
130
|
+
if (!selectedKnowledgeSourceId || !supersedeTargetId.trim()) return
|
|
131
|
+
try {
|
|
132
|
+
const nextDetail = await api<KnowledgeSourceDetail>('POST', `/knowledge/sources/${selectedKnowledgeSourceId}/supersede`, {
|
|
133
|
+
supersededBySourceId: supersedeTargetId.trim(),
|
|
134
|
+
})
|
|
135
|
+
setDetail(nextDetail)
|
|
136
|
+
setSupersedeTargetId('')
|
|
137
|
+
triggerKnowledgeRefresh()
|
|
138
|
+
toast.success('Knowledge source marked as superseded')
|
|
139
|
+
} catch (error) {
|
|
140
|
+
toast.error(error instanceof Error ? error.message : 'Failed to supersede knowledge source')
|
|
141
|
+
}
|
|
142
|
+
}, [selectedKnowledgeSourceId, supersedeTargetId, triggerKnowledgeRefresh])
|
|
143
|
+
|
|
144
|
+
const formatDateTime = (timestamp?: number | null) => {
|
|
145
|
+
if (!timestamp) return 'Not available'
|
|
146
|
+
return new Date(timestamp).toLocaleString()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!selectedKnowledgeSourceId) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex-1 flex items-center justify-center px-8">
|
|
152
|
+
<EmptyState
|
|
153
|
+
icon={
|
|
154
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-accent-bright">
|
|
155
|
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
|
156
|
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
|
157
|
+
</svg>
|
|
158
|
+
}
|
|
159
|
+
title="Select Knowledge"
|
|
160
|
+
subtitle="Choose a source from the sidebar to inspect its provenance and indexed chunks."
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!loaded) {
|
|
167
|
+
return <PageLoader label="Loading knowledge source..." />
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (error || !detail) {
|
|
171
|
+
return (
|
|
172
|
+
<div className="flex-1 flex items-center justify-center px-8">
|
|
173
|
+
<EmptyState
|
|
174
|
+
icon={
|
|
175
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" className="text-accent-bright">
|
|
176
|
+
<circle cx="12" cy="12" r="10" />
|
|
177
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
178
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
179
|
+
</svg>
|
|
180
|
+
}
|
|
181
|
+
title="Knowledge source unavailable"
|
|
182
|
+
subtitle={error || 'This source no longer exists.'}
|
|
183
|
+
action={{ label: 'Retry', onClick: () => { void loadDetail(selectedKnowledgeSourceId) } }}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { source, chunks } = detail
|
|
190
|
+
const scopedAgents = source.agentIds.map((id) => agents[id]).filter(Boolean)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="flex-1 overflow-y-auto">
|
|
194
|
+
<div className="max-w-[1040px] mx-auto px-6 py-6 space-y-6">
|
|
195
|
+
<div className="rounded-[20px] border border-white/[0.06] bg-raised/60 p-6">
|
|
196
|
+
<div className="flex items-start justify-between gap-4">
|
|
197
|
+
<div className="min-w-0">
|
|
198
|
+
<div className="flex items-center gap-2 flex-wrap mb-2">
|
|
199
|
+
<h1 className="font-display text-[24px] font-700 tracking-[-0.03em] text-text truncate">{source.title}</h1>
|
|
200
|
+
<Badge variant="secondary" className="uppercase text-[10px] px-2 py-0.5">{source.kind}</Badge>
|
|
201
|
+
<span className={`text-[11px] font-700 uppercase tracking-[0.08em] ${
|
|
202
|
+
source.syncStatus === 'error'
|
|
203
|
+
? 'text-red-300'
|
|
204
|
+
: source.stale
|
|
205
|
+
? 'text-amber-300'
|
|
206
|
+
: 'text-emerald-300'
|
|
207
|
+
}`}
|
|
208
|
+
>
|
|
209
|
+
{source.syncStatus === 'error' ? 'Sync error' : source.stale ? 'Stale' : 'Ready'}
|
|
210
|
+
</span>
|
|
211
|
+
{source.archivedAt ? <Badge variant="secondary" className="uppercase text-[10px] px-2 py-0.5 text-amber-200">archived</Badge> : null}
|
|
212
|
+
{source.supersededBySourceId ? <Badge variant="secondary" className="uppercase text-[10px] px-2 py-0.5 text-text-3">superseded</Badge> : null}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{source.topSnippet && (
|
|
216
|
+
<p className="text-[14px] text-text-3/75 max-w-[720px] leading-relaxed">{source.topSnippet}</p>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<div className="flex items-center gap-2 mt-3 flex-wrap">
|
|
220
|
+
<span className={`text-[11px] font-600 ${source.scope === 'global' ? 'text-emerald-400' : 'text-amber-400'}`}>
|
|
221
|
+
{source.scope === 'global' ? 'Global access' : `${source.agentIds.length} agent(s)`}
|
|
222
|
+
</span>
|
|
223
|
+
<span className="text-[11px] text-text-3/55">
|
|
224
|
+
{source.chunkCount} chunk{source.chunkCount === 1 ? '' : 's'}
|
|
225
|
+
</span>
|
|
226
|
+
<span className="text-[11px] text-text-3/55">
|
|
227
|
+
{source.contentLength.toLocaleString()} chars
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{source.tags.length > 0 && (
|
|
232
|
+
<div className="flex items-center gap-1.5 mt-3 flex-wrap">
|
|
233
|
+
{source.tags.map((tag) => (
|
|
234
|
+
<Badge key={tag} variant="secondary" className="text-[10px] px-2 py-0.5">{tag}</Badge>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{scopedAgents.length > 0 && (
|
|
240
|
+
<div className="flex items-center gap-2 mt-3">
|
|
241
|
+
<div className="flex items-center -space-x-1.5">
|
|
242
|
+
{scopedAgents.map((agent) => (
|
|
243
|
+
<AgentAvatar
|
|
244
|
+
key={agent.id}
|
|
245
|
+
seed={agent.avatarSeed}
|
|
246
|
+
avatarUrl={agent.avatarUrl}
|
|
247
|
+
name={agent.name}
|
|
248
|
+
size={20}
|
|
249
|
+
className="ring-1 ring-surface"
|
|
250
|
+
/>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
<span className="text-[11px] text-text-3/60">
|
|
254
|
+
{scopedAgents.map((agent) => agent.name).join(', ')}
|
|
255
|
+
</span>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
261
|
+
<button
|
|
262
|
+
onClick={() => { void handleSync() }}
|
|
263
|
+
disabled={syncing}
|
|
264
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] font-600 text-text-2 hover:bg-white/[0.05] disabled:opacity-50 transition-all cursor-pointer"
|
|
265
|
+
style={{ fontFamily: 'inherit' }}
|
|
266
|
+
>
|
|
267
|
+
{syncing ? 'Syncing...' : 'Sync'}
|
|
268
|
+
</button>
|
|
269
|
+
<button
|
|
270
|
+
onClick={openEdit}
|
|
271
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] font-600 text-text-2 hover:bg-white/[0.05] transition-all cursor-pointer"
|
|
272
|
+
style={{ fontFamily: 'inherit' }}
|
|
273
|
+
>
|
|
274
|
+
Edit
|
|
275
|
+
</button>
|
|
276
|
+
{source.archivedAt ? (
|
|
277
|
+
<button
|
|
278
|
+
onClick={() => { void handleRestore() }}
|
|
279
|
+
disabled={restoring}
|
|
280
|
+
className="px-3 py-2 rounded-[10px] border border-emerald-500/15 bg-emerald-500/[0.06] text-[12px] font-600 text-emerald-100 hover:bg-emerald-500/[0.1] disabled:opacity-50 transition-all cursor-pointer"
|
|
281
|
+
style={{ fontFamily: 'inherit' }}
|
|
282
|
+
>
|
|
283
|
+
{restoring ? 'Restoring...' : 'Restore'}
|
|
284
|
+
</button>
|
|
285
|
+
) : (
|
|
286
|
+
<button
|
|
287
|
+
onClick={() => { void handleArchive() }}
|
|
288
|
+
disabled={archiving}
|
|
289
|
+
className="px-3 py-2 rounded-[10px] border border-amber-500/15 bg-amber-500/[0.06] text-[12px] font-600 text-amber-100 hover:bg-amber-500/[0.1] disabled:opacity-50 transition-all cursor-pointer"
|
|
290
|
+
style={{ fontFamily: 'inherit' }}
|
|
291
|
+
>
|
|
292
|
+
{archiving ? 'Archiving...' : 'Archive'}
|
|
293
|
+
</button>
|
|
294
|
+
)}
|
|
295
|
+
<button
|
|
296
|
+
onClick={() => { void handleDelete() }}
|
|
297
|
+
disabled={deleting}
|
|
298
|
+
className="px-3 py-2 rounded-[10px] border border-red-500/15 bg-red-500/[0.06] text-[12px] font-600 text-red-200 hover:bg-red-500/[0.1] disabled:opacity-50 transition-all cursor-pointer"
|
|
299
|
+
style={{ fontFamily: 'inherit' }}
|
|
300
|
+
>
|
|
301
|
+
{deleting ? 'Deleting...' : 'Delete'}
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-6">
|
|
307
|
+
<div className="rounded-[14px] border border-white/[0.05] bg-white/[0.02] p-4">
|
|
308
|
+
<p className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/55 mb-1">Source</p>
|
|
309
|
+
<p className="text-[13px] text-text-2">{source.sourceLabel || 'Manual note'}</p>
|
|
310
|
+
{source.sourceUrl && (
|
|
311
|
+
<a href={source.sourceUrl} target="_blank" rel="noreferrer" className="text-[12px] text-accent-bright hover:underline break-all">
|
|
312
|
+
{source.sourceUrl}
|
|
313
|
+
</a>
|
|
314
|
+
)}
|
|
315
|
+
{source.sourcePath && (
|
|
316
|
+
<p className="text-[12px] text-text-3/65 break-all mt-1">{source.sourcePath}</p>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div className="rounded-[14px] border border-white/[0.05] bg-white/[0.02] p-4">
|
|
321
|
+
<p className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/55 mb-1">Indexing</p>
|
|
322
|
+
<p className="text-[12px] text-text-2">Last indexed: {formatDateTime(source.lastIndexedAt)}</p>
|
|
323
|
+
<p className="text-[12px] text-text-3/70 mt-1">Last sync: {formatDateTime(source.lastSyncedAt)}</p>
|
|
324
|
+
{source.maintenanceUpdatedAt ? (
|
|
325
|
+
<p className="text-[12px] text-text-3/70 mt-1">Last maintenance: {formatDateTime(source.maintenanceUpdatedAt)}</p>
|
|
326
|
+
) : null}
|
|
327
|
+
{source.maintenanceNotes ? (
|
|
328
|
+
<p className="text-[12px] text-text-3/70 mt-1">{source.maintenanceNotes}</p>
|
|
329
|
+
) : null}
|
|
330
|
+
{source.archivedReason ? (
|
|
331
|
+
<p className="text-[12px] text-text-3/70 mt-1">Archive reason: {source.archivedReason}</p>
|
|
332
|
+
) : null}
|
|
333
|
+
{source.lastError && (
|
|
334
|
+
<p className="text-[12px] text-red-200 mt-2">{source.lastError}</p>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className="mt-4 rounded-[14px] border border-white/[0.05] bg-white/[0.02] p-4">
|
|
340
|
+
<p className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/55 mb-2">Supersede Source</p>
|
|
341
|
+
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
342
|
+
<input
|
|
343
|
+
value={supersedeTargetId}
|
|
344
|
+
onChange={(event) => setSupersedeTargetId(event.target.value)}
|
|
345
|
+
placeholder="Replacement source id"
|
|
346
|
+
className="w-full rounded-[10px] border border-white/[0.08] bg-surface px-3 py-2 text-[13px] text-text outline-none"
|
|
347
|
+
/>
|
|
348
|
+
<button
|
|
349
|
+
onClick={() => { void handleSupersede() }}
|
|
350
|
+
disabled={!supersedeTargetId.trim()}
|
|
351
|
+
className="rounded-[10px] border border-white/[0.08] bg-white/[0.03] px-3 py-2 text-[12px] font-600 text-text-2 transition-all cursor-pointer disabled:opacity-50"
|
|
352
|
+
style={{ fontFamily: 'inherit' }}
|
|
353
|
+
>
|
|
354
|
+
Mark superseded
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
357
|
+
{source.supersededBySourceId && (
|
|
358
|
+
<p className="mt-2 text-[12px] text-text-3/70">Superseded by {source.supersededBySourceId}</p>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div className="space-y-3">
|
|
364
|
+
<div className="flex items-center justify-between gap-3">
|
|
365
|
+
<h2 className="font-display text-[16px] font-600 text-text-2 tracking-[-0.02em]">Indexed Chunks</h2>
|
|
366
|
+
<span className="text-[11px] text-text-3/55">{chunks.length} result{chunks.length === 1 ? '' : 's'}</span>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{chunks.map((chunk) => {
|
|
370
|
+
const metadata = chunk.metadata && typeof chunk.metadata === 'object'
|
|
371
|
+
? chunk.metadata as Record<string, unknown>
|
|
372
|
+
: {}
|
|
373
|
+
const sectionLabel = typeof metadata.sectionLabel === 'string' ? metadata.sectionLabel : null
|
|
374
|
+
const chunkIndex = typeof metadata.chunkIndex === 'number' ? metadata.chunkIndex : 0
|
|
375
|
+
const chunkCount = typeof metadata.chunkCount === 'number' ? metadata.chunkCount : chunks.length
|
|
376
|
+
const charStart = typeof metadata.charStart === 'number' ? metadata.charStart : 0
|
|
377
|
+
const charEnd = typeof metadata.charEnd === 'number' ? metadata.charEnd : chunk.content.length
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div key={chunk.id} className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4">
|
|
381
|
+
<div className="flex items-start justify-between gap-4 mb-3">
|
|
382
|
+
<div>
|
|
383
|
+
<p className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/55">
|
|
384
|
+
Chunk {chunkIndex + 1} of {chunkCount}
|
|
385
|
+
</p>
|
|
386
|
+
<h3 className="font-display text-[15px] font-600 text-text-2 mt-1">
|
|
387
|
+
{sectionLabel || chunk.title || source.title}
|
|
388
|
+
</h3>
|
|
389
|
+
</div>
|
|
390
|
+
<span className="text-[11px] text-text-3/55 font-mono">
|
|
391
|
+
{charStart.toLocaleString()}-{charEnd.toLocaleString()}
|
|
392
|
+
</span>
|
|
393
|
+
</div>
|
|
394
|
+
<p className="text-[13px] text-text-2/85 whitespace-pre-wrap break-words leading-relaxed">{chunk.content}</p>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
})}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
)
|
|
402
|
+
}
|