@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.
Files changed (73) hide show
  1. package/README.md +20 -76
  2. package/package.json +3 -2
  3. package/skills/swarmclaw.md +17 -0
  4. package/src/app/api/agents/[id]/dream/route.ts +45 -0
  5. package/src/app/api/knowledge/[id]/route.ts +48 -49
  6. package/src/app/api/knowledge/hygiene/route.ts +13 -0
  7. package/src/app/api/knowledge/route.ts +70 -42
  8. package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
  9. package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
  10. package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
  11. package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
  12. package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
  13. package/src/app/api/knowledge/sources/route.ts +1 -0
  14. package/src/app/api/knowledge/upload/route.ts +3 -51
  15. package/src/app/api/memory/dream/[id]/route.ts +19 -0
  16. package/src/app/api/memory/dream/route.ts +34 -0
  17. package/src/app/knowledge/layout.tsx +1 -1
  18. package/src/app/knowledge/page.tsx +2 -22
  19. package/src/app/protocols/page.tsx +21 -2
  20. package/src/cli/index.js +16 -0
  21. package/src/cli/spec.js +5 -0
  22. package/src/components/agents/agent-sheet.tsx +65 -0
  23. package/src/components/chat/message-bubble.tsx +10 -0
  24. package/src/components/knowledge/grounding-panel.tsx +99 -0
  25. package/src/components/knowledge/knowledge-detail.tsx +402 -0
  26. package/src/components/knowledge/knowledge-list.tsx +351 -126
  27. package/src/components/knowledge/knowledge-sheet.tsx +208 -119
  28. package/src/components/memory/dream-history.tsx +155 -0
  29. package/src/components/memory/memory-card.tsx +7 -0
  30. package/src/components/memory/memory-detail.tsx +46 -0
  31. package/src/components/runs/run-list.tsx +23 -0
  32. package/src/lib/server/api-routes.test.ts +43 -2
  33. package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
  34. package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
  35. package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +108 -0
  36. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
  37. package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
  38. package/src/lib/server/chat-execution/chat-execution.ts +1 -0
  39. package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
  40. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
  41. package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
  42. package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
  43. package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
  44. package/src/lib/server/execution-engine/task-attempt.ts +8 -2
  45. package/src/lib/server/knowledge-import.ts +159 -0
  46. package/src/lib/server/knowledge-sources.test.ts +261 -0
  47. package/src/lib/server/knowledge-sources.ts +1284 -0
  48. package/src/lib/server/memory/dream-cycles.ts +49 -0
  49. package/src/lib/server/memory/dream-idle-callback.ts +38 -0
  50. package/src/lib/server/memory/dream-service.ts +315 -0
  51. package/src/lib/server/memory/memory-db.ts +37 -2
  52. package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
  53. package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
  54. package/src/lib/server/protocols/protocol-service.test.ts +99 -0
  55. package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
  56. package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
  57. package/src/lib/server/protocols/protocol-types.ts +4 -0
  58. package/src/lib/server/runtime/daemon-state/core.ts +6 -1
  59. package/src/lib/server/runtime/run-ledger.test.ts +120 -0
  60. package/src/lib/server/runtime/run-ledger.ts +27 -1
  61. package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
  62. package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
  63. package/src/lib/server/storage-normalization.ts +5 -0
  64. package/src/lib/server/storage.ts +15 -0
  65. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
  66. package/src/stores/slices/ui-slice.ts +4 -0
  67. package/src/types/agent.ts +7 -0
  68. package/src/types/dream.ts +45 -0
  69. package/src/types/index.ts +1 -0
  70. package/src/types/message.ts +3 -0
  71. package/src/types/misc.ts +131 -0
  72. package/src/types/protocol.ts +4 -0
  73. 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
+ }