@yuku123/z-agent-frontend-component 0.1.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 (41) hide show
  1. package/README.md +66 -0
  2. package/dist/z-agent-frontend-component.css +1 -0
  3. package/dist/z-agent-frontend-component.es.js +9956 -0
  4. package/dist/z-agent-frontend-component.umd.js +219 -0
  5. package/package.json +77 -0
  6. package/src/api/apiRouter.js +78 -0
  7. package/src/api/index.js +23 -0
  8. package/src/api/request.js +59 -0
  9. package/src/api/routes.js +140 -0
  10. package/src/dev.jsx +80 -0
  11. package/src/index.js +86 -0
  12. package/src/pages/agent/app/index.jsx +2 -0
  13. package/src/pages/agent/editor/AgentAppEditor.jsx +456 -0
  14. package/src/pages/agent/editor/WorkflowEditor.jsx +495 -0
  15. package/src/pages/agent/editor/nodes/index.ts +225 -0
  16. package/src/pages/agent/index.jsx +1379 -0
  17. package/src/pages/agent/share.jsx +512 -0
  18. package/src/pages/ak/AkUsageDrawer.jsx +208 -0
  19. package/src/pages/ak/index.jsx +496 -0
  20. package/src/pages/llm/index.jsx +736 -0
  21. package/src/pages/llm/model/index.jsx +220 -0
  22. package/src/pages/llm/provider/index.jsx +173 -0
  23. package/src/pages/mcp/index.jsx +359 -0
  24. package/src/pages/oss/BucketList.jsx +320 -0
  25. package/src/pages/oss/ObjectBrowser.jsx +409 -0
  26. package/src/pages/product/execute.jsx +608 -0
  27. package/src/pages/product/index.jsx +628 -0
  28. package/src/pages/product/scene.jsx +746 -0
  29. package/src/pages/script/ApiBridgeEditor.jsx +255 -0
  30. package/src/pages/script/CurlImportModal.jsx +263 -0
  31. package/src/pages/script/FieldMappingEditor.jsx +131 -0
  32. package/src/pages/script/OpenApiImportModal.jsx +212 -0
  33. package/src/pages/script/index.jsx +532 -0
  34. package/src/pages/skill/index.jsx +1595 -0
  35. package/src/pages/trace/DebugPlayground.jsx +357 -0
  36. package/src/pages/trace/components/MetricsDashboard.jsx +164 -0
  37. package/src/pages/trace/components/RagFragments.jsx +134 -0
  38. package/src/pages/trace/components/Timeline.jsx +142 -0
  39. package/src/pages/trace/components/ToolCallTree.jsx +116 -0
  40. package/src/pages/trace/index.jsx +13 -0
  41. package/src/pages/usage/index.jsx +352 -0
@@ -0,0 +1,357 @@
1
+ import React, {useCallback, useEffect, useMemo, useState} from 'react';
2
+ import {
3
+ Alert,
4
+ AutoComplete,
5
+ Badge,
6
+ Button,
7
+ Card,
8
+ Col,
9
+ Divider,
10
+ Empty,
11
+ Input,
12
+ Layout,
13
+ List,
14
+ message,
15
+ Row,
16
+ Segmented,
17
+ Select,
18
+ Space,
19
+ Spin,
20
+ Tabs,
21
+ Tag,
22
+ Tooltip,
23
+ Tree,
24
+ Typography,
25
+ } from 'antd';
26
+ import {
27
+ ApiOutlined,
28
+ AppstoreOutlined,
29
+ ArrowLeftOutlined,
30
+ BulbOutlined,
31
+ ClockCircleOutlined,
32
+ CodeOutlined,
33
+ DashboardOutlined,
34
+ FileSearchOutlined,
35
+ FilterOutlined,
36
+ HistoryOutlined,
37
+ ReloadOutlined,
38
+ SearchOutlined,
39
+ ThunderboltOutlined,
40
+ } from '@ant-design/icons';
41
+ import MetricsDashboard from './components/MetricsDashboard';
42
+ import Timeline from './components/Timeline';
43
+ import ToolCallTree from './components/ToolCallTree';
44
+ import RagFragments from './components/RagFragments';
45
+ import {traceApi} from '@/api';
46
+
47
+ const {Sider, Content} = Layout;
48
+ const {Text, Title} = Typography;
49
+
50
+ /**
51
+ * Agent Debug Playground 主页面 (FEATURE013 T7)
52
+ *
53
+ * <p>布局:
54
+ * <pre>
55
+ * ┌──────────────────────────────────────────────────┐
56
+ * │ Toolbar: 刷新 / 状态过滤 / 类型过滤 / 搜索 │
57
+ * ├─────────┬────────────────────────────────────────┤
58
+ * │ Trace │ MetricsDashboard (KPI + 调用分布) │
59
+ * │ 列表 ├────────────────────────────────────────┤
60
+ * │ │ Tabs: Timeline / Tool Tree / RAG │
61
+ * └─────────┴────────────────────────────────────────┘
62
+ * </pre>
63
+ */
64
+ const DebugPlayground = () => {
65
+ // ====== Trace 列表 ======
66
+ const [traces, setTraces] = useState([]);
67
+ const [tracesLoading, setTracesLoading] = useState(false);
68
+ const [statusFilter, setStatusFilter] = useState('ALL');
69
+ const [keyword, setKeyword] = useState('');
70
+
71
+ // ====== 当前选中 trace ======
72
+ const [selectedTraceId, setSelectedTraceId] = useState(null);
73
+ const [traceDetail, setTraceDetail] = useState(null); // {trace, spans}
74
+ const [detailLoading, setDetailLoading] = useState(false);
75
+
76
+ // ====== Tab ======
77
+ const [activeTab, setActiveTab] = useState('timeline');
78
+
79
+ // ====== 健康检查 ======
80
+ const [health, setHealth] = useState(null);
81
+
82
+ // ============ 加载 trace 列表 ============
83
+ const loadTraces = useCallback(async () => {
84
+ setTracesLoading(true);
85
+ try {
86
+ const resp = await traceApi.list({pageNum: 1, pageSize: 50});
87
+ const data = resp?.data || resp?.records || resp || [];
88
+ setTraces(Array.isArray(data) ? data : []);
89
+ } catch (e) {
90
+ message.error('加载 trace 列表失败: ' + (e?.message || '未知错误'));
91
+ } finally {
92
+ setTracesLoading(false);
93
+ }
94
+ }, []);
95
+
96
+ // ============ 加载健康检查 ============
97
+ const loadHealth = useCallback(async () => {
98
+ try {
99
+ const resp = await traceApi.health();
100
+ setHealth(resp?.data || resp);
101
+ } catch {
102
+ setHealth(null);
103
+ }
104
+ }, []);
105
+
106
+ useEffect(() => {
107
+ loadTraces();
108
+ loadHealth();
109
+ }, [loadTraces, loadHealth]);
110
+
111
+ // ============ 过滤 trace 列表 ============
112
+ const filteredTraces = useMemo(() => {
113
+ let arr = traces;
114
+ if (statusFilter !== 'ALL') {
115
+ arr = arr.filter(t => t.status === statusFilter);
116
+ }
117
+ if (keyword.trim()) {
118
+ const kw = keyword.trim().toLowerCase();
119
+ arr = arr.filter(t =>
120
+ (t.traceId && t.traceId.toLowerCase().includes(kw)) ||
121
+ (t.agentName && t.agentName.toLowerCase().includes(kw)) ||
122
+ (t.userGoal && t.userGoal.toLowerCase().includes(kw)) ||
123
+ (t.appCode && t.appCode.toLowerCase().includes(kw)),
124
+ );
125
+ }
126
+ return arr;
127
+ }, [traces, statusFilter, keyword]);
128
+
129
+ // ============ 选中 trace 后加载详情 ============
130
+ const loadTraceDetail = useCallback(async (traceId) => {
131
+ setDetailLoading(true);
132
+ try {
133
+ const resp = await traceApi.get(traceId);
134
+ const data = resp?.data || resp;
135
+ if (data && data.trace) {
136
+ setTraceDetail(data);
137
+ setSelectedTraceId(traceId);
138
+ } else {
139
+ message.warning('未找到 trace: ' + traceId);
140
+ setTraceDetail(null);
141
+ }
142
+ } catch (e) {
143
+ message.error('加载 trace 详情失败: ' + (e?.message || '未知错误'));
144
+ setTraceDetail(null);
145
+ } finally {
146
+ setDetailLoading(false);
147
+ }
148
+ }, []);
149
+
150
+ // 自动加载最新一个 trace
151
+ useEffect(() => {
152
+ if (traces.length && !selectedTraceId) {
153
+ loadTraceDetail(traces[0].traceId);
154
+ }
155
+ }, [traces, selectedTraceId, loadTraceDetail]);
156
+
157
+ // ============ 渲染左侧 trace 列表项 ============
158
+ const renderTraceItem = (trace) => {
159
+ const isSelected = selectedTraceId === trace.traceId;
160
+ const statusBadge = {
161
+ SUCCESS: 'success', FAILURE: 'error', RUNNING: 'processing', ERROR: 'error',
162
+ }[trace.status] || 'default';
163
+
164
+ return (
165
+ <List.Item
166
+ key={trace.traceId}
167
+ style={{
168
+ padding: '10px 12px',
169
+ cursor: 'pointer',
170
+ background: isSelected ? '#e6f4ff' : 'transparent',
171
+ borderLeft: isSelected ? '3px solid #1677ff' : '3px solid transparent',
172
+ }}
173
+ onClick={() => loadTraceDetail(trace.traceId)}
174
+ >
175
+ <div style={{width: '100%'}}>
176
+ <div style={{display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4}}>
177
+ <Badge status={statusBadge}/>
178
+ <Tag color={statusBadge === 'success' ? 'green' : statusBadge === 'error' ? 'red' : 'blue'}>
179
+ {trace.status}
180
+ </Tag>
181
+ <Tooltip title="DB ID">
182
+ <span style={{fontSize: 11, color: '#999'}}>#{trace.id}</span>
183
+ </Tooltip>
184
+ <div style={{flex: 1}}/>
185
+ <span style={{fontSize: 11, color: '#999'}}>
186
+ {trace.durationMs != null ? `${trace.durationMs}ms` : '—'}
187
+ </span>
188
+ </div>
189
+ <div style={{fontSize: 12, color: '#333', fontWeight: 500, marginBottom: 4}}
190
+ title={trace.userGoal}>
191
+ {truncate(trace.userGoal, 60)}
192
+ </div>
193
+ <div style={{fontSize: 10, color: '#999', display: 'flex', gap: 6}}>
194
+ <Tag color="cyan" style={{margin: 0}}>{trace.agentName || '—'}</Tag>
195
+ <span>{trace.startedAt ? new Date(trace.startedAt).toLocaleTimeString() : '—'}</span>
196
+ </div>
197
+ </div>
198
+ </List.Item>
199
+ );
200
+ };
201
+
202
+ const trace = traceDetail?.trace;
203
+ const spans = traceDetail?.spans || [];
204
+
205
+ return (
206
+ <Layout style={{height: 'calc(100vh - 64px)', background: '#f5f5f5'}}>
207
+ {/* ============ 左侧 trace 列表 ============ */}
208
+ <Sider width={340} theme="light" style={{
209
+ borderRight: '1px solid #e8e8e8',
210
+ overflow: 'auto',
211
+ }}>
212
+ {/* 健康检查横幅 */}
213
+ {health && (
214
+ <div style={{
215
+ padding: '8px 12px',
216
+ background: health.trace_mapper_ready ? '#f6ffed' : '#fff7e6',
217
+ borderBottom: '1px solid #e8e8e8',
218
+ fontSize: 11,
219
+ }}>
220
+ <Space size={4}>
221
+ <Badge status={health.trace_mapper_ready ? 'success' : 'warning'}/>
222
+ <span>
223
+ trace 模块: {health.trace_mapper_ready ? '就绪' : '未启动'}
224
+ {health.total_traces != null && ` · 共 ${health.total_traces} 条`}
225
+ </span>
226
+ </Space>
227
+ </div>
228
+ )}
229
+
230
+ {/* Toolbar */}
231
+ <div style={{padding: '10px 12px', borderBottom: '1px solid #e8e8e8'}}>
232
+ <Title level={5} style={{margin: '0 0 8px 0'}}>
233
+ <HistoryOutlined/> Trace 列表
234
+ </Title>
235
+ <Space.Compact style={{width: '100%', marginBottom: 8}}>
236
+ <Input
237
+ prefix={<SearchOutlined/>}
238
+ placeholder="搜索 trace_id / agent / goal"
239
+ value={keyword}
240
+ onChange={e => setKeyword(e.target.value)}
241
+ allowClear
242
+ />
243
+ <Button icon={<ReloadOutlined/>} onClick={loadTraces}/>
244
+ </Space.Compact>
245
+ <Segmented
246
+ block
247
+ size="small"
248
+ value={statusFilter}
249
+ onChange={setStatusFilter}
250
+ options={[
251
+ {label: '全部', value: 'ALL'},
252
+ {label: '成功', value: 'SUCCESS'},
253
+ {label: '失败', value: 'FAILURE'},
254
+ {label: '运行中', value: 'RUNNING'},
255
+ ]}
256
+ />
257
+ </div>
258
+
259
+ {/* Trace 列表 */}
260
+ <Spin spinning={tracesLoading}>
261
+ {filteredTraces.length > 0 ? (
262
+ <List
263
+ size="small"
264
+ dataSource={filteredTraces}
265
+ renderItem={renderTraceItem}
266
+ split={false}
267
+ locale={{emptyText: <Empty description="无匹配 trace" image={Empty.PRESENTED_IMAGE_SIMPLE}/>}}
268
+ />
269
+ ) : !tracesLoading && (
270
+ <Empty
271
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
272
+ description={health?.trace_mapper_ready === false ? 'trace 表未启用' : '暂无 trace'}
273
+ style={{padding: '40px 0'}}
274
+ />
275
+ )}
276
+ </Spin>
277
+ </Sider>
278
+
279
+ {/* ============ 右侧详情 ============ */}
280
+ <Content style={{padding: '16px', overflow: 'auto'}}>
281
+ <Spin spinning={detailLoading}>
282
+ {!trace ? (
283
+ <Empty
284
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
285
+ description="选择左侧 trace 查看详情"
286
+ style={{paddingTop: 100}}
287
+ />
288
+ ) : (
289
+ <Space direction="vertical" size="middle" style={{width: '100%'}}>
290
+ {/* 顶部 Metrics 仪表盘 */}
291
+ <Card
292
+ title={<span><DashboardOutlined/> 调试仪表盘</span>}
293
+ size="small"
294
+ extra={
295
+ <Space>
296
+ <Tooltip title="刷新">
297
+ <Button
298
+ size="small" icon={<ReloadOutlined/>}
299
+ onClick={() => loadTraceDetail(trace.traceId)}
300
+ />
301
+ </Tooltip>
302
+ <Tag color="blue">{trace.traceId}</Tag>
303
+ </Space>
304
+ }
305
+ >
306
+ <MetricsDashboard trace={trace} spans={spans}/>
307
+ </Card>
308
+
309
+ {/* 4 段式 Tab */}
310
+ <Card size="small" styles={{body: {paddingTop: 4}}}>
311
+ <Tabs
312
+ activeKey={activeTab}
313
+ onChange={setActiveTab}
314
+ items={[
315
+ {
316
+ key: 'timeline',
317
+ label: <span><HistoryOutlined/> 时间线 ({spans.length})</span>,
318
+ children: <Timeline spans={spans}/>,
319
+ },
320
+ {
321
+ key: 'tools',
322
+ label: (
323
+ <span>
324
+ <CodeOutlined/> 工具/LLM 树 (
325
+ {spans.filter(s =>
326
+ s.spanType === 'tool_call' || s.spanType === 'llm_call').length})
327
+ </span>
328
+ ),
329
+ children: <ToolCallTree spans={spans}/>,
330
+ },
331
+ {
332
+ key: 'rag',
333
+ label: (
334
+ <span>
335
+ <FileSearchOutlined/> RAG 片段 (
336
+ {spans.filter(s => s.spanType === 'retrieve').length})
337
+ </span>
338
+ ),
339
+ children: <RagFragments spans={spans}/>,
340
+ },
341
+ ]}
342
+ />
343
+ </Card>
344
+ </Space>
345
+ )}
346
+ </Spin>
347
+ </Content>
348
+ </Layout>
349
+ );
350
+ };
351
+
352
+ function truncate(s, max) {
353
+ if (!s) return s;
354
+ return s.length > max ? s.substring(0, max) + '…' : s;
355
+ }
356
+
357
+ export default DebugPlayground;
@@ -0,0 +1,164 @@
1
+ import React from 'react';
2
+ import {Card, Col, Empty, Progress, Row, Statistic, Tag} from 'antd';
3
+ import {
4
+ ClockCircleOutlined,
5
+ CommentOutlined,
6
+ ThunderboltOutlined,
7
+ CodeOutlined,
8
+ FileSearchOutlined,
9
+ RocketOutlined,
10
+ } from '@ant-design/icons';
11
+
12
+ /**
13
+ * MetricsDashboard 组件 (FEATURE013 T7)
14
+ *
15
+ * <p>顶部 KPI 卡 + Token/延迟仪表盘:
16
+ * <ul>
17
+ * <li>总耗时 / 状态 / 调用计数 (LLM / Tool / RAG)</li>
18
+ * <li>各类型 span 数量占比</li>
19
+ * <li>Token 使用 (若 trace 包含 metrics 信息)</li>
20
+ * </ul>
21
+ */
22
+ const MetricsDashboard = ({trace, spans = []}) => {
23
+ if (!trace) return <Empty description="请选择左侧 trace" image={Empty.PRESENTED_IMAGE_SIMPLE}/>;
24
+
25
+ // 计算各类 span 数量
26
+ const counts = spans.reduce((acc, s) => {
27
+ acc[s.spanType] = (acc[s.spanType] || 0) + 1;
28
+ return acc;
29
+ }, {});
30
+ const totalSpans = spans.length;
31
+ const llmCalls = counts.llm_call || 0;
32
+ const toolCalls = counts.tool_call || 0;
33
+ const ragCalls = counts.retrieve || 0;
34
+ const thinkSteps = counts.think || 0;
35
+ const failedSpans = spans.filter(s => s.status === 'FAILURE' || s.status === 'ERROR').length;
36
+
37
+ // 状态颜色
38
+ const statusColor = {
39
+ SUCCESS: 'success', FAILURE: 'error', RUNNING: 'processing', ERROR: 'error',
40
+ }[trace.status] || 'default';
41
+
42
+ return (
43
+ <div>
44
+ {/* 顶部 KPI 卡 */}
45
+ <Row gutter={[12, 12]}>
46
+ <Col xs={24} sm={12} md={4}>
47
+ <Card size="small" hoverable>
48
+ <Statistic
49
+ title={<span><RocketOutlined/> 状态</span>}
50
+ value={trace.status}
51
+ valueStyle={{color: statusColor === 'success' ? '#3f8600' :
52
+ statusColor === 'error' ? '#cf1322' : '#1677ff', fontSize: 16}}
53
+ />
54
+ </Card>
55
+ </Col>
56
+ <Col xs={24} sm={12} md={4}>
57
+ <Card size="small" hoverable>
58
+ <Statistic
59
+ title={<span><ClockCircleOutlined/> 总耗时</span>}
60
+ value={trace.durationMs || 0}
61
+ suffix="ms"
62
+ valueStyle={{color: '#1677ff'}}
63
+ />
64
+ </Card>
65
+ </Col>
66
+ <Col xs={24} sm={12} md={4}>
67
+ <Card size="small" hoverable>
68
+ <Statistic
69
+ title={<span><CommentOutlined/> Span 总数</span>}
70
+ value={totalSpans}
71
+ suffix="个"
72
+ />
73
+ </Card>
74
+ </Col>
75
+ <Col xs={24} sm={12} md={4}>
76
+ <Card size="small" hoverable>
77
+ <Statistic
78
+ title="失败 Span"
79
+ value={failedSpans}
80
+ suffix="个"
81
+ valueStyle={{color: failedSpans > 0 ? '#cf1322' : '#3f8600'}}
82
+ />
83
+ </Card>
84
+ </Col>
85
+ <Col xs={24} sm={12} md={8}>
86
+ <Card size="small" hoverable>
87
+ <Statistic
88
+ title={<span><CommentOutlined/> 用户目标 / Agent</span>}
89
+ value={trace.userGoal || '(空)'}
90
+ valueStyle={{fontSize: 13, lineHeight: 1.4}}
91
+ />
92
+ <div style={{marginTop: 6, fontSize: 11, color: '#666'}}>
93
+ agent: <Tag>{trace.agentName || '—'}</Tag>
94
+ app: <Tag>{trace.appCode || '—'}</Tag>
95
+ session: <Tag>{trace.sessionId || '—'}</Tag>
96
+ </div>
97
+ </Card>
98
+ </Col>
99
+ </Row>
100
+
101
+ {/* 调用分布 */}
102
+ <Row gutter={[12, 12]} style={{marginTop: 12}}>
103
+ <Col xs={24} sm={8}>
104
+ <Card size="small" title={<span><ThunderboltOutlined/> LLM 调用</span>}>
105
+ <Statistic value={llmCalls} suffix="次" valueStyle={{color: '#1677ff'}}/>
106
+ <Progress
107
+ percent={totalSpans ? Math.round(llmCalls * 100 / totalSpans) : 0}
108
+ strokeColor="#1677ff" size="small" showInfo={false}
109
+ style={{marginTop: 6}}
110
+ />
111
+ </Card>
112
+ </Col>
113
+ <Col xs={24} sm={8}>
114
+ <Card size="small" title={<span><CodeOutlined/> 工具调用</span>}>
115
+ <Statistic value={toolCalls} suffix="次" valueStyle={{color: '#fa8c16'}}/>
116
+ <Progress
117
+ percent={totalSpans ? Math.round(toolCalls * 100 / totalSpans) : 0}
118
+ strokeColor="#fa8c16" size="small" showInfo={false}
119
+ style={{marginTop: 6}}
120
+ />
121
+ </Card>
122
+ </Col>
123
+ <Col xs={24} sm={8}>
124
+ <Card size="small" title={<span><FileSearchOutlined/> RAG 检索</span>}>
125
+ <Statistic value={ragCalls} suffix="次" valueStyle={{color: '#52c41a'}}/>
126
+ <Progress
127
+ percent={totalSpans ? Math.round(ragCalls * 100 / totalSpans) : 0}
128
+ strokeColor="#52c41a" size="small" showInfo={false}
129
+ style={{marginTop: 6}}
130
+ />
131
+ </Card>
132
+ </Col>
133
+ </Row>
134
+
135
+ {/* 时间线 + Trace 元信息 */}
136
+ <Card size="small" title="Trace 元信息" style={{marginTop: 12}}>
137
+ <Row gutter={[8, 4]}>
138
+ <Col span={12}>
139
+ <span style={{color: '#666'}}>trace_id:</span>{' '}
140
+ <code>{trace.traceId}</code>
141
+ </Col>
142
+ <Col span={12}>
143
+ <span style={{color: '#666'}}>DB ID:</span> {trace.id}
144
+ </Col>
145
+ <Col span={12}>
146
+ <span style={{color: '#666'}}>开始:</span> {trace.startedAt}
147
+ </Col>
148
+ <Col span={12}>
149
+ <span style={{color: '#666'}}>结束:</span> {trace.endedAt || '(进行中)'}
150
+ </Col>
151
+ <Col span={24}>
152
+ <span style={{color: '#666'}}>LLM 调用 / Tool 调用 / RAG / Think:</span>{' '}
153
+ <Tag color="blue">{llmCalls}</Tag>
154
+ <Tag color="orange">{toolCalls}</Tag>
155
+ <Tag color="green">{ragCalls}</Tag>
156
+ <Tag color="gold">{thinkSteps}</Tag>
157
+ </Col>
158
+ </Row>
159
+ </Card>
160
+ </div>
161
+ );
162
+ };
163
+
164
+ export default MetricsDashboard;
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import {Empty, Tag, Tooltip} from 'antd';
3
+ import {FileTextOutlined} from '@ant-design/icons';
4
+
5
+ /**
6
+ * RagFragments 组件 (FEATURE013 T7)
7
+ *
8
+ * <p>展示 retrieve 类型的 span 召回的 top-k 分块, 含相似度分数.
9
+ */
10
+ const RagFragments = ({spans = []}) => {
11
+ // 找所有 retrieve span
12
+ const retrieveSpans = spans.filter(s => s.spanType === 'retrieve');
13
+ if (!retrieveSpans.length) {
14
+ return (
15
+ <Empty
16
+ description="本次执行没有 RAG 检索"
17
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
18
+ />
19
+ );
20
+ }
21
+
22
+ let totalFragments = 0;
23
+ let topScore = -1;
24
+
25
+ return (
26
+ <div style={{display: 'flex', flexDirection: 'column', gap: 10}}>
27
+ {retrieveSpans.map((span, idx) => {
28
+ let chunks = [];
29
+ let retrievalType = 'VECTOR';
30
+ let knowledgeBaseCode = null;
31
+ try {
32
+ const attrs = span.attributes
33
+ ? (typeof span.attributes === 'string' ? JSON.parse(span.attributes) : span.attributes)
34
+ : null;
35
+ if (attrs) {
36
+ chunks = attrs.chunks || attrs.results || [];
37
+ retrievalType = attrs.retrieval_type || attrs.retrievalType || 'VECTOR';
38
+ knowledgeBaseCode = attrs.knowledge_base_code || attrs.knowledgeBaseCode;
39
+ }
40
+ } catch { /* ignore */ }
41
+ if (!Array.isArray(chunks)) chunks = [];
42
+
43
+ totalFragments += chunks.length;
44
+ chunks.forEach(c => {
45
+ const s = parseFloat(c.score);
46
+ if (!isNaN(s) && s > topScore) topScore = s;
47
+ });
48
+
49
+ return (
50
+ <div key={span.spanId || idx} style={{
51
+ border: '1px solid #b7eb8f',
52
+ borderRadius: 6,
53
+ background: '#f6ffed',
54
+ padding: '10px 12px',
55
+ }}>
56
+ <div style={{
57
+ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8,
58
+ }}>
59
+ <FileTextOutlined style={{color: '#52c41a'}}/>
60
+ <Tag color="green">{retrievalType}</Tag>
61
+ {knowledgeBaseCode && (
62
+ <Tag color="blue">知识库: {knowledgeBaseCode}</Tag>
63
+ )}
64
+ <Tag>{chunks.length} 个分块</Tag>
65
+ <div style={{flex: 1}}/>
66
+ <span style={{fontSize: 11, color: '#999'}}>
67
+ 耗时 {span.durationMs != null ? `${span.durationMs}ms` : '—'}
68
+ </span>
69
+ </div>
70
+ {chunks.length > 0 ? (
71
+ <div style={{display: 'flex', flexDirection: 'column', gap: 6}}>
72
+ {chunks.map((chunk, ci) => {
73
+ const score = parseFloat(chunk.score) || 0;
74
+ const scoreColor = score >= 0.8 ? 'green' : score >= 0.5 ? 'orange' : 'red';
75
+ return (
76
+ <div key={ci} style={{
77
+ background: '#fff',
78
+ padding: '8px 10px',
79
+ borderRadius: 4,
80
+ border: '1px solid #d9f7be',
81
+ }}>
82
+ <div style={{
83
+ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4,
84
+ }}>
85
+ <Tag color="cyan">#{ci + 1}</Tag>
86
+ {chunk.document_code && (
87
+ <span style={{fontSize: 11, color: '#666'}}>
88
+ doc: {chunk.document_code}
89
+ </span>
90
+ )}
91
+ {chunk.chunk_index != null && (
92
+ <span style={{fontSize: 11, color: '#999'}}>
93
+ chunk #{chunk.chunk_index}
94
+ </span>
95
+ )}
96
+ <div style={{flex: 1}}/>
97
+ <Tooltip title="相似度分数">
98
+ <Tag color={scoreColor}>{(score * 100).toFixed(1)}%</Tag>
99
+ </Tooltip>
100
+ </div>
101
+ <div style={{
102
+ fontSize: 12, color: '#333',
103
+ lineHeight: 1.5,
104
+ whiteSpace: 'pre-wrap',
105
+ wordBreak: 'break-word',
106
+ maxHeight: 100, overflow: 'auto',
107
+ }}>
108
+ {truncate(chunk.chunk_content || chunk.content || '(空)', 300)}
109
+ </div>
110
+ </div>
111
+ );
112
+ })}
113
+ </div>
114
+ ) : (
115
+ <div style={{fontSize: 12, color: '#999', padding: '6px 8px'}}>
116
+ 无召回结果
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ })}
122
+ <div style={{fontSize: 11, color: '#666', textAlign: 'right'}}>
123
+ 汇总: {retrieveSpans.length} 次检索 / {totalFragments} 个分块 / top score {topScore >= 0 ? topScore.toFixed(3) : '—'}
124
+ </div>
125
+ </div>
126
+ );
127
+ };
128
+
129
+ function truncate(s, max) {
130
+ if (!s) return s;
131
+ return s.length > max ? s.substring(0, max) + '…' : s;
132
+ }
133
+
134
+ export default RagFragments;