@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,746 @@
1
+ import {useEffect, useState} from 'react'
2
+ import {
3
+ Badge,
4
+ Button,
5
+ Card,
6
+ Col,
7
+ Divider,
8
+ Drawer,
9
+ Empty,
10
+ Form,
11
+ Input,
12
+ message,
13
+ Modal,
14
+ Popconfirm,
15
+ Row,
16
+ Select,
17
+ Space,
18
+ Switch,
19
+ Table,
20
+ Tabs,
21
+ Tag,
22
+ Tooltip
23
+ } from 'antd'
24
+ import {
25
+ ApiOutlined,
26
+ CheckCircleOutlined,
27
+ CopyOutlined,
28
+ DeleteOutlined,
29
+ EditOutlined,
30
+ EyeOutlined,
31
+ NodeIndexOutlined,
32
+ NodeOutlined,
33
+ PlayCircleOutlined,
34
+ PlusOutlined,
35
+ SettingOutlined,
36
+ StopOutlined,
37
+ ThunderboltOutlined
38
+ } from '@ant-design/icons'
39
+ import {productApi, sceneApi} from '@/api'
40
+
41
+ const {TextArea} = Input
42
+ const {Option} = Select
43
+ const {TabPane} = Tabs
44
+
45
+ // ===== 场景管理页 =====
46
+ const SceneManagementPage = () => {
47
+ const [activeTab, setActiveTab] = useState('list')
48
+ return (
49
+ <Card style={{minHeight: 'calc(100vh - 140px)'}}>
50
+ <Tabs activeKey={activeTab} onChange={setActiveTab}>
51
+ <TabPane tab={<span><NodeIndexOutlined/> 场景管理</span>} key="list">
52
+ <SceneTablePage/>
53
+ </TabPane>
54
+ <TabPane tab={<span><SettingOutlined/> 节点配置</span>} key="nodes">
55
+ <NodeConfigPage/>
56
+ </TabPane>
57
+ <TabPane tab={<span><ApiOutlined/> 场景编排</span>} key="canvas">
58
+ <SceneCanvasPage/>
59
+ </TabPane>
60
+ </Tabs>
61
+ </Card>
62
+ )
63
+ }
64
+
65
+ // ===== 场景列表 =====
66
+ const SceneTablePage = () => {
67
+ const [dataSource, setDataSource] = useState([])
68
+ const [loading, setLoading] = useState(false)
69
+ const [searchKey, setSearchKey] = useState('')
70
+ const [statusFilter, setStatusFilter] = useState('')
71
+ const [pageNum, setPageNum] = useState(1)
72
+ const [pageSize] = useState(12)
73
+ const [total, setTotal] = useState(0)
74
+ const [modalVisible, setModalVisible] = useState(false)
75
+ const [form] = Form.useForm()
76
+ const [editingScene, setEditingScene] = useState(null)
77
+ const [createLoading, setCreateLoading] = useState(false)
78
+ const [detailVisible, setDetailVisible] = useState(false)
79
+ const [detailScene, setDetailScene] = useState(null)
80
+ const [productOptions, setProductOptions] = useState([])
81
+
82
+ useEffect(() => {
83
+ productApi.page({id: 1, pageSize: 100}).then(res => {
84
+ setProductOptions(res.data?.records || [])
85
+ })
86
+ }, [])
87
+
88
+ useEffect(() => {
89
+ fetchData()
90
+ }, [pageNum, searchKey, statusFilter])
91
+
92
+ const fetchData = async () => {
93
+ setLoading(true)
94
+ try {
95
+ const res = await sceneApi.page({sceneName: searchKey, status: statusFilter, id: pageNum})
96
+ if (res.data) {
97
+ setDataSource(res.data.records || [])
98
+ setTotal(res.data.total || 0)
99
+ }
100
+ } catch (e) {
101
+ message.error('加载失败: ' + (e.message || ''))
102
+ }
103
+ setLoading(false)
104
+ }
105
+
106
+ const handleCreate = () => {
107
+ setEditingScene(null)
108
+ form.resetFields()
109
+ form.setFieldsValue({status: 'DRAFT', sceneType: 'CONVERSATION'})
110
+ setModalVisible(true)
111
+ }
112
+
113
+ const handleEdit = (record) => {
114
+ setEditingScene(record)
115
+ form.setFieldsValue({
116
+ sceneCode: record.sceneCode,
117
+ sceneName: record.sceneName,
118
+ description: record.description,
119
+ productCode: record.productCode,
120
+ sceneType: record.sceneType || 'CONVERSATION',
121
+ version: record.version || '1.0.0',
122
+ status: record.status,
123
+ iconUrl: record.iconUrl,
124
+ })
125
+ setModalVisible(true)
126
+ }
127
+
128
+ const handleSubmit = async () => {
129
+ try {
130
+ const values = await form.validateFields()
131
+ setCreateLoading(true)
132
+ if (editingScene) {
133
+ await sceneApi.update({id: editingScene.id, ...values})
134
+ message.success('更新成功')
135
+ } else {
136
+ await sceneApi.create(values)
137
+ message.success('创建成功')
138
+ }
139
+ setModalVisible(false)
140
+ fetchData()
141
+ } catch (e) {
142
+ if (!e.errorFields) message.error('操作失败')
143
+ } finally {
144
+ setCreateLoading(false)
145
+ }
146
+ }
147
+
148
+ const handleDelete = async (id) => {
149
+ try {
150
+ await sceneApi.delete(id)
151
+ message.success('删除成功')
152
+ fetchData()
153
+ } catch (e) {
154
+ message.error('删除失败')
155
+ }
156
+ }
157
+
158
+ const handlePublish = async (sceneCode) => {
159
+ try {
160
+ await sceneApi.publish(sceneCode)
161
+ message.success('发布成功')
162
+ fetchData()
163
+ } catch (e) {
164
+ message.error('发布失败')
165
+ }
166
+ }
167
+
168
+ const handleOffline = async (sceneCode) => {
169
+ try {
170
+ await sceneApi.offline(sceneCode)
171
+ message.success('下架成功')
172
+ fetchData()
173
+ } catch (e) {
174
+ message.error('下架失败')
175
+ }
176
+ }
177
+
178
+ const handleViewDetail = (record) => {
179
+ setDetailScene(record)
180
+ setDetailVisible(true)
181
+ }
182
+
183
+ const handleDuplicate = async (record) => {
184
+ try {
185
+ await sceneApi.duplicate(record.id)
186
+ message.success('复制成功')
187
+ fetchData()
188
+ } catch (e) {
189
+ message.error('复制失败')
190
+ }
191
+ }
192
+
193
+ const copySceneCode = (code) => {
194
+ navigator.clipboard.writeText(code).then(() => message.success('场景编码已复制')).catch(() => {
195
+ })
196
+ }
197
+
198
+ const columns = [
199
+ {
200
+ title: '场景',
201
+ key: 'scene',
202
+ width: 280,
203
+ render: (_, record) => (
204
+ <div style={{display: 'flex', alignItems: 'center', gap: 10}}>
205
+ <div style={{
206
+ width: 40, height: 40, borderRadius: 8,
207
+ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
208
+ display: 'flex', alignItems: 'center', justifyContent: 'center'
209
+ }}>
210
+ <NodeIndexOutlined style={{fontSize: 18, color: '#fff'}}/>
211
+ </div>
212
+ <div>
213
+ <div style={{fontWeight: 500}}>{record.sceneName}</div>
214
+ <div style={{fontSize: 12, color: '#999'}}>
215
+ <span style={{fontFamily: 'monospace'}}>{record.sceneCode}</span>
216
+ <Tooltip title="复制编码">
217
+ <Button size="small" type="text" icon={<CopyOutlined/>}
218
+ onClick={() => copySceneCode(record.sceneCode)}
219
+ style={{marginLeft: 4, padding: 0}}/>
220
+ </Tooltip>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ )
225
+ },
226
+ {
227
+ title: '所属产品',
228
+ dataIndex: 'productCode',
229
+ width: 160,
230
+ render: (val) => val ? <Tag color="blue">{val}</Tag> : <span style={{color: '#ccc'}}>未关联</span>
231
+ },
232
+ {
233
+ title: '类型',
234
+ dataIndex: 'sceneType',
235
+ width: 100,
236
+ render: (val) => {
237
+ const map = {CONVERSATION: '对话', TASK: '任务', WORKFLOW: '工作流'}
238
+ return <Tag icon={<NodeOutlined/>}>{map[val] || val}</Tag>
239
+ }
240
+ },
241
+ {
242
+ title: '状态',
243
+ dataIndex: 'status',
244
+ width: 100,
245
+ render: (val) => {
246
+ const map = {PUBLISHED: 'success', DRAFT: 'warning', OFFLINE: 'error', ARCHIVED: 'default'}
247
+ const label = {PUBLISHED: '已发布', DRAFT: '草稿', OFFLINE: '已下架', ARCHIVED: '已归档'}
248
+ return <Badge status={map[val] || 'default'} text={label[val] || val}/>
249
+ }
250
+ },
251
+ {
252
+ title: '版本',
253
+ dataIndex: 'version',
254
+ width: 90,
255
+ render: (v) => <span style={{fontFamily: 'monospace'}}>v{v || '1.0.0'}</span>
256
+ },
257
+ {
258
+ title: '描述',
259
+ dataIndex: 'description',
260
+ ellipsis: true,
261
+ },
262
+ {
263
+ title: '操作',
264
+ width: 340,
265
+ render: (_, record) => (
266
+ <Space size="small" wrap>
267
+ <Tooltip title="查看详情"><Button size="small" icon={<EyeOutlined/>}
268
+ onClick={() => handleViewDetail(record)}>详情</Button></Tooltip>
269
+ <Tooltip title="编辑配置"><Button size="small" icon={<EditOutlined/>}
270
+ onClick={() => handleEdit(record)}>编辑</Button></Tooltip>
271
+ <Tooltip title="复制场景"><Button size="small" icon={<CopyOutlined/>}
272
+ onClick={() => handleDuplicate(record)}>复制</Button></Tooltip>
273
+ {record.status === 'DRAFT' && (
274
+ <Tooltip title="发布场景"><Button size="small" type="primary" icon={<CheckCircleOutlined/>}
275
+ onClick={() => handlePublish(record.sceneCode)}>发布</Button></Tooltip>
276
+ )}
277
+ {record.status === 'PUBLISHED' && (
278
+ <Tooltip title="下架场景"><Button size="small" danger icon={<StopOutlined/>}
279
+ onClick={() => handleOffline(record.sceneCode)}>下架</Button></Tooltip>
280
+ )}
281
+ <Tooltip title="执行场景">
282
+ <Button size="small" type="primary" icon={<PlayCircleOutlined/>}
283
+ onClick={() => window.open(`/ai/product/execute?sceneCode=${record.sceneCode}`, '_blank')}>执行</Button>
284
+ </Tooltip>
285
+ <Popconfirm title="确定删除此场景?" onConfirm={() => handleDelete(record.id)} okText="删除"
286
+ okButtonProps={{danger: true}}>
287
+ <Button size="small" danger icon={<DeleteOutlined/>}/>
288
+ </Popconfirm>
289
+ </Space>
290
+ )
291
+ }
292
+ ]
293
+
294
+ return (
295
+ <div>
296
+ <div
297
+ style={{display: 'flex', justifyContent: 'space-between', marginBottom: 16, gap: 12, flexWrap: 'wrap'}}>
298
+ <Space>
299
+ <Input.Search placeholder="搜索场景名称/编码" onSearch={v => {
300
+ setSearchKey(v);
301
+ setPageNum(1)
302
+ }} style={{width: 220}} allowClear/>
303
+ <Select placeholder="状态筛选" allowClear style={{width: 120}} onChange={v => {
304
+ setStatusFilter(v);
305
+ setPageNum(1)
306
+ }}>
307
+ <Option value="DRAFT">草稿</Option>
308
+ <Option value="PUBLISHED">已发布</Option>
309
+ <Option value="OFFLINE">已下架</Option>
310
+ </Select>
311
+ <Select placeholder="类型筛选" allowClear style={{width: 120}} onChange={v => {
312
+ setStatusFilter(v);
313
+ setPageNum(1)
314
+ }}>
315
+ <Option value="CONVERSATION">对话</Option>
316
+ <Option value="TASK">任务</Option>
317
+ <Option value="WORKFLOW">工作流</Option>
318
+ </Select>
319
+ </Space>
320
+ <Button type="primary" icon={<PlusOutlined/>} onClick={handleCreate}>新建场景</Button>
321
+ </div>
322
+
323
+ <Table
324
+ dataSource={dataSource}
325
+ columns={columns}
326
+ loading={loading}
327
+ rowKey="id"
328
+ pagination={{
329
+ current: pageNum,
330
+ pageSize,
331
+ total,
332
+ showSizeChanger: false,
333
+ showTotal: t => `共 ${t} 个场景`,
334
+ onChange: p => setPageNum(p),
335
+ }}
336
+ locale={{
337
+ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="还没有场景,点击新建开始创建"/>
338
+ }}
339
+ />
340
+
341
+ {/* 创建/编辑 Modal */}
342
+ <Modal
343
+ title={editingScene ? `编辑场景: ${editingScene.sceneName}` : '新建场景'}
344
+ open={modalVisible}
345
+ onOk={handleSubmit}
346
+ onCancel={() => setModalVisible(false)}
347
+ width={640}
348
+ confirmLoading={createLoading}
349
+ okText={editingScene ? '保存' : '创建'}
350
+ >
351
+ <Form form={form} layout="vertical" size="middle">
352
+ <Row gutter={16}>
353
+ <Col span={12}>
354
+ <Form.Item name="sceneName" label="场景名称"
355
+ rules={[{required: true, message: '请输入场景名称'}]}>
356
+ <Input placeholder="如 智能问答助手"/>
357
+ </Form.Item>
358
+ </Col>
359
+ <Col span={12}>
360
+ <Form.Item name="sceneCode" label="场景编码"
361
+ rules={[{required: true, message: '请输入场景编码'}]}>
362
+ <Input placeholder="如 chat_scene" disabled={!!editingScene}/>
363
+ </Form.Item>
364
+ </Col>
365
+ </Row>
366
+
367
+ <Row gutter={16}>
368
+ <Col span={12}>
369
+ <Form.Item name="sceneType" label="场景类型" initialValue="CONVERSATION">
370
+ <Select>
371
+ <Option value="CONVERSATION">对话场景</Option>
372
+ <Option value="TASK">任务场景</Option>
373
+ <Option value="WORKFLOW">工作流场景</Option>
374
+ </Select>
375
+ </Form.Item>
376
+ </Col>
377
+ <Col span={12}>
378
+ <Form.Item name="productCode" label="所属产品">
379
+ <Select allowClear placeholder="选择产品">
380
+ {productOptions.map(p => <Option key={p.productCode}
381
+ value={p.productCode}>{p.productName}</Option>)}
382
+ </Select>
383
+ </Form.Item>
384
+ </Col>
385
+ </Row>
386
+
387
+ <Form.Item name="description" label="场景描述">
388
+ <TextArea rows={2} placeholder="简短描述这个场景的能力和使用方式"/>
389
+ </Form.Item>
390
+
391
+ <Form.Item name="version" label="版本号" initialValue="1.0.0">
392
+ <Input placeholder="1.0.0"/>
393
+ </Form.Item>
394
+
395
+ {!editingScene && (
396
+ <Form.Item name="status" label="创建后状态" initialValue="DRAFT">
397
+ <Select>
398
+ <Option value="DRAFT">草稿</Option>
399
+ <Option value="PUBLISHED">直接发布</Option>
400
+ </Select>
401
+ </Form.Item>
402
+ )}
403
+ </Form>
404
+ </Modal>
405
+
406
+ {/* 详情 Drawer */}
407
+ <Drawer
408
+ title="场景详情"
409
+ open={detailVisible}
410
+ onClose={() => setDetailVisible(false)}
411
+ width={560}
412
+ >
413
+ {detailScene && (
414
+ <div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
415
+ <div style={{display: 'flex', alignItems: 'center', gap: 12}}>
416
+ <div style={{
417
+ width: 48, height: 48, borderRadius: 10,
418
+ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
419
+ display: 'flex', alignItems: 'center', justifyContent: 'center'
420
+ }}>
421
+ <NodeIndexOutlined style={{fontSize: 22, color: '#fff'}}/>
422
+ </div>
423
+ <div>
424
+ <div style={{fontSize: 16, fontWeight: 600}}>{detailScene.sceneName}</div>
425
+ <div style={{fontSize: 12, color: '#999'}}>{detailScene.sceneCode}</div>
426
+ </div>
427
+ </div>
428
+
429
+ <Divider style={{margin: '8px 0'}}/>
430
+
431
+ <Row gutter={16}>
432
+ <Col span={12}>
433
+ <div style={{fontSize: 12, color: '#999', marginBottom: 4}}>类型</div>
434
+ <Tag>
435
+ {detailScene.sceneType === 'CONVERSATION' ? '对话' :
436
+ detailScene.sceneType === 'TASK' ? '任务' : '工作流'}
437
+ </Tag>
438
+ </Col>
439
+ <Col span={12}>
440
+ <div style={{fontSize: 12, color: '#999', marginBottom: 4}}>版本</div>
441
+ <span style={{fontFamily: 'monospace'}}>{detailScene.version || '1.0.0'}</span>
442
+ </Col>
443
+ </Row>
444
+
445
+ <div>
446
+ <div style={{fontSize: 12, color: '#999', marginBottom: 4}}>状态</div>
447
+ <Badge status={
448
+ detailScene.status === 'PUBLISHED' ? 'success' :
449
+ detailScene.status === 'DRAFT' ? 'warning' : 'error'
450
+ } text={
451
+ detailScene.status === 'PUBLISHED' ? '已发布' :
452
+ detailScene.status === 'DRAFT' ? '草稿' :
453
+ detailScene.status === 'OFFLINE' ? '已下架' : '已归档'
454
+ }/>
455
+ </div>
456
+
457
+ <div>
458
+ <div style={{fontSize: 12, color: '#999', marginBottom: 4}}>描述</div>
459
+ <div style={{color: '#333', lineHeight: 1.6}}>{detailScene.description || '暂无描述'}</div>
460
+ </div>
461
+ </div>
462
+ )}
463
+ </Drawer>
464
+ </div>
465
+ )
466
+ }
467
+
468
+ // ===== 节点配置页 =====
469
+ const NodeConfigPage = () => {
470
+ const [scenes, setScenes] = useState([])
471
+ const [selectedScene, setSelectedScene] = useState(null)
472
+ const [nodes, setNodes] = useState([])
473
+ const [loading, setLoading] = useState(false)
474
+ const [nodeModalVisible, setNodeModalVisible] = useState(false)
475
+ const [form] = Form.useForm()
476
+
477
+ useEffect(() => {
478
+ sceneApi.page({id: 1, pageSize: 100}).then(res => {
479
+ setScenes(res.data?.records || [])
480
+ })
481
+ }, [])
482
+
483
+ useEffect(() => {
484
+ if (!selectedScene) return
485
+ loadNodes(selectedScene.sceneCode)
486
+ }, [selectedScene])
487
+
488
+ const loadNodes = async (sceneCode) => {
489
+ setLoading(true)
490
+ try {
491
+ const res = await sceneApi.nodes(sceneCode)
492
+ setNodes(res.data || [])
493
+ } catch (e) {
494
+ setNodes([])
495
+ }
496
+ setLoading(false)
497
+ }
498
+
499
+ const handleAddNode = () => {
500
+ form.resetFields()
501
+ form.setFieldsValue({nodeType: 'LLM', isEnabled: true})
502
+ setNodeModalVisible(true)
503
+ }
504
+
505
+ const handleSubmitNode = async () => {
506
+ try {
507
+ const values = await form.validateFields()
508
+ setNodes(prev => [...(prev || []), {...values, id: Date.now()}])
509
+ setNodeModalVisible(false)
510
+ message.success('节点已添加')
511
+ } catch (e) {
512
+ }
513
+ }
514
+
515
+ const handleRemoveNode = (id) => {
516
+ setNodes(prev => (prev || []).filter(n => n.id !== id))
517
+ }
518
+
519
+ const handleToggleNode = (id) => {
520
+ setNodes(prev => prev.map(n => n.id === id ? {...n, isEnabled: !n.isEnabled} : n))
521
+ }
522
+
523
+ const nodeColumns = [
524
+ {title: '节点名称', dataIndex: 'nodeName', key: 'nodeName'},
525
+ {title: '节点类型', dataIndex: 'nodeType', key: 'nodeType', render: (v) => <Tag color="blue">{v}</Tag>},
526
+ {title: '配置', dataIndex: 'config', key: 'config', ellipsis: true},
527
+ {
528
+ title: '启用',
529
+ dataIndex: 'isEnabled',
530
+ key: 'isEnabled',
531
+ render: (v, record) => <Switch checked={v} onChange={() => handleToggleNode(record.id)}/>
532
+ },
533
+ {
534
+ title: '操作',
535
+ width: 100,
536
+ render: (_, record) => (
537
+ <Button size="small" danger icon={<DeleteOutlined/>} onClick={() => handleRemoveNode(record.id)}/>
538
+ )
539
+ }
540
+ ]
541
+
542
+ return (
543
+ <div style={{display: 'flex', gap: 16}}>
544
+ {/* 左侧场景列表 */}
545
+ <Card
546
+ size="small"
547
+ title={<span style={{fontWeight: 600}}><NodeIndexOutlined style={{color: '#4facfe', marginRight: 6}}/>选择场景</span>}
548
+ style={{width: 260, flexShrink: 0, borderRadius: 10}}
549
+ styles={{header: {borderBottom: '1px solid #f0f0f0', padding: '12px 16px'}, body: {padding: 8}}}
550
+ >
551
+ {(scenes || []).map(s => (
552
+ <div
553
+ key={s.id}
554
+ onClick={() => setSelectedScene(s)}
555
+ style={{
556
+ padding: '10px 12px',
557
+ borderRadius: 8,
558
+ cursor: 'pointer',
559
+ marginBottom: 4,
560
+ background: selectedScene?.id === s.id ? 'linear-gradient(135deg, #4facfe22, #00f2fe22)' : 'transparent',
561
+ border: selectedScene?.id === s.id ? '1px solid #4facfe44' : '1px solid transparent',
562
+ }}
563
+ >
564
+ <div style={{fontSize: 13, fontWeight: 500}}>{s.sceneName}</div>
565
+ <div style={{fontSize: 11, color: '#999'}}>{s.sceneCode}</div>
566
+ </div>
567
+ ))}
568
+ {scenes.length === 0 && <Empty description="暂无场景" style={{padding: 20}}/>}
569
+ </Card>
570
+
571
+ {/* 右侧节点配置 */}
572
+ <Card
573
+ title={<span
574
+ style={{fontWeight: 600}}>节点配置 {selectedScene ? `- ${selectedScene.sceneName}` : ''}</span>}
575
+ extra={<Button size="small" icon={<PlusOutlined/>} onClick={handleAddNode}
576
+ disabled={!selectedScene}>添加节点</Button>}
577
+ style={{flex: 1, borderRadius: 10}}
578
+ >
579
+ {!selectedScene ? (
580
+ <Empty description="请先选择场景" style={{padding: 60}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
581
+ ) : (
582
+ <Table
583
+ dataSource={nodes}
584
+ columns={nodeColumns}
585
+ rowKey="id"
586
+ pagination={false}
587
+ locale={{
588
+ emptyText: <Empty description="暂无节点,点击添加开始配置"
589
+ image={Empty.PRESENTED_IMAGE_SIMPLE}/>
590
+ }}
591
+ />
592
+ )}
593
+ </Card>
594
+
595
+ {/* 添加节点 Modal */}
596
+ <Modal
597
+ title="添加节点"
598
+ open={nodeModalVisible}
599
+ onOk={handleSubmitNode}
600
+ onCancel={() => setNodeModalVisible(false)}
601
+ okText="添加"
602
+ >
603
+ <Form form={form} layout="vertical">
604
+ <Form.Item name="nodeName" label="节点名称" rules={[{required: true, message: '请输入节点名称'}]}>
605
+ <Input placeholder="如 LLM调用节点"/>
606
+ </Form.Item>
607
+ <Form.Item name="nodeType" label="节点类型" initialValue="LLM">
608
+ <Select>
609
+ <Option value="LLM">LLM调用</Option>
610
+ <Option value="TOOL">工具调用</Option>
611
+ <Option value="CONDITION">条件分支</Option>
612
+ <Option value="TRANSFORM">数据转换</Option>
613
+ <Option value="HTTP">HTTP请求</Option>
614
+ </Select>
615
+ </Form.Item>
616
+ <Form.Item name="config" label="节点配置">
617
+ <TextArea rows={3} placeholder='JSON格式配置,如 {"model":"gpt-4"}'/>
618
+ </Form.Item>
619
+ </Form>
620
+ </Modal>
621
+ </div>
622
+ )
623
+ }
624
+
625
+ // ===== 场景编排页 =====
626
+ const SceneCanvasPage = () => {
627
+ const [scenes, setScenes] = useState([])
628
+ const [selectedScene, setSelectedScene] = useState(null)
629
+ const [canvasNodes, setCanvasNodes] = useState([])
630
+ const [canvasEdges, setCanvasEdges] = useState([])
631
+
632
+ useEffect(() => {
633
+ sceneApi.page({id: 1, pageSize: 100}).then(res => {
634
+ setScenes(res.data?.records || [])
635
+ })
636
+ }, [])
637
+
638
+ const handleSceneSelect = async (scene) => {
639
+ setSelectedScene(scene)
640
+ try {
641
+ const res = await sceneApi.canvasGet(scene.sceneCode)
642
+ if (res.data) {
643
+ setCanvasNodes(res.data.nodes || [])
644
+ setCanvasEdges(res.data.edges || [])
645
+ }
646
+ } catch (e) {
647
+ setCanvasNodes([])
648
+ setCanvasEdges([])
649
+ }
650
+ }
651
+
652
+ const canvasColumns = [
653
+ {title: '节点', dataIndex: 'label', key: 'label', width: 200},
654
+ {title: '类型', dataIndex: 'type', key: 'type', render: (v) => <Tag color="purple">{v}</Tag>},
655
+ {
656
+ title: '位置',
657
+ dataIndex: 'position',
658
+ key: 'position',
659
+ render: (p) => <span style={{fontSize: 11, color: '#999'}}>x:{p?.x || 0} y:{p?.y || 0}</span>
660
+ },
661
+ ]
662
+
663
+ return (
664
+ <div style={{display: 'flex', gap: 16, minHeight: 500}}>
665
+ {/* 左侧场景选择 */}
666
+ <Card
667
+ size="small"
668
+ title={<span style={{fontWeight: 600}}><NodeIndexOutlined style={{color: '#4facfe', marginRight: 6}}/>场景列表</span>}
669
+ style={{width: 240, flexShrink: 0, borderRadius: 10}}
670
+ styles={{header: {borderBottom: '1px solid #f0f0f0', padding: '12px 16px'}, body: {padding: 8}}}
671
+ >
672
+ {(scenes || []).map(s => (
673
+ <div
674
+ key={s.id}
675
+ onClick={() => handleSceneSelect(s)}
676
+ style={{
677
+ padding: '10px 12px',
678
+ borderRadius: 8,
679
+ cursor: 'pointer',
680
+ marginBottom: 4,
681
+ background: selectedScene?.id === s.id ? 'linear-gradient(135deg, #4facfe22, #00f2fe22)' : 'transparent',
682
+ border: selectedScene?.id === s.id ? '1px solid #4facfe44' : '1px solid transparent',
683
+ }}
684
+ >
685
+ <div style={{fontSize: 13, fontWeight: 500}}>{s.sceneName}</div>
686
+ <Tag style={{fontSize: 10, marginTop: 2}}>{s.sceneType}</Tag>
687
+ </div>
688
+ ))}
689
+ </Card>
690
+
691
+ {/* 右侧画布 */}
692
+ <Card
693
+ title={<span
694
+ style={{fontWeight: 600}}>场景编排画布 {selectedScene ? `- ${selectedScene.sceneName}` : ''}</span>}
695
+ style={{flex: 1, borderRadius: 10}}
696
+ styles={{body: {padding: 16, display: 'flex', flexDirection: 'column', gap: 12}}}
697
+ >
698
+ {!selectedScene ? (
699
+ <Empty description="请选择场景进行编排" style={{padding: 60}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
700
+ ) : canvasNodes.length === 0 ? (
701
+ <Empty description="暂无节点配置,请先在节点配置中添加节点" style={{padding: 40}}
702
+ image={Empty.PRESENTED_IMAGE_SIMPLE}/>
703
+ ) : (
704
+ <>
705
+ <div style={{
706
+ display: 'flex',
707
+ alignItems: 'center',
708
+ gap: 8,
709
+ padding: '8px 0',
710
+ borderBottom: '1px solid #f0f0f0'
711
+ }}>
712
+ <ThunderboltOutlined style={{color: '#4facfe'}}/>
713
+ <span style={{
714
+ fontSize: 13,
715
+ color: '#666'
716
+ }}>编排节点 {canvasNodes.length} 个,连接 {canvasEdges.length} 条</span>
717
+ </div>
718
+ <Table
719
+ dataSource={canvasNodes}
720
+ columns={canvasColumns}
721
+ rowKey="id"
722
+ pagination={false}
723
+ size="small"
724
+ />
725
+ {canvasEdges.length > 0 && (
726
+ <>
727
+ <div style={{fontSize: 13, fontWeight: 500, marginTop: 8}}>连线</div>
728
+ <div style={{display: 'flex', flexDirection: 'column', gap: 4}}>
729
+ {canvasEdges.map((edge, idx) => (
730
+ <Card key={idx} size="small" style={{background: '#fafafa', border: 'none'}}>
731
+ <span style={{fontFamily: 'monospace', fontSize: 12}}>
732
+ {edge.source} → {edge.target}
733
+ </span>
734
+ </Card>
735
+ ))}
736
+ </div>
737
+ </>
738
+ )}
739
+ </>
740
+ )}
741
+ </Card>
742
+ </div>
743
+ )
744
+ }
745
+
746
+ export default SceneManagementPage