@nextsparkjs/plugin-langchain 0.1.0-beta.1

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 (67) hide show
  1. package/.env.example +41 -0
  2. package/api/observability/metrics/route.ts +110 -0
  3. package/api/observability/traces/[traceId]/route.ts +398 -0
  4. package/api/observability/traces/route.ts +205 -0
  5. package/api/sessions/route.ts +332 -0
  6. package/components/observability/CollapsibleJson.tsx +71 -0
  7. package/components/observability/CompactTimeline.tsx +75 -0
  8. package/components/observability/ConversationFlow.tsx +271 -0
  9. package/components/observability/DisabledMessage.tsx +21 -0
  10. package/components/observability/FiltersPanel.tsx +82 -0
  11. package/components/observability/ObservabilityDashboard.tsx +230 -0
  12. package/components/observability/SpansList.tsx +210 -0
  13. package/components/observability/TraceDetail.tsx +335 -0
  14. package/components/observability/TraceStatusBadge.tsx +39 -0
  15. package/components/observability/TracesTable.tsx +97 -0
  16. package/components/observability/index.ts +7 -0
  17. package/docs/01-getting-started/01-overview.md +196 -0
  18. package/docs/01-getting-started/02-installation.md +368 -0
  19. package/docs/01-getting-started/03-configuration.md +794 -0
  20. package/docs/02-core-concepts/01-architecture.md +566 -0
  21. package/docs/02-core-concepts/02-agents.md +597 -0
  22. package/docs/02-core-concepts/03-tools.md +689 -0
  23. package/docs/03-orchestration/01-graph-orchestrator.md +809 -0
  24. package/docs/03-orchestration/02-legacy-react.md +650 -0
  25. package/docs/04-advanced/01-observability.md +645 -0
  26. package/docs/04-advanced/02-token-tracking.md +469 -0
  27. package/docs/04-advanced/03-streaming.md +476 -0
  28. package/docs/04-advanced/04-guardrails.md +597 -0
  29. package/docs/05-reference/01-api-reference.md +1403 -0
  30. package/docs/05-reference/02-customization.md +646 -0
  31. package/docs/05-reference/03-examples.md +881 -0
  32. package/docs/index.md +85 -0
  33. package/hooks/observability/useMetrics.ts +31 -0
  34. package/hooks/observability/useTraceDetail.ts +48 -0
  35. package/hooks/observability/useTraces.ts +59 -0
  36. package/lib/agent-factory.ts +354 -0
  37. package/lib/agent-helpers.ts +201 -0
  38. package/lib/db-memory-store.ts +417 -0
  39. package/lib/graph/index.ts +58 -0
  40. package/lib/graph/nodes/combiner.ts +399 -0
  41. package/lib/graph/nodes/router.ts +440 -0
  42. package/lib/graph/orchestrator-graph.ts +386 -0
  43. package/lib/graph/prompts/combiner.md +131 -0
  44. package/lib/graph/prompts/router.md +193 -0
  45. package/lib/graph/types.ts +365 -0
  46. package/lib/guardrails.ts +230 -0
  47. package/lib/index.ts +44 -0
  48. package/lib/logger.ts +70 -0
  49. package/lib/memory-store.ts +168 -0
  50. package/lib/message-serializer.ts +110 -0
  51. package/lib/prompt-renderer.ts +94 -0
  52. package/lib/providers.ts +226 -0
  53. package/lib/streaming.ts +232 -0
  54. package/lib/token-tracker.ts +298 -0
  55. package/lib/tools-builder.ts +192 -0
  56. package/lib/tracer-callbacks.ts +342 -0
  57. package/lib/tracer.ts +350 -0
  58. package/migrations/001_langchain_memory.sql +83 -0
  59. package/migrations/002_token_usage.sql +127 -0
  60. package/migrations/003_observability.sql +257 -0
  61. package/package.json +28 -0
  62. package/plugin.config.ts +170 -0
  63. package/presets/lib/langchain.config.ts.preset +142 -0
  64. package/presets/templates/sector7/ai-observability/[traceId]/page.tsx +91 -0
  65. package/presets/templates/sector7/ai-observability/page.tsx +54 -0
  66. package/types/langchain.types.ts +274 -0
  67. package/types/observability.types.ts +270 -0
@@ -0,0 +1,210 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { Button } from '@nextsparkjs/core/components/ui/button'
6
+ import type { Span } from '../../types/observability.types'
7
+ import { TraceStatusBadge } from './TraceStatusBadge'
8
+
9
+ // Internal chain patterns to filter (LangChain infrastructure noise)
10
+ const INTERNAL_CHAIN_PATTERNS = [
11
+ /^Chain:\s*(Runnable|Channel|Compiled)/i,
12
+ /^RunnableSequence$/i,
13
+ /^RunnableLambda$/i,
14
+ /^ChannelWrite$/i,
15
+ /^ChannelRead$/i,
16
+ ]
17
+
18
+ interface SpansListProps {
19
+ spans: Span[]
20
+ className?: string
21
+ }
22
+
23
+ export function SpansList({ spans, className = '' }: SpansListProps) {
24
+ const t = useTranslations('observability')
25
+ const [expandedSpans, setExpandedSpans] = useState<Set<string>>(new Set())
26
+ const [showInternalChains, setShowInternalChains] = useState(false)
27
+
28
+ // Filter internal chain spans
29
+ const { displaySpans, hiddenCount } = useMemo(() => {
30
+ if (showInternalChains) {
31
+ return { displaySpans: spans, hiddenCount: 0 }
32
+ }
33
+
34
+ const filtered = spans.filter((span) => {
35
+ // Always show non-chain spans
36
+ if (span.type !== 'chain') return true
37
+ // Always show chains with errors
38
+ if (span.error) return true
39
+ // Filter internal chains by pattern
40
+ return !INTERNAL_CHAIN_PATTERNS.some((pattern) => pattern.test(span.name))
41
+ })
42
+
43
+ return {
44
+ displaySpans: filtered,
45
+ hiddenCount: spans.length - filtered.length,
46
+ }
47
+ }, [spans, showInternalChains])
48
+
49
+ const toggleSpan = (spanId: string) => {
50
+ setExpandedSpans((prev) => {
51
+ const next = new Set(prev)
52
+ if (next.has(spanId)) {
53
+ next.delete(spanId)
54
+ } else {
55
+ next.add(spanId)
56
+ }
57
+ return next
58
+ })
59
+ }
60
+
61
+ const formatDuration = (ms?: number) => {
62
+ if (!ms) return '-'
63
+ if (ms < 1000) return `${ms}ms`
64
+ return `${(ms / 1000).toFixed(2)}s`
65
+ }
66
+
67
+ const formatTokens = (input?: number, output?: number) => {
68
+ if (!input && !output) return null
69
+ return `${input || 0}/${output || 0} tokens`
70
+ }
71
+
72
+ const getSpanTypeLabel = (type: string) => {
73
+ return type.toUpperCase()
74
+ }
75
+
76
+ if (spans.length === 0) {
77
+ return (
78
+ <div className="text-center py-8 text-muted-foreground" data-cy="spans-list-empty">
79
+ {t('detail.noSpans')}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ return (
85
+ <div className={className} data-cy="spans-list">
86
+ {/* Filter toggle */}
87
+ {hiddenCount > 0 && (
88
+ <div className="flex items-center justify-between mb-3 p-2 bg-muted/50 rounded-lg" data-cy="spans-filter-info">
89
+ <span className="text-sm text-muted-foreground">
90
+ {t('detail.hiddenSpans', { count: hiddenCount })}
91
+ </span>
92
+ <Button
93
+ variant="ghost"
94
+ size="sm"
95
+ onClick={() => setShowInternalChains(!showInternalChains)}
96
+ data-cy="toggle-internal-chains"
97
+ >
98
+ {showInternalChains ? t('detail.hideInternalChains') : t('detail.showInternalChains')}
99
+ </Button>
100
+ </div>
101
+ )}
102
+ {showInternalChains && hiddenCount > 0 && (
103
+ <div className="flex justify-end mb-3">
104
+ <Button
105
+ variant="ghost"
106
+ size="sm"
107
+ onClick={() => setShowInternalChains(false)}
108
+ data-cy="hide-internal-chains"
109
+ >
110
+ {t('detail.hideInternalChains')}
111
+ </Button>
112
+ </div>
113
+ )}
114
+
115
+ <div className="space-y-2">
116
+ {displaySpans.map((span) => {
117
+ const isExpanded = expandedSpans.has(span.spanId)
118
+ const hasDetails =
119
+ (span.toolInput !== undefined && span.toolInput !== null) ||
120
+ (span.toolOutput !== undefined && span.toolOutput !== null) ||
121
+ !!span.error
122
+
123
+ return (
124
+ <div
125
+ key={span.spanId}
126
+ className="border border-border rounded-lg p-3 bg-card"
127
+ data-cy={`span-item-${span.spanId}`}
128
+ style={{ marginLeft: `${span.depth * 24}px` }}
129
+ >
130
+ <div className="flex items-start justify-between gap-4">
131
+ <div className="flex-1">
132
+ <div className="flex items-center gap-2 mb-1">
133
+ <span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground bg-muted px-1.5 py-0.5 rounded" data-cy="span-type">
134
+ {getSpanTypeLabel(span.type)}
135
+ </span>
136
+ <span className="font-medium text-foreground" data-cy="span-name">
137
+ {span.name}
138
+ </span>
139
+ <TraceStatusBadge status={span.status} />
140
+ </div>
141
+
142
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
143
+ {span.provider && (
144
+ <span data-cy="span-provider">
145
+ {span.provider} / {span.model}
146
+ </span>
147
+ )}
148
+
149
+ {span.toolName && <span data-cy="span-tool-name">{span.toolName}</span>}
150
+
151
+ <span data-cy="span-duration">{formatDuration(span.durationMs)}</span>
152
+
153
+ {formatTokens(span.inputTokens, span.outputTokens) && (
154
+ <span data-cy="span-tokens">{formatTokens(span.inputTokens, span.outputTokens)}</span>
155
+ )}
156
+ </div>
157
+
158
+ {hasDetails && (
159
+ <button
160
+ onClick={() => toggleSpan(span.spanId)}
161
+ className="mt-2 text-sm text-primary hover:underline"
162
+ data-cy={`span-toggle-${span.spanId}`}
163
+ >
164
+ {isExpanded ? t('detail.hideDetails') : t('detail.showDetails')}
165
+ </button>
166
+ )}
167
+
168
+ {isExpanded && (
169
+ <div className="mt-3 space-y-2" data-cy={`span-details-${span.spanId}`}>
170
+ {span.toolInput !== undefined && span.toolInput !== null && (
171
+ <div>
172
+ <div className="text-sm font-medium text-foreground mb-1">
173
+ {t('detail.toolInput')}
174
+ </div>
175
+ <pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
176
+ {JSON.stringify(span.toolInput, null, 2)}
177
+ </pre>
178
+ </div>
179
+ )}
180
+
181
+ {span.toolOutput !== undefined && span.toolOutput !== null && (
182
+ <div>
183
+ <div className="text-sm font-medium text-foreground mb-1">
184
+ {t('detail.toolOutput')}
185
+ </div>
186
+ <pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
187
+ {JSON.stringify(span.toolOutput, null, 2)}
188
+ </pre>
189
+ </div>
190
+ )}
191
+
192
+ {span.error && (
193
+ <div>
194
+ <div className="text-sm font-medium text-destructive mb-1">{t('detail.error')}</div>
195
+ <pre className="text-xs bg-destructive/10 text-destructive p-2 rounded overflow-x-auto">
196
+ {span.error}
197
+ </pre>
198
+ </div>
199
+ )}
200
+ </div>
201
+ )}
202
+ </div>
203
+ </div>
204
+ </div>
205
+ )
206
+ })}
207
+ </div>
208
+ </div>
209
+ )
210
+ }
@@ -0,0 +1,335 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { Button } from '@nextsparkjs/core/components/ui/button'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@nextsparkjs/core/components/ui/card'
7
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@nextsparkjs/core/components/ui/accordion'
8
+ import type { Trace, Span } from '../../types/observability.types'
9
+ import { TraceStatusBadge } from './TraceStatusBadge'
10
+ import { SpansList } from './SpansList'
11
+ import { ConversationFlow } from './ConversationFlow'
12
+ import { CompactTimeline } from './CompactTimeline'
13
+
14
+ interface ParentTraceInfo {
15
+ traceId: string
16
+ agentName: string
17
+ }
18
+
19
+ interface TraceDetailProps {
20
+ trace: Trace
21
+ spans: Span[]
22
+ childTraces?: Trace[]
23
+ childSpansMap?: Record<string, Span[]>
24
+ parentTrace?: ParentTraceInfo
25
+ onBack: () => void
26
+ onSelectChildTrace?: (traceId: string) => void
27
+ onSelectParentTrace?: (traceId: string) => void
28
+ className?: string
29
+ }
30
+
31
+ export function TraceDetail({ trace, spans, childTraces = [], childSpansMap = {}, parentTrace, onBack, onSelectChildTrace, onSelectParentTrace, className = '' }: TraceDetailProps) {
32
+ const t = useTranslations('observability')
33
+
34
+ const formatDuration = (ms?: number) => {
35
+ if (!ms) return '-'
36
+ if (ms < 1000) return `${ms}ms`
37
+ return `${(ms / 1000).toFixed(2)}s`
38
+ }
39
+
40
+ const formatTokens = (tokens: number) => {
41
+ if (tokens === 0) return '-'
42
+ return tokens.toLocaleString()
43
+ }
44
+
45
+ // Extract unique tools used in this trace
46
+ const toolsUsed = useMemo(() => {
47
+ return [...new Set(
48
+ spans
49
+ .filter((s) => s.type === 'tool' && s.toolName)
50
+ .map((s) => s.toolName!)
51
+ )]
52
+ }, [spans])
53
+
54
+ // Extract model info from LLM spans
55
+ const modelInfo = useMemo(() => {
56
+ const llmSpan = spans.find((s) => s.type === 'llm' && s.model)
57
+ if (!llmSpan) return null
58
+ return {
59
+ model: llmSpan.model,
60
+ provider: llmSpan.provider,
61
+ }
62
+ }, [spans])
63
+
64
+ return (
65
+ <div className={className} data-cy="trace-detail">
66
+ <div className="mb-6">
67
+ <div className="flex items-center gap-2 mb-4">
68
+ <Button variant="ghost" onClick={onBack} data-cy="trace-back-button">
69
+ ← {t('detail.back')}
70
+ </Button>
71
+ {parentTrace && onSelectParentTrace && (
72
+ <>
73
+ <span className="text-muted-foreground">/</span>
74
+ <Button
75
+ variant="link"
76
+ onClick={() => onSelectParentTrace(parentTrace.traceId)}
77
+ className="text-primary hover:underline p-0 h-auto font-medium"
78
+ data-cy="parent-trace-link"
79
+ >
80
+ {parentTrace.agentName}
81
+ </Button>
82
+ <span className="text-muted-foreground">/</span>
83
+ <span className="font-medium">{trace.agentName}</span>
84
+ </>
85
+ )}
86
+ </div>
87
+
88
+ <div className="flex items-start justify-between">
89
+ <div>
90
+ <h2 className="text-2xl font-bold mb-2">{t('detail.title')}</h2>
91
+ <p className="text-sm text-muted-foreground font-mono">{trace.traceId}</p>
92
+ </div>
93
+ <TraceStatusBadge status={trace.status} />
94
+ </div>
95
+ </div>
96
+
97
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
98
+ <Card>
99
+ <CardHeader className="pb-2">
100
+ <CardTitle className="text-sm font-medium text-muted-foreground">
101
+ {t('detail.agent')}
102
+ </CardTitle>
103
+ </CardHeader>
104
+ <CardContent>
105
+ <p className="text-lg font-semibold" data-cy="trace-agent">
106
+ {trace.agentName}
107
+ </p>
108
+ {trace.agentType && (
109
+ <p className="text-sm text-muted-foreground">{trace.agentType}</p>
110
+ )}
111
+ </CardContent>
112
+ </Card>
113
+
114
+ <Card>
115
+ <CardHeader className="pb-2">
116
+ <CardTitle className="text-sm font-medium text-muted-foreground">
117
+ {t('detail.model')}
118
+ </CardTitle>
119
+ </CardHeader>
120
+ <CardContent>
121
+ <p className="text-lg font-semibold" data-cy="trace-model">
122
+ {modelInfo?.model || '-'}
123
+ </p>
124
+ {modelInfo?.provider && (
125
+ <p className="text-sm text-muted-foreground" data-cy="trace-provider">
126
+ {modelInfo.provider}
127
+ </p>
128
+ )}
129
+ </CardContent>
130
+ </Card>
131
+
132
+ <Card>
133
+ <CardHeader className="pb-2">
134
+ <CardTitle className="text-sm font-medium text-muted-foreground">
135
+ {t('detail.duration')}
136
+ </CardTitle>
137
+ </CardHeader>
138
+ <CardContent>
139
+ <p className="text-lg font-semibold" data-cy="trace-duration">
140
+ {formatDuration(trace.durationMs)}
141
+ </p>
142
+ </CardContent>
143
+ </Card>
144
+
145
+ <Card>
146
+ <CardHeader className="pb-2">
147
+ <CardTitle className="text-sm font-medium text-muted-foreground">
148
+ {t('detail.tokens')}
149
+ </CardTitle>
150
+ </CardHeader>
151
+ <CardContent>
152
+ <p className="text-lg font-semibold" data-cy="trace-tokens">
153
+ {formatTokens(trace.totalTokens)}
154
+ </p>
155
+ <p className="text-xs text-muted-foreground">
156
+ {formatTokens(trace.inputTokens)} in / {formatTokens(trace.outputTokens)} out
157
+ </p>
158
+ </CardContent>
159
+ </Card>
160
+
161
+ <Card>
162
+ <CardHeader className="pb-2">
163
+ <CardTitle className="text-sm font-medium text-muted-foreground">
164
+ {t('detail.calls')}
165
+ </CardTitle>
166
+ </CardHeader>
167
+ <CardContent>
168
+ <p className="text-lg font-semibold" data-cy="trace-calls">
169
+ {trace.llmCalls} LLM / {trace.toolCalls} {t('detail.tools')}
170
+ </p>
171
+ {toolsUsed.length > 0 && (
172
+ <p className="text-xs text-muted-foreground mt-1" data-cy="trace-tools-used">
173
+ {toolsUsed.join(', ')}
174
+ </p>
175
+ )}
176
+ </CardContent>
177
+ </Card>
178
+ </div>
179
+
180
+ <div className="space-y-6">
181
+ {/* Error box - shown first when there's an error */}
182
+ {trace.error && (
183
+ <Card className="border-destructive">
184
+ <CardHeader>
185
+ <CardTitle className="text-destructive">{t('detail.error')}</CardTitle>
186
+ </CardHeader>
187
+ <CardContent>
188
+ <pre
189
+ className="text-sm bg-destructive/10 text-destructive p-4 rounded-lg overflow-x-auto whitespace-pre-wrap"
190
+ data-cy="trace-error"
191
+ >
192
+ {trace.error}
193
+ </pre>
194
+ {trace.errorStack && (
195
+ <details className="mt-4">
196
+ <summary className="cursor-pointer text-sm font-medium mb-2">
197
+ {t('detail.stackTrace')}
198
+ </summary>
199
+ <pre className="text-xs bg-destructive/10 text-destructive p-3 rounded overflow-x-auto">
200
+ {trace.errorStack}
201
+ </pre>
202
+ </details>
203
+ )}
204
+ </CardContent>
205
+ </Card>
206
+ )}
207
+
208
+ <Card>
209
+ <CardHeader>
210
+ <CardTitle>{t('detail.input')}</CardTitle>
211
+ </CardHeader>
212
+ <CardContent>
213
+ <pre
214
+ className="text-sm bg-muted p-4 rounded-lg overflow-x-auto whitespace-pre-wrap"
215
+ data-cy="trace-input"
216
+ >
217
+ {trace.input}
218
+ </pre>
219
+ </CardContent>
220
+ </Card>
221
+
222
+ {/* Conversation Flow - Human-readable execution view */}
223
+ <ConversationFlow trace={trace} spans={spans} />
224
+
225
+ {/* Child Traces - Sub-agent invocations with accordion */}
226
+ {childTraces.length > 0 && (
227
+ <Card>
228
+ <CardHeader>
229
+ <CardTitle>{t('detail.childTraces')}</CardTitle>
230
+ </CardHeader>
231
+ <CardContent>
232
+ <Accordion type="multiple" className="w-full" data-cy="child-traces-list">
233
+ {childTraces.map((child) => (
234
+ <AccordionItem
235
+ key={child.traceId}
236
+ value={child.traceId}
237
+ className="border rounded-lg mb-3 last:mb-0"
238
+ data-cy={`child-trace-${child.agentName}`}
239
+ >
240
+ <AccordionTrigger className="px-4 hover:no-underline">
241
+ <div className="flex items-center justify-between w-full pr-4">
242
+ <div className="flex items-center gap-3">
243
+ <TraceStatusBadge status={child.status} />
244
+ <div className="text-left">
245
+ <p className="font-medium">{child.agentName}</p>
246
+ <p className="text-xs text-muted-foreground font-mono">
247
+ {child.traceId.slice(0, 8)}...
248
+ </p>
249
+ </div>
250
+ </div>
251
+ <div className="text-right text-sm">
252
+ <p>{formatDuration(child.durationMs)}</p>
253
+ <p className="text-xs text-muted-foreground">
254
+ {formatTokens(child.totalTokens)} tokens
255
+ </p>
256
+ </div>
257
+ </div>
258
+ </AccordionTrigger>
259
+ <AccordionContent className="px-4">
260
+ <div className="space-y-4 pt-2">
261
+ {/* Stats row */}
262
+ <div className="grid grid-cols-3 gap-4 text-sm">
263
+ <div className="bg-muted/50 rounded p-2">
264
+ <p className="text-muted-foreground text-xs">{t('detail.duration')}</p>
265
+ <p className="font-medium">{formatDuration(child.durationMs)}</p>
266
+ </div>
267
+ <div className="bg-muted/50 rounded p-2">
268
+ <p className="text-muted-foreground text-xs">{t('detail.tokens')}</p>
269
+ <p className="font-medium">
270
+ {formatTokens(child.inputTokens)} / {formatTokens(child.outputTokens)}
271
+ </p>
272
+ </div>
273
+ <div className="bg-muted/50 rounded p-2">
274
+ <p className="text-muted-foreground text-xs">{t('detail.calls')}</p>
275
+ <p className="font-medium">
276
+ {child.llmCalls} LLM / {child.toolCalls} tools
277
+ </p>
278
+ </div>
279
+ </div>
280
+
281
+ {/* Compact Timeline - execution flow visualization */}
282
+ {childSpansMap[child.traceId] && childSpansMap[child.traceId].length > 0 && (
283
+ <div className="bg-muted/30 rounded-lg p-3">
284
+ <CompactTimeline spans={childSpansMap[child.traceId]} />
285
+ </div>
286
+ )}
287
+
288
+ {/* Error if exists */}
289
+ {child.error && (
290
+ <div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
291
+ <p className="text-sm font-medium text-destructive mb-1">{t('detail.error')}</p>
292
+ <pre className="text-xs text-destructive whitespace-pre-wrap">
293
+ {child.error}
294
+ </pre>
295
+ </div>
296
+ )}
297
+
298
+ {/* Input */}
299
+ <div>
300
+ <p className="text-sm font-medium mb-2">{t('detail.input')}</p>
301
+ <pre className="text-sm bg-muted p-3 rounded-lg overflow-x-auto whitespace-pre-wrap max-h-40 overflow-y-auto">
302
+ {child.input}
303
+ </pre>
304
+ </div>
305
+
306
+ {/* Output */}
307
+ {child.output && (
308
+ <div>
309
+ <p className="text-sm font-medium mb-2">{t('detail.output')}</p>
310
+ <pre className="text-sm bg-muted p-3 rounded-lg overflow-x-auto whitespace-pre-wrap max-h-60 overflow-y-auto">
311
+ {child.output}
312
+ </pre>
313
+ </div>
314
+ )}
315
+ </div>
316
+ </AccordionContent>
317
+ </AccordionItem>
318
+ ))}
319
+ </Accordion>
320
+ </CardContent>
321
+ </Card>
322
+ )}
323
+
324
+ <Card>
325
+ <CardHeader>
326
+ <CardTitle>{t('detail.spans')}</CardTitle>
327
+ </CardHeader>
328
+ <CardContent>
329
+ <SpansList spans={spans} />
330
+ </CardContent>
331
+ </Card>
332
+ </div>
333
+ </div>
334
+ )
335
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { useTranslations } from 'next-intl'
4
+ import type { TraceStatus } from '../../types/observability.types'
5
+
6
+ interface TraceStatusBadgeProps {
7
+ status: TraceStatus
8
+ className?: string
9
+ }
10
+
11
+ export function TraceStatusBadge({ status, className = '' }: TraceStatusBadgeProps) {
12
+ const t = useTranslations('observability')
13
+
14
+ const statusConfig = {
15
+ running: {
16
+ label: t('status.running'),
17
+ className: 'bg-muted text-muted-foreground border-border',
18
+ },
19
+ success: {
20
+ label: t('status.success'),
21
+ className: 'bg-muted text-foreground border-border',
22
+ },
23
+ error: {
24
+ label: t('status.error'),
25
+ className: 'bg-destructive/10 text-destructive border-destructive/20',
26
+ },
27
+ }
28
+
29
+ const config = statusConfig[status]
30
+
31
+ return (
32
+ <span
33
+ className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border ${config.className} ${className}`}
34
+ data-cy={`trace-status-${status}`}
35
+ >
36
+ {config.label}
37
+ </span>
38
+ )
39
+ }
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import { useTranslations } from 'next-intl'
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@nextsparkjs/core/components/ui/table'
12
+ import type { Trace } from '../../types/observability.types'
13
+ import { TraceStatusBadge } from './TraceStatusBadge'
14
+
15
+ interface TracesTableProps {
16
+ traces: Trace[]
17
+ onSelect: (traceId: string) => void
18
+ className?: string
19
+ }
20
+
21
+ export function TracesTable({ traces, onSelect, className = '' }: TracesTableProps) {
22
+ const t = useTranslations('observability')
23
+
24
+ const formatDuration = (ms?: number) => {
25
+ if (!ms) return '-'
26
+ if (ms < 1000) return `${ms}ms`
27
+ return `${(ms / 1000).toFixed(2)}s`
28
+ }
29
+
30
+ const formatTokens = (tokens: number) => {
31
+ if (tokens === 0) return '-'
32
+ return tokens.toLocaleString()
33
+ }
34
+
35
+ const truncateTraceId = (traceId: string) => {
36
+ return `${traceId.substring(0, 8)}...${traceId.substring(traceId.length - 4)}`
37
+ }
38
+
39
+ const truncateInput = (input: string, maxLength = 60) => {
40
+ if (!input) return '-'
41
+ if (input.length <= maxLength) return input
42
+ return `${input.substring(0, maxLength)}...`
43
+ }
44
+
45
+ if (traces.length === 0) {
46
+ return (
47
+ <div className="text-center py-12" data-cy="traces-table-empty">
48
+ <p className="text-muted-foreground">{t('table.empty')}</p>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ return (
54
+ <div className={className} data-cy="traces-table">
55
+ <Table>
56
+ <TableHeader>
57
+ <TableRow>
58
+ <TableHead>{t('table.traceId')}</TableHead>
59
+ <TableHead className="min-w-[200px]">{t('table.input')}</TableHead>
60
+ <TableHead>{t('table.agent')}</TableHead>
61
+ <TableHead>{t('table.status')}</TableHead>
62
+ <TableHead>{t('table.duration')}</TableHead>
63
+ <TableHead>{t('table.tokens')}</TableHead>
64
+ <TableHead>{t('table.startedAt')}</TableHead>
65
+ </TableRow>
66
+ </TableHeader>
67
+ <TableBody>
68
+ {traces.map((trace) => (
69
+ <TableRow
70
+ key={trace.traceId}
71
+ onClick={() => onSelect(trace.traceId)}
72
+ className="cursor-pointer hover:bg-muted/50"
73
+ data-cy="trace-row"
74
+ data-trace-id={trace.traceId}
75
+ >
76
+ <TableCell className="font-mono text-xs" data-cy="trace-id">
77
+ {truncateTraceId(trace.traceId)}
78
+ </TableCell>
79
+ <TableCell className="text-sm max-w-[300px]" data-cy="trace-input" title={trace.input}>
80
+ {truncateInput(trace.input)}
81
+ </TableCell>
82
+ <TableCell data-cy="trace-agent">{trace.agentName}</TableCell>
83
+ <TableCell data-cy="trace-status">
84
+ <TraceStatusBadge status={trace.status} />
85
+ </TableCell>
86
+ <TableCell data-cy="trace-duration">{formatDuration(trace.durationMs)}</TableCell>
87
+ <TableCell data-cy="trace-tokens">{formatTokens(trace.totalTokens)}</TableCell>
88
+ <TableCell className="text-sm text-muted-foreground" data-cy="trace-started-at">
89
+ {new Date(trace.startedAt).toLocaleString()}
90
+ </TableCell>
91
+ </TableRow>
92
+ ))}
93
+ </TableBody>
94
+ </Table>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,7 @@
1
+ export { ObservabilityDashboard } from './ObservabilityDashboard'
2
+ export { TracesTable } from './TracesTable'
3
+ export { TraceDetail } from './TraceDetail'
4
+ export { SpansList } from './SpansList'
5
+ export { FiltersPanel } from './FiltersPanel'
6
+ export { TraceStatusBadge } from './TraceStatusBadge'
7
+ export { DisabledMessage } from './DisabledMessage'