init
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .DS_Store +0 -0
- README.md +30 -12
- app/agents/AgentFilters.tsx +80 -0
- app/agents/AgentList.tsx +204 -0
- app/agents/create/AgentBuilder.tsx +150 -0
- app/agents/create/BasicSettings.tsx +168 -0
- app/agents/create/KnowledgeBase.tsx +511 -0
- app/agents/create/ModelConfig.tsx +111 -0
- app/agents/create/TestPanel.tsx +194 -0
- app/agents/create/WorkflowConfig.tsx +313 -0
- app/agents/create/page.tsx +27 -0
- app/agents/page.tsx +40 -0
- app/dashboard/ActivityFeed.tsx +67 -0
- app/dashboard/DashboardStats.tsx +63 -0
- app/dashboard/QuickActions.tsx +64 -0
- app/dashboard/RecentAgents.tsx +89 -0
- app/dashboard/page.tsx +41 -0
- app/globals.css +4 -0
- app/knowledge/KnowledgeFilters.tsx +121 -0
- app/knowledge/KnowledgeLibrary.tsx +383 -0
- app/knowledge/create/KnowledgeBuilder.tsx +549 -0
- app/knowledge/create/page.tsx +26 -0
- app/knowledge/page.tsx +47 -0
- app/layout.tsx +41 -0
- app/not-found.tsx +9 -0
- app/page.tsx +145 -0
- app/workflows/WorkflowFilters.tsx +80 -0
- app/workflows/WorkflowList.tsx +265 -0
- app/workflows/[id]/analytics/ExecutionHistory.tsx +230 -0
- app/workflows/[id]/analytics/NodeAnalytics.tsx +126 -0
- app/workflows/[id]/analytics/PerformanceChart.tsx +80 -0
- app/workflows/[id]/analytics/WorkflowAnalytics.tsx +232 -0
- app/workflows/[id]/analytics/page.tsx +34 -0
- app/workflows/create/NodeLibrary.tsx +159 -0
- app/workflows/create/WorkflowBuilder.tsx +141 -0
- app/workflows/create/WorkflowCanvas.tsx +232 -0
- app/workflows/create/WorkflowSettings.tsx +338 -0
- app/workflows/create/page.tsx +26 -0
- app/workflows/page.tsx +52 -0
- app/workflows/templates/TemplateFilters.tsx +101 -0
- app/workflows/templates/TemplateLibrary.tsx +290 -0
- app/workflows/templates/page.tsx +53 -0
- components/Header.tsx +80 -0
- components/Sidebar.tsx +49 -0
- eslint.config.mjs +21 -0
- index.html +0 -20
- next.config.ts +13 -0
- package.json +28 -0
- postcss.config.mjs +8 -0
- style.css +0 -28
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
README.md
CHANGED
|
@@ -1,12 +1,30 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
app/agents/AgentFilters.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
export default function AgentFilters() {
|
| 6 |
+
const [activeFilter, setActiveFilter] = useState('all');
|
| 7 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 8 |
+
|
| 9 |
+
const filters = [
|
| 10 |
+
{ key: 'all', label: '全部', count: 24 },
|
| 11 |
+
{ key: 'active', label: '运行中', count: 18 },
|
| 12 |
+
{ key: 'paused', label: '暂停', count: 4 },
|
| 13 |
+
{ key: 'draft', label: '草稿', count: 2 }
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
const types = [
|
| 17 |
+
{ key: 'all', label: '全部类型' },
|
| 18 |
+
{ key: 'chat', label: '对话型' },
|
| 19 |
+
{ key: 'analysis', label: '分析型' },
|
| 20 |
+
{ key: 'generation', label: '生成型' },
|
| 21 |
+
{ key: 'workflow', label: '工作流型' }
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
| 26 |
+
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
| 27 |
+
<div className="flex items-center space-x-4">
|
| 28 |
+
{filters.map((filter) => (
|
| 29 |
+
<button
|
| 30 |
+
key={filter.key}
|
| 31 |
+
onClick={() => setActiveFilter(filter.key)}
|
| 32 |
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors cursor-pointer whitespace-nowrap ${
|
| 33 |
+
activeFilter === filter.key
|
| 34 |
+
? 'bg-blue-100 text-blue-700'
|
| 35 |
+
: 'text-gray-600 hover:bg-gray-100'
|
| 36 |
+
}`}
|
| 37 |
+
>
|
| 38 |
+
{filter.label} ({filter.count})
|
| 39 |
+
</button>
|
| 40 |
+
))}
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div className="flex items-center space-x-4">
|
| 44 |
+
<div className="relative">
|
| 45 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 46 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 47 |
+
<i className="ri-search-line text-gray-400"></i>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<input
|
| 51 |
+
type="text"
|
| 52 |
+
placeholder="搜索Agent..."
|
| 53 |
+
value={searchTerm}
|
| 54 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 55 |
+
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 56 |
+
/>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div className="relative">
|
| 60 |
+
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 61 |
+
类型筛选
|
| 62 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block ml-2">
|
| 63 |
+
<i className="ri-arrow-down-s-line"></i>
|
| 64 |
+
</div>
|
| 65 |
+
</button>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div className="relative">
|
| 69 |
+
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 70 |
+
排序
|
| 71 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block ml-2">
|
| 72 |
+
<i className="ri-arrow-down-s-line"></i>
|
| 73 |
+
</div>
|
| 74 |
+
</button>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
);
|
| 80 |
+
}
|
app/agents/AgentList.tsx
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import Link from 'next/link';
|
| 5 |
+
|
| 6 |
+
export default function AgentList() {
|
| 7 |
+
const [selectedAgents, setSelectedAgents] = useState<number[]>([]);
|
| 8 |
+
|
| 9 |
+
const agents = [
|
| 10 |
+
{
|
| 11 |
+
id: 1,
|
| 12 |
+
name: '智能客服助手',
|
| 13 |
+
description: '处理客户咨询和问题解答的智能助手',
|
| 14 |
+
type: '对话型',
|
| 15 |
+
status: '运行中',
|
| 16 |
+
version: 'v2.1',
|
| 17 |
+
created: '2024-01-15',
|
| 18 |
+
lastUsed: '2分钟前',
|
| 19 |
+
calls: 2845,
|
| 20 |
+
avatar: '🤖',
|
| 21 |
+
tags: ['客服', '问答', '多轮对话']
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
id: 2,
|
| 25 |
+
name: '文档分析器',
|
| 26 |
+
description: '自动分析和提取文档关键信息',
|
| 27 |
+
type: '分析型',
|
| 28 |
+
status: '运行中',
|
| 29 |
+
version: 'v1.8',
|
| 30 |
+
created: '2024-01-10',
|
| 31 |
+
lastUsed: '15分钟前',
|
| 32 |
+
calls: 1256,
|
| 33 |
+
avatar: '📄',
|
| 34 |
+
tags: ['文档', '分析', 'OCR']
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
id: 3,
|
| 38 |
+
name: '代码生成器',
|
| 39 |
+
description: '根据需求自动生成代码片段',
|
| 40 |
+
type: '生成型',
|
| 41 |
+
status: '暂停',
|
| 42 |
+
version: 'v1.5',
|
| 43 |
+
created: '2024-01-08',
|
| 44 |
+
lastUsed: '1小时前',
|
| 45 |
+
calls: 892,
|
| 46 |
+
avatar: '💻',
|
| 47 |
+
tags: ['编程', '代码', '自动化']
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
id: 4,
|
| 51 |
+
name: '数据可视化',
|
| 52 |
+
description: '将数据转换为图表和可视化报告',
|
| 53 |
+
type: '图表型',
|
| 54 |
+
status: '运行中',
|
| 55 |
+
version: 'v2.0',
|
| 56 |
+
created: '2024-01-05',
|
| 57 |
+
lastUsed: '30分钟前',
|
| 58 |
+
calls: 674,
|
| 59 |
+
avatar: '📊',
|
| 60 |
+
tags: ['数据', '图表', '报告']
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
id: 5,
|
| 64 |
+
name: '邮件智能助手',
|
| 65 |
+
description: '自动分类和回复邮件',
|
| 66 |
+
type: '对话型',
|
| 67 |
+
status: '运行中',
|
| 68 |
+
version: 'v1.3',
|
| 69 |
+
created: '2024-01-03',
|
| 70 |
+
lastUsed: '45分钟前',
|
| 71 |
+
calls: 458,
|
| 72 |
+
avatar: '📧',
|
| 73 |
+
tags: ['邮件', '自动回复', '分类']
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
id: 6,
|
| 77 |
+
name: '语音转文字',
|
| 78 |
+
description: '实时语音识别和转录',
|
| 79 |
+
type: '转换型',
|
| 80 |
+
status: '运行中',
|
| 81 |
+
version: 'v1.7',
|
| 82 |
+
created: '2024-01-01',
|
| 83 |
+
lastUsed: '1小时前',
|
| 84 |
+
calls: 325,
|
| 85 |
+
avatar: '🎤',
|
| 86 |
+
tags: ['语音', '转录', '实时']
|
| 87 |
+
}
|
| 88 |
+
];
|
| 89 |
+
|
| 90 |
+
const handleSelectAgent = (agentId: number) => {
|
| 91 |
+
setSelectedAgents(prev =>
|
| 92 |
+
prev.includes(agentId)
|
| 93 |
+
? prev.filter(id => id !== agentId)
|
| 94 |
+
: [...prev, agentId]
|
| 95 |
+
);
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
const handleSelectAll = () => {
|
| 99 |
+
setSelectedAgents(selectedAgents.length === agents.length ? [] : agents.map(a => a.id));
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 104 |
+
<div className="p-6 border-b border-gray-200">
|
| 105 |
+
<div className="flex items-center justify-between">
|
| 106 |
+
<div className="flex items-center space-x-4">
|
| 107 |
+
<input
|
| 108 |
+
type="checkbox"
|
| 109 |
+
checked={selectedAgents.length === agents.length}
|
| 110 |
+
onChange={handleSelectAll}
|
| 111 |
+
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
|
| 112 |
+
/>
|
| 113 |
+
<span className="text-sm text-gray-600">
|
| 114 |
+
{selectedAgents.length > 0 ? `已选择 ${selectedAgents.length} 个Agent` : '全选'}
|
| 115 |
+
</span>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
{selectedAgents.length > 0 && (
|
| 119 |
+
<div className="flex items-center space-x-3">
|
| 120 |
+
<button className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer whitespace-nowrap">
|
| 121 |
+
批量启动
|
| 122 |
+
</button>
|
| 123 |
+
<button className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer whitespace-nowrap">
|
| 124 |
+
批量暂停
|
| 125 |
+
</button>
|
| 126 |
+
<button className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer whitespace-nowrap">
|
| 127 |
+
批量删除
|
| 128 |
+
</button>
|
| 129 |
+
</div>
|
| 130 |
+
)}
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div className="divide-y divide-gray-200">
|
| 135 |
+
{agents.map((agent) => (
|
| 136 |
+
<div key={agent.id} className="p-6 hover:bg-gray-50 transition-colors">
|
| 137 |
+
<div className="flex items-center justify-between">
|
| 138 |
+
<div className="flex items-center space-x-4">
|
| 139 |
+
<input
|
| 140 |
+
type="checkbox"
|
| 141 |
+
checked={selectedAgents.includes(agent.id)}
|
| 142 |
+
onChange={() => handleSelectAgent(agent.id)}
|
| 143 |
+
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
|
| 144 |
+
/>
|
| 145 |
+
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
| 146 |
+
<span className="text-xl">{agent.avatar}</span>
|
| 147 |
+
</div>
|
| 148 |
+
<div className="flex-1">
|
| 149 |
+
<div className="flex items-center space-x-3">
|
| 150 |
+
<h3 className="text-lg font-semibold text-gray-900">{agent.name}</h3>
|
| 151 |
+
<span className="text-sm text-gray-500">{agent.version}</span>
|
| 152 |
+
<span className={`px-2 py-1 text-xs rounded-full ${
|
| 153 |
+
agent.status === '运行中'
|
| 154 |
+
? 'bg-green-100 text-green-800'
|
| 155 |
+
: 'bg-yellow-100 text-yellow-800'
|
| 156 |
+
}`}>
|
| 157 |
+
{agent.status}
|
| 158 |
+
</span>
|
| 159 |
+
</div>
|
| 160 |
+
<p className="text-gray-600 mt-1">{agent.description}</p>
|
| 161 |
+
<div className="flex items-center space-x-4 mt-3">
|
| 162 |
+
<span className="text-sm text-gray-500">类型: {agent.type}</span>
|
| 163 |
+
<span className="text-sm text-gray-500">调用: {agent.calls.toLocaleString()}</span>
|
| 164 |
+
<span className="text-sm text-gray-500">最后使用: {agent.lastUsed}</span>
|
| 165 |
+
</div>
|
| 166 |
+
<div className="flex items-center space-x-2 mt-3">
|
| 167 |
+
{agent.tags.map((tag, index) => (
|
| 168 |
+
<span key={index} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full">
|
| 169 |
+
{tag}
|
| 170 |
+
</span>
|
| 171 |
+
))}
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<div className="flex items-center space-x-3">
|
| 177 |
+
<Link href={`/agents/${agent.id}/analytics`} className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 178 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 179 |
+
<i className="ri-bar-chart-line"></i>
|
| 180 |
+
</div>
|
| 181 |
+
</Link>
|
| 182 |
+
<Link href={`/agents/${agent.id}/edit`} className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 183 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 184 |
+
<i className="ri-edit-line"></i>
|
| 185 |
+
</div>
|
| 186 |
+
</Link>
|
| 187 |
+
<button className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 188 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 189 |
+
<i className="ri-settings-line"></i>
|
| 190 |
+
</div>
|
| 191 |
+
</button>
|
| 192 |
+
<button className="p-2 text-gray-400 hover:text-red-600 transition-colors cursor-pointer">
|
| 193 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 194 |
+
<i className="ri-delete-bin-line"></i>
|
| 195 |
+
</div>
|
| 196 |
+
</button>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
))}
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
);
|
| 204 |
+
}
|
app/agents/create/AgentBuilder.tsx
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { useRouter } from 'next/navigation';
|
| 5 |
+
import BasicSettings from './BasicSettings';
|
| 6 |
+
import ModelConfig from './ModelConfig';
|
| 7 |
+
import WorkflowConfig from './WorkflowConfig';
|
| 8 |
+
import KnowledgeBase from './KnowledgeBase';
|
| 9 |
+
import TestPanel from './TestPanel';
|
| 10 |
+
|
| 11 |
+
export default function AgentBuilder() {
|
| 12 |
+
const router = useRouter();
|
| 13 |
+
const [currentStep, setCurrentStep] = useState(0);
|
| 14 |
+
const [agentData, setAgentData] = useState({
|
| 15 |
+
name: '',
|
| 16 |
+
description: '',
|
| 17 |
+
type: 'chat',
|
| 18 |
+
avatar: '🤖',
|
| 19 |
+
model: 'gpt-4',
|
| 20 |
+
temperature: 0.7,
|
| 21 |
+
maxTokens: 2000,
|
| 22 |
+
systemPrompt: '',
|
| 23 |
+
workflows: [],
|
| 24 |
+
knowledgeBase: [],
|
| 25 |
+
tags: []
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
const steps = [
|
| 29 |
+
{ id: 0, name: '基本设置', icon: 'ri-settings-line' },
|
| 30 |
+
{ id: 1, name: '模型配置', icon: 'ri-cpu-line' },
|
| 31 |
+
{ id: 2, name: '工作流配置', icon: 'ri-flow-chart' },
|
| 32 |
+
{ id: 3, name: '知识库', icon: 'ri-book-line' },
|
| 33 |
+
{ id: 4, name: '测试调试', icon: 'ri-play-line' }
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
const handleNext = () => {
|
| 37 |
+
if (currentStep < steps.length - 1) {
|
| 38 |
+
setCurrentStep(currentStep + 1);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const handlePrev = () => {
|
| 43 |
+
if (currentStep > 0) {
|
| 44 |
+
setCurrentStep(currentStep - 1);
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
const handleSave = () => {
|
| 49 |
+
console.log('保存Agent:', agentData);
|
| 50 |
+
router.push('/agents');
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
const handleDeploy = () => {
|
| 54 |
+
console.log('部署Agent:', agentData);
|
| 55 |
+
router.push('/agents');
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
const renderStepContent = () => {
|
| 59 |
+
switch (currentStep) {
|
| 60 |
+
case 0:
|
| 61 |
+
return <BasicSettings data={agentData} onChange={setAgentData} />;
|
| 62 |
+
case 1:
|
| 63 |
+
return <ModelConfig data={agentData} onChange={setAgentData} />;
|
| 64 |
+
case 2:
|
| 65 |
+
return <WorkflowConfig data={agentData} onChange={setAgentData} />;
|
| 66 |
+
case 3:
|
| 67 |
+
return <KnowledgeBase data={agentData} onChange={setAgentData} />;
|
| 68 |
+
case 4:
|
| 69 |
+
return <TestPanel data={agentData} />;
|
| 70 |
+
default:
|
| 71 |
+
return null;
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
return (
|
| 76 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 77 |
+
{/* 步骤导航 */}
|
| 78 |
+
<div className="p-6 border-b border-gray-200">
|
| 79 |
+
<div className="flex items-center justify-between">
|
| 80 |
+
{steps.map((step, index) => (
|
| 81 |
+
<div key={step.id} className="flex items-center">
|
| 82 |
+
<div className={`flex items-center space-x-3 px-4 py-2 rounded-lg cursor-pointer transition-colors ${
|
| 83 |
+
currentStep === index
|
| 84 |
+
? 'bg-blue-100 text-blue-700'
|
| 85 |
+
: currentStep > index
|
| 86 |
+
? 'bg-green-100 text-green-700'
|
| 87 |
+
: 'text-gray-500'
|
| 88 |
+
}`} onClick={() => setCurrentStep(index)}>
|
| 89 |
+
<div className="w-6 h-6 flex items-center justify-center">
|
| 90 |
+
<i className={step.icon}></i>
|
| 91 |
+
</div>
|
| 92 |
+
<span className="font-medium">{step.name}</span>
|
| 93 |
+
</div>
|
| 94 |
+
{index < steps.length - 1 && (
|
| 95 |
+
<div className="mx-4 w-8 h-px bg-gray-300"></div>
|
| 96 |
+
)}
|
| 97 |
+
</div>
|
| 98 |
+
))}
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
{/* 步骤内容 */}
|
| 103 |
+
<div className="p-8">
|
| 104 |
+
{renderStepContent()}
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
{/* 底部操作按钮 */}
|
| 108 |
+
<div className="p-6 border-t border-gray-200 flex items-center justify-between">
|
| 109 |
+
<div className="flex space-x-3">
|
| 110 |
+
{currentStep > 0 && (
|
| 111 |
+
<button
|
| 112 |
+
onClick={handlePrev}
|
| 113 |
+
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 114 |
+
>
|
| 115 |
+
上一步
|
| 116 |
+
</button>
|
| 117 |
+
)}
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<div className="flex space-x-3">
|
| 121 |
+
<button
|
| 122 |
+
onClick={handleSave}
|
| 123 |
+
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 124 |
+
>
|
| 125 |
+
保存草稿
|
| 126 |
+
</button>
|
| 127 |
+
|
| 128 |
+
{currentStep < steps.length - 1 ? (
|
| 129 |
+
<button
|
| 130 |
+
onClick={handleNext}
|
| 131 |
+
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 132 |
+
>
|
| 133 |
+
下一步
|
| 134 |
+
</button>
|
| 135 |
+
) : (
|
| 136 |
+
<button
|
| 137 |
+
onClick={handleDeploy}
|
| 138 |
+
className="bg-green-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-green-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 139 |
+
>
|
| 140 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 141 |
+
<i className="ri-rocket-line"></i>
|
| 142 |
+
</div>
|
| 143 |
+
立即部署
|
| 144 |
+
</button>
|
| 145 |
+
)}
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
);
|
| 150 |
+
}
|
app/agents/create/BasicSettings.tsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
|
| 6 |
+
interface BasicSettingsProps {
|
| 7 |
+
data: any;
|
| 8 |
+
onChange: (data: any) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function BasicSettings({ data, onChange }: BasicSettingsProps) {
|
| 12 |
+
const [tags, setTags] = useState(data.tags || []);
|
| 13 |
+
const [newTag, setNewTag] = useState('');
|
| 14 |
+
|
| 15 |
+
const agentTypes = [
|
| 16 |
+
{ value: 'chat', label: '对话型Agent', desc: '专注于多轮对话和问答' },
|
| 17 |
+
{ value: 'analysis', label: '分析型Agent', desc: '数据分析和报告生成' },
|
| 18 |
+
{ value: 'generation', label: '生成型Agent', desc: '内容创作和代码生成' },
|
| 19 |
+
{ value: 'workflow', label: '工作流型Agent', desc: '复杂任务自动化处理' }
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
const avatars = ['🤖', '🎯', '📊', '💻', '📝', '🔍', '⚡', '🚀', '💡', '🎨'];
|
| 23 |
+
|
| 24 |
+
const handleInputChange = (field: string, value: any) => {
|
| 25 |
+
onChange({ ...data, [field]: value });
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
const addTag = () => {
|
| 29 |
+
if (newTag && !tags.includes(newTag)) {
|
| 30 |
+
const updatedTags = [...tags, newTag];
|
| 31 |
+
setTags(updatedTags);
|
| 32 |
+
onChange({ ...data, tags: updatedTags });
|
| 33 |
+
setNewTag('');
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
const removeTag = (tagToRemove: string) => {
|
| 38 |
+
const updatedTags = tags.filter(tag => tag !== tagToRemove);
|
| 39 |
+
setTags(updatedTags);
|
| 40 |
+
onChange({ ...data, tags: updatedTags });
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<div className="space-y-8">
|
| 45 |
+
<div>
|
| 46 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-6">基本信息</h3>
|
| 47 |
+
|
| 48 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 49 |
+
<div className="space-y-6">
|
| 50 |
+
<div>
|
| 51 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">Agent名称</label>
|
| 52 |
+
<input
|
| 53 |
+
type="text"
|
| 54 |
+
placeholder="输入Agent名称"
|
| 55 |
+
value={data.name}
|
| 56 |
+
onChange={(e) => handleInputChange('name', e.target.value)}
|
| 57 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 58 |
+
/>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div>
|
| 62 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">描述</label>
|
| 63 |
+
<textarea
|
| 64 |
+
placeholder="描述Agent的功能和用途"
|
| 65 |
+
rows={4}
|
| 66 |
+
maxLength={500}
|
| 67 |
+
value={data.description}
|
| 68 |
+
onChange={(e) => handleInputChange('description', e.target.value)}
|
| 69 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none"
|
| 70 |
+
/>
|
| 71 |
+
<div className="text-xs text-gray-500 mt-1">{data.description.length}/500</div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div>
|
| 75 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">标签</label>
|
| 76 |
+
<div className="flex flex-wrap gap-2 mb-3">
|
| 77 |
+
{tags.map((tag, index) => (
|
| 78 |
+
<span
|
| 79 |
+
key={index}
|
| 80 |
+
className="inline-flex items-center px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
|
| 81 |
+
>
|
| 82 |
+
{tag}
|
| 83 |
+
<button
|
| 84 |
+
onClick={() => removeTag(tag)}
|
| 85 |
+
className="ml-2 w-4 h-4 flex items-center justify-center hover:bg-blue-200 rounded-full cursor-pointer"
|
| 86 |
+
>
|
| 87 |
+
<i className="ri-close-line text-xs"></i>
|
| 88 |
+
</button>
|
| 89 |
+
</span>
|
| 90 |
+
))}
|
| 91 |
+
</div>
|
| 92 |
+
<div className="flex space-x-2">
|
| 93 |
+
<input
|
| 94 |
+
type="text"
|
| 95 |
+
placeholder="添加标签"
|
| 96 |
+
value={newTag}
|
| 97 |
+
onChange={(e) => setNewTag(e.target.value)}
|
| 98 |
+
onKeyPress={(e) => e.key === 'Enter' && addTag()}
|
| 99 |
+
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 100 |
+
/>
|
| 101 |
+
<button
|
| 102 |
+
onClick={addTag}
|
| 103 |
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap text-sm"
|
| 104 |
+
>
|
| 105 |
+
添加
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div className="space-y-6">
|
| 112 |
+
<div>
|
| 113 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">选择头像</label>
|
| 114 |
+
<div className="grid grid-cols-5 gap-3">
|
| 115 |
+
{avatars.map((avatar) => (
|
| 116 |
+
<button
|
| 117 |
+
key={avatar}
|
| 118 |
+
onClick={() => handleInputChange('avatar', avatar)}
|
| 119 |
+
className={`w-12 h-12 rounded-xl flex items-center justify-center text-xl cursor-pointer transition-colors ${
|
| 120 |
+
data.avatar === avatar
|
| 121 |
+
? 'bg-blue-100 border-2 border-blue-500'
|
| 122 |
+
: 'bg-gray-100 hover:bg-gray-200 border-2 border-transparent'
|
| 123 |
+
}`}
|
| 124 |
+
>
|
| 125 |
+
{avatar}
|
| 126 |
+
</button>
|
| 127 |
+
))}
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<div>
|
| 132 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">Agent类型</label>
|
| 133 |
+
<div className="space-y-3">
|
| 134 |
+
{agentTypes.map((type) => (
|
| 135 |
+
<div
|
| 136 |
+
key={type.value}
|
| 137 |
+
onClick={() => handleInputChange('type', type.value)}
|
| 138 |
+
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
|
| 139 |
+
data.type === type.value
|
| 140 |
+
? 'border-blue-500 bg-blue-50'
|
| 141 |
+
: 'border-gray-300 hover:border-gray-400'
|
| 142 |
+
}`}
|
| 143 |
+
>
|
| 144 |
+
<div className="flex items-center justify-between">
|
| 145 |
+
<div>
|
| 146 |
+
<h4 className="font-medium text-gray-900">{type.label}</h4>
|
| 147 |
+
<p className="text-sm text-gray-600 mt-1">{type.desc}</p>
|
| 148 |
+
</div>
|
| 149 |
+
<div className={`w-4 h-4 rounded-full border-2 ${
|
| 150 |
+
data.type === type.value
|
| 151 |
+
? 'border-blue-500 bg-blue-500'
|
| 152 |
+
: 'border-gray-300'
|
| 153 |
+
}`}>
|
| 154 |
+
{data.type === type.value && (
|
| 155 |
+
<div className="w-2 h-2 bg-white rounded-full mx-auto mt-0.5"></div>
|
| 156 |
+
)}
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
))}
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
);
|
| 168 |
+
}
|
app/agents/create/KnowledgeBase.tsx
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
|
| 6 |
+
interface KnowledgeBaseProps {
|
| 7 |
+
data: any;
|
| 8 |
+
onChange: (data: any) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function KnowledgeBase({ data, onChange }: KnowledgeBaseProps) {
|
| 12 |
+
const [uploadedFiles, setUploadedFiles] = useState([
|
| 13 |
+
{ id: 1, name: '产品手册.pdf', size: '2.4 MB', status: 'uploaded', type: 'pdf', chunks: 45, lastModified: '2024-01-15' },
|
| 14 |
+
{ id: 2, name: '常见问题FAQ.docx', size: '856 KB', status: 'uploaded', type: 'docx', chunks: 23, lastModified: '2024-01-14' },
|
| 15 |
+
{ id: 3, name: '技术文档.md', size: '1.2 MB', status: 'uploaded', type: 'md', chunks: 67, lastModified: '2024-01-13' },
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
const [webSources, setWebSources] = useState([
|
| 19 |
+
{ id: 1, url: 'https://docs.example.com', title: '官方文档', status: 'crawled', pages: 15, lastUpdate: '2024-01-15' },
|
| 20 |
+
{ id: 2, url: 'https://help.example.com', title: '帮助中心', status: 'crawling', pages: 8, lastUpdate: '2024-01-14' }
|
| 21 |
+
]);
|
| 22 |
+
|
| 23 |
+
const [manualContent, setManualContent] = useState('');
|
| 24 |
+
const [newUrl, setNewUrl] = useState('');
|
| 25 |
+
const [activeTab, setActiveTab] = useState('files');
|
| 26 |
+
const [processingSettings, setProcessingSettings] = useState({
|
| 27 |
+
autoChunk: true,
|
| 28 |
+
smartDedup: true,
|
| 29 |
+
generateSummary: false,
|
| 30 |
+
chunkSize: 1000,
|
| 31 |
+
overlap: 200
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
const knowledgeBases = [
|
| 35 |
+
{ id: 'kb-001', name: '通用知识库', description: '包含基础常识和通用信息', items: 1240, isDefault: true },
|
| 36 |
+
{ id: 'kb-002', name: '技术文档库', description: '技术相关的文档和资料', items: 567, isDefault: false },
|
| 37 |
+
{ id: 'kb-003', name: '产品说明库', description: '产品相关的说明和手册', items: 892, isDefault: false },
|
| 38 |
+
{ id: 'kb-004', name: '客服问答库', description: '客服常见问题和解答', items: 345, isDefault: false }
|
| 39 |
+
];
|
| 40 |
+
|
| 41 |
+
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState(['kb-001']);
|
| 42 |
+
|
| 43 |
+
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 44 |
+
const files = Array.from(event.target.files || []);
|
| 45 |
+
const newFiles = files.map((file, index) => ({
|
| 46 |
+
id: uploadedFiles.length + index + 1,
|
| 47 |
+
name: file.name,
|
| 48 |
+
size: `${(file.size / 1024 / 1024).toFixed(1)} MB`,
|
| 49 |
+
status: 'uploading',
|
| 50 |
+
type: file.name.split('.').pop() || 'unknown',
|
| 51 |
+
chunks: 0,
|
| 52 |
+
lastModified: new Date().toISOString().split('T')[0]
|
| 53 |
+
}));
|
| 54 |
+
|
| 55 |
+
setUploadedFiles([...uploadedFiles, ...newFiles]);
|
| 56 |
+
|
| 57 |
+
setTimeout(() => {
|
| 58 |
+
setUploadedFiles(prev => prev.map(file =>
|
| 59 |
+
newFiles.some(nf => nf.id === file.id)
|
| 60 |
+
? { ...file, status: 'uploaded', chunks: Math.floor(Math.random() * 50) + 10 }
|
| 61 |
+
: file
|
| 62 |
+
));
|
| 63 |
+
}, 2000);
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
const removeFile = (fileId: number) => {
|
| 67 |
+
setUploadedFiles(prev => prev.filter(file => file.id !== fileId));
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
const addWebSource = () => {
|
| 71 |
+
if (newUrl) {
|
| 72 |
+
const newSource = {
|
| 73 |
+
id: webSources.length + 1,
|
| 74 |
+
url: newUrl,
|
| 75 |
+
title: '正在获取标题...',
|
| 76 |
+
status: 'crawling',
|
| 77 |
+
pages: 0,
|
| 78 |
+
lastUpdate: new Date().toISOString().split('T')[0]
|
| 79 |
+
};
|
| 80 |
+
setWebSources([...webSources, newSource]);
|
| 81 |
+
setNewUrl('');
|
| 82 |
+
|
| 83 |
+
setTimeout(() => {
|
| 84 |
+
setWebSources(prev => prev.map(source =>
|
| 85 |
+
source.id === newSource.id
|
| 86 |
+
? { ...source, status: 'crawled', title: `${newUrl.split('/')[2]} - 文档`, pages: Math.floor(Math.random() * 20) + 5 }
|
| 87 |
+
: source
|
| 88 |
+
));
|
| 89 |
+
}, 3000);
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
const removeWebSource = (sourceId: number) => {
|
| 94 |
+
setWebSources(prev => prev.filter(source => source.id !== sourceId));
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const toggleKnowledgeBase = (kbId: string) => {
|
| 98 |
+
setSelectedKnowledgeBases(prev =>
|
| 99 |
+
prev.includes(kbId)
|
| 100 |
+
? prev.filter(id => id !== kbId)
|
| 101 |
+
: [...prev, kbId]
|
| 102 |
+
);
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const addManualContent = () => {
|
| 106 |
+
if (manualContent.trim()) {
|
| 107 |
+
const newFile = {
|
| 108 |
+
id: uploadedFiles.length + 1,
|
| 109 |
+
name: `手动内容_${new Date().toLocaleTimeString()}`,
|
| 110 |
+
size: `${Math.ceil(manualContent.length / 1024)} KB`,
|
| 111 |
+
status: 'uploaded',
|
| 112 |
+
type: 'text',
|
| 113 |
+
chunks: Math.ceil(manualContent.length / processingSettings.chunkSize),
|
| 114 |
+
lastModified: new Date().toISOString().split('T')[0]
|
| 115 |
+
};
|
| 116 |
+
setUploadedFiles([...uploadedFiles, newFile]);
|
| 117 |
+
setManualContent('');
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<div className="space-y-8">
|
| 123 |
+
<div>
|
| 124 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">知识库配置</h3>
|
| 125 |
+
<p className="text-gray-600 mb-6">为Agent配置知识来源,提升回答的准确性和专业性</p>
|
| 126 |
+
|
| 127 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 128 |
+
<div className="lg:col-span-2 space-y-6">
|
| 129 |
+
{/* 选择现有知识库 */}
|
| 130 |
+
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
| 131 |
+
<h4 className="font-medium text-gray-900 mb-4">选择现有知识库</h4>
|
| 132 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 133 |
+
{knowledgeBases.map((kb) => (
|
| 134 |
+
<div
|
| 135 |
+
key={kb.id}
|
| 136 |
+
onClick={() => toggleKnowledgeBase(kb.id)}
|
| 137 |
+
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
| 138 |
+
selectedKnowledgeBases.includes(kb.id)
|
| 139 |
+
? 'border-blue-500 bg-blue-50'
|
| 140 |
+
: 'border-gray-200 hover:border-gray-300'
|
| 141 |
+
}`}
|
| 142 |
+
>
|
| 143 |
+
<div className="flex items-start justify-between">
|
| 144 |
+
<div className="flex-1">
|
| 145 |
+
<div className="flex items-center space-x-2">
|
| 146 |
+
<h5 className="font-medium text-gray-900">{kb.name}</h5>
|
| 147 |
+
{kb.isDefault && (
|
| 148 |
+
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">默认</span>
|
| 149 |
+
)}
|
| 150 |
+
</div>
|
| 151 |
+
<p className="text-sm text-gray-600 mt-1">{kb.description}</p>
|
| 152 |
+
<p className="text-xs text-gray-500 mt-2">{kb.items.toLocaleString()} 条记录</p>
|
| 153 |
+
</div>
|
| 154 |
+
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
| 155 |
+
selectedKnowledgeBases.includes(kb.id)
|
| 156 |
+
? 'border-blue-500 bg-blue-500'
|
| 157 |
+
: 'border-gray-300'
|
| 158 |
+
}`}>
|
| 159 |
+
{selectedKnowledgeBases.includes(kb.id) && (
|
| 160 |
+
<i className="ri-check-line text-white text-xs"></i>
|
| 161 |
+
)}
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
))}
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
{/* 添加新内容 */}
|
| 170 |
+
<div className="bg-white border border-gray-200 rounded-lg">
|
| 171 |
+
<div className="border-b border-gray-200">
|
| 172 |
+
<div className="flex space-x-0">
|
| 173 |
+
{[
|
| 174 |
+
{ key: 'files', label: '文件上传', icon: 'ri-file-line' },
|
| 175 |
+
{ key: 'web', label: '网页抓取', icon: 'ri-global-line' },
|
| 176 |
+
{ key: 'manual', label: '手动输入', icon: 'ri-edit-line' }
|
| 177 |
+
].map((tab) => (
|
| 178 |
+
<button
|
| 179 |
+
key={tab.key}
|
| 180 |
+
onClick={() => setActiveTab(tab.key)}
|
| 181 |
+
className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
| 182 |
+
activeTab === tab.key
|
| 183 |
+
? 'border-blue-500 text-blue-600 bg-blue-50'
|
| 184 |
+
: 'border-transparent text-gray-500 hover:text-gray-700'
|
| 185 |
+
}`}
|
| 186 |
+
>
|
| 187 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block mr-2">
|
| 188 |
+
<i className={tab.icon}></i>
|
| 189 |
+
</div>
|
| 190 |
+
{tab.label}
|
| 191 |
+
</button>
|
| 192 |
+
))}
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<div className="p-6">
|
| 197 |
+
{activeTab === 'files' && (
|
| 198 |
+
<div className="space-y-4">
|
| 199 |
+
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
|
| 200 |
+
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-4">
|
| 201 |
+
<i className="ri-upload-cloud-line text-xl text-gray-400"></i>
|
| 202 |
+
</div>
|
| 203 |
+
<p className="text-gray-600 mb-2">拖拽文件到此处,或者</p>
|
| 204 |
+
<label className="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700 transition-colors text-sm">
|
| 205 |
+
选择文件
|
| 206 |
+
<input
|
| 207 |
+
type="file"
|
| 208 |
+
multiple
|
| 209 |
+
accept=".pdf,.doc,.docx,.txt,.md,.csv,.xlsx"
|
| 210 |
+
onChange={handleFileUpload}
|
| 211 |
+
className="hidden"
|
| 212 |
+
/>
|
| 213 |
+
</label>
|
| 214 |
+
<p className="text-xs text-gray-500 mt-2">支持 PDF, DOC, DOCX, TXT, MD, CSV, XLSX 格式,最大 50MB</p>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
)}
|
| 218 |
+
|
| 219 |
+
{activeTab === 'web' && (
|
| 220 |
+
<div className="space-y-4">
|
| 221 |
+
<div className="flex space-x-2">
|
| 222 |
+
<input
|
| 223 |
+
type="url"
|
| 224 |
+
placeholder="输入要抓取的网址(如:https://docs.example.com)"
|
| 225 |
+
value={newUrl}
|
| 226 |
+
onChange={(e) => setNewUrl(e.target.value)}
|
| 227 |
+
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 228 |
+
/>
|
| 229 |
+
<button
|
| 230 |
+
onClick={addWebSource}
|
| 231 |
+
className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap text-sm"
|
| 232 |
+
>
|
| 233 |
+
开始抓取
|
| 234 |
+
</button>
|
| 235 |
+
</div>
|
| 236 |
+
<div className="text-xs text-gray-500">
|
| 237 |
+
支持抓取文档站点、博客文章、产品页面等,自动提取文本内容
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
)}
|
| 241 |
+
|
| 242 |
+
{activeTab === 'manual' && (
|
| 243 |
+
<div className="space-y-4">
|
| 244 |
+
<textarea
|
| 245 |
+
placeholder="直接输入要添加到知识库的内容... 例如: • 产品使用说明 • 常见问题解答 • 操作步骤指南 • 专业知识条目"
|
| 246 |
+
rows={8}
|
| 247 |
+
maxLength={500}
|
| 248 |
+
value={manualContent}
|
| 249 |
+
onChange={(e) => setManualContent(e.target.value)}
|
| 250 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none"
|
| 251 |
+
/>
|
| 252 |
+
<div className="flex items-center justify-between">
|
| 253 |
+
<div className="text-xs text-gray-500">{manualContent.length}/500 字符</div>
|
| 254 |
+
<button
|
| 255 |
+
onClick={addManualContent}
|
| 256 |
+
disabled={!manualContent.trim()}
|
| 257 |
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors cursor-pointer whitespace-nowrap text-sm"
|
| 258 |
+
>
|
| 259 |
+
添加内容
|
| 260 |
+
</button>
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
)}
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
|
| 268 |
+
<div className="space-y-6">
|
| 269 |
+
{/* 已添加内容统计 */}
|
| 270 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 271 |
+
<h4 className="font-medium text-gray-900 mb-3">内容统计</h4>
|
| 272 |
+
<div className="space-y-3">
|
| 273 |
+
<div className="flex items-center justify-between">
|
| 274 |
+
<span className="text-sm text-gray-600">文档数量</span>
|
| 275 |
+
<span className="font-medium text-gray-900">{uploadedFiles.length}</span>
|
| 276 |
+
</div>
|
| 277 |
+
<div className="flex items-center justify-between">
|
| 278 |
+
<span className="text-sm text-gray-600">网页来源</span>
|
| 279 |
+
<span className="font-medium text-gray-900">{webSources.length}</span>
|
| 280 |
+
</div>
|
| 281 |
+
<div className="flex items-center justify-between">
|
| 282 |
+
<span className="text-sm text-gray-600">总分块数</span>
|
| 283 |
+
<span className="font-medium text-gray-900">{uploadedFiles.reduce((sum, file) => sum + file.chunks, 0)}</span>
|
| 284 |
+
</div>
|
| 285 |
+
<div className="flex items-center justify-between">
|
| 286 |
+
<span className="text-sm text-gray-600">知识库</span>
|
| 287 |
+
<span className="font-medium text-gray-900">{selectedKnowledgeBases.length} 个</span>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
|
| 292 |
+
{/* 处理设置 */}
|
| 293 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 294 |
+
<h4 className="font-medium text-gray-900 mb-3">处理设置</h4>
|
| 295 |
+
<div className="space-y-4">
|
| 296 |
+
<div className="flex items-center justify-between">
|
| 297 |
+
<span className="text-sm text-gray-700">自动分块处理</span>
|
| 298 |
+
<div
|
| 299 |
+
onClick={() => setProcessingSettings(prev => ({...prev, autoChunk: !prev.autoChunk}))}
|
| 300 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 301 |
+
processingSettings.autoChunk ? 'bg-blue-600' : 'bg-gray-300'
|
| 302 |
+
}`}
|
| 303 |
+
>
|
| 304 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 305 |
+
processingSettings.autoChunk ? 'right-1' : 'left-1'
|
| 306 |
+
}`}></div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<div className="flex items-center justify-between">
|
| 311 |
+
<span className="text-sm text-gray-700">智能去重</span>
|
| 312 |
+
<div
|
| 313 |
+
onClick={() => setProcessingSettings(prev => ({...prev, smartDedup: !prev.smartDedup}))}
|
| 314 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 315 |
+
processingSettings.smartDedup ? 'bg-blue-600' : 'bg-gray-300'
|
| 316 |
+
}`}
|
| 317 |
+
>
|
| 318 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 319 |
+
processingSettings.smartDedup ? 'right-1' : 'left-1'
|
| 320 |
+
}`}></div>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
|
| 324 |
+
<div className="flex items-center justify-between">
|
| 325 |
+
<span className="text-sm text-gray-700">生成摘要</span>
|
| 326 |
+
<div
|
| 327 |
+
onClick={() => setProcessingSettings(prev => ({...prev, generateSummary: !prev.generateSummary}))}
|
| 328 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 329 |
+
processingSettings.generateSummary ? 'bg-blue-600' : 'bg-gray-300'
|
| 330 |
+
}`}
|
| 331 |
+
>
|
| 332 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 333 |
+
processingSettings.generateSummary ? 'right-1' : 'left-1'
|
| 334 |
+
}`}></div>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
|
| 338 |
+
<div className="pt-2 border-t border-gray-200">
|
| 339 |
+
<div className="mb-3">
|
| 340 |
+
<label className="block text-sm text-gray-700 mb-1">
|
| 341 |
+
分块大小: {processingSettings.chunkSize} 字符
|
| 342 |
+
</label>
|
| 343 |
+
<input
|
| 344 |
+
type="range"
|
| 345 |
+
min="500"
|
| 346 |
+
max="2000"
|
| 347 |
+
step="100"
|
| 348 |
+
value={processingSettings.chunkSize}
|
| 349 |
+
onChange={(e) => setProcessingSettings(prev => ({...prev, chunkSize: parseInt(e.target.value)}))}
|
| 350 |
+
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
| 351 |
+
/>
|
| 352 |
+
</div>
|
| 353 |
+
|
| 354 |
+
<div>
|
| 355 |
+
<label className="block text-sm text-gray-700 mb-1">
|
| 356 |
+
重叠字符: {processingSettings.overlap}
|
| 357 |
+
</label>
|
| 358 |
+
<input
|
| 359 |
+
type="range"
|
| 360 |
+
min="0"
|
| 361 |
+
max="500"
|
| 362 |
+
step="50"
|
| 363 |
+
value={processingSettings.overlap}
|
| 364 |
+
onChange={(e) => setProcessingSettings(prev => ({...prev, overlap: parseInt(e.target.value)}))}
|
| 365 |
+
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
| 366 |
+
/>
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
|
| 372 |
+
{/* 使用提示 */}
|
| 373 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
| 374 |
+
<div className="flex items-start space-x-3">
|
| 375 |
+
<div className="w-5 h-5 flex items-center justify-center mt-0.5">
|
| 376 |
+
<i className="ri-lightbulb-line text-blue-600"></i>
|
| 377 |
+
</div>
|
| 378 |
+
<div>
|
| 379 |
+
<h4 className="font-medium text-blue-900 mb-2">优化建议</h4>
|
| 380 |
+
<ul className="text-sm text-blue-800 space-y-1">
|
| 381 |
+
<li>• 上传结构化的文档获得更好效果</li>
|
| 382 |
+
<li>• 定期更新知识库内容</li>
|
| 383 |
+
<li>• 避免重复和冗余信息</li>
|
| 384 |
+
<li>• 使用标准的文档格式</li>
|
| 385 |
+
</ul>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
{/* 已上传文件列表 */}
|
| 393 |
+
{uploadedFiles.length > 0 && (
|
| 394 |
+
<div className="mt-8">
|
| 395 |
+
<h4 className="font-medium text-gray-900 mb-4">已添加的文件</h4>
|
| 396 |
+
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
| 397 |
+
<div className="divide-y divide-gray-200">
|
| 398 |
+
{uploadedFiles.map((file) => (
|
| 399 |
+
<div key={file.id} className="p-4 hover:bg-gray-50 transition-colors">
|
| 400 |
+
<div className="flex items-center justify-between">
|
| 401 |
+
<div className="flex items-center space-x-4">
|
| 402 |
+
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 403 |
+
<i className={`${
|
| 404 |
+
file.type === 'pdf' ? 'ri-file-pdf-line text-red-600' :
|
| 405 |
+
file.type === 'docx' ? 'ri-file-word-line text-blue-600' :
|
| 406 |
+
file.type === 'md' ? 'ri-markdown-line text-purple-600' :
|
| 407 |
+
'ri-file-text-line text-gray-600'
|
| 408 |
+
}`}></i>
|
| 409 |
+
</div>
|
| 410 |
+
<div className="flex-1">
|
| 411 |
+
<div className="flex items-center space-x-2">
|
| 412 |
+
<p className="font-medium text-gray-900">{file.name}</p>
|
| 413 |
+
{file.status === 'uploading' ? (
|
| 414 |
+
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full">处理中</span>
|
| 415 |
+
) : (
|
| 416 |
+
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">已完成</span>
|
| 417 |
+
)}
|
| 418 |
+
</div>
|
| 419 |
+
<div className="flex items-center space-x-4 mt-1 text-sm text-gray-500">
|
| 420 |
+
<span>{file.size}</span>
|
| 421 |
+
<span>{file.chunks} 个分块</span>
|
| 422 |
+
<span>修改于 {file.lastModified}</span>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
<div className="flex items-center space-x-2">
|
| 427 |
+
{file.status === 'uploading' ? (
|
| 428 |
+
<div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
| 429 |
+
) : (
|
| 430 |
+
<button className="p-1 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 431 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 432 |
+
<i className="ri-eye-line"></i>
|
| 433 |
+
</div>
|
| 434 |
+
</button>
|
| 435 |
+
)}
|
| 436 |
+
<button
|
| 437 |
+
onClick={() => removeFile(file.id)}
|
| 438 |
+
className="p-1 text-gray-400 hover:text-red-600 transition-colors cursor-pointer"
|
| 439 |
+
>
|
| 440 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 441 |
+
<i className="ri-delete-bin-line"></i>
|
| 442 |
+
</div>
|
| 443 |
+
</button>
|
| 444 |
+
</div>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
))}
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
)}
|
| 452 |
+
|
| 453 |
+
{/* 网页来源列表 */}
|
| 454 |
+
{webSources.length > 0 && (
|
| 455 |
+
<div className="mt-8">
|
| 456 |
+
<h4 className="font-medium text-gray-900 mb-4">网页来源</h4>
|
| 457 |
+
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
| 458 |
+
<div className="divide-y divide-gray-200">
|
| 459 |
+
{webSources.map((source) => (
|
| 460 |
+
<div key={source.id} className="p-4 hover:bg-gray-50 transition-colors">
|
| 461 |
+
<div className="flex items-center justify-between">
|
| 462 |
+
<div className="flex items-center space-x-4">
|
| 463 |
+
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
| 464 |
+
<i className="ri-global-line text-green-600"></i>
|
| 465 |
+
</div>
|
| 466 |
+
<div className="flex-1">
|
| 467 |
+
<div className="flex items-center space-x-2">
|
| 468 |
+
<p className="font-medium text-gray-900">{source.title}</p>
|
| 469 |
+
{source.status === 'crawling' ? (
|
| 470 |
+
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full">抓取中</span>
|
| 471 |
+
) : (
|
| 472 |
+
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">已完成</span>
|
| 473 |
+
)}
|
| 474 |
+
</div>
|
| 475 |
+
<div className="flex items-center space-x-4 mt-1 text-sm text-gray-500">
|
| 476 |
+
<span>{source.url}</span>
|
| 477 |
+
<span>{source.pages} 个页面</span>
|
| 478 |
+
<span>更新于 {source.lastUpdate}</span>
|
| 479 |
+
</div>
|
| 480 |
+
</div>
|
| 481 |
+
</div>
|
| 482 |
+
<div className="flex items-center space-x-2">
|
| 483 |
+
{source.status === 'crawling' ? (
|
| 484 |
+
<div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
| 485 |
+
) : (
|
| 486 |
+
<button className="p-1 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 487 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 488 |
+
<i className="ri-refresh-line"></i>
|
| 489 |
+
</div>
|
| 490 |
+
</button>
|
| 491 |
+
)}
|
| 492 |
+
<button
|
| 493 |
+
onClick={() => removeWebSource(source.id)}
|
| 494 |
+
className="p-1 text-gray-400 hover:text-red-600 transition-colors cursor-pointer"
|
| 495 |
+
>
|
| 496 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 497 |
+
<i className="ri-delete-bin-line"></i>
|
| 498 |
+
</div>
|
| 499 |
+
</button>
|
| 500 |
+
</div>
|
| 501 |
+
</div>
|
| 502 |
+
</div>
|
| 503 |
+
))}
|
| 504 |
+
</div>
|
| 505 |
+
</div>
|
| 506 |
+
</div>
|
| 507 |
+
)}
|
| 508 |
+
</div>
|
| 509 |
+
</div>
|
| 510 |
+
);
|
| 511 |
+
}
|
app/agents/create/ModelConfig.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
interface ModelConfigProps {
|
| 5 |
+
data: any;
|
| 6 |
+
onChange: (data: any) => void;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function ModelConfig({ data, onChange }: ModelConfigProps) {
|
| 10 |
+
const models = [
|
| 11 |
+
{ id: 'gpt-4', name: 'GPT-4', desc: '最先进的语言模型,适合复杂任务', cost: '高' },
|
| 12 |
+
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', desc: '性价比最优,响应快速', cost: '中' },
|
| 13 |
+
{ id: 'claude-3', name: 'Claude-3', desc: '擅长分析和推理任务', cost: '中' },
|
| 14 |
+
{ id: 'gemini-pro', name: 'Gemini Pro', desc: '多模态能力强', cost: '中' }
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
const handleInputChange = (field: string, value: any) => {
|
| 18 |
+
onChange({ ...data, [field]: value });
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="space-y-8">
|
| 23 |
+
<div>
|
| 24 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-6">模型配置</h3>
|
| 25 |
+
|
| 26 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 27 |
+
<div className="space-y-6">
|
| 28 |
+
<div>
|
| 29 |
+
<label className="block text-sm font-medium text-gray-700 mb-3">选择基础模型</label>
|
| 30 |
+
<div className="space-y-3">
|
| 31 |
+
{models.map((model) => (
|
| 32 |
+
<div
|
| 33 |
+
key={model.id}
|
| 34 |
+
onClick={() => handleInputChange('model', model.id)}
|
| 35 |
+
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
|
| 36 |
+
data.model === model.id
|
| 37 |
+
? 'border-blue-500 bg-blue-50'
|
| 38 |
+
: 'border-gray-300 hover:border-gray-400'
|
| 39 |
+
}`}
|
| 40 |
+
>
|
| 41 |
+
<div className="flex items-center justify-between">
|
| 42 |
+
<div>
|
| 43 |
+
<h4 className="font-medium text-gray-900">{model.name}</h4>
|
| 44 |
+
<p className="text-sm text-gray-600 mt-1">{model.desc}</p>
|
| 45 |
+
</div>
|
| 46 |
+
<div className="text-right">
|
| 47 |
+
<span className={`px-2 py-1 text-xs rounded-full ${
|
| 48 |
+
model.cost === '高' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'
|
| 49 |
+
}`}>
|
| 50 |
+
成本{model.cost}
|
| 51 |
+
</span>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
))}
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div className="space-y-6">
|
| 61 |
+
<div>
|
| 62 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 63 |
+
创造性 (Temperature): {data.temperature}
|
| 64 |
+
</label>
|
| 65 |
+
<input
|
| 66 |
+
type="range"
|
| 67 |
+
min="0"
|
| 68 |
+
max="2"
|
| 69 |
+
step="0.1"
|
| 70 |
+
value={data.temperature}
|
| 71 |
+
onChange={(e) => handleInputChange('temperature', parseFloat(e.target.value))}
|
| 72 |
+
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
| 73 |
+
/>
|
| 74 |
+
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
| 75 |
+
<span>保守</span>
|
| 76 |
+
<span>创新</span>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<div>
|
| 81 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">最大输出长度</label>
|
| 82 |
+
<input
|
| 83 |
+
type="number"
|
| 84 |
+
min="100"
|
| 85 |
+
max="4000"
|
| 86 |
+
step="100"
|
| 87 |
+
value={data.maxTokens}
|
| 88 |
+
onChange={(e) => handleInputChange('maxTokens', parseInt(e.target.value))}
|
| 89 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 90 |
+
/>
|
| 91 |
+
<p className="text-xs text-gray-500 mt-1">建议范围: 100-4000 tokens</p>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div>
|
| 95 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">系统提示词</label>
|
| 96 |
+
<textarea
|
| 97 |
+
placeholder="定义Agent的角色、行为和回答风格..."
|
| 98 |
+
rows={8}
|
| 99 |
+
maxLength={500}
|
| 100 |
+
value={data.systemPrompt}
|
| 101 |
+
onChange={(e) => handleInputChange('systemPrompt', e.target.value)}
|
| 102 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none"
|
| 103 |
+
/>
|
| 104 |
+
<div className="text-xs text-gray-500 mt-1">{data.systemPrompt.length}/500</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
);
|
| 111 |
+
}
|
app/agents/create/TestPanel.tsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
|
| 6 |
+
interface TestPanelProps {
|
| 7 |
+
data: any;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function TestPanel({ data }: TestPanelProps) {
|
| 11 |
+
const [messages, setMessages] = useState([
|
| 12 |
+
{
|
| 13 |
+
id: 1,
|
| 14 |
+
role: 'assistant',
|
| 15 |
+
content: `你好!我是${data.name || 'AI Assistant'}。我已经准备好为您提供帮助了。请告诉我您需要什么协助?`,
|
| 16 |
+
timestamp: new Date()
|
| 17 |
+
}
|
| 18 |
+
]);
|
| 19 |
+
const [inputMessage, setInputMessage] = useState('');
|
| 20 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 21 |
+
|
| 22 |
+
const sendMessage = async () => {
|
| 23 |
+
if (!inputMessage.trim()) return;
|
| 24 |
+
|
| 25 |
+
const userMessage = {
|
| 26 |
+
id: messages.length + 1,
|
| 27 |
+
role: 'user',
|
| 28 |
+
content: inputMessage,
|
| 29 |
+
timestamp: new Date()
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
setMessages(prev => [...prev, userMessage]);
|
| 33 |
+
setInputMessage('');
|
| 34 |
+
setIsLoading(true);
|
| 35 |
+
|
| 36 |
+
setTimeout(() => {
|
| 37 |
+
const aiResponse = {
|
| 38 |
+
id: messages.length + 2,
|
| 39 |
+
role: 'assistant',
|
| 40 |
+
content: `这是一个测试响应。在实际部署后,我将根据您配置的模型和知识库来回答问题。当前配置:模型 ${data.model},温度 ${data.temperature}。`,
|
| 41 |
+
timestamp: new Date()
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
setMessages(prev => [...prev, aiResponse]);
|
| 45 |
+
setIsLoading(false);
|
| 46 |
+
}, 1500);
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const clearChat = () => {
|
| 50 |
+
setMessages([{
|
| 51 |
+
id: 1,
|
| 52 |
+
role: 'assistant',
|
| 53 |
+
content: `你好!我是${data.name || 'AI Assistant'}。我已经准备好为您提供帮助了。请告诉我您需要什么协助?`,
|
| 54 |
+
timestamp: new Date()
|
| 55 |
+
}]);
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
return (
|
| 59 |
+
<div className="space-y-8">
|
| 60 |
+
<div>
|
| 61 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-6">测试对话</h3>
|
| 62 |
+
|
| 63 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 64 |
+
<div className="lg:col-span-2">
|
| 65 |
+
<div className="bg-gray-50 rounded-xl border border-gray-200 h-96 flex flex-col">
|
| 66 |
+
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
| 67 |
+
<div className="flex items-center space-x-3">
|
| 68 |
+
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 69 |
+
<span className="text-sm">{data.avatar}</span>
|
| 70 |
+
</div>
|
| 71 |
+
<div>
|
| 72 |
+
<h4 className="font-medium text-gray-900">{data.name || '未命名Agent'}</h4>
|
| 73 |
+
<p className="text-xs text-gray-500">测试模式</p>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
<button
|
| 77 |
+
onClick={clearChat}
|
| 78 |
+
className="px-3 py-2 text-sm text-gray-600 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
|
| 79 |
+
>
|
| 80 |
+
清空对话
|
| 81 |
+
</button>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div className="flex-1 p-4 overflow-y-auto space-y-4">
|
| 85 |
+
{messages.map((message) => (
|
| 86 |
+
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
| 87 |
+
<div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
|
| 88 |
+
message.role === 'user'
|
| 89 |
+
? 'bg-blue-600 text-white'
|
| 90 |
+
: 'bg-white border border-gray-200 text-gray-900'
|
| 91 |
+
}`}>
|
| 92 |
+
<p className="text-sm">{message.content}</p>
|
| 93 |
+
<p className={`text-xs mt-1 ${
|
| 94 |
+
message.role === 'user' ? 'text-blue-100' : 'text-gray-500'
|
| 95 |
+
}`}>
|
| 96 |
+
{message.timestamp.toLocaleTimeString()}
|
| 97 |
+
</p>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
))}
|
| 101 |
+
|
| 102 |
+
{isLoading && (
|
| 103 |
+
<div className="flex justify-start">
|
| 104 |
+
<div className="bg-white border border-gray-200 rounded-lg px-4 py-2">
|
| 105 |
+
<div className="flex items-center space-x-2">
|
| 106 |
+
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
| 107 |
+
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
| 108 |
+
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
)}
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div className="p-4 border-t border-gray-200">
|
| 116 |
+
<div className="flex space-x-2">
|
| 117 |
+
<input
|
| 118 |
+
type="text"
|
| 119 |
+
placeholder="输入测试消息..."
|
| 120 |
+
value={inputMessage}
|
| 121 |
+
onChange={(e) => setInputMessage(e.target.value)}
|
| 122 |
+
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
| 123 |
+
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 124 |
+
/>
|
| 125 |
+
<button
|
| 126 |
+
onClick={sendMessage}
|
| 127 |
+
disabled={isLoading || !inputMessage.trim()}
|
| 128 |
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors cursor-pointer whitespace-nowrap"
|
| 129 |
+
>
|
| 130 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 131 |
+
<i className="ri-send-plane-line"></i>
|
| 132 |
+
</div>
|
| 133 |
+
</button>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div className="space-y-6">
|
| 140 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 141 |
+
<h4 className="font-medium text-gray-900 mb-3">配置预览</h4>
|
| 142 |
+
<div className="space-y-2 text-sm">
|
| 143 |
+
<div className="flex justify-between">
|
| 144 |
+
<span className="text-gray-600">名称:</span>
|
| 145 |
+
<span className="text-gray-900">{data.name || '未设置'}</span>
|
| 146 |
+
</div>
|
| 147 |
+
<div className="flex justify-between">
|
| 148 |
+
<span className="text-gray-600">类型:</span>
|
| 149 |
+
<span className="text-gray-900">{data.type}</span>
|
| 150 |
+
</div>
|
| 151 |
+
<div className="flex justify-between">
|
| 152 |
+
<span className="text-gray-600">模型:</span>
|
| 153 |
+
<span className="text-gray-900">{data.model}</span>
|
| 154 |
+
</div>
|
| 155 |
+
<div className="flex justify-between">
|
| 156 |
+
<span className="text-gray-600">创造性:</span>
|
| 157 |
+
<span className="text-gray-900">{data.temperature}</span>
|
| 158 |
+
</div>
|
| 159 |
+
<div className="flex justify-between">
|
| 160 |
+
<span className="text-gray-600">最大长度:</span>
|
| 161 |
+
<span className="text-gray-900">{data.maxTokens}</span>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 167 |
+
<h4 className="font-medium text-gray-900 mb-3">测试建议</h4>
|
| 168 |
+
<div className="space-y-2">
|
| 169 |
+
<button
|
| 170 |
+
onClick={() => setInputMessage('你好,请介绍一下自己')}
|
| 171 |
+
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded cursor-pointer"
|
| 172 |
+
>
|
| 173 |
+
基础问候测试
|
| 174 |
+
</button>
|
| 175 |
+
<button
|
| 176 |
+
onClick={() => setInputMessage('请帮我分析一下这个问题')}
|
| 177 |
+
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded cursor-pointer"
|
| 178 |
+
>
|
| 179 |
+
功能测试
|
| 180 |
+
</button>
|
| 181 |
+
<button
|
| 182 |
+
onClick={() => setInputMessage('你能做什么?有什么限制吗?')}
|
| 183 |
+
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded cursor-pointer"
|
| 184 |
+
>
|
| 185 |
+
能力边界测试
|
| 186 |
+
</button>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
);
|
| 194 |
+
}
|
app/agents/create/WorkflowConfig.tsx
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
interface WorkflowConfigProps {
|
| 6 |
+
data: any;
|
| 7 |
+
onChange: (data: any) => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function WorkflowConfig({ data, onChange }: WorkflowConfigProps) {
|
| 11 |
+
const [selectedWorkflows, setSelectedWorkflows] = useState(data.workflows || []);
|
| 12 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 13 |
+
const [filterCategory, setFilterCategory] = useState('all');
|
| 14 |
+
|
| 15 |
+
const availableWorkflows = [
|
| 16 |
+
{
|
| 17 |
+
id: 'wf-001',
|
| 18 |
+
name: '数据分析流程',
|
| 19 |
+
description: '自动化数据收集、清洗和分析',
|
| 20 |
+
category: 'data-processing',
|
| 21 |
+
icon: '📊',
|
| 22 |
+
status: 'active',
|
| 23 |
+
lastUsed: '2024-01-15',
|
| 24 |
+
executions: 156
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
id: 'wf-002',
|
| 28 |
+
name: '内容审核工作流',
|
| 29 |
+
description: '文本内容智能审核和分类',
|
| 30 |
+
category: 'content-moderation',
|
| 31 |
+
icon: '🔍',
|
| 32 |
+
status: 'active',
|
| 33 |
+
lastUsed: '2024-01-14',
|
| 34 |
+
executions: 89
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
id: 'wf-003',
|
| 38 |
+
name: '客服自动回复',
|
| 39 |
+
description: '基于知识库的智能客服响应',
|
| 40 |
+
category: 'customer-service',
|
| 41 |
+
icon: '💬',
|
| 42 |
+
status: 'active',
|
| 43 |
+
lastUsed: '2024-01-13',
|
| 44 |
+
executions: 234
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
id: 'wf-004',
|
| 48 |
+
name: '报告生成器',
|
| 49 |
+
description: '自动生成业务分析报告',
|
| 50 |
+
category: 'report-generation',
|
| 51 |
+
icon: '📄',
|
| 52 |
+
status: 'active',
|
| 53 |
+
lastUsed: '2024-01-12',
|
| 54 |
+
executions: 67
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
id: 'wf-005',
|
| 58 |
+
name: '邮件处理流程',
|
| 59 |
+
description: '邮件分类和自动回复',
|
| 60 |
+
category: 'email-automation',
|
| 61 |
+
icon: '📧',
|
| 62 |
+
status: 'active',
|
| 63 |
+
lastUsed: '2024-01-11',
|
| 64 |
+
executions: 145
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
id: 'wf-006',
|
| 68 |
+
name: '社交媒体监控',
|
| 69 |
+
description: '社交媒体内容监控和分析',
|
| 70 |
+
category: 'social-media',
|
| 71 |
+
icon: '📱',
|
| 72 |
+
status: 'active',
|
| 73 |
+
lastUsed: '2024-01-10',
|
| 74 |
+
executions: 78
|
| 75 |
+
}
|
| 76 |
+
];
|
| 77 |
+
|
| 78 |
+
const categories = [
|
| 79 |
+
{ key: 'all', label: '全部分类', count: availableWorkflows.length },
|
| 80 |
+
{ key: 'data-processing', label: '数据处理', count: 1 },
|
| 81 |
+
{ key: 'content-moderation', label: '内容审核', count: 1 },
|
| 82 |
+
{ key: 'customer-service', label: '客服服务', count: 1 },
|
| 83 |
+
{ key: 'report-generation', label: '报告生成', count: 1 },
|
| 84 |
+
{ key: 'email-automation', label: '邮件自动化', count: 1 },
|
| 85 |
+
{ key: 'social-media', label: '社交媒体', count: 1 }
|
| 86 |
+
];
|
| 87 |
+
|
| 88 |
+
const filteredWorkflows = availableWorkflows.filter(workflow => {
|
| 89 |
+
const matchesSearch = workflow.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 90 |
+
workflow.description.toLowerCase().includes(searchTerm.toLowerCase());
|
| 91 |
+
const matchesCategory = filterCategory === 'all' || workflow.category === filterCategory;
|
| 92 |
+
return matchesSearch && matchesCategory;
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
const handleWorkflowToggle = (workflowId: string) => {
|
| 96 |
+
const isSelected = selectedWorkflows.includes(workflowId);
|
| 97 |
+
const updatedWorkflows = isSelected
|
| 98 |
+
? selectedWorkflows.filter(id => id !== workflowId)
|
| 99 |
+
: [...selectedWorkflows, workflowId];
|
| 100 |
+
|
| 101 |
+
setSelectedWorkflows(updatedWorkflows);
|
| 102 |
+
onChange({ ...data, workflows: updatedWorkflows });
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const handleSelectAll = () => {
|
| 106 |
+
const allIds = filteredWorkflows.map(wf => wf.id);
|
| 107 |
+
setSelectedWorkflows(allIds);
|
| 108 |
+
onChange({ ...data, workflows: allIds });
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
const handleDeselectAll = () => {
|
| 112 |
+
setSelectedWorkflows([]);
|
| 113 |
+
onChange({ ...data, workflows: [] });
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
return (
|
| 117 |
+
<div className="space-y-8">
|
| 118 |
+
<div>
|
| 119 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">工作流配置</h3>
|
| 120 |
+
<p className="text-gray-600 mb-6">选择Agent可以调用的工作流程,增强自动化处理能力</p>
|
| 121 |
+
|
| 122 |
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
| 123 |
+
<div className="lg:col-span-3 space-y-6">
|
| 124 |
+
{/* 搜索和筛选 */}
|
| 125 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 126 |
+
<div className="flex flex-col sm:flex-row gap-4">
|
| 127 |
+
<div className="flex-1 relative">
|
| 128 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 129 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 130 |
+
<i className="ri-search-line text-gray-400"></i>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
<input
|
| 134 |
+
type="text"
|
| 135 |
+
placeholder="搜索工作流..."
|
| 136 |
+
value={searchTerm}
|
| 137 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 138 |
+
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 139 |
+
/>
|
| 140 |
+
</div>
|
| 141 |
+
<select
|
| 142 |
+
value={filterCategory}
|
| 143 |
+
onChange={(e) => setFilterCategory(e.target.value)}
|
| 144 |
+
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm pr-8"
|
| 145 |
+
>
|
| 146 |
+
{categories.map(category => (
|
| 147 |
+
<option key={category.key} value={category.key}>
|
| 148 |
+
{category.label} ({category.count})
|
| 149 |
+
</option>
|
| 150 |
+
))}
|
| 151 |
+
</select>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<div className="flex items-center justify-between mt-4">
|
| 155 |
+
<div className="text-sm text-gray-600">
|
| 156 |
+
已选择 {selectedWorkflows.length} 个工作流
|
| 157 |
+
</div>
|
| 158 |
+
<div className="flex space-x-2">
|
| 159 |
+
<button
|
| 160 |
+
onClick={handleSelectAll}
|
| 161 |
+
className="px-3 py-1 text-sm text-blue-600 hover:bg-blue-50 rounded cursor-pointer"
|
| 162 |
+
>
|
| 163 |
+
全选
|
| 164 |
+
</button>
|
| 165 |
+
<button
|
| 166 |
+
onClick={handleDeselectAll}
|
| 167 |
+
className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-50 rounded cursor-pointer"
|
| 168 |
+
>
|
| 169 |
+
清空
|
| 170 |
+
</button>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
{/* 工作流列表 */}
|
| 176 |
+
<div className="space-y-4">
|
| 177 |
+
{filteredWorkflows.map((workflow) => (
|
| 178 |
+
<div
|
| 179 |
+
key={workflow.id}
|
| 180 |
+
className={`bg-white border rounded-lg p-4 cursor-pointer transition-all ${
|
| 181 |
+
selectedWorkflows.includes(workflow.id)
|
| 182 |
+
? 'border-blue-500 bg-blue-50'
|
| 183 |
+
: 'border-gray-200 hover:border-gray-300'
|
| 184 |
+
}`}
|
| 185 |
+
onClick={() => handleWorkflowToggle(workflow.id)}
|
| 186 |
+
>
|
| 187 |
+
<div className="flex items-center justify-between">
|
| 188 |
+
<div className="flex items-center space-x-4">
|
| 189 |
+
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center text-xl">
|
| 190 |
+
{workflow.icon}
|
| 191 |
+
</div>
|
| 192 |
+
<div className="flex-1">
|
| 193 |
+
<div className="flex items-center space-x-2">
|
| 194 |
+
<h4 className="font-medium text-gray-900">{workflow.name}</h4>
|
| 195 |
+
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
| 196 |
+
{workflow.status === 'active' ? '活跃' : '暂停'}
|
| 197 |
+
</span>
|
| 198 |
+
</div>
|
| 199 |
+
<p className="text-sm text-gray-600 mt-1">{workflow.description}</p>
|
| 200 |
+
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
|
| 201 |
+
<span>执行次数: {workflow.executions}</span>
|
| 202 |
+
<span>最后使用: {workflow.lastUsed}</span>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
<div className="flex items-center space-x-3">
|
| 207 |
+
<button
|
| 208 |
+
onClick={(e) => {
|
| 209 |
+
e.stopPropagation();
|
| 210 |
+
alert('工作流详情功能开发中...');
|
| 211 |
+
}}
|
| 212 |
+
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors cursor-pointer"
|
| 213 |
+
>
|
| 214 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 215 |
+
<i className="ri-eye-line"></i>
|
| 216 |
+
</div>
|
| 217 |
+
</button>
|
| 218 |
+
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
| 219 |
+
selectedWorkflows.includes(workflow.id)
|
| 220 |
+
? 'border-blue-500 bg-blue-500'
|
| 221 |
+
: 'border-gray-300'
|
| 222 |
+
}`}>
|
| 223 |
+
{selectedWorkflows.includes(workflow.id) && (
|
| 224 |
+
<i className="ri-check-line text-white text-xs"></i>
|
| 225 |
+
)}
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
))}
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
{filteredWorkflows.length === 0 && (
|
| 234 |
+
<div className="text-center py-12">
|
| 235 |
+
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 236 |
+
<i className="ri-search-line text-2xl text-gray-400"></i>
|
| 237 |
+
</div>
|
| 238 |
+
<p className="text-gray-500">没有找到匹配的工作流</p>
|
| 239 |
+
</div>
|
| 240 |
+
)}
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div className="space-y-6">
|
| 244 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 245 |
+
<h4 className="font-medium text-gray-900 mb-3">已选择的工作流</h4>
|
| 246 |
+
{selectedWorkflows.length === 0 ? (
|
| 247 |
+
<p className="text-sm text-gray-500">还未选择任何工作流</p>
|
| 248 |
+
) : (
|
| 249 |
+
<div className="space-y-2">
|
| 250 |
+
{selectedWorkflows.map((workflowId) => {
|
| 251 |
+
const workflow = availableWorkflows.find(w => w.id === workflowId);
|
| 252 |
+
return workflow ? (
|
| 253 |
+
<div key={workflowId} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded">
|
| 254 |
+
<div className="flex items-center space-x-2">
|
| 255 |
+
<span className="text-sm">{workflow.icon}</span>
|
| 256 |
+
<span className="text-sm font-medium text-gray-900">{workflow.name}</span>
|
| 257 |
+
</div>
|
| 258 |
+
<button
|
| 259 |
+
onClick={() => handleWorkflowToggle(workflowId)}
|
| 260 |
+
className="w-4 h-4 flex items-center justify-center text-gray-400 hover:text-red-600 cursor-pointer"
|
| 261 |
+
>
|
| 262 |
+
<i className="ri-close-line text-xs"></i>
|
| 263 |
+
</button>
|
| 264 |
+
</div>
|
| 265 |
+
) : null;
|
| 266 |
+
})}
|
| 267 |
+
</div>
|
| 268 |
+
)}
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 272 |
+
<h4 className="font-medium text-gray-900 mb-3">工作流配置</h4>
|
| 273 |
+
<div className="space-y-3">
|
| 274 |
+
<div className="flex items-center justify-between">
|
| 275 |
+
<span className="text-sm text-gray-700">自动触发</span>
|
| 276 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 277 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
<div className="flex items-center justify-between">
|
| 281 |
+
<span className="text-sm text-gray-700">并行执行</span>
|
| 282 |
+
<div className="w-10 h-6 bg-gray-300 rounded-full relative cursor-pointer">
|
| 283 |
+
<div className="w-4 h-4 bg-white rounded-full absolute left-1 top-1"></div>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
<div className="flex items-center justify-between">
|
| 287 |
+
<span className="text-sm text-gray-700">错误重试</span>
|
| 288 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 289 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
| 296 |
+
<div className="flex items-start space-x-3">
|
| 297 |
+
<div className="w-5 h-5 flex items-center justify-center mt-0.5">
|
| 298 |
+
<i className="ri-information-line text-blue-600"></i>
|
| 299 |
+
</div>
|
| 300 |
+
<div>
|
| 301 |
+
<h4 className="font-medium text-blue-900 mb-1">使用提示</h4>
|
| 302 |
+
<p className="text-sm text-blue-800">
|
| 303 |
+
选择的工作流将与Agent集成,用户可以通过对话触发相应的自动化流程。建议选择与Agent功能相关的工作流。
|
| 304 |
+
</p>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
);
|
| 313 |
+
}
|
app/agents/create/page.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
import Header from '@/components/Header';
|
| 5 |
+
import Sidebar from '@/components/Sidebar';
|
| 6 |
+
import AgentBuilder from './AgentBuilder';
|
| 7 |
+
|
| 8 |
+
export default function CreateAgentPage() {
|
| 9 |
+
return (
|
| 10 |
+
<div className="min-h-screen bg-gray-50">
|
| 11 |
+
<Header />
|
| 12 |
+
<div className="flex">
|
| 13 |
+
<Sidebar />
|
| 14 |
+
<main className="flex-1 p-8">
|
| 15 |
+
<div className="max-w-7xl mx-auto">
|
| 16 |
+
<div className="mb-8">
|
| 17 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">创建新Agent</h1>
|
| 18 |
+
<p className="text-gray-600">配置和定制您的AI Agent功能</p>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<AgentBuilder />
|
| 22 |
+
</div>
|
| 23 |
+
</main>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
);
|
| 27 |
+
}
|
app/agents/page.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
import Header from '@/components/Header';
|
| 5 |
+
import Sidebar from '@/components/Sidebar';
|
| 6 |
+
import AgentList from './AgentList';
|
| 7 |
+
import AgentFilters from './AgentFilters';
|
| 8 |
+
import Link from 'next/link';
|
| 9 |
+
|
| 10 |
+
export default function AgentsPage() {
|
| 11 |
+
return (
|
| 12 |
+
<div className="min-h-screen bg-gray-50">
|
| 13 |
+
<Header />
|
| 14 |
+
<div className="flex">
|
| 15 |
+
<Sidebar />
|
| 16 |
+
<main className="flex-1 p-8">
|
| 17 |
+
<div className="max-w-7xl mx-auto">
|
| 18 |
+
<div className="mb-8">
|
| 19 |
+
<div className="flex items-center justify-between">
|
| 20 |
+
<div>
|
| 21 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">Agent管理</h1>
|
| 22 |
+
<p className="text-gray-600">创建、管理和部署您的AI Agent</p>
|
| 23 |
+
</div>
|
| 24 |
+
<Link href="/agents/create" className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 25 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 26 |
+
<i className="ri-add-line"></i>
|
| 27 |
+
</div>
|
| 28 |
+
创建Agent
|
| 29 |
+
</Link>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<AgentFilters />
|
| 34 |
+
<AgentList />
|
| 35 |
+
</div>
|
| 36 |
+
</main>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
);
|
| 40 |
+
}
|
app/dashboard/ActivityFeed.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
export default function ActivityFeed() {
|
| 4 |
+
const activities = [
|
| 5 |
+
{
|
| 6 |
+
id: 1,
|
| 7 |
+
type: 'create',
|
| 8 |
+
title: '创建了新的Agent',
|
| 9 |
+
description: '智能客服助手 v2.0',
|
| 10 |
+
time: '2小时前',
|
| 11 |
+
icon: 'ri-add-circle-line',
|
| 12 |
+
color: 'blue'
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
id: 2,
|
| 16 |
+
type: 'update',
|
| 17 |
+
title: '更新了工作流',
|
| 18 |
+
description: '文档处理流程优化',
|
| 19 |
+
time: '4小时前',
|
| 20 |
+
icon: 'ri-edit-line',
|
| 21 |
+
color: 'green'
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
id: 3,
|
| 25 |
+
type: 'deploy',
|
| 26 |
+
title: '部署了Agent',
|
| 27 |
+
description: '代码生成器已上线',
|
| 28 |
+
time: '6小时前',
|
| 29 |
+
icon: 'ri-rocket-line',
|
| 30 |
+
color: 'purple'
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: 4,
|
| 34 |
+
type: 'upload',
|
| 35 |
+
title: '上传了知识库',
|
| 36 |
+
description: '产品文档已更新',
|
| 37 |
+
time: '8小时前',
|
| 38 |
+
icon: 'ri-upload-line',
|
| 39 |
+
color: 'orange'
|
| 40 |
+
}
|
| 41 |
+
];
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 45 |
+
<div className="p-6 border-b border-gray-200">
|
| 46 |
+
<h2 className="text-lg font-semibold text-gray-900">最新动态</h2>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div className="p-6">
|
| 50 |
+
<div className="space-y-4">
|
| 51 |
+
{activities.map((activity) => (
|
| 52 |
+
<div key={activity.id} className="flex items-start space-x-4">
|
| 53 |
+
<div className={`w-8 h-8 rounded-full flex items-center justify-center bg-${activity.color}-100 mt-1`}>
|
| 54 |
+
<i className={`${activity.icon} text-sm text-${activity.color}-600`}></i>
|
| 55 |
+
</div>
|
| 56 |
+
<div className="flex-1">
|
| 57 |
+
<h4 className="font-medium text-gray-900">{activity.title}</h4>
|
| 58 |
+
<p className="text-sm text-gray-600 mt-1">{activity.description}</p>
|
| 59 |
+
<p className="text-xs text-gray-500 mt-2">{activity.time}</p>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
))}
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
}
|
app/dashboard/DashboardStats.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
export default function DashboardStats() {
|
| 4 |
+
const stats = [
|
| 5 |
+
{
|
| 6 |
+
title: '活跃Agent',
|
| 7 |
+
value: '12',
|
| 8 |
+
change: '+2.5%',
|
| 9 |
+
trend: 'up',
|
| 10 |
+
icon: 'ri-robot-line',
|
| 11 |
+
color: 'blue'
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
title: '今日调用',
|
| 15 |
+
value: '2,845',
|
| 16 |
+
change: '+12.3%',
|
| 17 |
+
trend: 'up',
|
| 18 |
+
icon: 'ri-rocket-line',
|
| 19 |
+
color: 'green'
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
title: '工作流运行',
|
| 23 |
+
value: '156',
|
| 24 |
+
change: '+8.7%',
|
| 25 |
+
trend: 'up',
|
| 26 |
+
icon: 'ri-flow-chart',
|
| 27 |
+
color: 'purple'
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
title: '知识库文档',
|
| 31 |
+
value: '234',
|
| 32 |
+
change: '+5.2%',
|
| 33 |
+
trend: 'up',
|
| 34 |
+
icon: 'ri-database-line',
|
| 35 |
+
color: 'orange'
|
| 36 |
+
}
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 41 |
+
{stats.map((stat, index) => (
|
| 42 |
+
<div key={index} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 43 |
+
<div className="flex items-center justify-between">
|
| 44 |
+
<div>
|
| 45 |
+
<p className="text-sm font-medium text-gray-600">{stat.title}</p>
|
| 46 |
+
<p className="text-2xl font-bold text-gray-900 mt-1">{stat.value}</p>
|
| 47 |
+
</div>
|
| 48 |
+
<div className={`w-12 h-12 rounded-lg flex items-center justify-center bg-${stat.color}-100`}>
|
| 49 |
+
<i className={`${stat.icon} text-xl text-${stat.color}-600`}></i>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div className="mt-4 flex items-center">
|
| 53 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 54 |
+
<i className="ri-arrow-up-line text-green-500 text-sm"></i>
|
| 55 |
+
</div>
|
| 56 |
+
<span className="text-sm text-green-600 ml-1">{stat.change}</span>
|
| 57 |
+
<span className="text-sm text-gray-500 ml-2">较上周</span>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
))}
|
| 61 |
+
</div>
|
| 62 |
+
);
|
| 63 |
+
}
|
app/dashboard/QuickActions.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link';
|
| 4 |
+
|
| 5 |
+
export default function QuickActions() {
|
| 6 |
+
const actions = [
|
| 7 |
+
{
|
| 8 |
+
title: '创建Agent',
|
| 9 |
+
description: '从零开始构建新的AI Agent',
|
| 10 |
+
icon: 'ri-add-circle-line',
|
| 11 |
+
href: '/agents/create',
|
| 12 |
+
color: 'blue'
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
title: '导入模板',
|
| 16 |
+
description: '使用预设模板快速开始',
|
| 17 |
+
icon: 'ri-download-line',
|
| 18 |
+
href: '/templates',
|
| 19 |
+
color: 'green'
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
title: '创建工作流',
|
| 23 |
+
description: '设计复杂的自动化流程',
|
| 24 |
+
icon: 'ri-flow-chart',
|
| 25 |
+
href: '/workflows/create',
|
| 26 |
+
color: 'purple'
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
title: '上传知识库',
|
| 30 |
+
description: '添加文档和数据源',
|
| 31 |
+
icon: 'ri-upload-line',
|
| 32 |
+
href: '/knowledge/upload',
|
| 33 |
+
color: 'orange'
|
| 34 |
+
}
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
return (
|
| 38 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 39 |
+
<div className="p-6 border-b border-gray-200">
|
| 40 |
+
<h2 className="text-lg font-semibold text-gray-900">快速操作</h2>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div className="p-6">
|
| 44 |
+
<div className="space-y-4">
|
| 45 |
+
{actions.map((action, index) => (
|
| 46 |
+
<Link
|
| 47 |
+
key={index}
|
| 48 |
+
href={action.href}
|
| 49 |
+
className="flex items-center space-x-4 p-4 hover:bg-gray-50 rounded-lg cursor-pointer transition-colors"
|
| 50 |
+
>
|
| 51 |
+
<div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-${action.color}-100`}>
|
| 52 |
+
<i className={`${action.icon} text-lg text-${action.color}-600`}></i>
|
| 53 |
+
</div>
|
| 54 |
+
<div>
|
| 55 |
+
<h3 className="font-medium text-gray-900">{action.title}</h3>
|
| 56 |
+
<p className="text-sm text-gray-500">{action.description}</p>
|
| 57 |
+
</div>
|
| 58 |
+
</Link>
|
| 59 |
+
))}
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
);
|
| 64 |
+
}
|
app/dashboard/RecentAgents.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link';
|
| 4 |
+
|
| 5 |
+
export default function RecentAgents() {
|
| 6 |
+
const agents = [
|
| 7 |
+
{
|
| 8 |
+
id: 1,
|
| 9 |
+
name: '智能客服助手',
|
| 10 |
+
type: '对话型',
|
| 11 |
+
status: '运行中',
|
| 12 |
+
lastUsed: '2分钟前',
|
| 13 |
+
calls: 245,
|
| 14 |
+
avatar: '🤖'
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
id: 2,
|
| 18 |
+
name: '文档分析器',
|
| 19 |
+
type: '分析型',
|
| 20 |
+
status: '运行中',
|
| 21 |
+
lastUsed: '15分钟前',
|
| 22 |
+
calls: 128,
|
| 23 |
+
avatar: '📄'
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
id: 3,
|
| 27 |
+
name: '代码生成器',
|
| 28 |
+
type: '生成型',
|
| 29 |
+
status: '暂停',
|
| 30 |
+
lastUsed: '1小时前',
|
| 31 |
+
calls: 89,
|
| 32 |
+
avatar: '💻'
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
id: 4,
|
| 36 |
+
name: '数据可视化',
|
| 37 |
+
type: '图表型',
|
| 38 |
+
status: '运行中',
|
| 39 |
+
lastUsed: '30分钟前',
|
| 40 |
+
calls: 67,
|
| 41 |
+
avatar: '📊'
|
| 42 |
+
}
|
| 43 |
+
];
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 47 |
+
<div className="p-6 border-b border-gray-200">
|
| 48 |
+
<div className="flex items-center justify-between">
|
| 49 |
+
<h2 className="text-lg font-semibold text-gray-900">最近使用的Agent</h2>
|
| 50 |
+
<Link href="/agents" className="text-blue-600 hover:text-blue-700 text-sm font-medium cursor-pointer">
|
| 51 |
+
查看全部
|
| 52 |
+
</Link>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div className="p-6">
|
| 57 |
+
<div className="space-y-4">
|
| 58 |
+
{agents.map((agent) => (
|
| 59 |
+
<div key={agent.id} className="flex items-center justify-between p-4 hover:bg-gray-50 rounded-lg cursor-pointer">
|
| 60 |
+
<div className="flex items-center space-x-4">
|
| 61 |
+
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 62 |
+
<span className="text-lg">{agent.avatar}</span>
|
| 63 |
+
</div>
|
| 64 |
+
<div>
|
| 65 |
+
<h3 className="font-medium text-gray-900">{agent.name}</h3>
|
| 66 |
+
<div className="flex items-center space-x-4 mt-1">
|
| 67 |
+
<span className="text-sm text-gray-500">{agent.type}</span>
|
| 68 |
+
<span className={`px-2 py-1 text-xs rounded-full ${
|
| 69 |
+
agent.status === '运行中'
|
| 70 |
+
? 'bg-green-100 text-green-800'
|
| 71 |
+
: 'bg-yellow-100 text-yellow-800'
|
| 72 |
+
}`}>
|
| 73 |
+
{agent.status}
|
| 74 |
+
</span>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div className="text-right">
|
| 80 |
+
<p className="text-sm font-medium text-gray-900">{agent.calls} 次调用</p>
|
| 81 |
+
<p className="text-xs text-gray-500">{agent.lastUsed}</p>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
))}
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
}
|
app/dashboard/page.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import DashboardStats from './DashboardStats';
|
| 6 |
+
import RecentAgents from './RecentAgents';
|
| 7 |
+
import ActivityFeed from './ActivityFeed';
|
| 8 |
+
import QuickActions from './QuickActions';
|
| 9 |
+
|
| 10 |
+
export default function DashboardPage() {
|
| 11 |
+
return (
|
| 12 |
+
<div className="min-h-screen bg-gray-50">
|
| 13 |
+
<Header />
|
| 14 |
+
<div className="flex">
|
| 15 |
+
<Sidebar />
|
| 16 |
+
<main className="flex-1 p-8">
|
| 17 |
+
<div className="max-w-7xl mx-auto">
|
| 18 |
+
<div className="mb-8">
|
| 19 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">工作台</h1>
|
| 20 |
+
<p className="text-gray-600">管理您的AI Agent应用和工作流</p>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<DashboardStats />
|
| 24 |
+
|
| 25 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
|
| 26 |
+
<div className="lg:col-span-2">
|
| 27 |
+
<RecentAgents />
|
| 28 |
+
</div>
|
| 29 |
+
<div>
|
| 30 |
+
<QuickActions />
|
| 31 |
+
<div className="mt-8">
|
| 32 |
+
<ActivityFeed />
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</main>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
);
|
| 41 |
+
}
|
app/globals.css
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.5.0/remixicon.min.css');
|
| 2 |
+
@tailwind base;
|
| 3 |
+
@tailwind components;
|
| 4 |
+
@tailwind utilities;
|
app/knowledge/KnowledgeFilters.tsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
export default function KnowledgeFilters() {
|
| 6 |
+
const [activeCategory, setActiveCategory] = useState('all');
|
| 7 |
+
const [activeStatus, setActiveStatus] = useState('all');
|
| 8 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 9 |
+
const [sortBy, setSortBy] = useState('lastUpdated');
|
| 10 |
+
|
| 11 |
+
const categories = [
|
| 12 |
+
{ key: 'all', label: '全部分类', count: 6 },
|
| 13 |
+
{ key: 'general', label: '通用', count: 1 },
|
| 14 |
+
{ key: 'tech', label: '技术', count: 1 },
|
| 15 |
+
{ key: 'product', label: '产品', count: 1 },
|
| 16 |
+
{ key: 'service', label: '客服', count: 1 },
|
| 17 |
+
{ key: 'legal', label: '法务', count: 1 },
|
| 18 |
+
{ key: 'sales', label: '销售', count: 1 }
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
const statuses = [
|
| 22 |
+
{ key: 'all', label: '全部状态' },
|
| 23 |
+
{ key: 'active', label: '运行中' },
|
| 24 |
+
{ key: 'inactive', label: '已停用' },
|
| 25 |
+
{ key: 'updating', label: '更新中' }
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
const sortOptions = [
|
| 29 |
+
{ key: 'lastUpdated', label: '最后更新' },
|
| 30 |
+
{ key: 'name', label: '名称' },
|
| 31 |
+
{ key: 'items', label: '内容数量' },
|
| 32 |
+
{ key: 'usage', label: '使用次数' },
|
| 33 |
+
{ key: 'size', label: '文件大小' }
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
|
| 38 |
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
| 39 |
+
{/* 搜索 */}
|
| 40 |
+
<div className="lg:col-span-1">
|
| 41 |
+
<div className="relative">
|
| 42 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 43 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 44 |
+
<i className="ri-search-line text-gray-400"></i>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
<input
|
| 48 |
+
type="text"
|
| 49 |
+
placeholder="搜索知识库..."
|
| 50 |
+
value={searchTerm}
|
| 51 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 52 |
+
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 53 |
+
/>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
{/* 分类筛选 */}
|
| 58 |
+
<div className="lg:col-span-1">
|
| 59 |
+
<select
|
| 60 |
+
value={activeCategory}
|
| 61 |
+
onChange={(e) => setActiveCategory(e.target.value)}
|
| 62 |
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white pr-8"
|
| 63 |
+
>
|
| 64 |
+
{categories.map((category) => (
|
| 65 |
+
<option key={category.key} value={category.key}>
|
| 66 |
+
{category.label} ({category.count})
|
| 67 |
+
</option>
|
| 68 |
+
))}
|
| 69 |
+
</select>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
{/* 状态筛选 */}
|
| 73 |
+
<div className="lg:col-span-1">
|
| 74 |
+
<select
|
| 75 |
+
value={activeStatus}
|
| 76 |
+
onChange={(e) => setActiveStatus(e.target.value)}
|
| 77 |
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white pr-8"
|
| 78 |
+
>
|
| 79 |
+
{statuses.map((status) => (
|
| 80 |
+
<option key={status.key} value={status.key}>
|
| 81 |
+
{status.label}
|
| 82 |
+
</option>
|
| 83 |
+
))}
|
| 84 |
+
</select>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
{/* 排序 */}
|
| 88 |
+
<div className="lg:col-span-1">
|
| 89 |
+
<select
|
| 90 |
+
value={sortBy}
|
| 91 |
+
onChange={(e) => setSortBy(e.target.value)}
|
| 92 |
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white pr-8"
|
| 93 |
+
>
|
| 94 |
+
{sortOptions.map((option) => (
|
| 95 |
+
<option key={option.key} value={option.key}>
|
| 96 |
+
按{option.label}排序
|
| 97 |
+
</option>
|
| 98 |
+
))}
|
| 99 |
+
</select>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
{/* 快速标签 */}
|
| 104 |
+
<div className="mt-4 pt-4 border-t border-gray-200">
|
| 105 |
+
<div className="flex items-center space-x-2">
|
| 106 |
+
<span className="text-sm text-gray-600">快速筛选:</span>
|
| 107 |
+
<div className="flex flex-wrap gap-2">
|
| 108 |
+
{['默认知识库', '高使用率', '最近更新', '大容量', '技术相关'].map((tag) => (
|
| 109 |
+
<button
|
| 110 |
+
key={tag}
|
| 111 |
+
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-gray-200 transition-colors cursor-pointer whitespace-nowrap"
|
| 112 |
+
>
|
| 113 |
+
{tag}
|
| 114 |
+
</button>
|
| 115 |
+
))}
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
);
|
| 121 |
+
}
|
app/knowledge/KnowledgeLibrary.tsx
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import Link from 'next/link';
|
| 5 |
+
|
| 6 |
+
export default function KnowledgeLibrary() {
|
| 7 |
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
| 8 |
+
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<string[]>([]);
|
| 9 |
+
|
| 10 |
+
const knowledgeBases = [
|
| 11 |
+
{
|
| 12 |
+
id: 'kb-001',
|
| 13 |
+
name: '通用知识库',
|
| 14 |
+
description: '包含基础常识和通用信息,适用于大多数场景的问答需求',
|
| 15 |
+
category: '通用',
|
| 16 |
+
items: 1240,
|
| 17 |
+
size: '15.6 MB',
|
| 18 |
+
status: 'active',
|
| 19 |
+
lastUpdated: '2024-01-15',
|
| 20 |
+
creator: '系统默认',
|
| 21 |
+
isDefault: true,
|
| 22 |
+
usage: 856,
|
| 23 |
+
tags: ['通用', '基础', '默认']
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
id: 'kb-002',
|
| 27 |
+
name: '技术文档库',
|
| 28 |
+
description: '收录了各种技术文档、API说明和开发指南,为技术问题提供专业解答',
|
| 29 |
+
category: '技术',
|
| 30 |
+
items: 567,
|
| 31 |
+
size: '8.3 MB',
|
| 32 |
+
status: 'active',
|
| 33 |
+
lastUpdated: '2024-01-14',
|
| 34 |
+
creator: '张工程师',
|
| 35 |
+
isDefault: false,
|
| 36 |
+
usage: 423,
|
| 37 |
+
tags: ['技术', 'API', '开发']
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: 'kb-003',
|
| 41 |
+
name: '产品说明库',
|
| 42 |
+
description: '产品功能介绍、使用手册和操作指南,帮助用户更好地了解产品特性',
|
| 43 |
+
category: '产品',
|
| 44 |
+
items: 892,
|
| 45 |
+
size: '12.1 MB',
|
| 46 |
+
status: 'active',
|
| 47 |
+
lastUpdated: '2024-01-13',
|
| 48 |
+
creator: '李产品经理',
|
| 49 |
+
isDefault: false,
|
| 50 |
+
usage: 678,
|
| 51 |
+
tags: ['产品', '手册', '功能']
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
id: 'kb-004',
|
| 55 |
+
name: '客服问答库',
|
| 56 |
+
description: '客服常见问题和标准解答,提升客户服务效率和质量',
|
| 57 |
+
category: '客服',
|
| 58 |
+
items: 345,
|
| 59 |
+
size: '4.2 MB',
|
| 60 |
+
status: 'active',
|
| 61 |
+
lastUpdated: '2024-01-12',
|
| 62 |
+
creator: '王客服主管',
|
| 63 |
+
isDefault: false,
|
| 64 |
+
usage: 234,
|
| 65 |
+
tags: ['客服', '问答', 'FAQ']
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
id: 'kb-005',
|
| 69 |
+
name: '法律法规库',
|
| 70 |
+
description: '相关法律条文、政策解读和合规要求,确保业务合规运营',
|
| 71 |
+
category: '法务',
|
| 72 |
+
items: 178,
|
| 73 |
+
size: '6.8 MB',
|
| 74 |
+
status: 'active',
|
| 75 |
+
lastUpdated: '2024-01-11',
|
| 76 |
+
creator: '赵法务',
|
| 77 |
+
isDefault: false,
|
| 78 |
+
usage: 89,
|
| 79 |
+
tags: ['法律', '合规', '政策']
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
id: 'kb-006',
|
| 83 |
+
name: '销售话术库',
|
| 84 |
+
description: '销售技巧、话术模板和客户案例,提升销售团队的专业水平',
|
| 85 |
+
category: '销售',
|
| 86 |
+
items: 456,
|
| 87 |
+
size: '5.9 MB',
|
| 88 |
+
status: 'inactive',
|
| 89 |
+
lastUpdated: '2024-01-10',
|
| 90 |
+
creator: '陈销售总监',
|
| 91 |
+
isDefault: false,
|
| 92 |
+
usage: 156,
|
| 93 |
+
tags: ['销售', '话术', '案例']
|
| 94 |
+
}
|
| 95 |
+
];
|
| 96 |
+
|
| 97 |
+
const toggleSelection = (id: string) => {
|
| 98 |
+
setSelectedKnowledgeBases(prev =>
|
| 99 |
+
prev.includes(id)
|
| 100 |
+
? prev.filter(kbId => kbId !== id)
|
| 101 |
+
: [...prev, id]
|
| 102 |
+
);
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const selectAll = () => {
|
| 106 |
+
setSelectedKnowledgeBases(knowledgeBases.map(kb => kb.id));
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
const clearSelection = () => {
|
| 110 |
+
setSelectedKnowledgeBases([]);
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
const getStatusColor = (status: string) => {
|
| 114 |
+
switch (status) {
|
| 115 |
+
case 'active':
|
| 116 |
+
return 'bg-green-100 text-green-800';
|
| 117 |
+
case 'inactive':
|
| 118 |
+
return 'bg-red-100 text-red-800';
|
| 119 |
+
case 'updating':
|
| 120 |
+
return 'bg-yellow-100 text-yellow-800';
|
| 121 |
+
default:
|
| 122 |
+
return 'bg-gray-100 text-gray-800';
|
| 123 |
+
}
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
const getStatusText = (status: string) => {
|
| 127 |
+
switch (status) {
|
| 128 |
+
case 'active':
|
| 129 |
+
return '运行中';
|
| 130 |
+
case 'inactive':
|
| 131 |
+
return '已停用';
|
| 132 |
+
case 'updating':
|
| 133 |
+
return '更新中';
|
| 134 |
+
default:
|
| 135 |
+
return '未知';
|
| 136 |
+
}
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
return (
|
| 140 |
+
<div className="space-y-6">
|
| 141 |
+
{/* 操作栏 */}
|
| 142 |
+
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
| 143 |
+
<div className="flex items-center justify-between">
|
| 144 |
+
<div className="flex items-center space-x-4">
|
| 145 |
+
<div className="flex items-center space-x-2">
|
| 146 |
+
<input
|
| 147 |
+
type="checkbox"
|
| 148 |
+
checked={selectedKnowledgeBases.length === knowledgeBases.length}
|
| 149 |
+
onChange={(e) => e.target.checked ? selectAll() : clearSelection()}
|
| 150 |
+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
| 151 |
+
/>
|
| 152 |
+
<span className="text-sm text-gray-600">
|
| 153 |
+
{selectedKnowledgeBases.length > 0
|
| 154 |
+
? `已选择 ${selectedKnowledgeBases.length} 个知识库`
|
| 155 |
+
: '全选'
|
| 156 |
+
}
|
| 157 |
+
</span>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
{selectedKnowledgeBases.length > 0 && (
|
| 161 |
+
<div className="flex items-center space-x-2">
|
| 162 |
+
<button className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors cursor-pointer whitespace-nowrap">
|
| 163 |
+
批量启用
|
| 164 |
+
</button>
|
| 165 |
+
<button className="px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors cursor-pointer whitespace-nowrap">
|
| 166 |
+
批量停用
|
| 167 |
+
</button>
|
| 168 |
+
<button className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors cursor-pointer whitespace-nowrap">
|
| 169 |
+
批量删除
|
| 170 |
+
</button>
|
| 171 |
+
</div>
|
| 172 |
+
)}
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<div className="flex items-center space-x-2">
|
| 176 |
+
<button
|
| 177 |
+
onClick={() => setViewMode('grid')}
|
| 178 |
+
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'} transition-colors cursor-pointer`}
|
| 179 |
+
>
|
| 180 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 181 |
+
<i className="ri-layout-grid-line"></i>
|
| 182 |
+
</div>
|
| 183 |
+
</button>
|
| 184 |
+
<button
|
| 185 |
+
onClick={() => setViewMode('list')}
|
| 186 |
+
className={`p-2 rounded ${viewMode === 'list' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'} transition-colors cursor-pointer`}
|
| 187 |
+
>
|
| 188 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 189 |
+
<i className="ri-list-unordered"></i>
|
| 190 |
+
</div>
|
| 191 |
+
</button>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
{/* 知识库列表 */}
|
| 197 |
+
{viewMode === 'grid' ? (
|
| 198 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 199 |
+
{knowledgeBases.map((kb) => (
|
| 200 |
+
<div key={kb.id} className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200">
|
| 201 |
+
<div className="p-6">
|
| 202 |
+
<div className="flex items-start justify-between mb-4">
|
| 203 |
+
<div className="flex items-center space-x-3">
|
| 204 |
+
<input
|
| 205 |
+
type="checkbox"
|
| 206 |
+
checked={selectedKnowledgeBases.includes(kb.id)}
|
| 207 |
+
onChange={() => toggleSelection(kb.id)}
|
| 208 |
+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
| 209 |
+
/>
|
| 210 |
+
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
| 211 |
+
<i className="ri-book-line text-xl text-white"></i>
|
| 212 |
+
</div>
|
| 213 |
+
<div className="flex-1">
|
| 214 |
+
<div className="flex items-center space-x-2">
|
| 215 |
+
<h3 className="font-semibold text-gray-900">{kb.name}</h3>
|
| 216 |
+
{kb.isDefault && (
|
| 217 |
+
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">默认</span>
|
| 218 |
+
)}
|
| 219 |
+
</div>
|
| 220 |
+
<span className={`inline-block px-2 py-1 text-xs rounded-full mt-1 ${getStatusColor(kb.status)}`}>
|
| 221 |
+
{getStatusText(kb.status)}
|
| 222 |
+
</span>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<div className="relative">
|
| 227 |
+
<button className="p-2 text-gray-400 hover:text-gray-600 transition-colors cursor-pointer">
|
| 228 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 229 |
+
<i className="ri-more-line"></i>
|
| 230 |
+
</div>
|
| 231 |
+
</button>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
|
| 235 |
+
<p className="text-gray-600 text-sm mb-4 line-clamp-3">{kb.description}</p>
|
| 236 |
+
|
| 237 |
+
<div className="space-y-3 mb-4">
|
| 238 |
+
<div className="flex items-center justify-between text-sm">
|
| 239 |
+
<span className="text-gray-500">内容条目</span>
|
| 240 |
+
<span className="font-medium text-gray-900">{kb.items.toLocaleString()}</span>
|
| 241 |
+
</div>
|
| 242 |
+
<div className="flex items-center justify-between text-sm">
|
| 243 |
+
<span className="text-gray-500">存储大小</span>
|
| 244 |
+
<span className="font-medium text-gray-900">{kb.size}</span>
|
| 245 |
+
</div>
|
| 246 |
+
<div className="flex items-center justify-between text-sm">
|
| 247 |
+
<span className="text-gray-500">使用次数</span>
|
| 248 |
+
<span className="font-medium text-gray-900">{kb.usage.toLocaleString()}</span>
|
| 249 |
+
</div>
|
| 250 |
+
<div className="flex items-center justify-between text-sm">
|
| 251 |
+
<span className="text-gray-500">最后更新</span>
|
| 252 |
+
<span className="font-medium text-gray-900">{kb.lastUpdated}</span>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
<div className="flex flex-wrap gap-1 mb-4">
|
| 257 |
+
{kb.tags.map((tag, index) => (
|
| 258 |
+
<span key={index} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
|
| 259 |
+
{tag}
|
| 260 |
+
</span>
|
| 261 |
+
))}
|
| 262 |
+
</div>
|
| 263 |
+
|
| 264 |
+
<div className="flex items-center justify-between pt-4 border-t border-gray-200">
|
| 265 |
+
<div className="flex items-center space-x-2">
|
| 266 |
+
<div className="w-6 h-6 bg-gray-200 rounded-full flex items-center justify-center">
|
| 267 |
+
<i className="ri-user-line text-xs text-gray-600"></i>
|
| 268 |
+
</div>
|
| 269 |
+
<span className="text-xs text-gray-500">{kb.creator}</span>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<div className="flex items-center space-x-2">
|
| 273 |
+
<Link
|
| 274 |
+
href={`/knowledge/${kb.id}`}
|
| 275 |
+
className="px-3 py-1 bg-blue-100 text-blue-700 text-xs rounded hover:bg-blue-200 transition-colors cursor-pointer whitespace-nowrap"
|
| 276 |
+
>
|
| 277 |
+
查看详情
|
| 278 |
+
</Link>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
))}
|
| 284 |
+
</div>
|
| 285 |
+
) : (
|
| 286 |
+
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
| 287 |
+
<div className="overflow-x-auto">
|
| 288 |
+
<table className="w-full">
|
| 289 |
+
<thead className="bg-gray-50 border-b border-gray-200">
|
| 290 |
+
<tr>
|
| 291 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 292 |
+
<input
|
| 293 |
+
type="checkbox"
|
| 294 |
+
checked={selectedKnowledgeBases.length === knowledgeBases.length}
|
| 295 |
+
onChange={(e) => e.target.checked ? selectAll() : clearSelection()}
|
| 296 |
+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
| 297 |
+
/>
|
| 298 |
+
</th>
|
| 299 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名称</th>
|
| 300 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分类</th>
|
| 301 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">条目数</th>
|
| 302 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">大小</th>
|
| 303 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">使用次数</th>
|
| 304 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
| 305 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最后更新</th>
|
| 306 |
+
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
| 307 |
+
</tr>
|
| 308 |
+
</thead>
|
| 309 |
+
<tbody className="bg-white divide-y divide-gray-200">
|
| 310 |
+
{knowledgeBases.map((kb) => (
|
| 311 |
+
<tr key={kb.id} className="hover:bg-gray-50 transition-colors">
|
| 312 |
+
<td className="px-6 py-4 whitespace-nowrap">
|
| 313 |
+
<input
|
| 314 |
+
type="checkbox"
|
| 315 |
+
checked={selectedKnowledgeBases.includes(kb.id)}
|
| 316 |
+
onChange={() => toggleSelection(kb.id)}
|
| 317 |
+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
| 318 |
+
/>
|
| 319 |
+
</td>
|
| 320 |
+
<td className="px-6 py-4 whitespace-nowrap">
|
| 321 |
+
<div className="flex items-center">
|
| 322 |
+
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center mr-3">
|
| 323 |
+
<i className="ri-book-line text-white"></i>
|
| 324 |
+
</div>
|
| 325 |
+
<div>
|
| 326 |
+
<div className="flex items-center space-x-2">
|
| 327 |
+
<div className="text-sm font-medium text-gray-900">{kb.name}</div>
|
| 328 |
+
{kb.isDefault && (
|
| 329 |
+
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">默认</span>
|
| 330 |
+
)}
|
| 331 |
+
</div>
|
| 332 |
+
<div className="text-sm text-gray-500 max-w-xs truncate">{kb.description}</div>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
</td>
|
| 336 |
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{kb.category}</td>
|
| 337 |
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{kb.items.toLocaleString()}</td>
|
| 338 |
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{kb.size}</td>
|
| 339 |
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{kb.usage.toLocaleString()}</td>
|
| 340 |
+
<td className="px-6 py-4 whitespace-nowrap">
|
| 341 |
+
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(kb.status)}`}>
|
| 342 |
+
{getStatusText(kb.status)}
|
| 343 |
+
</span>
|
| 344 |
+
</td>
|
| 345 |
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{kb.lastUpdated}</td>
|
| 346 |
+
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
| 347 |
+
<div className="flex items-center space-x-3">
|
| 348 |
+
<Link
|
| 349 |
+
href={`/knowledge/${kb.id}`}
|
| 350 |
+
className="text-blue-600 hover:text-blue-900 cursor-pointer"
|
| 351 |
+
>
|
| 352 |
+
查看
|
| 353 |
+
</Link>
|
| 354 |
+
<button className="text-gray-600 hover:text-gray-900 cursor-pointer">编辑</button>
|
| 355 |
+
<button className="text-red-600 hover:text-red-900 cursor-pointer">删除</button>
|
| 356 |
+
</div>
|
| 357 |
+
</td>
|
| 358 |
+
</tr>
|
| 359 |
+
))}
|
| 360 |
+
</tbody>
|
| 361 |
+
</table>
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
)}
|
| 365 |
+
|
| 366 |
+
{/* 分页 */}
|
| 367 |
+
<div className="flex items-center justify-between">
|
| 368 |
+
<div className="text-sm text-gray-500">
|
| 369 |
+
共 {knowledgeBases.length} 个知识库
|
| 370 |
+
</div>
|
| 371 |
+
<div className="flex items-center space-x-2">
|
| 372 |
+
<button className="px-3 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap text-sm">
|
| 373 |
+
上一页
|
| 374 |
+
</button>
|
| 375 |
+
<span className="px-3 py-2 bg-blue-600 text-white rounded-lg text-sm">1</span>
|
| 376 |
+
<button className="px-3 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap text-sm">
|
| 377 |
+
下一页
|
| 378 |
+
</button>
|
| 379 |
+
</div>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
);
|
| 383 |
+
}
|
app/knowledge/create/KnowledgeBuilder.tsx
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { useRouter } from 'next/navigation';
|
| 5 |
+
|
| 6 |
+
export default function KnowledgeBuilder() {
|
| 7 |
+
const router = useRouter();
|
| 8 |
+
const [currentStep, setCurrentStep] = useState(0);
|
| 9 |
+
const [knowledgeData, setKnowledgeData] = useState({
|
| 10 |
+
name: '',
|
| 11 |
+
description: '',
|
| 12 |
+
category: 'general',
|
| 13 |
+
isPublic: false,
|
| 14 |
+
tags: [],
|
| 15 |
+
files: [],
|
| 16 |
+
webSources: [],
|
| 17 |
+
manualContent: '',
|
| 18 |
+
processingSettings: {
|
| 19 |
+
autoChunk: true,
|
| 20 |
+
smartDedup: true,
|
| 21 |
+
generateSummary: false,
|
| 22 |
+
chunkSize: 1000,
|
| 23 |
+
overlap: 200
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
const [uploadedFiles, setUploadedFiles] = useState([]);
|
| 28 |
+
const [webSources, setWebSources] = useState([]);
|
| 29 |
+
const [newUrl, setNewUrl] = useState('');
|
| 30 |
+
const [activeTab, setActiveTab] = useState('files');
|
| 31 |
+
|
| 32 |
+
const steps = [
|
| 33 |
+
{ id: 0, name: '基本信息', icon: 'ri-information-line' },
|
| 34 |
+
{ id: 1, name: '内容添加', icon: 'ri-file-add-line' },
|
| 35 |
+
{ id: 2, name: '处理配置', icon: 'ri-settings-3-line' },
|
| 36 |
+
{ id: 3, name: '预览测试', icon: 'ri-eye-line' }
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
+
const categories = [
|
| 40 |
+
{ key: 'general', label: '通用知识', icon: 'ri-book-line' },
|
| 41 |
+
{ key: 'tech', label: '技术文档', icon: 'ri-code-line' },
|
| 42 |
+
{ key: 'product', label: '产品说明', icon: 'ri-product-hunt-line' },
|
| 43 |
+
{ key: 'service', label: '客服问答', icon: 'ri-customer-service-line' },
|
| 44 |
+
{ key: 'legal', label: '法务合规', icon: 'ri-scales-line' },
|
| 45 |
+
{ key: 'sales', label: '销售材料', icon: 'ri-line-chart-line' }
|
| 46 |
+
];
|
| 47 |
+
|
| 48 |
+
const handleNext = () => {
|
| 49 |
+
if (currentStep < steps.length - 1) {
|
| 50 |
+
setCurrentStep(currentStep + 1);
|
| 51 |
+
}
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
const handlePrev = () => {
|
| 55 |
+
if (currentStep > 0) {
|
| 56 |
+
setCurrentStep(currentStep - 1);
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const handleSave = () => {
|
| 61 |
+
console.log('保存知识库:', knowledgeData);
|
| 62 |
+
router.push('/knowledge');
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
const handlePublish = () => {
|
| 66 |
+
console.log('发布知识库:', knowledgeData);
|
| 67 |
+
router.push('/knowledge');
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
const handleFileUpload = (event) => {
|
| 71 |
+
const files = Array.from(event.target.files || []);
|
| 72 |
+
const newFiles = files.map((file, index) => ({
|
| 73 |
+
id: uploadedFiles.length + index + 1,
|
| 74 |
+
name: file.name,
|
| 75 |
+
size: `${(file.size / 1024 / 1024).toFixed(1)} MB`,
|
| 76 |
+
status: 'uploading',
|
| 77 |
+
type: file.name.split('.').pop() || 'unknown',
|
| 78 |
+
chunks: 0,
|
| 79 |
+
lastModified: new Date().toISOString().split('T')[0]
|
| 80 |
+
}));
|
| 81 |
+
|
| 82 |
+
setUploadedFiles([...uploadedFiles, ...newFiles]);
|
| 83 |
+
|
| 84 |
+
setTimeout(() => {
|
| 85 |
+
setUploadedFiles(prev => prev.map(file =>
|
| 86 |
+
newFiles.some(nf => nf.id === file.id)
|
| 87 |
+
? { ...file, status: 'uploaded', chunks: Math.floor(Math.random() * 50) + 10 }
|
| 88 |
+
: file
|
| 89 |
+
));
|
| 90 |
+
}, 2000);
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
const addWebSource = () => {
|
| 94 |
+
if (newUrl) {
|
| 95 |
+
const newSource = {
|
| 96 |
+
id: webSources.length + 1,
|
| 97 |
+
url: newUrl,
|
| 98 |
+
title: '正在获取标题...',
|
| 99 |
+
status: 'crawling',
|
| 100 |
+
pages: 0,
|
| 101 |
+
lastUpdate: new Date().toISOString().split('T')[0]
|
| 102 |
+
};
|
| 103 |
+
setWebSources([...webSources, newSource]);
|
| 104 |
+
setNewUrl('');
|
| 105 |
+
|
| 106 |
+
setTimeout(() => {
|
| 107 |
+
setWebSources(prev => prev.map(source =>
|
| 108 |
+
source.id === newSource.id
|
| 109 |
+
? { ...source, status: 'crawled', title: `${newUrl.split('/')[2]} - 文档`, pages: Math.floor(Math.random() * 20) + 5 }
|
| 110 |
+
: source
|
| 111 |
+
));
|
| 112 |
+
}, 3000);
|
| 113 |
+
}
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const renderStepContent = () => {
|
| 117 |
+
switch (currentStep) {
|
| 118 |
+
case 0:
|
| 119 |
+
return (
|
| 120 |
+
<div className="space-y-6">
|
| 121 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 122 |
+
<div className="space-y-6">
|
| 123 |
+
<div>
|
| 124 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">知识库名称</label>
|
| 125 |
+
<input
|
| 126 |
+
type="text"
|
| 127 |
+
placeholder="输入知识库名称..."
|
| 128 |
+
value={knowledgeData.name}
|
| 129 |
+
onChange={(e) => setKnowledgeData({...knowledgeData, name: e.target.value})}
|
| 130 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
| 131 |
+
/>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div>
|
| 135 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">描述说明</label>
|
| 136 |
+
<textarea
|
| 137 |
+
placeholder="描述知识库的用途和内容范围..."
|
| 138 |
+
rows={4}
|
| 139 |
+
maxLength={500}
|
| 140 |
+
value={knowledgeData.description}
|
| 141 |
+
onChange={(e) => setKnowledgeData({...knowledgeData, description: e.target.value})}
|
| 142 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
| 143 |
+
/>
|
| 144 |
+
<div className="text-xs text-gray-500 mt-1">{knowledgeData.description.length}/500 字符</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div>
|
| 148 |
+
<label className="block text-sm font-medium text-gray-700 mb-3">知识库分类</label>
|
| 149 |
+
<div className="grid grid-cols-2 gap-3">
|
| 150 |
+
{categories.map((category) => (
|
| 151 |
+
<div
|
| 152 |
+
key={category.key}
|
| 153 |
+
onClick={() => setKnowledgeData({...knowledgeData, category: category.key})}
|
| 154 |
+
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
| 155 |
+
knowledgeData.category === category.key
|
| 156 |
+
? 'border-blue-500 bg-blue-50'
|
| 157 |
+
: 'border-gray-200 hover:border-gray-300'
|
| 158 |
+
}`}
|
| 159 |
+
>
|
| 160 |
+
<div className="flex items-center space-x-3">
|
| 161 |
+
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 162 |
+
<i className={`${category.icon} text-blue-600`}></i>
|
| 163 |
+
</div>
|
| 164 |
+
<span className="font-medium text-gray-900">{category.label}</span>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
))}
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div className="space-y-6">
|
| 173 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
| 174 |
+
<div className="flex items-start space-x-3">
|
| 175 |
+
<div className="w-6 h-6 flex items-center justify-center mt-0.5">
|
| 176 |
+
<i className="ri-lightbulb-line text-blue-600"></i>
|
| 177 |
+
</div>
|
| 178 |
+
<div>
|
| 179 |
+
<h4 className="font-medium text-blue-900 mb-2">创建建议</h4>
|
| 180 |
+
<ul className="text-sm text-blue-800 space-y-1">
|
| 181 |
+
<li>• 选择描述性强的名称,便于后续查找</li>
|
| 182 |
+
<li>• 详细描述知识库的应用场景</li>
|
| 183 |
+
<li>• 根据内容类型选择合适的分类</li>
|
| 184 |
+
<li>• 考虑知识库的长期维护和更新</li>
|
| 185 |
+
</ul>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div className="border border-gray-200 rounded-lg p-6">
|
| 191 |
+
<h4 className="font-medium text-gray-900 mb-4">权限设置</h4>
|
| 192 |
+
<div className="space-y-4">
|
| 193 |
+
<div className="flex items-center justify-between">
|
| 194 |
+
<div>
|
| 195 |
+
<div className="font-medium text-gray-900">公开知识库</div>
|
| 196 |
+
<div className="text-sm text-gray-500">其他用户可以查看和使用此知识库</div>
|
| 197 |
+
</div>
|
| 198 |
+
<div
|
| 199 |
+
onClick={() => setKnowledgeData({...knowledgeData, isPublic: !knowledgeData.isPublic})}
|
| 200 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 201 |
+
knowledgeData.isPublic ? 'bg-blue-600' : 'bg-gray-300'
|
| 202 |
+
}`}
|
| 203 |
+
>
|
| 204 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 205 |
+
knowledgeData.isPublic ? 'right-1' : 'left-1'
|
| 206 |
+
}`}></div>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
);
|
| 215 |
+
|
| 216 |
+
case 1:
|
| 217 |
+
return (
|
| 218 |
+
<div className="space-y-6">
|
| 219 |
+
<div className="bg-white border border-gray-200 rounded-lg">
|
| 220 |
+
<div className="border-b border-gray-200">
|
| 221 |
+
<div className="flex space-x-0">
|
| 222 |
+
{[
|
| 223 |
+
{ key: 'files', label: '文件上传', icon: 'ri-file-line' },
|
| 224 |
+
{ key: 'web', label: '网页抓取', icon: 'ri-global-line' },
|
| 225 |
+
{ key: 'manual', label: '手动输入', icon: 'ri-edit-line' }
|
| 226 |
+
].map((tab) => (
|
| 227 |
+
<button
|
| 228 |
+
key={tab.key}
|
| 229 |
+
onClick={() => setActiveTab(tab.key)}
|
| 230 |
+
className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
| 231 |
+
activeTab === tab.key
|
| 232 |
+
? 'border-blue-500 text-blue-600 bg-blue-50'
|
| 233 |
+
: 'border-transparent text-gray-500 hover:text-gray-700'
|
| 234 |
+
}`}
|
| 235 |
+
>
|
| 236 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block mr-2">
|
| 237 |
+
<i className={tab.icon}></i>
|
| 238 |
+
</div>
|
| 239 |
+
{tab.label}
|
| 240 |
+
</button>
|
| 241 |
+
))}
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div className="p-6">
|
| 246 |
+
{activeTab === 'files' && (
|
| 247 |
+
<div className="space-y-4">
|
| 248 |
+
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
|
| 249 |
+
<div className="w-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-4">
|
| 250 |
+
<i className="ri-upload-cloud-line text-2xl text-gray-400"></i>
|
| 251 |
+
</div>
|
| 252 |
+
<p className="text-lg text-gray-600 mb-2">拖拽文件到此处,或者</p>
|
| 253 |
+
<label className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700 transition-colors">
|
| 254 |
+
选择文件
|
| 255 |
+
<input
|
| 256 |
+
type="file"
|
| 257 |
+
multiple
|
| 258 |
+
accept=".pdf,.doc,.docx,.txt,.md,.csv,.xlsx"
|
| 259 |
+
onChange={handleFileUpload}
|
| 260 |
+
className="hidden"
|
| 261 |
+
/>
|
| 262 |
+
</label>
|
| 263 |
+
<p className="text-sm text-gray-500 mt-4">支持 PDF、DOC、DOCX、TXT、MD、CSV、XLSX 格式,单文件最大 50MB</p>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
)}
|
| 267 |
+
|
| 268 |
+
{activeTab === 'web' && (
|
| 269 |
+
<div className="space-y-6">
|
| 270 |
+
<div className="flex space-x-3">
|
| 271 |
+
<input
|
| 272 |
+
type="url"
|
| 273 |
+
placeholder="输入要抓取的网址(如:https://docs.example.com)"
|
| 274 |
+
value={newUrl}
|
| 275 |
+
onChange={(e) => setNewUrl(e.target.value)}
|
| 276 |
+
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
| 277 |
+
/>
|
| 278 |
+
<button
|
| 279 |
+
onClick={addWebSource}
|
| 280 |
+
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 281 |
+
>
|
| 282 |
+
开始抓取
|
| 283 |
+
</button>
|
| 284 |
+
</div>
|
| 285 |
+
<div className="text-sm text-gray-500 space-y-1">
|
| 286 |
+
<p>• 支持抓取文档站点、博客文章、产品页面等</p>
|
| 287 |
+
<p>• 自动识别并提取页面文本内容</p>
|
| 288 |
+
<p>• 支持批量抓取同站点多个页面</p>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
)}
|
| 292 |
+
|
| 293 |
+
{activeTab === 'manual' && (
|
| 294 |
+
<div className="space-y-4">
|
| 295 |
+
<textarea
|
| 296 |
+
placeholder="直接输入要添加到知识库的内容... 您可以添加: • 产品使用说明和操作指南 • 常见问题及详细解答 • 专业术语和概念解释 • 业务流程和规范文档"
|
| 297 |
+
rows={12}
|
| 298 |
+
maxLength={500}
|
| 299 |
+
value={knowledgeData.manualContent}
|
| 300 |
+
onChange={(e) => setKnowledgeData({...knowledgeData, manualContent: e.target.value})}
|
| 301 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
| 302 |
+
/>
|
| 303 |
+
<div className="flex items-center justify-between">
|
| 304 |
+
<div className="text-sm text-gray-500">{knowledgeData.manualContent.length}/500 字符</div>
|
| 305 |
+
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 306 |
+
添加内容
|
| 307 |
+
</button>
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
)}
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
);
|
| 315 |
+
|
| 316 |
+
case 2:
|
| 317 |
+
return (
|
| 318 |
+
<div className="space-y-6">
|
| 319 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 320 |
+
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
| 321 |
+
<h4 className="font-medium text-gray-900 mb-4">处理选项</h4>
|
| 322 |
+
<div className="space-y-4">
|
| 323 |
+
<div className="flex items-center justify-between">
|
| 324 |
+
<div>
|
| 325 |
+
<span className="font-medium text-gray-900">自动分块处理</span>
|
| 326 |
+
<p className="text-sm text-gray-500">将长文档自动分割为合适大小的片段</p>
|
| 327 |
+
</div>
|
| 328 |
+
<div
|
| 329 |
+
onClick={() => setKnowledgeData(prev => ({
|
| 330 |
+
...prev,
|
| 331 |
+
processingSettings: {...prev.processingSettings, autoChunk: !prev.processingSettings.autoChunk}
|
| 332 |
+
}))}
|
| 333 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 334 |
+
knowledgeData.processingSettings.autoChunk ? 'bg-blue-600' : 'bg-gray-300'
|
| 335 |
+
}`}
|
| 336 |
+
>
|
| 337 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 338 |
+
knowledgeData.processingSettings.autoChunk ? 'right-1' : 'left-1'
|
| 339 |
+
}`}></div>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<div className="flex items-center justify-between">
|
| 344 |
+
<div>
|
| 345 |
+
<span className="font-medium text-gray-900">智能去重</span>
|
| 346 |
+
<p className="text-sm text-gray-500">自动识别并合并重复或相似内容</p>
|
| 347 |
+
</div>
|
| 348 |
+
<div
|
| 349 |
+
onClick={() => setKnowledgeData(prev => ({
|
| 350 |
+
...prev,
|
| 351 |
+
processingSettings: {...prev.processingSettings, smartDedup: !prev.processingSettings.smartDedup}
|
| 352 |
+
}))}
|
| 353 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 354 |
+
knowledgeData.processingSettings.smartDedup ? 'bg-blue-600' : 'bg-gray-300'
|
| 355 |
+
}`}
|
| 356 |
+
>
|
| 357 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 358 |
+
knowledgeData.processingSettings.smartDedup ? 'right-1' : 'left-1'
|
| 359 |
+
}`}></div>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<div className="flex items-center justify-between">
|
| 364 |
+
<div>
|
| 365 |
+
<span className="font-medium text-gray-900">生成摘要</span>
|
| 366 |
+
<p className="text-sm text-gray-500">为每个内容片段生成简要摘要</p>
|
| 367 |
+
</div>
|
| 368 |
+
<div
|
| 369 |
+
onClick={() => setKnowledgeData(prev => ({
|
| 370 |
+
...prev,
|
| 371 |
+
processingSettings: {...prev.processingSettings, generateSummary: !prev.processingSettings.generateSummary}
|
| 372 |
+
}))}
|
| 373 |
+
className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${
|
| 374 |
+
knowledgeData.processingSettings.generateSummary ? 'bg-blue-600' : 'bg-gray-300'
|
| 375 |
+
}`}
|
| 376 |
+
>
|
| 377 |
+
<div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${
|
| 378 |
+
knowledgeData.processingSettings.generateSummary ? 'right-1' : 'left-1'
|
| 379 |
+
}`}></div>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
| 386 |
+
<h4 className="font-medium text-gray-900 mb-4">参数设置</h4>
|
| 387 |
+
<div className="space-y-6">
|
| 388 |
+
<div>
|
| 389 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 390 |
+
分块大小: {knowledgeData.processingSettings.chunkSize} 字符
|
| 391 |
+
</label>
|
| 392 |
+
<input
|
| 393 |
+
type="range"
|
| 394 |
+
min="500"
|
| 395 |
+
max="2000"
|
| 396 |
+
step="100"
|
| 397 |
+
value={knowledgeData.processingSettings.chunkSize}
|
| 398 |
+
onChange={(e) => setKnowledgeData(prev => ({
|
| 399 |
+
...prev,
|
| 400 |
+
processingSettings: {...prev.processingSettings, chunkSize: parseInt(e.target.value)}
|
| 401 |
+
}))}
|
| 402 |
+
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
| 403 |
+
/>
|
| 404 |
+
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
| 405 |
+
<span>500</span>
|
| 406 |
+
<span>2000</span>
|
| 407 |
+
</div>
|
| 408 |
+
</div>
|
| 409 |
+
|
| 410 |
+
<div>
|
| 411 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 412 |
+
重叠字符: {knowledgeData.processingSettings.overlap}
|
| 413 |
+
</label>
|
| 414 |
+
<input
|
| 415 |
+
type="range"
|
| 416 |
+
min="0"
|
| 417 |
+
max="500"
|
| 418 |
+
step="50"
|
| 419 |
+
value={knowledgeData.processingSettings.overlap}
|
| 420 |
+
onChange={(e) => setKnowledgeData(prev => ({
|
| 421 |
+
...prev,
|
| 422 |
+
processingSettings: {...prev.processingSettings, overlap: parseInt(e.target.value)}
|
| 423 |
+
}))}
|
| 424 |
+
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
| 425 |
+
/>
|
| 426 |
+
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
| 427 |
+
<span>0</span>
|
| 428 |
+
<span>500</span>
|
| 429 |
+
</div>
|
| 430 |
+
</div>
|
| 431 |
+
</div>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
);
|
| 436 |
+
|
| 437 |
+
case 3:
|
| 438 |
+
return (
|
| 439 |
+
<div className="space-y-6">
|
| 440 |
+
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
| 441 |
+
<h4 className="font-medium text-gray-900 mb-4">知识库预览</h4>
|
| 442 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 443 |
+
<div className="space-y-4">
|
| 444 |
+
<div className="p-4 bg-gray-50 rounded-lg">
|
| 445 |
+
<div className="text-sm text-gray-600 mb-2">基本信息</div>
|
| 446 |
+
<div className="space-y-2">
|
| 447 |
+
<div><span className="font-medium">名称:</span>{knowledgeData.name || '未设置'}</div>
|
| 448 |
+
<div><span className="font-medium">分类:</span>{categories.find(c => c.key === knowledgeData.category)?.label}</div>
|
| 449 |
+
<div><span className="font-medium">描述:</span>{knowledgeData.description || '无描述'}</div>
|
| 450 |
+
</div>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
<div className="space-y-4">
|
| 455 |
+
<div className="p-4 bg-gray-50 rounded-lg">
|
| 456 |
+
<div className="text-sm text-gray-600 mb-2">内容统计</div>
|
| 457 |
+
<div className="space-y-2">
|
| 458 |
+
<div><span className="font-medium">文档数量:</span>{uploadedFiles.length} 个</div>
|
| 459 |
+
<div><span className="font-medium">网页来源:</span>{webSources.length} 个</div>
|
| 460 |
+
<div><span className="font-medium">总分块数:</span>{uploadedFiles.reduce((sum, file) => sum + file.chunks, 0)} 个</div>
|
| 461 |
+
</div>
|
| 462 |
+
</div>
|
| 463 |
+
</div>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
);
|
| 468 |
+
|
| 469 |
+
default:
|
| 470 |
+
return null;
|
| 471 |
+
}
|
| 472 |
+
};
|
| 473 |
+
|
| 474 |
+
return (
|
| 475 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 476 |
+
{/* 步骤导航 */}
|
| 477 |
+
<div className="p-6 border-b border-gray-200">
|
| 478 |
+
<div className="flex items-center justify-between">
|
| 479 |
+
{steps.map((step, index) => (
|
| 480 |
+
<div key={step.id} className="flex items-center">
|
| 481 |
+
<div className={`flex items-center space-x-3 px-4 py-2 rounded-lg cursor-pointer transition-colors ${
|
| 482 |
+
currentStep === index
|
| 483 |
+
? 'bg-blue-100 text-blue-700'
|
| 484 |
+
: currentStep > index
|
| 485 |
+
? 'bg-green-100 text-green-700'
|
| 486 |
+
: 'text-gray-500'
|
| 487 |
+
}`} onClick={() => setCurrentStep(index)}>
|
| 488 |
+
<div className="w-6 h-6 flex items-center justify-center">
|
| 489 |
+
<i className={step.icon}></i>
|
| 490 |
+
</div>
|
| 491 |
+
<span className="font-medium">{step.name}</span>
|
| 492 |
+
</div>
|
| 493 |
+
{index < steps.length - 1 && (
|
| 494 |
+
<div className="mx-4 w-8 h-px bg-gray-300"></div>
|
| 495 |
+
)}
|
| 496 |
+
</div>
|
| 497 |
+
))}
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
{/* 步骤内容 */}
|
| 502 |
+
<div className="p-8">
|
| 503 |
+
{renderStepContent()}
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
{/* 底部操作按钮 */}
|
| 507 |
+
<div className="p-6 border-t border-gray-200 flex items-center justify-between">
|
| 508 |
+
<div className="flex space-x-3">
|
| 509 |
+
{currentStep > 0 && (
|
| 510 |
+
<button
|
| 511 |
+
onClick={handlePrev}
|
| 512 |
+
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 513 |
+
>
|
| 514 |
+
上一步
|
| 515 |
+
</button>
|
| 516 |
+
)}
|
| 517 |
+
</div>
|
| 518 |
+
|
| 519 |
+
<div className="flex space-x-3">
|
| 520 |
+
<button
|
| 521 |
+
onClick={handleSave}
|
| 522 |
+
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 523 |
+
>
|
| 524 |
+
保存草稿
|
| 525 |
+
</button>
|
| 526 |
+
|
| 527 |
+
{currentStep < steps.length - 1 ? (
|
| 528 |
+
<button
|
| 529 |
+
onClick={handleNext}
|
| 530 |
+
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 531 |
+
>
|
| 532 |
+
下一步
|
| 533 |
+
</button>
|
| 534 |
+
) : (
|
| 535 |
+
<button
|
| 536 |
+
onClick={handlePublish}
|
| 537 |
+
className="bg-green-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-green-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 538 |
+
>
|
| 539 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 540 |
+
<i className="ri-check-line"></i>
|
| 541 |
+
</div>
|
| 542 |
+
创建知识库
|
| 543 |
+
</button>
|
| 544 |
+
)}
|
| 545 |
+
</div>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
);
|
| 549 |
+
}
|
app/knowledge/create/page.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import KnowledgeBuilder from './KnowledgeBuilder';
|
| 6 |
+
|
| 7 |
+
export default function CreateKnowledgePage() {
|
| 8 |
+
return (
|
| 9 |
+
<div className="min-h-screen bg-gray-50">
|
| 10 |
+
<Header />
|
| 11 |
+
<div className="flex">
|
| 12 |
+
<Sidebar />
|
| 13 |
+
<main className="flex-1 p-8">
|
| 14 |
+
<div className="max-w-7xl mx-auto">
|
| 15 |
+
<div className="mb-8">
|
| 16 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">创建知识库</h1>
|
| 17 |
+
<p className="text-gray-600">构建专业的知识体系,为AI提供准确的信息支持</p>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<KnowledgeBuilder />
|
| 21 |
+
</div>
|
| 22 |
+
</main>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
);
|
| 26 |
+
}
|
app/knowledge/page.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import KnowledgeLibrary from './KnowledgeLibrary';
|
| 6 |
+
import KnowledgeFilters from './KnowledgeFilters';
|
| 7 |
+
import Link from 'next/link';
|
| 8 |
+
|
| 9 |
+
export default function KnowledgePage() {
|
| 10 |
+
return (
|
| 11 |
+
<div className="min-h-screen bg-gray-50">
|
| 12 |
+
<Header />
|
| 13 |
+
<div className="flex">
|
| 14 |
+
<Sidebar />
|
| 15 |
+
<main className="flex-1 p-8">
|
| 16 |
+
<div className="max-w-7xl mx-auto">
|
| 17 |
+
<div className="mb-8">
|
| 18 |
+
<div className="flex items-center justify-between">
|
| 19 |
+
<div>
|
| 20 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">知识库管理</h1>
|
| 21 |
+
<p className="text-gray-600">管理和组织您的知识内容,为AI提供专业知识支持</p>
|
| 22 |
+
</div>
|
| 23 |
+
<div className="flex space-x-3">
|
| 24 |
+
<Link href="/knowledge/create" className="border border-gray-300 text-gray-700 px-6 py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 25 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 26 |
+
<i className="ri-upload-line"></i>
|
| 27 |
+
</div>
|
| 28 |
+
批量导入
|
| 29 |
+
</Link>
|
| 30 |
+
<Link href="/knowledge/create" className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 31 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 32 |
+
<i className="ri-add-line"></i>
|
| 33 |
+
</div>
|
| 34 |
+
新建知识库
|
| 35 |
+
</Link>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<KnowledgeFilters />
|
| 41 |
+
<KnowledgeLibrary />
|
| 42 |
+
</div>
|
| 43 |
+
</main>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
);
|
| 47 |
+
}
|
app/layout.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono, Pacifico } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const pacifico = Pacifico({
|
| 6 |
+
weight: '400',
|
| 7 |
+
subsets: ['latin'],
|
| 8 |
+
display: 'swap',
|
| 9 |
+
variable: '--font-pacifico',
|
| 10 |
+
})
|
| 11 |
+
|
| 12 |
+
const geistSans = Geist({
|
| 13 |
+
variable: "--font-geist-sans",
|
| 14 |
+
subsets: ["latin"],
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
const geistMono = Geist_Mono({
|
| 18 |
+
variable: "--font-geist-mono",
|
| 19 |
+
subsets: ["latin"],
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
export const metadata: Metadata = {
|
| 23 |
+
title: "Readdy Site",
|
| 24 |
+
description: "Generated by Readdy",
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export default function RootLayout({
|
| 28 |
+
children,
|
| 29 |
+
}: Readonly<{
|
| 30 |
+
children: React.ReactNode;
|
| 31 |
+
}>) {
|
| 32 |
+
return (
|
| 33 |
+
<html lang="en" suppressHydrationWarning={true}>
|
| 34 |
+
<body
|
| 35 |
+
className={`${geistSans.variable} ${geistMono.variable} ${pacifico.variable} antialiased`}
|
| 36 |
+
>
|
| 37 |
+
{children}
|
| 38 |
+
</body>
|
| 39 |
+
</html>
|
| 40 |
+
);
|
| 41 |
+
}
|
app/not-found.tsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function NotFound() {
|
| 2 |
+
return (
|
| 3 |
+
<div className="flex flex-col items-center justify-center h-screen text-center px-4">
|
| 4 |
+
<h1 className="text-5xl md:text-5xl font-semibold text-gray-100">404</h1>
|
| 5 |
+
<h1 className="text-2xl md:text-3xl font-semibold mt-6">This page has not been generated</h1>
|
| 6 |
+
<p className="mt-4 text-xl md:text-2xl text-gray-500">Tell me what you would like on this page</p>
|
| 7 |
+
</div>
|
| 8 |
+
);
|
| 9 |
+
}
|
app/page.tsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link';
|
| 4 |
+
|
| 5 |
+
export default function Home() {
|
| 6 |
+
return (
|
| 7 |
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
| 8 |
+
<div className="relative">
|
| 9 |
+
<div
|
| 10 |
+
className="absolute inset-0 bg-cover bg-center opacity-10"
|
| 11 |
+
style={{
|
| 12 |
+
backgroundImage: `url('https://readdy.ai/api/search-image?query=Modern%20AI%20technology%20workspace%20with%20holographic%20displays%2C%20futuristic%20interface%20elements%2C%20clean%20minimalist%20design%2C%20soft%20blue%20and%20purple%20lighting%2C%20high-tech%20environment%2C%20digital%20screens%20showing%20data%20visualizations%2C%20sleek%20modern%20architecture%2C%20professional%20technology%20atmosphere%2C%20ultra-modern%20setting&width=1920&height=1080&seq=hero-bg&orientation=landscape')`
|
| 13 |
+
}}
|
| 14 |
+
/>
|
| 15 |
+
|
| 16 |
+
<div className="relative z-10">
|
| 17 |
+
<header className="px-6 py-8">
|
| 18 |
+
<div className="max-w-7xl mx-auto">
|
| 19 |
+
<div className="flex items-center justify-between">
|
| 20 |
+
<div className="text-3xl font-bold text-blue-600" style={{ fontFamily: 'Pacifico, serif' }}>
|
| 21 |
+
AI Agent Studio
|
| 22 |
+
</div>
|
| 23 |
+
<div className="flex items-center space-x-6">
|
| 24 |
+
<Link href="/dashboard" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 25 |
+
产品介绍
|
| 26 |
+
</Link>
|
| 27 |
+
<Link href="/templates" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 28 |
+
模板市场
|
| 29 |
+
</Link>
|
| 30 |
+
<Link href="/dashboard" className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 31 |
+
立即体验
|
| 32 |
+
</Link>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</header>
|
| 37 |
+
|
| 38 |
+
<section className="px-6 py-20">
|
| 39 |
+
<div className="max-w-7xl mx-auto">
|
| 40 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 41 |
+
<div>
|
| 42 |
+
<h1 className="text-5xl font-bold text-gray-900 mb-6">
|
| 43 |
+
构建智能Agent
|
| 44 |
+
<br />
|
| 45 |
+
<span className="text-blue-600">无需编程</span>
|
| 46 |
+
</h1>
|
| 47 |
+
<p className="text-xl text-gray-600 mb-8 leading-relaxed">
|
| 48 |
+
通过可视化界面快速创建、部署和管理AI Agent应用。支持多种Agent类型,丰富的模板库,让人工智能触手可及。
|
| 49 |
+
</p>
|
| 50 |
+
<div className="flex items-center space-x-4">
|
| 51 |
+
<Link href="/dashboard" className="bg-blue-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 52 |
+
开始创建
|
| 53 |
+
</Link>
|
| 54 |
+
<Link href="/templates" className="border border-gray-300 text-gray-700 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 55 |
+
浏览模板
|
| 56 |
+
</Link>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div className="relative">
|
| 61 |
+
<img
|
| 62 |
+
src="https://readdy.ai/api/search-image?query=Professional%20AI%20dashboard%20interface%20mockup%20showing%20agent%20management%20panels%2C%20workflow%20diagrams%2C%20data%20analytics%20charts%2C%20modern%20UI%20design%20with%20blue%20accent%20colors%2C%20clean%20interface%20elements%2C%20desktop%20application%20screenshot%2C%20sophisticated%20technology%20platform%2C%20user-friendly%20design%2C%20comprehensive%20control%20panel%20layout&width=600&height=400&seq=hero-image&orientation=landscape"
|
| 63 |
+
alt="AI Agent Studio Platform"
|
| 64 |
+
className="rounded-xl shadow-2xl object-cover w-full h-96"
|
| 65 |
+
/>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</section>
|
| 70 |
+
|
| 71 |
+
<section className="px-6 py-20 bg-white/80 backdrop-blur-sm">
|
| 72 |
+
<div className="max-w-7xl mx-auto">
|
| 73 |
+
<div className="text-center mb-16">
|
| 74 |
+
<h2 className="text-3xl font-bold text-gray-900 mb-4">强大的功能特性</h2>
|
| 75 |
+
<p className="text-xl text-gray-600">一站式AI Agent开发平台</p>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 79 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border border-gray-200">
|
| 80 |
+
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mb-6">
|
| 81 |
+
<i className="ri-robot-line text-2xl text-blue-600"></i>
|
| 82 |
+
</div>
|
| 83 |
+
<h3 className="text-xl font-semibold text-gray-900 mb-4">可视化Agent构建</h3>
|
| 84 |
+
<p className="text-gray-600">拖拽式界面设计,无需编程知识即可创建复杂的AI Agent应用</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border border-gray-200">
|
| 88 |
+
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mb-6">
|
| 89 |
+
<i className="ri-flow-chart text-2xl text-green-600"></i>
|
| 90 |
+
</div>
|
| 91 |
+
<h3 className="text-xl font-semibold text-gray-900 mb-4">智能工作流</h3>
|
| 92 |
+
<p className="text-gray-600">设计复杂的自动化工作流,实现多Agent协同工作</p>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border border-gray-200">
|
| 96 |
+
<div className="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mb-6">
|
| 97 |
+
<i className="ri-database-line text-2xl text-purple-600"></i>
|
| 98 |
+
</div>
|
| 99 |
+
<h3 className="text-xl font-semibold text-gray-900 mb-4">知识库管理</h3>
|
| 100 |
+
<p className="text-gray-600">上传文档、数据源,构建专业的知识库为Agent提供支持</p>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border border-gray-200">
|
| 104 |
+
<div className="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center mb-6">
|
| 105 |
+
<i className="ri-store-line text-2xl text-orange-600"></i>
|
| 106 |
+
</div>
|
| 107 |
+
<h3 className="text-xl font-semibold text-gray-900 mb-4">模板市场</h3>
|
| 108 |
+
<p className="text-gray-600">丰富的预设模板库,快速启动各种场景的Agent应用</p>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border border-gray-200">
|
| 112 |
+
<div className="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center mb-6">
|
| 113 |
+
<i className="ri-bar-chart-line text-2xl text-red-600"></i>
|
| 114 |
+
</div>
|
| 115 |
+
<h3 className="text-xl font-semibold text-gray-900 mb-4">数据分析</h3>
|
| 116 |
+
<p className="text-gray-600">实时监控Agent性能,深入分析使用数据和效果指标</p>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border border-gray-200">
|
| 120 |
+
<div className="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center mb-6">
|
| 121 |
+
<i className="ri-cloud-line text-2xl text-indigo-600"></i>
|
| 122 |
+
</div>
|
| 123 |
+
<h3 className="text-xl font-semibold text-gray-900 mb-4">云端部署</h3>
|
| 124 |
+
<p className="text-gray-600">一键部署到云端,支持高并发访问和弹性扩展</p>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</section>
|
| 129 |
+
|
| 130 |
+
<section className="px-6 py-20">
|
| 131 |
+
<div className="max-w-4xl mx-auto text-center">
|
| 132 |
+
<h2 className="text-3xl font-bold text-gray-900 mb-8">准备开始您的AI之旅?</h2>
|
| 133 |
+
<p className="text-xl text-gray-600 mb-8">
|
| 134 |
+
立即体验AI Agent Studio,无需编程经验即可构建强大的智能应用
|
| 135 |
+
</p>
|
| 136 |
+
<Link href="/dashboard" className="bg-blue-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 137 |
+
免费开始使用
|
| 138 |
+
</Link>
|
| 139 |
+
</div>
|
| 140 |
+
</section>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
);
|
| 145 |
+
}
|
app/workflows/WorkflowFilters.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
export default function WorkflowFilters() {
|
| 6 |
+
const [activeFilter, setActiveFilter] = useState('all');
|
| 7 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 8 |
+
|
| 9 |
+
const filters = [
|
| 10 |
+
{ key: 'all', label: '全部', count: 32 },
|
| 11 |
+
{ key: 'active', label: '运行中', count: 24 },
|
| 12 |
+
{ key: 'paused', label: '暂停', count: 5 },
|
| 13 |
+
{ key: 'draft', label: '草稿', count: 3 }
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
const categories = [
|
| 17 |
+
{ key: 'all', label: '全部类型' },
|
| 18 |
+
{ key: 'data-processing', label: '数据处理' },
|
| 19 |
+
{ key: 'content-creation', label: '内容创作' },
|
| 20 |
+
{ key: 'automation', label: '任务自动化' },
|
| 21 |
+
{ key: 'integration', label: '系统集成' }
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
| 26 |
+
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
| 27 |
+
<div className="flex items-center space-x-4">
|
| 28 |
+
{filters.map((filter) => (
|
| 29 |
+
<button
|
| 30 |
+
key={filter.key}
|
| 31 |
+
onClick={() => setActiveFilter(filter.key)}
|
| 32 |
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors cursor-pointer whitespace-nowrap ${
|
| 33 |
+
activeFilter === filter.key
|
| 34 |
+
? 'bg-blue-100 text-blue-700'
|
| 35 |
+
: 'text-gray-600 hover:bg-gray-100'
|
| 36 |
+
}`}
|
| 37 |
+
>
|
| 38 |
+
{filter.label} ({filter.count})
|
| 39 |
+
</button>
|
| 40 |
+
))}
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div className="flex items-center space-x-4">
|
| 44 |
+
<div className="relative">
|
| 45 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 46 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 47 |
+
<i className="ri-search-line text-gray-400"></i>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<input
|
| 51 |
+
type="text"
|
| 52 |
+
placeholder="搜索工作流..."
|
| 53 |
+
value={searchTerm}
|
| 54 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 55 |
+
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 56 |
+
/>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div className="relative">
|
| 60 |
+
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 61 |
+
类型筛选
|
| 62 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block ml-2">
|
| 63 |
+
<i className="ri-arrow-down-s-line"></i>
|
| 64 |
+
</div>
|
| 65 |
+
</button>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div className="relative">
|
| 69 |
+
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 70 |
+
执行状态
|
| 71 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block ml-2">
|
| 72 |
+
<i className="ri-arrow-down-s-line"></i>
|
| 73 |
+
</div>
|
| 74 |
+
</button>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
);
|
| 80 |
+
}
|
app/workflows/WorkflowList.tsx
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import Link from 'next/link';
|
| 5 |
+
|
| 6 |
+
export default function WorkflowList() {
|
| 7 |
+
const [selectedWorkflows, setSelectedWorkflows] = useState<number[]>([]);
|
| 8 |
+
|
| 9 |
+
const workflows = [
|
| 10 |
+
{
|
| 11 |
+
id: 1,
|
| 12 |
+
name: '客户数据处理流程',
|
| 13 |
+
description: '自动处理客户数据,包括清洗、分类和分析',
|
| 14 |
+
category: '数据处理',
|
| 15 |
+
status: '运行中',
|
| 16 |
+
version: 'v3.2',
|
| 17 |
+
created: '2024-01-20',
|
| 18 |
+
lastRun: '2分钟前',
|
| 19 |
+
executions: 1245,
|
| 20 |
+
successRate: 98.5,
|
| 21 |
+
avgRuntime: '3.2分钟',
|
| 22 |
+
icon: '📊',
|
| 23 |
+
tags: ['数据清洗', 'ETL', '自动化'],
|
| 24 |
+
nodes: 8,
|
| 25 |
+
triggers: ['定时触发', 'API调用']
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
id: 2,
|
| 29 |
+
name: '内容生成与发布',
|
| 30 |
+
description: '根据关键词自动生成文章并发布到多个平台',
|
| 31 |
+
category: '内容创作',
|
| 32 |
+
status: '运行中',
|
| 33 |
+
version: 'v2.1',
|
| 34 |
+
created: '2024-01-18',
|
| 35 |
+
lastRun: '15分钟前',
|
| 36 |
+
executions: 892,
|
| 37 |
+
successRate: 96.2,
|
| 38 |
+
avgRuntime: '5.8分钟',
|
| 39 |
+
icon: '✍️',
|
| 40 |
+
tags: ['内容生成', 'SEO', '多平台'],
|
| 41 |
+
nodes: 12,
|
| 42 |
+
triggers: ['手动触发', '定时触发']
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
id: 3,
|
| 46 |
+
name: '邮件智能分类回复',
|
| 47 |
+
description: '自动分类邮件并生成智能回复',
|
| 48 |
+
category: '任务自动化',
|
| 49 |
+
status: '暂停',
|
| 50 |
+
version: 'v1.8',
|
| 51 |
+
created: '2024-01-15',
|
| 52 |
+
lastRun: '2小时前',
|
| 53 |
+
executions: 2156,
|
| 54 |
+
successRate: 94.8,
|
| 55 |
+
avgRuntime: '1.5分钟',
|
| 56 |
+
icon: '📧',
|
| 57 |
+
tags: ['邮件处理', 'NLP', '自动回复'],
|
| 58 |
+
nodes: 6,
|
| 59 |
+
triggers: ['邮件触发', '定时检查']
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
id: 4,
|
| 63 |
+
name: '订单处理自动化',
|
| 64 |
+
description: '从下单到发货的完整自动化处理流程',
|
| 65 |
+
category: '系统集成',
|
| 66 |
+
status: '运行中',
|
| 67 |
+
version: 'v4.0',
|
| 68 |
+
created: '2024-01-12',
|
| 69 |
+
lastRun: '30分钟前',
|
| 70 |
+
executions: 3245,
|
| 71 |
+
successRate: 99.1,
|
| 72 |
+
avgRuntime: '2.1分钟',
|
| 73 |
+
icon: '📦',
|
| 74 |
+
tags: ['订单处理', 'ERP集成', '库存管理'],
|
| 75 |
+
nodes: 15,
|
| 76 |
+
triggers: ['订单事件', 'Webhook']
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
id: 5,
|
| 80 |
+
name: '社交媒体监控',
|
| 81 |
+
description: '监控品牌提及并自动生成报告',
|
| 82 |
+
category: '数据处理',
|
| 83 |
+
status: '运行中',
|
| 84 |
+
version: 'v2.5',
|
| 85 |
+
created: '2024-01-10',
|
| 86 |
+
lastRun: '1小时前',
|
| 87 |
+
executions: 678,
|
| 88 |
+
successRate: 97.3,
|
| 89 |
+
avgRuntime: '4.2分钟',
|
| 90 |
+
icon: '📱',
|
| 91 |
+
tags: ['社媒监控', '情感分析', '报告生成'],
|
| 92 |
+
nodes: 10,
|
| 93 |
+
triggers: ['定时触发', '关键词匹配']
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
id: 6,
|
| 97 |
+
name: '财务报表生成',
|
| 98 |
+
description: '自动收集财务数据并生成可视化报表',
|
| 99 |
+
category: '数据处理',
|
| 100 |
+
status: '运行中',
|
| 101 |
+
version: 'v1.9',
|
| 102 |
+
created: '2024-01-08',
|
| 103 |
+
lastRun: '45分钟前',
|
| 104 |
+
executions: 456,
|
| 105 |
+
successRate: 99.8,
|
| 106 |
+
avgRuntime: '6.5分钟',
|
| 107 |
+
icon: '💰',
|
| 108 |
+
tags: ['财务分析', '数据可视化', 'BI'],
|
| 109 |
+
nodes: 11,
|
| 110 |
+
triggers: ['定时触发', '手动执行']
|
| 111 |
+
}
|
| 112 |
+
];
|
| 113 |
+
|
| 114 |
+
const handleSelectWorkflow = (workflowId: number) => {
|
| 115 |
+
setSelectedWorkflows(prev =>
|
| 116 |
+
prev.includes(workflowId)
|
| 117 |
+
? prev.filter(id => id !== workflowId)
|
| 118 |
+
: [...prev, workflowId]
|
| 119 |
+
);
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
const handleSelectAll = () => {
|
| 123 |
+
setSelectedWorkflows(selectedWorkflows.length === workflows.length ? [] : workflows.map(w => w.id));
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
return (
|
| 127 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 128 |
+
<div className="p-6 border-b border-gray-200">
|
| 129 |
+
<div className="flex items-center justify-between">
|
| 130 |
+
<div className="flex items-center space-x-4">
|
| 131 |
+
<input
|
| 132 |
+
type="checkbox"
|
| 133 |
+
checked={selectedWorkflows.length === workflows.length}
|
| 134 |
+
onChange={handleSelectAll}
|
| 135 |
+
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
|
| 136 |
+
/>
|
| 137 |
+
<span className="text-sm text-gray-600">
|
| 138 |
+
{selectedWorkflows.length > 0 ? `已选择 ${selectedWorkflows.length} 个工作流` : '全选'}
|
| 139 |
+
</span>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
{selectedWorkflows.length > 0 && (
|
| 143 |
+
<div className="flex items-center space-x-3">
|
| 144 |
+
<button className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer whitespace-nowrap">
|
| 145 |
+
批量启动
|
| 146 |
+
</button>
|
| 147 |
+
<button className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer whitespace-nowrap">
|
| 148 |
+
批量暂停
|
| 149 |
+
</button>
|
| 150 |
+
<button className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer whitespace-nowrap">
|
| 151 |
+
批量删除
|
| 152 |
+
</button>
|
| 153 |
+
</div>
|
| 154 |
+
)}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div className="divide-y divide-gray-200">
|
| 159 |
+
{workflows.map((workflow) => (
|
| 160 |
+
<div key={workflow.id} className="p-6 hover:bg-gray-50 transition-colors">
|
| 161 |
+
<div className="flex items-start justify-between">
|
| 162 |
+
<div className="flex items-start space-x-4 flex-1">
|
| 163 |
+
<input
|
| 164 |
+
type="checkbox"
|
| 165 |
+
checked={selectedWorkflows.includes(workflow.id)}
|
| 166 |
+
onChange={() => handleSelectWorkflow(workflow.id)}
|
| 167 |
+
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 mt-1"
|
| 168 |
+
/>
|
| 169 |
+
|
| 170 |
+
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
| 171 |
+
<span className="text-xl">{workflow.icon}</span>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<div className="flex-1">
|
| 175 |
+
<div className="flex items-center space-x-3 mb-2">
|
| 176 |
+
<h3 className="text-lg font-semibold text-gray-900">{workflow.name}</h3>
|
| 177 |
+
<span className="text-sm text-gray-500">{workflow.version}</span>
|
| 178 |
+
<span className={`px-2 py-1 text-xs rounded-full ${
|
| 179 |
+
workflow.status === '运行中'
|
| 180 |
+
? 'bg-green-100 text-green-800'
|
| 181 |
+
: 'bg-yellow-100 text-yellow-800'
|
| 182 |
+
}`}>
|
| 183 |
+
{workflow.status}
|
| 184 |
+
</span>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<p className="text-gray-600 mb-3">{workflow.description}</p>
|
| 188 |
+
|
| 189 |
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-3">
|
| 190 |
+
<div>
|
| 191 |
+
<span className="text-sm text-gray-500">类型</span>
|
| 192 |
+
<p className="text-sm font-medium text-gray-900">{workflow.category}</p>
|
| 193 |
+
</div>
|
| 194 |
+
<div>
|
| 195 |
+
<span className="text-sm text-gray-500">节点数</span>
|
| 196 |
+
<p className="text-sm font-medium text-gray-900">{workflow.nodes} 个</p>
|
| 197 |
+
</div>
|
| 198 |
+
<div>
|
| 199 |
+
<span className="text-sm text-gray-500">成功率</span>
|
| 200 |
+
<p className="text-sm font-medium text-green-600">{workflow.successRate}%</p>
|
| 201 |
+
</div>
|
| 202 |
+
<div>
|
| 203 |
+
<span className="text-sm text-gray-500">平均运行时间</span>
|
| 204 |
+
<p className="text-sm font-medium text-gray-900">{workflow.avgRuntime}</p>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div className="flex items-center space-x-4 mb-3">
|
| 209 |
+
<span className="text-sm text-gray-500">执行次数: {workflow.executions.toLocaleString()}</span>
|
| 210 |
+
<span className="text-sm text-gray-500">最后运行: {workflow.lastRun}</span>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div className="flex items-center space-x-2 mb-3">
|
| 214 |
+
<span className="text-sm text-gray-500">触发方式:</span>
|
| 215 |
+
{workflow.triggers.map((trigger, index) => (
|
| 216 |
+
<span key={index} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full">
|
| 217 |
+
{trigger}
|
| 218 |
+
</span>
|
| 219 |
+
))}
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<div className="flex items-center space-x-2">
|
| 223 |
+
{workflow.tags.map((tag, index) => (
|
| 224 |
+
<span key={index} className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-full">
|
| 225 |
+
{tag}
|
| 226 |
+
</span>
|
| 227 |
+
))}
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<div className="flex items-center space-x-3">
|
| 233 |
+
<Link href={`/workflows/${workflow.id}/analytics`} className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 234 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 235 |
+
<i className="ri-bar-chart-line"></i>
|
| 236 |
+
</div>
|
| 237 |
+
</Link>
|
| 238 |
+
<button className="p-2 text-gray-400 hover:text-green-600 transition-colors cursor-pointer">
|
| 239 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 240 |
+
<i className="ri-play-line"></i>
|
| 241 |
+
</div>
|
| 242 |
+
</button>
|
| 243 |
+
<Link href={`/workflows/${workflow.id}/edit`} className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 244 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 245 |
+
<i className="ri-edit-line"></i>
|
| 246 |
+
</div>
|
| 247 |
+
</Link>
|
| 248 |
+
<button className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 249 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 250 |
+
<i className="ri-settings-line"></i>
|
| 251 |
+
</div>
|
| 252 |
+
</button>
|
| 253 |
+
<button className="p-2 text-gray-400 hover:text-red-600 transition-colors cursor-pointer">
|
| 254 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 255 |
+
<i className="ri-delete-bin-line"></i>
|
| 256 |
+
</div>
|
| 257 |
+
</button>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
))}
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
);
|
| 265 |
+
}
|
app/workflows/[id]/analytics/ExecutionHistory.tsx
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
interface ExecutionHistoryProps {
|
| 4 |
+
workflowId: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export default function ExecutionHistory({ workflowId }: ExecutionHistoryProps) {
|
| 8 |
+
const executions = [
|
| 9 |
+
{
|
| 10 |
+
id: 1,
|
| 11 |
+
startTime: '2024-01-20 14:32:15',
|
| 12 |
+
endTime: '2024-01-20 14:35:28',
|
| 13 |
+
duration: '3分13秒',
|
| 14 |
+
status: 'success',
|
| 15 |
+
trigger: '定时触发',
|
| 16 |
+
processedRecords: 156,
|
| 17 |
+
errors: 0
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
id: 2,
|
| 21 |
+
startTime: '2024-01-20 14:02:45',
|
| 22 |
+
endTime: '2024-01-20 14:05:52',
|
| 23 |
+
duration: '3分7秒',
|
| 24 |
+
status: 'success',
|
| 25 |
+
trigger: 'API调用',
|
| 26 |
+
processedRecords: 142,
|
| 27 |
+
errors: 0
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
id: 3,
|
| 31 |
+
startTime: '2024-01-20 13:32:10',
|
| 32 |
+
endTime: '2024-01-20 13:35:45',
|
| 33 |
+
duration: '3分35秒',
|
| 34 |
+
status: 'warning',
|
| 35 |
+
trigger: '定时触发',
|
| 36 |
+
processedRecords: 134,
|
| 37 |
+
errors: 2
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: 4,
|
| 41 |
+
startTime: '2024-01-20 13:02:38',
|
| 42 |
+
endTime: '2024-01-20 13:05:12',
|
| 43 |
+
duration: '2分34秒',
|
| 44 |
+
status: 'success',
|
| 45 |
+
trigger: '手动执行',
|
| 46 |
+
processedRecords: 98,
|
| 47 |
+
errors: 0
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
id: 5,
|
| 51 |
+
startTime: '2024-01-20 12:32:22',
|
| 52 |
+
endTime: '2024-01-20 12:38:15',
|
| 53 |
+
duration: '5分53秒',
|
| 54 |
+
status: 'error',
|
| 55 |
+
trigger: '定时触发',
|
| 56 |
+
processedRecords: 0,
|
| 57 |
+
errors: 1
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: 6,
|
| 61 |
+
startTime: '2024-01-20 12:02:01',
|
| 62 |
+
endTime: '2024-01-20 12:04:58',
|
| 63 |
+
duration: '2分57秒',
|
| 64 |
+
status: 'success',
|
| 65 |
+
trigger: 'API调用',
|
| 66 |
+
processedRecords: 189,
|
| 67 |
+
errors: 0
|
| 68 |
+
}
|
| 69 |
+
];
|
| 70 |
+
|
| 71 |
+
const getStatusColor = (status: string) => {
|
| 72 |
+
switch (status) {
|
| 73 |
+
case 'success':
|
| 74 |
+
return 'bg-green-100 text-green-800';
|
| 75 |
+
case 'warning':
|
| 76 |
+
return 'bg-yellow-100 text-yellow-800';
|
| 77 |
+
case 'error':
|
| 78 |
+
return 'bg-red-100 text-red-800';
|
| 79 |
+
case 'running':
|
| 80 |
+
return 'bg-blue-100 text-blue-800';
|
| 81 |
+
default:
|
| 82 |
+
return 'bg-gray-100 text-gray-800';
|
| 83 |
+
}
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
const getStatusText = (status: string) => {
|
| 87 |
+
switch (status) {
|
| 88 |
+
case 'success':
|
| 89 |
+
return '成功';
|
| 90 |
+
case 'warning':
|
| 91 |
+
return '警告';
|
| 92 |
+
case 'error':
|
| 93 |
+
return '失败';
|
| 94 |
+
case 'running':
|
| 95 |
+
return '运行中';
|
| 96 |
+
default:
|
| 97 |
+
return '未知';
|
| 98 |
+
}
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
const getStatusIcon = (status: string) => {
|
| 102 |
+
switch (status) {
|
| 103 |
+
case 'success':
|
| 104 |
+
return 'ri-check-line';
|
| 105 |
+
case 'warning':
|
| 106 |
+
return 'ri-alert-line';
|
| 107 |
+
case 'error':
|
| 108 |
+
return 'ri-close-line';
|
| 109 |
+
case 'running':
|
| 110 |
+
return 'ri-loader-line';
|
| 111 |
+
default:
|
| 112 |
+
return 'ri-question-line';
|
| 113 |
+
}
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
return (
|
| 117 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 118 |
+
<div className="p-6 border-b border-gray-200">
|
| 119 |
+
<div className="flex items-center justify-between">
|
| 120 |
+
<h3 className="text-lg font-semibold text-gray-900">执行历史</h3>
|
| 121 |
+
<div className="flex items-center space-x-3">
|
| 122 |
+
<button className="text-sm text-gray-600 hover:text-gray-900 cursor-pointer">
|
| 123 |
+
导出日志
|
| 124 |
+
</button>
|
| 125 |
+
<button className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer">
|
| 126 |
+
刷新
|
| 127 |
+
</button>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<div className="overflow-x-auto">
|
| 133 |
+
<table className="w-full">
|
| 134 |
+
<thead className="bg-gray-50">
|
| 135 |
+
<tr>
|
| 136 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 137 |
+
执行时间
|
| 138 |
+
</th>
|
| 139 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 140 |
+
状态
|
| 141 |
+
</th>
|
| 142 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 143 |
+
触发方式
|
| 144 |
+
</th>
|
| 145 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 146 |
+
执行时长
|
| 147 |
+
</th>
|
| 148 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 149 |
+
处理记录
|
| 150 |
+
</th>
|
| 151 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 152 |
+
错误数
|
| 153 |
+
</th>
|
| 154 |
+
<th className="text-left py-3 px-6 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 155 |
+
操作
|
| 156 |
+
</th>
|
| 157 |
+
</tr>
|
| 158 |
+
</thead>
|
| 159 |
+
<tbody className="bg-white divide-y divide-gray-200">
|
| 160 |
+
{executions.map((execution) => (
|
| 161 |
+
<tr key={execution.id} className="hover:bg-gray-50">
|
| 162 |
+
<td className="py-4 px-6">
|
| 163 |
+
<div>
|
| 164 |
+
<p className="text-sm font-medium text-gray-900">{execution.startTime}</p>
|
| 165 |
+
<p className="text-xs text-gray-500">至 {execution.endTime.split(' ')[1]}</p>
|
| 166 |
+
</div>
|
| 167 |
+
</td>
|
| 168 |
+
<td className="py-4 px-6">
|
| 169 |
+
<div className="flex items-center space-x-2">
|
| 170 |
+
<div className={`flex items-center space-x-1 px-2 py-1 text-xs rounded-full ${getStatusColor(execution.status)}`}>
|
| 171 |
+
<i className={`${getStatusIcon(execution.status)} text-xs`}></i>
|
| 172 |
+
<span>{getStatusText(execution.status)}</span>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
</td>
|
| 176 |
+
<td className="py-4 px-6">
|
| 177 |
+
<span className="text-sm text-gray-900">{execution.trigger}</span>
|
| 178 |
+
</td>
|
| 179 |
+
<td className="py-4 px-6">
|
| 180 |
+
<span className="text-sm text-gray-900">{execution.duration}</span>
|
| 181 |
+
</td>
|
| 182 |
+
<td className="py-4 px-6">
|
| 183 |
+
<span className="text-sm text-gray-900">{execution.processedRecords.toLocaleString()}</span>
|
| 184 |
+
</td>
|
| 185 |
+
<td className="py-4 px-6">
|
| 186 |
+
<span className={`text-sm ${execution.errors > 0 ? 'text-red-600' : 'text-gray-900'}`}>
|
| 187 |
+
{execution.errors}
|
| 188 |
+
</span>
|
| 189 |
+
</td>
|
| 190 |
+
<td className="py-4 px-6">
|
| 191 |
+
<div className="flex items-center space-x-2">
|
| 192 |
+
<button className="text-blue-600 hover:text-blue-800 text-sm cursor-pointer">
|
| 193 |
+
查看详情
|
| 194 |
+
</button>
|
| 195 |
+
{execution.status === 'error' && (
|
| 196 |
+
<button className="text-orange-600 hover:text-orange-800 text-sm cursor-pointer">
|
| 197 |
+
重试
|
| 198 |
+
</button>
|
| 199 |
+
)}
|
| 200 |
+
</div>
|
| 201 |
+
</td>
|
| 202 |
+
</tr>
|
| 203 |
+
))}
|
| 204 |
+
</tbody>
|
| 205 |
+
</table>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div className="p-6 border-t border-gray-200 flex items-center justify-between">
|
| 209 |
+
<span className="text-sm text-gray-700">显示 1-6 条,共 156 条记录</span>
|
| 210 |
+
<div className="flex items-center space-x-2">
|
| 211 |
+
<button className="px-3 py-1 border border-gray-300 rounded text-sm text-gray-600 hover:bg-gray-50 cursor-pointer">
|
| 212 |
+
上一页
|
| 213 |
+
</button>
|
| 214 |
+
<button className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 cursor-pointer">
|
| 215 |
+
1
|
| 216 |
+
</button>
|
| 217 |
+
<button className="px-3 py-1 border border-gray-300 rounded text-sm text-gray-600 hover:bg-gray-50 cursor-pointer">
|
| 218 |
+
2
|
| 219 |
+
</button>
|
| 220 |
+
<button className="px-3 py-1 border border-gray-300 rounded text-sm text-gray-600 hover:bg-gray-50 cursor-pointer">
|
| 221 |
+
3
|
| 222 |
+
</button>
|
| 223 |
+
<button className="px-3 py-1 border border-gray-300 rounded text-sm text-gray-600 hover:bg-gray-50 cursor-pointer">
|
| 224 |
+
下一页
|
| 225 |
+
</button>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
);
|
| 230 |
+
}
|
app/workflows/[id]/analytics/NodeAnalytics.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
interface NodeAnalyticsProps {
|
| 4 |
+
workflowId: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export default function NodeAnalytics({ workflowId }: NodeAnalyticsProps) {
|
| 8 |
+
const nodeData = [
|
| 9 |
+
{
|
| 10 |
+
id: 1,
|
| 11 |
+
name: '数据接收',
|
| 12 |
+
type: '触发器',
|
| 13 |
+
executions: 1245,
|
| 14 |
+
avgTime: '0.2s',
|
| 15 |
+
successRate: 100,
|
| 16 |
+
status: 'healthy'
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
id: 2,
|
| 20 |
+
name: '数据验证',
|
| 21 |
+
type: '条件判断',
|
| 22 |
+
executions: 1245,
|
| 23 |
+
avgTime: '0.8s',
|
| 24 |
+
successRate: 98.9,
|
| 25 |
+
status: 'healthy'
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
id: 3,
|
| 29 |
+
name: '数据清洗',
|
| 30 |
+
type: '数据处理',
|
| 31 |
+
executions: 1231,
|
| 32 |
+
avgTime: '1.5s',
|
| 33 |
+
successRate: 97.2,
|
| 34 |
+
status: 'warning'
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
id: 4,
|
| 38 |
+
name: 'AI分析',
|
| 39 |
+
type: 'AI节点',
|
| 40 |
+
executions: 1196,
|
| 41 |
+
avgTime: '2.1s',
|
| 42 |
+
successRate: 96.8,
|
| 43 |
+
status: 'warning'
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: 5,
|
| 47 |
+
name: '结果存储',
|
| 48 |
+
type: '数据库',
|
| 49 |
+
executions: 1158,
|
| 50 |
+
avgTime: '0.6s',
|
| 51 |
+
successRate: 99.5,
|
| 52 |
+
status: 'healthy'
|
| 53 |
+
}
|
| 54 |
+
];
|
| 55 |
+
|
| 56 |
+
const getStatusColor = (status: string) => {
|
| 57 |
+
switch (status) {
|
| 58 |
+
case 'healthy':
|
| 59 |
+
return 'text-green-600 bg-green-100';
|
| 60 |
+
case 'warning':
|
| 61 |
+
return 'text-yellow-600 bg-yellow-100';
|
| 62 |
+
case 'error':
|
| 63 |
+
return 'text-red-600 bg-red-100';
|
| 64 |
+
default:
|
| 65 |
+
return 'text-gray-600 bg-gray-100';
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
return (
|
| 70 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 71 |
+
<div className="flex items-center justify-between mb-6">
|
| 72 |
+
<h3 className="text-lg font-semibold text-gray-900">节点性能分析</h3>
|
| 73 |
+
<button className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer">
|
| 74 |
+
查看详情
|
| 75 |
+
</button>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div className="space-y-4">
|
| 79 |
+
{nodeData.map((node) => (
|
| 80 |
+
<div key={node.id} className="border border-gray-200 rounded-lg p-4">
|
| 81 |
+
<div className="flex items-center justify-between mb-3">
|
| 82 |
+
<div className="flex items-center space-x-3">
|
| 83 |
+
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
|
| 84 |
+
<span className="font-medium text-gray-900">{node.name}</span>
|
| 85 |
+
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
| 86 |
+
{node.type}
|
| 87 |
+
</span>
|
| 88 |
+
</div>
|
| 89 |
+
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(node.status)}`}>
|
| 90 |
+
{node.status === 'healthy' ? '正常' : '警告'}
|
| 91 |
+
</span>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div className="grid grid-cols-3 gap-4">
|
| 95 |
+
<div>
|
| 96 |
+
<span className="text-xs text-gray-500">执行次数</span>
|
| 97 |
+
<p className="text-sm font-medium text-gray-900">{node.executions.toLocaleString()}</p>
|
| 98 |
+
</div>
|
| 99 |
+
<div>
|
| 100 |
+
<span className="text-xs text-gray-500">平均耗时</span>
|
| 101 |
+
<p className="text-sm font-medium text-gray-900">{node.avgTime}</p>
|
| 102 |
+
</div>
|
| 103 |
+
<div>
|
| 104 |
+
<span className="text-xs text-gray-500">成功率</span>
|
| 105 |
+
<p className="text-sm font-medium text-green-600">{node.successRate}%</p>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
{/* 性能条 */}
|
| 110 |
+
<div className="mt-3">
|
| 111 |
+
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
| 112 |
+
<div
|
| 113 |
+
className={`h-1.5 rounded-full ${
|
| 114 |
+
node.successRate >= 99 ? 'bg-green-500' :
|
| 115 |
+
node.successRate >= 95 ? 'bg-yellow-500' : 'bg-red-500'
|
| 116 |
+
}`}
|
| 117 |
+
style={{ width: `${node.successRate}%` }}
|
| 118 |
+
></div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
))}
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
);
|
| 126 |
+
}
|
app/workflows/[id]/analytics/PerformanceChart.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line } from 'recharts';
|
| 4 |
+
|
| 5 |
+
interface PerformanceChartProps {
|
| 6 |
+
workflowId: string;
|
| 7 |
+
timeRange: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function PerformanceChart({ workflowId, timeRange }: PerformanceChartProps) {
|
| 11 |
+
const performanceData = [
|
| 12 |
+
{ date: '01-01', executions: 45, successRate: 98.2, avgRuntime: 3.1 },
|
| 13 |
+
{ date: '01-02', executions: 52, successRate: 97.8, avgRuntime: 3.3 },
|
| 14 |
+
{ date: '01-03', executions: 48, successRate: 98.9, avgRuntime: 2.9 },
|
| 15 |
+
{ date: '01-04', executions: 61, successRate: 98.1, avgRuntime: 3.4 },
|
| 16 |
+
{ date: '01-05', executions: 55, successRate: 99.1, avgRuntime: 3.0 },
|
| 17 |
+
{ date: '01-06', executions: 49, successRate: 97.5, avgRuntime: 3.6 },
|
| 18 |
+
{ date: '01-07', executions: 58, successRate: 98.7, avgRuntime: 3.2 }
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 23 |
+
<div className="flex items-center justify-between mb-6">
|
| 24 |
+
<h3 className="text-lg font-semibold text-gray-900">执行趋势</h3>
|
| 25 |
+
<div className="flex items-center space-x-4">
|
| 26 |
+
<div className="flex items-center space-x-2">
|
| 27 |
+
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
|
| 28 |
+
<span className="text-sm text-gray-600">执行次数</span>
|
| 29 |
+
</div>
|
| 30 |
+
<div className="flex items-center space-x-2">
|
| 31 |
+
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
| 32 |
+
<span className="text-sm text-gray-600">成功率</span>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div className="h-80">
|
| 38 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 39 |
+
<AreaChart data={performanceData}>
|
| 40 |
+
<defs>
|
| 41 |
+
<linearGradient id="colorExecutions" x1="0" y1="0" x2="0" y2="1">
|
| 42 |
+
<stop offset="5%" stopColor="#3B82F6" stopOpacity={0.8}/>
|
| 43 |
+
<stop offset="95%" stopColor="#3B82F6" stopOpacity={0.1}/>
|
| 44 |
+
</linearGradient>
|
| 45 |
+
</defs>
|
| 46 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#E5E7EB" />
|
| 47 |
+
<XAxis
|
| 48 |
+
dataKey="date"
|
| 49 |
+
stroke="#6B7280"
|
| 50 |
+
fontSize={12}
|
| 51 |
+
tick={{ fill: '#6B7280' }}
|
| 52 |
+
/>
|
| 53 |
+
<YAxis
|
| 54 |
+
stroke="#6B7280"
|
| 55 |
+
fontSize={12}
|
| 56 |
+
tick={{ fill: '#6B7280' }}
|
| 57 |
+
/>
|
| 58 |
+
<Tooltip
|
| 59 |
+
contentStyle={{
|
| 60 |
+
backgroundColor: '#1F2937',
|
| 61 |
+
border: 'none',
|
| 62 |
+
borderRadius: '8px',
|
| 63 |
+
color: 'white'
|
| 64 |
+
}}
|
| 65 |
+
labelStyle={{ color: '#F3F4F6' }}
|
| 66 |
+
/>
|
| 67 |
+
<Area
|
| 68 |
+
type="monotone"
|
| 69 |
+
dataKey="executions"
|
| 70 |
+
stroke="#3B82F6"
|
| 71 |
+
strokeWidth={2}
|
| 72 |
+
fillOpacity={1}
|
| 73 |
+
fill="url(#colorExecutions)"
|
| 74 |
+
/>
|
| 75 |
+
</AreaChart>
|
| 76 |
+
</ResponsiveContainer>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
);
|
| 80 |
+
}
|
app/workflows/[id]/analytics/WorkflowAnalytics.tsx
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import Link from 'next/link';
|
| 5 |
+
import PerformanceChart from './PerformanceChart';
|
| 6 |
+
import ExecutionHistory from './ExecutionHistory';
|
| 7 |
+
import NodeAnalytics from './NodeAnalytics';
|
| 8 |
+
|
| 9 |
+
interface WorkflowAnalyticsProps {
|
| 10 |
+
workflowId: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export default function WorkflowAnalytics({ workflowId }: WorkflowAnalyticsProps) {
|
| 14 |
+
const [timeRange, setTimeRange] = useState('7d');
|
| 15 |
+
|
| 16 |
+
const workflowData = {
|
| 17 |
+
'1': {
|
| 18 |
+
name: '客户数据处理流程',
|
| 19 |
+
description: '自动处理客户数据,包括清洗、分类和分析',
|
| 20 |
+
icon: '📊',
|
| 21 |
+
status: '运行中',
|
| 22 |
+
totalExecutions: 1245,
|
| 23 |
+
successRate: 98.5,
|
| 24 |
+
avgRuntime: 3.2,
|
| 25 |
+
totalRuntime: 4392,
|
| 26 |
+
errorCount: 19
|
| 27 |
+
},
|
| 28 |
+
'2': {
|
| 29 |
+
name: '内容生成与发布',
|
| 30 |
+
description: '根据关键词自动生成文章并发布到多个平台',
|
| 31 |
+
icon: '✍️',
|
| 32 |
+
status: '运行中',
|
| 33 |
+
totalExecutions: 892,
|
| 34 |
+
successRate: 96.2,
|
| 35 |
+
avgRuntime: 5.8,
|
| 36 |
+
totalRuntime: 5174,
|
| 37 |
+
errorCount: 34
|
| 38 |
+
},
|
| 39 |
+
'3': {
|
| 40 |
+
name: '邮件智能分类回复',
|
| 41 |
+
description: '自动分类邮件并生成智能回复',
|
| 42 |
+
icon: '📧',
|
| 43 |
+
status: '暂停',
|
| 44 |
+
totalExecutions: 2156,
|
| 45 |
+
successRate: 94.8,
|
| 46 |
+
avgRuntime: 1.5,
|
| 47 |
+
totalRuntime: 3234,
|
| 48 |
+
errorCount: 112
|
| 49 |
+
},
|
| 50 |
+
'4': {
|
| 51 |
+
name: '订单处理自动化',
|
| 52 |
+
description: '从下单到发货的完整自动化处理流程',
|
| 53 |
+
icon: '📦',
|
| 54 |
+
status: '运行中',
|
| 55 |
+
totalExecutions: 3245,
|
| 56 |
+
successRate: 99.1,
|
| 57 |
+
avgRuntime: 2.1,
|
| 58 |
+
totalRuntime: 6815,
|
| 59 |
+
errorCount: 29
|
| 60 |
+
},
|
| 61 |
+
'5': {
|
| 62 |
+
name: '社交媒体监控',
|
| 63 |
+
description: '监控品牌提及并自动生成报告',
|
| 64 |
+
icon: '📱',
|
| 65 |
+
status: '运行中',
|
| 66 |
+
totalExecutions: 678,
|
| 67 |
+
successRate: 97.3,
|
| 68 |
+
avgRuntime: 4.2,
|
| 69 |
+
totalRuntime: 2848,
|
| 70 |
+
errorCount: 18
|
| 71 |
+
},
|
| 72 |
+
'6': {
|
| 73 |
+
name: '财务报表生成',
|
| 74 |
+
description: '自动收集财务数据并生成可视化报表',
|
| 75 |
+
icon: '💰',
|
| 76 |
+
status: '运行中',
|
| 77 |
+
totalExecutions: 456,
|
| 78 |
+
successRate: 99.8,
|
| 79 |
+
avgRuntime: 6.5,
|
| 80 |
+
totalRuntime: 2964,
|
| 81 |
+
errorCount: 1
|
| 82 |
+
}
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const workflow = workflowData[workflowId as keyof typeof workflowData] || workflowData['1'];
|
| 86 |
+
|
| 87 |
+
const timeRanges = [
|
| 88 |
+
{ value: '1d', label: '最近1天' },
|
| 89 |
+
{ value: '7d', label: '最近7天' },
|
| 90 |
+
{ value: '30d', label: '最近30天' },
|
| 91 |
+
{ value: '90d', label: '最近90天' }
|
| 92 |
+
];
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
<div className="space-y-8">
|
| 96 |
+
{/* 页面头部 */}
|
| 97 |
+
<div className="flex items-center justify-between">
|
| 98 |
+
<div className="flex items-center space-x-4">
|
| 99 |
+
<Link href="/workflows" className="p-2 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer">
|
| 100 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 101 |
+
<i className="ri-arrow-left-line"></i>
|
| 102 |
+
</div>
|
| 103 |
+
</Link>
|
| 104 |
+
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
| 105 |
+
<span className="text-xl">{workflow.icon}</span>
|
| 106 |
+
</div>
|
| 107 |
+
<div>
|
| 108 |
+
<h1 className="text-3xl font-bold text-gray-900">{workflow.name}</h1>
|
| 109 |
+
<p className="text-gray-600 mt-1">{workflow.description}</p>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div className="flex items-center space-x-4">
|
| 114 |
+
<div className="flex bg-white rounded-lg border border-gray-200 p-1">
|
| 115 |
+
{timeRanges.map((range) => (
|
| 116 |
+
<button
|
| 117 |
+
key={range.value}
|
| 118 |
+
onClick={() => setTimeRange(range.value)}
|
| 119 |
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors cursor-pointer whitespace-nowrap \${
|
| 120 |
+
timeRange === range.value
|
| 121 |
+
? 'bg-blue-100 text-blue-700'
|
| 122 |
+
: 'text-gray-600 hover:text-gray-900'
|
| 123 |
+
}`}
|
| 124 |
+
>
|
| 125 |
+
{range.label}
|
| 126 |
+
</button>
|
| 127 |
+
))}
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<Link href={`/workflows/${workflowId}/edit`} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 131 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block mr-2">
|
| 132 |
+
<i className="ri-edit-line"></i>
|
| 133 |
+
</div>
|
| 134 |
+
编辑工作流
|
| 135 |
+
</Link>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
{/* 核心指标卡片 */}
|
| 140 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
| 141 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 142 |
+
<div className="flex items-center justify-between">
|
| 143 |
+
<div>
|
| 144 |
+
<p className="text-sm font-medium text-gray-600">总执行次数</p>
|
| 145 |
+
<p className="text-2xl font-bold text-gray-900 mt-2">{workflow.totalExecutions.toLocaleString()}</p>
|
| 146 |
+
</div>
|
| 147 |
+
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 148 |
+
<i className="ri-play-line text-blue-600 text-xl"></i>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
<div className="flex items-center mt-4">
|
| 152 |
+
<span className="text-sm text-green-600">+12.5%</span>
|
| 153 |
+
<span className="text-sm text-gray-500 ml-2">较上期</span>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 158 |
+
<div className="flex items-center justify-between">
|
| 159 |
+
<div>
|
| 160 |
+
<p className="text-sm font-medium text-gray-600">成功率</p>
|
| 161 |
+
<p className="text-2xl font-bold text-green-600 mt-2">{workflow.successRate}%</p>
|
| 162 |
+
</div>
|
| 163 |
+
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
| 164 |
+
<i className="ri-check-line text-green-600 text-xl"></i>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
<div className="flex items-center mt-4">
|
| 168 |
+
<span className="text-sm text-green-600">+2.1%</span>
|
| 169 |
+
<span className="text-sm text-gray-500 ml-2">较上期</span>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 174 |
+
<div className="flex items-center justify-between">
|
| 175 |
+
<div>
|
| 176 |
+
<p className="text-sm font-medium text-gray-600">平均运行时间</p>
|
| 177 |
+
<p className="text-2xl font-bold text-gray-900 mt-2">{workflow.avgRuntime}分</p>
|
| 178 |
+
</div>
|
| 179 |
+
<div className="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
|
| 180 |
+
<i className="ri-time-line text-orange-600 text-xl"></i>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
<div className="flex items-center mt-4">
|
| 184 |
+
<span className="text-sm text-red-600">+0.3分</span>
|
| 185 |
+
<span className="text-sm text-gray-500 ml-2">较上期</span>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 190 |
+
<div className="flex items-center justify-between">
|
| 191 |
+
<div>
|
| 192 |
+
<p className="text-sm font-medium text-gray-600">总运行时间</p>
|
| 193 |
+
<p className="text-2xl font-bold text-gray-900 mt-2">{workflow.totalRuntime}分</p>
|
| 194 |
+
</div>
|
| 195 |
+
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
| 196 |
+
<i className="ri-timer-line text-purple-600 text-xl"></i>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
<div className="flex items-center mt-4">
|
| 200 |
+
<span className="text-sm text-green-600">-5.2%</span>
|
| 201 |
+
<span className="text-sm text-gray-500 ml-2">较上期</span>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
| 206 |
+
<div className="flex items-center justify-between">
|
| 207 |
+
<div>
|
| 208 |
+
<p className="text-sm font-medium text-gray-600">错误次数</p>
|
| 209 |
+
<p className="text-2xl font-bold text-red-600 mt-2">{workflow.errorCount}</p>
|
| 210 |
+
</div>
|
| 211 |
+
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
| 212 |
+
<i className="ri-error-warning-line text-red-600 text-xl"></i>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
<div className="flex items-center mt-4">
|
| 216 |
+
<span className="text-sm text-green-600">-18.3%</span>
|
| 217 |
+
<span className="text-sm text-gray-500 ml-2">较上期</span>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
{/* 性能趋势图表 */}
|
| 223 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 224 |
+
<PerformanceChart workflowId={workflowId} timeRange={timeRange} />
|
| 225 |
+
<NodeAnalytics workflowId={workflowId} />
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
{/* 执行历史 */}
|
| 229 |
+
<ExecutionHistory workflowId={workflowId} />
|
| 230 |
+
</div>
|
| 231 |
+
);
|
| 232 |
+
}
|
app/workflows/[id]/analytics/page.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { Suspense } from 'react';
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import WorkflowAnalytics from './WorkflowAnalytics';
|
| 6 |
+
|
| 7 |
+
export async function generateStaticParams() {
|
| 8 |
+
return [
|
| 9 |
+
{ id: '1' },
|
| 10 |
+
{ id: '2' },
|
| 11 |
+
{ id: '3' },
|
| 12 |
+
{ id: '4' },
|
| 13 |
+
{ id: '5' },
|
| 14 |
+
{ id: '6' },
|
| 15 |
+
];
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export default function WorkflowAnalyticsPage({ params }: { params: { id: string } }) {
|
| 19 |
+
return (
|
| 20 |
+
<div className="min-h-screen bg-gray-50">
|
| 21 |
+
<Header />
|
| 22 |
+
<div className="flex">
|
| 23 |
+
<Sidebar />
|
| 24 |
+
<main className="flex-1 p-8">
|
| 25 |
+
<div className="max-w-7xl mx-auto">
|
| 26 |
+
<Suspense fallback={<div className="text-center py-8">Loading...</div>}>
|
| 27 |
+
<WorkflowAnalytics workflowId={params.id} />
|
| 28 |
+
</Suspense>
|
| 29 |
+
</div>
|
| 30 |
+
</main>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
);
|
| 34 |
+
}
|
app/workflows/create/NodeLibrary.tsx
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
export default function NodeLibrary() {
|
| 6 |
+
const [activeCategory, setActiveCategory] = useState('trigger');
|
| 7 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 8 |
+
|
| 9 |
+
const nodeCategories = [
|
| 10 |
+
{ id: 'trigger', name: '触发器', icon: 'ri-play-line' },
|
| 11 |
+
{ id: 'action', name: '动作', icon: 'ri-tools-line' },
|
| 12 |
+
{ id: 'condition', name: '条件', icon: 'ri-question-line' },
|
| 13 |
+
{ id: 'data', name: '数据', icon: 'ri-database-line' },
|
| 14 |
+
{ id: 'ai', name: 'AI', icon: 'ri-brain-line' },
|
| 15 |
+
{ id: 'integration', name: '集成', icon: 'ri-links-line' }
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
const nodes = {
|
| 19 |
+
trigger: [
|
| 20 |
+
{ id: 'schedule', name: '定时触发', icon: '⏰', desc: '按时间表执行' },
|
| 21 |
+
{ id: 'webhook', name: 'Webhook', icon: '🔗', desc: 'HTTP请求触发' },
|
| 22 |
+
{ id: 'file-upload', name: '文件上传', icon: '📁', desc: '文件上传时触发' },
|
| 23 |
+
{ id: 'email', name: '邮件触发', icon: '📧', desc: '收到邮件时触发' },
|
| 24 |
+
{ id: 'database', name: '数据库变更', icon: '🗄️', desc: '数据变更时触发' }
|
| 25 |
+
],
|
| 26 |
+
action: [
|
| 27 |
+
{ id: 'http-request', name: 'HTTP请求', icon: '🌐', desc: '发送HTTP请求' },
|
| 28 |
+
{ id: 'send-email', name: '发送邮件', icon: '📮', desc: '发送电子邮件' },
|
| 29 |
+
{ id: 'file-operation', name: '文件操作', icon: '📄', desc: '文件增删改查' },
|
| 30 |
+
{ id: 'notification', name: '消息通知', icon: '🔔', desc: '发送通知消息' },
|
| 31 |
+
{ id: 'data-transform', name: '数据转换', icon: '🔄', desc: '转换数据格式' }
|
| 32 |
+
],
|
| 33 |
+
condition: [
|
| 34 |
+
{ id: 'if-else', name: '条件判断', icon: '🤔', desc: '根据条件分支' },
|
| 35 |
+
{ id: 'filter', name: '数据过滤', icon: '🔍', desc: '过滤数据项' },
|
| 36 |
+
{ id: 'loop', name: '循环处理', icon: '🔁', desc: '循环执行操作' },
|
| 37 |
+
{ id: 'delay', name: '延时等待', icon: '⏱️', desc: '延时一段时间' },
|
| 38 |
+
{ id: 'merge', name: '数据合并', icon: '🔀', desc: '合并多个数据源' }
|
| 39 |
+
],
|
| 40 |
+
data: [
|
| 41 |
+
{ id: 'mysql', name: 'MySQL', icon: '🐬', desc: 'MySQL数据库' },
|
| 42 |
+
{ id: 'mongodb', name: 'MongoDB', icon: '🍃', desc: 'MongoDB数据库' },
|
| 43 |
+
{ id: 'redis', name: 'Redis', icon: '📦', desc: 'Redis缓存' },
|
| 44 |
+
{ id: 'csv', name: 'CSV文件', icon: '📊', desc: 'CSV格式数据' },
|
| 45 |
+
{ id: 'json', name: 'JSON数据', icon: '📋', desc: 'JSON格式数据' }
|
| 46 |
+
],
|
| 47 |
+
ai: [
|
| 48 |
+
{ id: 'text-generation', name: '文本生成', icon: '✍️', desc: 'AI文本生成' },
|
| 49 |
+
{ id: 'text-analysis', name: '文本分析', icon: '🔎', desc: '文本内容分析' },
|
| 50 |
+
{ id: 'translation', name: '文本翻译', icon: '🌍', desc: '多语言翻译' },
|
| 51 |
+
{ id: 'sentiment', name: '情感分析', icon: '😊', desc: '情感倾向分析' },
|
| 52 |
+
{ id: 'ocr', name: '图片识别', icon: '👁️', desc: 'OCR文字识别' }
|
| 53 |
+
],
|
| 54 |
+
integration: [
|
| 55 |
+
{ id: 'slack', name: 'Slack', icon: '💬', desc: 'Slack消息推送' },
|
| 56 |
+
{ id: 'wechat', name: '微信', icon: '💚', desc: '微信消息推送' },
|
| 57 |
+
{ id: 'dingtalk', name: '钉钉', icon: '📱', desc: '钉钉消息推送' },
|
| 58 |
+
{ id: 'github', name: 'GitHub', icon: '🐙', desc: 'GitHub集成' },
|
| 59 |
+
{ id: 'google-sheets', name: 'Google Sheets', icon: '📈', desc: 'Google表格' }
|
| 60 |
+
]
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
const handleDragStart = (e: React.DragEvent, node: any) => {
|
| 64 |
+
e.dataTransfer.setData('application/json', JSON.stringify({
|
| 65 |
+
type: 'node',
|
| 66 |
+
nodeType: node.id,
|
| 67 |
+
...node
|
| 68 |
+
}));
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
const filteredNodes = searchTerm
|
| 72 |
+
? nodes[activeCategory as keyof typeof nodes]?.filter(node =>
|
| 73 |
+
node.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 74 |
+
node.desc.toLowerCase().includes(searchTerm.toLowerCase())
|
| 75 |
+
)
|
| 76 |
+
: nodes[activeCategory as keyof typeof nodes];
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<div className="h-full flex flex-col">
|
| 80 |
+
{/* 搜索框 */}
|
| 81 |
+
<div className="p-4 border-b border-gray-200">
|
| 82 |
+
<div className="relative">
|
| 83 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 84 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 85 |
+
<i className="ri-search-line text-gray-400"></i>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
<input
|
| 89 |
+
type="text"
|
| 90 |
+
placeholder="搜索节点..."
|
| 91 |
+
value={searchTerm}
|
| 92 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 93 |
+
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 94 |
+
/>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
{/* 分类标签 */}
|
| 99 |
+
<div className="p-4 border-b border-gray-200">
|
| 100 |
+
<div className="grid grid-cols-2 gap-2">
|
| 101 |
+
{nodeCategories.map((category) => (
|
| 102 |
+
<button
|
| 103 |
+
key={category.id}
|
| 104 |
+
onClick={() => setActiveCategory(category.id)}
|
| 105 |
+
className={`p-2 rounded-lg text-sm font-medium transition-colors cursor-pointer ${
|
| 106 |
+
activeCategory === category.id
|
| 107 |
+
? 'bg-blue-100 text-blue-700'
|
| 108 |
+
: 'text-gray-600 hover:bg-gray-100'
|
| 109 |
+
}`}
|
| 110 |
+
>
|
| 111 |
+
<div className="flex items-center space-x-2">
|
| 112 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 113 |
+
<i className={category.icon}></i>
|
| 114 |
+
</div>
|
| 115 |
+
<span>{category.name}</span>
|
| 116 |
+
</div>
|
| 117 |
+
</button>
|
| 118 |
+
))}
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
{/* 节点列表 */}
|
| 123 |
+
<div className="flex-1 overflow-y-auto p-4">
|
| 124 |
+
<div className="space-y-2">
|
| 125 |
+
{filteredNodes?.map((node) => (
|
| 126 |
+
<div
|
| 127 |
+
key={node.id}
|
| 128 |
+
draggable
|
| 129 |
+
onDragStart={(e) => handleDragStart(e, node)}
|
| 130 |
+
className="p-3 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all cursor-move"
|
| 131 |
+
>
|
| 132 |
+
<div className="flex items-start space-x-3">
|
| 133 |
+
<div className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
| 134 |
+
<span className="text-sm">{node.icon}</span>
|
| 135 |
+
</div>
|
| 136 |
+
<div className="flex-1 min-w-0">
|
| 137 |
+
<h4 className="font-medium text-gray-900 text-sm">{node.name}</h4>
|
| 138 |
+
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{node.desc}</p>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
))}
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
{/* 模板快捷入口 */}
|
| 147 |
+
<div className="p-4 border-t border-gray-200">
|
| 148 |
+
<button className="w-full p-3 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors cursor-pointer">
|
| 149 |
+
<div className="flex items-center justify-center space-x-2">
|
| 150 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 151 |
+
<i className="ri-template-line"></i>
|
| 152 |
+
</div>
|
| 153 |
+
<span className="text-sm font-medium">使用模板</span>
|
| 154 |
+
</div>
|
| 155 |
+
</button>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
);
|
| 159 |
+
}
|
app/workflows/create/WorkflowBuilder.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import WorkflowCanvas from './WorkflowCanvas';
|
| 5 |
+
import NodeLibrary from './NodeLibrary';
|
| 6 |
+
import WorkflowSettings from './WorkflowSettings';
|
| 7 |
+
|
| 8 |
+
export default function WorkflowBuilder() {
|
| 9 |
+
const [workflowData, setWorkflowData] = useState({
|
| 10 |
+
name: '',
|
| 11 |
+
description: '',
|
| 12 |
+
category: 'data-processing',
|
| 13 |
+
triggers: [],
|
| 14 |
+
nodes: [],
|
| 15 |
+
connections: []
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
const [selectedNode, setSelectedNode] = useState(null);
|
| 19 |
+
const [showSettings, setShowSettings] = useState(false);
|
| 20 |
+
|
| 21 |
+
const handleSave = () => {
|
| 22 |
+
console.log('保存工作流:', workflowData);
|
| 23 |
+
alert('工作流保存成功!');
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const handleDeploy = () => {
|
| 27 |
+
console.log('部署工作流:', workflowData);
|
| 28 |
+
alert('工作流部署成功!');
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const handleTest = () => {
|
| 32 |
+
console.log('测试工作流:', workflowData);
|
| 33 |
+
alert('开始测试工作流...');
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
| 38 |
+
{/* 顶部工具栏 */}
|
| 39 |
+
<div className="p-6 border-b border-gray-200">
|
| 40 |
+
<div className="flex items-center justify-between">
|
| 41 |
+
<div className="flex items-center space-x-6">
|
| 42 |
+
<div>
|
| 43 |
+
<input
|
| 44 |
+
type="text"
|
| 45 |
+
placeholder="输入工作流名称"
|
| 46 |
+
value={workflowData.name}
|
| 47 |
+
onChange={(e) => setWorkflowData({...workflowData, name: e.target.value})}
|
| 48 |
+
className="text-xl font-semibold bg-transparent border-none focus:outline-none focus:ring-0 p-0 text-gray-900 placeholder-gray-500"
|
| 49 |
+
/>
|
| 50 |
+
<p className="text-sm text-gray-500 mt-1">设计您的自动化流程</p>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div className="flex items-center space-x-3">
|
| 54 |
+
<button
|
| 55 |
+
onClick={() => setShowSettings(!showSettings)}
|
| 56 |
+
className={`p-2 rounded-lg transition-colors cursor-pointer ${
|
| 57 |
+
showSettings ? 'bg-blue-100 text-blue-600' : 'text-gray-500 hover:bg-gray-100'
|
| 58 |
+
}`}
|
| 59 |
+
>
|
| 60 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 61 |
+
<i className="ri-settings-line"></i>
|
| 62 |
+
</div>
|
| 63 |
+
</button>
|
| 64 |
+
<button className="p-2 text-gray-500 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
| 65 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 66 |
+
<i className="ri-zoom-in-line"></i>
|
| 67 |
+
</div>
|
| 68 |
+
</button>
|
| 69 |
+
<button className="p-2 text-gray-500 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
| 70 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 71 |
+
<i className="ri-zoom-out-line"></i>
|
| 72 |
+
</div>
|
| 73 |
+
</button>
|
| 74 |
+
<button className="p-2 text-gray-500 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
| 75 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 76 |
+
<i className="ri-fullscreen-line"></i>
|
| 77 |
+
</div>
|
| 78 |
+
</button>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<div className="flex items-center space-x-3">
|
| 83 |
+
<button
|
| 84 |
+
onClick={handleTest}
|
| 85 |
+
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 86 |
+
>
|
| 87 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block mr-2">
|
| 88 |
+
<i className="ri-play-line"></i>
|
| 89 |
+
</div>
|
| 90 |
+
测试运行
|
| 91 |
+
</button>
|
| 92 |
+
<button
|
| 93 |
+
onClick={handleSave}
|
| 94 |
+
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 95 |
+
>
|
| 96 |
+
保存草稿
|
| 97 |
+
</button>
|
| 98 |
+
<button
|
| 99 |
+
onClick={handleDeploy}
|
| 100 |
+
className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 101 |
+
>
|
| 102 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block mr-2">
|
| 103 |
+
<i className="ri-rocket-line"></i>
|
| 104 |
+
</div>
|
| 105 |
+
部署
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
{/* 主内容区域 */}
|
| 112 |
+
<div className="flex h-screen">
|
| 113 |
+
{/* 左侧节点库 */}
|
| 114 |
+
<div className="w-80 border-r border-gray-200 bg-gray-50">
|
| 115 |
+
<NodeLibrary />
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
{/* 中间画布区域 */}
|
| 119 |
+
<div className="flex-1">
|
| 120 |
+
<WorkflowCanvas
|
| 121 |
+
nodes={workflowData.nodes}
|
| 122 |
+
connections={workflowData.connections}
|
| 123 |
+
onNodeSelect={setSelectedNode}
|
| 124 |
+
onUpdateWorkflow={setWorkflowData}
|
| 125 |
+
/>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
{/* 右侧设置面板 */}
|
| 129 |
+
{showSettings && (
|
| 130 |
+
<div className="w-80 border-l border-gray-200 bg-gray-50">
|
| 131 |
+
<WorkflowSettings
|
| 132 |
+
workflowData={workflowData}
|
| 133 |
+
selectedNode={selectedNode}
|
| 134 |
+
onUpdateWorkflow={setWorkflowData}
|
| 135 |
+
/>
|
| 136 |
+
</div>
|
| 137 |
+
)}
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
);
|
| 141 |
+
}
|
app/workflows/create/WorkflowCanvas.tsx
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState, useRef } from 'react';
|
| 4 |
+
|
| 5 |
+
interface WorkflowCanvasProps {
|
| 6 |
+
nodes: any[];
|
| 7 |
+
connections: any[];
|
| 8 |
+
onNodeSelect: (node: any) => void;
|
| 9 |
+
onUpdateWorkflow: (data: any) => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function WorkflowCanvas({ nodes, connections, onNodeSelect, onUpdateWorkflow }: WorkflowCanvasProps) {
|
| 13 |
+
const canvasRef = useRef<HTMLDivElement>(null);
|
| 14 |
+
const [draggedNode, setDraggedNode] = useState(null);
|
| 15 |
+
const [selectedNode, setSelectedNode] = useState(null);
|
| 16 |
+
const [canvasNodes, setCanvasNodes] = useState([
|
| 17 |
+
{
|
| 18 |
+
id: 'start',
|
| 19 |
+
type: 'trigger',
|
| 20 |
+
name: '开始',
|
| 21 |
+
icon: '🚀',
|
| 22 |
+
x: 100,
|
| 23 |
+
y: 100,
|
| 24 |
+
isFixed: true
|
| 25 |
+
}
|
| 26 |
+
]);
|
| 27 |
+
|
| 28 |
+
const handleDragOver = (e: React.DragEvent) => {
|
| 29 |
+
e.preventDefault();
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const handleDrop = (e: React.DragEvent) => {
|
| 33 |
+
e.preventDefault();
|
| 34 |
+
|
| 35 |
+
const nodeData = JSON.parse(e.dataTransfer.getData('application/json'));
|
| 36 |
+
const rect = canvasRef.current?.getBoundingClientRect();
|
| 37 |
+
|
| 38 |
+
if (rect) {
|
| 39 |
+
const newNode = {
|
| 40 |
+
id: `node_${Date.now()}`,
|
| 41 |
+
type: nodeData.type,
|
| 42 |
+
name: nodeData.name,
|
| 43 |
+
icon: nodeData.icon,
|
| 44 |
+
nodeType: nodeData.nodeType,
|
| 45 |
+
x: e.clientX - rect.left - 50,
|
| 46 |
+
y: e.clientY - rect.top - 25,
|
| 47 |
+
config: {}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
setCanvasNodes(prev => [...prev, newNode]);
|
| 51 |
+
}
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
const handleNodeClick = (node: any) => {
|
| 55 |
+
setSelectedNode(node);
|
| 56 |
+
onNodeSelect(node);
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const handleNodeDrag = (nodeId: string, newX: number, newY: number) => {
|
| 60 |
+
setCanvasNodes(prev =>
|
| 61 |
+
prev.map(node =>
|
| 62 |
+
node.id === nodeId ? { ...node, x: newX, y: newY } : node
|
| 63 |
+
)
|
| 64 |
+
);
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
const deleteNode = (nodeId: string) => {
|
| 68 |
+
setCanvasNodes(prev => prev.filter(node => node.id !== nodeId && !node.isFixed));
|
| 69 |
+
if (selectedNode && selectedNode.id === nodeId) {
|
| 70 |
+
setSelectedNode(null);
|
| 71 |
+
onNodeSelect(null);
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
return (
|
| 76 |
+
<div
|
| 77 |
+
ref={canvasRef}
|
| 78 |
+
className="relative w-full h-full bg-gray-50 overflow-auto"
|
| 79 |
+
onDragOver={handleDragOver}
|
| 80 |
+
onDrop={handleDrop}
|
| 81 |
+
style={{
|
| 82 |
+
backgroundImage: 'radial-gradient(circle, #d1d5db 1px, transparent 1px)',
|
| 83 |
+
backgroundSize: '20px 20px'
|
| 84 |
+
}}
|
| 85 |
+
>
|
| 86 |
+
{/* 画布提示 */}
|
| 87 |
+
{canvasNodes.length <= 1 && (
|
| 88 |
+
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
| 89 |
+
<div className="text-center text-gray-500">
|
| 90 |
+
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 91 |
+
<div className="w-8 h-8 flex items-center justify-center">
|
| 92 |
+
<i className="ri-drag-drop-line text-2xl"></i>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
<p className="text-lg font-medium mb-2">开始构建您的工作流</p>
|
| 96 |
+
<p className="text-sm">从左侧拖拽节点到画布上</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
)}
|
| 100 |
+
|
| 101 |
+
{/* 渲染节点 */}
|
| 102 |
+
{canvasNodes.map((node) => (
|
| 103 |
+
<WorkflowNode
|
| 104 |
+
key={node.id}
|
| 105 |
+
node={node}
|
| 106 |
+
isSelected={selectedNode?.id === node.id}
|
| 107 |
+
onClick={() => handleNodeClick(node)}
|
| 108 |
+
onDrag={handleNodeDrag}
|
| 109 |
+
onDelete={deleteNode}
|
| 110 |
+
/>
|
| 111 |
+
))}
|
| 112 |
+
|
| 113 |
+
{/* 连接线 */}
|
| 114 |
+
<svg className="absolute inset-0 pointer-events-none">
|
| 115 |
+
{connections.map((connection, index) => {
|
| 116 |
+
const startNode = canvasNodes.find(n => n.id === connection.from);
|
| 117 |
+
const endNode = canvasNodes.find(n => n.id === connection.to);
|
| 118 |
+
|
| 119 |
+
if (startNode && endNode) {
|
| 120 |
+
return (
|
| 121 |
+
<line
|
| 122 |
+
key={index}
|
| 123 |
+
x1={startNode.x + 50}
|
| 124 |
+
y1={startNode.y + 25}
|
| 125 |
+
x2={endNode.x}
|
| 126 |
+
y2={endNode.y + 25}
|
| 127 |
+
stroke="#6b7280"
|
| 128 |
+
strokeWidth="2"
|
| 129 |
+
markerEnd="url(#arrowhead)"
|
| 130 |
+
/>
|
| 131 |
+
);
|
| 132 |
+
}
|
| 133 |
+
return null;
|
| 134 |
+
})}
|
| 135 |
+
|
| 136 |
+
{/* 箭头定义 */}
|
| 137 |
+
<defs>
|
| 138 |
+
<marker
|
| 139 |
+
id="arrowhead"
|
| 140 |
+
markerWidth="10"
|
| 141 |
+
markerHeight="7"
|
| 142 |
+
refX="9"
|
| 143 |
+
refY="3.5"
|
| 144 |
+
orient="auto"
|
| 145 |
+
>
|
| 146 |
+
<polygon
|
| 147 |
+
points="0 0, 10 3.5, 0 7"
|
| 148 |
+
fill="#6b7280"
|
| 149 |
+
/>
|
| 150 |
+
</marker>
|
| 151 |
+
</defs>
|
| 152 |
+
</svg>
|
| 153 |
+
</div>
|
| 154 |
+
);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// 工作流节点组件
|
| 158 |
+
function WorkflowNode({ node, isSelected, onClick, onDrag, onDelete }: any) {
|
| 159 |
+
const [isDragging, setIsDragging] = useState(false);
|
| 160 |
+
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
| 161 |
+
|
| 162 |
+
const handleMouseDown = (e: React.MouseEvent) => {
|
| 163 |
+
if (node.isFixed) return;
|
| 164 |
+
|
| 165 |
+
setIsDragging(true);
|
| 166 |
+
setDragStart({
|
| 167 |
+
x: e.clientX - node.x,
|
| 168 |
+
y: e.clientY - node.y
|
| 169 |
+
});
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
const handleMouseMove = (e: React.MouseEvent) => {
|
| 173 |
+
if (!isDragging || node.isFixed) return;
|
| 174 |
+
|
| 175 |
+
onDrag(node.id, e.clientX - dragStart.x, e.clientY - dragStart.y);
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
const handleMouseUp = () => {
|
| 179 |
+
setIsDragging(false);
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
return (
|
| 183 |
+
<div
|
| 184 |
+
className={`absolute bg-white border-2 rounded-lg shadow-md cursor-pointer transition-all ${
|
| 185 |
+
isSelected ? 'border-blue-500 shadow-lg' : 'border-gray-200 hover:shadow-lg'
|
| 186 |
+
} ${isDragging ? 'cursor-move' : ''}`}
|
| 187 |
+
style={{
|
| 188 |
+
left: node.x,
|
| 189 |
+
top: node.y,
|
| 190 |
+
width: 100,
|
| 191 |
+
height: 50,
|
| 192 |
+
zIndex: isSelected ? 10 : 1
|
| 193 |
+
}}
|
| 194 |
+
onClick={onClick}
|
| 195 |
+
onMouseDown={handleMouseDown}
|
| 196 |
+
onMouseMove={handleMouseMove}
|
| 197 |
+
onMouseUp={handleMouseUp}
|
| 198 |
+
>
|
| 199 |
+
<div className="p-2 h-full flex flex-col items-center justify-center">
|
| 200 |
+
<div className="text-lg mb-1">{node.icon}</div>
|
| 201 |
+
<div className="text-xs font-medium text-gray-700 text-center line-clamp-1">
|
| 202 |
+
{node.name}
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
{/* 连接点 */}
|
| 207 |
+
{!node.isFixed && (
|
| 208 |
+
<>
|
| 209 |
+
<div className="absolute -left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 bg-blue-500 rounded-full border-2 border-white cursor-pointer"></div>
|
| 210 |
+
<div className="absolute -right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 bg-blue-500 rounded-full border-2 border-white cursor-pointer"></div>
|
| 211 |
+
</>
|
| 212 |
+
)}
|
| 213 |
+
|
| 214 |
+
{node.isFixed && (
|
| 215 |
+
<div className="absolute -right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 bg-blue-500 rounded-full border-2 border-white cursor-pointer"></div>
|
| 216 |
+
)}
|
| 217 |
+
|
| 218 |
+
{/* 删除按钮 */}
|
| 219 |
+
{!node.isFixed && isSelected && (
|
| 220 |
+
<button
|
| 221 |
+
onClick={(e) => {
|
| 222 |
+
e.stopPropagation();
|
| 223 |
+
onDelete(node.id);
|
| 224 |
+
}}
|
| 225 |
+
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition-colors cursor-pointer"
|
| 226 |
+
>
|
| 227 |
+
<i className="ri-close-line text-xs"></i>
|
| 228 |
+
</button>
|
| 229 |
+
)}
|
| 230 |
+
</div>
|
| 231 |
+
);
|
| 232 |
+
}
|
app/workflows/create/WorkflowSettings.tsx
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
interface WorkflowSettingsProps {
|
| 6 |
+
workflowData: any;
|
| 7 |
+
selectedNode: any;
|
| 8 |
+
onUpdateWorkflow: (data: any) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function WorkflowSettings({ workflowData, selectedNode, onUpdateWorkflow }: WorkflowSettingsProps) {
|
| 12 |
+
const [activeTab, setActiveTab] = useState('general');
|
| 13 |
+
|
| 14 |
+
const tabs = [
|
| 15 |
+
{ id: 'general', name: '基本设置', icon: 'ri-settings-line' },
|
| 16 |
+
{ id: 'triggers', name: '触发器', icon: 'ri-play-line' },
|
| 17 |
+
{ id: 'variables', name: '变量', icon: 'ri-code-line' },
|
| 18 |
+
{ id: 'security', name: '权限', icon: 'ri-shield-line' }
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
const categories = [
|
| 22 |
+
{ value: 'data-processing', label: '数据处理' },
|
| 23 |
+
{ value: 'content-creation', label: '内容创作' },
|
| 24 |
+
{ value: 'automation', label: '任务自动化' },
|
| 25 |
+
{ value: 'integration', label: '系统集成' }
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
const handleInputChange = (field: string, value: any) => {
|
| 29 |
+
onUpdateWorkflow({ ...workflowData, [field]: value });
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const renderGeneralSettings = () => (
|
| 33 |
+
<div className="space-y-6">
|
| 34 |
+
<div>
|
| 35 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">工作流名称</label>
|
| 36 |
+
<input
|
| 37 |
+
type="text"
|
| 38 |
+
placeholder="输入工作流名称"
|
| 39 |
+
value={workflowData.name}
|
| 40 |
+
onChange={(e) => handleInputChange('name', e.target.value)}
|
| 41 |
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 42 |
+
/>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div>
|
| 46 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">描述</label>
|
| 47 |
+
<textarea
|
| 48 |
+
placeholder="描述工作流的功能和用途"
|
| 49 |
+
rows={3}
|
| 50 |
+
maxLength={500}
|
| 51 |
+
value={workflowData.description}
|
| 52 |
+
onChange={(e) => handleInputChange('description', e.target.value)}
|
| 53 |
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none"
|
| 54 |
+
/>
|
| 55 |
+
<div className="text-xs text-gray-500 mt-1">{workflowData.description.length}/500</div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div>
|
| 59 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">分类</label>
|
| 60 |
+
<select
|
| 61 |
+
value={workflowData.category}
|
| 62 |
+
onChange={(e) => handleInputChange('category', e.target.value)}
|
| 63 |
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm pr-8"
|
| 64 |
+
>
|
| 65 |
+
{categories.map((category) => (
|
| 66 |
+
<option key={category.value} value={category.value}>
|
| 67 |
+
{category.label}
|
| 68 |
+
</option>
|
| 69 |
+
))}
|
| 70 |
+
</select>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div>
|
| 74 |
+
<label className="block text-sm font-medium text-gray-700 mb-3">执行设置</label>
|
| 75 |
+
<div className="space-y-3">
|
| 76 |
+
<div className="flex items-center justify-between">
|
| 77 |
+
<span className="text-sm text-gray-700">并行执行</span>
|
| 78 |
+
<div className="w-10 h-6 bg-gray-300 rounded-full relative cursor-pointer">
|
| 79 |
+
<div className="w-4 h-4 bg-white rounded-full absolute left-1 top-1"></div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
<div className="flex items-center justify-between">
|
| 83 |
+
<span className="text-sm text-gray-700">错误时停止</span>
|
| 84 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 85 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
<div className="flex items-center justify-between">
|
| 89 |
+
<span className="text-sm text-gray-700">保存执行日志</span>
|
| 90 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 91 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
const renderTriggerSettings = () => (
|
| 100 |
+
<div className="space-y-6">
|
| 101 |
+
<div>
|
| 102 |
+
<h4 className="font-medium text-gray-900 mb-3">触发条件</h4>
|
| 103 |
+
<div className="space-y-3">
|
| 104 |
+
<div className="p-3 border border-gray-200 rounded-lg">
|
| 105 |
+
<div className="flex items-center justify-between mb-2">
|
| 106 |
+
<span className="font-medium text-sm">定时触发</span>
|
| 107 |
+
<div className="w-8 h-5 bg-blue-600 rounded-full relative cursor-pointer">
|
| 108 |
+
<div className="w-3 h-3 bg-white rounded-full absolute right-1 top-1"></div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
<input
|
| 112 |
+
type="text"
|
| 113 |
+
placeholder="0 0 * * *"
|
| 114 |
+
className="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
| 115 |
+
/>
|
| 116 |
+
<p className="text-xs text-gray-500 mt-1">Cron表达式格式</p>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<div className="p-3 border border-gray-200 rounded-lg">
|
| 120 |
+
<div className="flex items-center justify-between mb-2">
|
| 121 |
+
<span className="font-medium text-sm">Webhook触发</span>
|
| 122 |
+
<div className="w-8 h-5 bg-gray-300 rounded-full relative cursor-pointer">
|
| 123 |
+
<div className="w-3 h-3 bg-white rounded-full absolute left-1 top-1"></div>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
<div className="text-xs text-gray-500">
|
| 127 |
+
URL: https://api.example.com/webhook/abc123
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
);
|
| 134 |
+
|
| 135 |
+
const renderVariableSettings = () => (
|
| 136 |
+
<div className="space-y-6">
|
| 137 |
+
<div>
|
| 138 |
+
<div className="flex items-center justify-between mb-3">
|
| 139 |
+
<h4 className="font-medium text-gray-900">环境变量</h4>
|
| 140 |
+
<button className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap">
|
| 141 |
+
添加变量
|
| 142 |
+
</button>
|
| 143 |
+
</div>
|
| 144 |
+
<div className="space-y-2">
|
| 145 |
+
<div className="flex items-center space-x-2">
|
| 146 |
+
<input
|
| 147 |
+
type="text"
|
| 148 |
+
placeholder="变量名"
|
| 149 |
+
className="flex-1 px-3 py-2 border border-gray-300 rounded text-sm"
|
| 150 |
+
/>
|
| 151 |
+
<input
|
| 152 |
+
type="text"
|
| 153 |
+
placeholder="变量值"
|
| 154 |
+
className="flex-1 px-3 py-2 border border-gray-300 rounded text-sm"
|
| 155 |
+
/>
|
| 156 |
+
<button className="p-2 text-red-600 hover:bg-red-50 rounded cursor-pointer">
|
| 157 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 158 |
+
<i className="ri-delete-bin-line"></i>
|
| 159 |
+
</div>
|
| 160 |
+
</button>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<div>
|
| 166 |
+
<h4 className="font-medium text-gray-900 mb-3">全局配置</h4>
|
| 167 |
+
<div className="space-y-3">
|
| 168 |
+
<div>
|
| 169 |
+
<label className="block text-sm text-gray-700 mb-1">超时时间(秒)</label>
|
| 170 |
+
<input
|
| 171 |
+
type="number"
|
| 172 |
+
defaultValue="300"
|
| 173 |
+
className="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
| 174 |
+
/>
|
| 175 |
+
</div>
|
| 176 |
+
<div>
|
| 177 |
+
<label className="block text-sm text-gray-700 mb-1">重试次数</label>
|
| 178 |
+
<input
|
| 179 |
+
type="number"
|
| 180 |
+
defaultValue="3"
|
| 181 |
+
className="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
| 182 |
+
/>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
);
|
| 188 |
+
|
| 189 |
+
const renderSecuritySettings = () => (
|
| 190 |
+
<div className="space-y-6">
|
| 191 |
+
<div>
|
| 192 |
+
<h4 className="font-medium text-gray-900 mb-3">访问权限</h4>
|
| 193 |
+
<div className="space-y-3">
|
| 194 |
+
<div className="flex items-center justify-between">
|
| 195 |
+
<span className="text-sm text-gray-700">公开工作流</span>
|
| 196 |
+
<div className="w-10 h-6 bg-gray-300 rounded-full relative cursor-pointer">
|
| 197 |
+
<div className="w-4 h-4 bg-white rounded-full absolute left-1 top-1"></div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
<div className="flex items-center justify-between">
|
| 201 |
+
<span className="text-sm text-gray-700">需要API密钥</span>
|
| 202 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 203 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<div>
|
| 210 |
+
<h4 className="font-medium text-gray-900 mb-3">数据安全</h4>
|
| 211 |
+
<div className="space-y-3">
|
| 212 |
+
<div className="flex items-center justify-between">
|
| 213 |
+
<span className="text-sm text-gray-700">加密存储</span>
|
| 214 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 215 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
<div className="flex items-center justify-between">
|
| 219 |
+
<span className="text-sm text-gray-700">审计日志</span>
|
| 220 |
+
<div className="w-10 h-6 bg-blue-600 rounded-full relative cursor-pointer">
|
| 221 |
+
<div className="w-4 h-4 bg-white rounded-full absolute right-1 top-1"></div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
);
|
| 228 |
+
|
| 229 |
+
const renderNodeSettings = () => {
|
| 230 |
+
if (!selectedNode) {
|
| 231 |
+
return (
|
| 232 |
+
<div className="flex items-center justify-center h-64 text-gray-500">
|
| 233 |
+
<div className="text-center">
|
| 234 |
+
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 235 |
+
<div className="w-8 h-8 flex items-center justify-center">
|
| 236 |
+
<i className="ri-cursor-line text-2xl"></i>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
<p className="font-medium">选择一个节点</p>
|
| 240 |
+
<p className="text-sm mt-1">点击画布上的节点来配置参数</p>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return (
|
| 247 |
+
<div className="space-y-6">
|
| 248 |
+
<div className="flex items-center space-x-3">
|
| 249 |
+
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 250 |
+
<span className="text-lg">{selectedNode.icon}</span>
|
| 251 |
+
</div>
|
| 252 |
+
<div>
|
| 253 |
+
<h3 className="font-semibold text-gray-900">{selectedNode.name}</h3>
|
| 254 |
+
<p className="text-sm text-gray-500">节点配置</p>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
|
| 258 |
+
<div>
|
| 259 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">节点名称</label>
|
| 260 |
+
<input
|
| 261 |
+
type="text"
|
| 262 |
+
defaultValue={selectedNode.name}
|
| 263 |
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 264 |
+
/>
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
<div>
|
| 268 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">描述</label>
|
| 269 |
+
<textarea
|
| 270 |
+
placeholder="节点功能描述"
|
| 271 |
+
rows={3}
|
| 272 |
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none"
|
| 273 |
+
/>
|
| 274 |
+
</div>
|
| 275 |
+
|
| 276 |
+
{/* 根据节点类型显示不同的配置选项 */}
|
| 277 |
+
<div>
|
| 278 |
+
<h4 className="font-medium text-gray-900 mb-3">参数配置</h4>
|
| 279 |
+
<div className="space-y-3">
|
| 280 |
+
<div>
|
| 281 |
+
<label className="block text-sm text-gray-700 mb-1">输入参数</label>
|
| 282 |
+
<input
|
| 283 |
+
type="text"
|
| 284 |
+
placeholder="参数值"
|
| 285 |
+
className="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
| 286 |
+
/>
|
| 287 |
+
</div>
|
| 288 |
+
<div>
|
| 289 |
+
<label className="block text-sm text-gray-700 mb-1">输出格式</label>
|
| 290 |
+
<select className="w-full px-3 py-2 border border-gray-300 rounded text-sm pr-8">
|
| 291 |
+
<option>JSON</option>
|
| 292 |
+
<option>XML</option>
|
| 293 |
+
<option>TEXT</option>
|
| 294 |
+
</select>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
);
|
| 300 |
+
};
|
| 301 |
+
|
| 302 |
+
return (
|
| 303 |
+
<div className="h-full flex flex-col">
|
| 304 |
+
{/* 标签页 */}
|
| 305 |
+
<div className="p-4 border-b border-gray-200">
|
| 306 |
+
<div className="grid grid-cols-2 gap-1">
|
| 307 |
+
{tabs.map((tab) => (
|
| 308 |
+
<button
|
| 309 |
+
key={tab.id}
|
| 310 |
+
onClick={() => setActiveTab(tab.id)}
|
| 311 |
+
className={`p-2 rounded-lg text-xs font-medium transition-colors cursor-pointer ${
|
| 312 |
+
activeTab === tab.id
|
| 313 |
+
? 'bg-blue-100 text-blue-700'
|
| 314 |
+
: 'text-gray-600 hover:bg-gray-100'
|
| 315 |
+
}`}
|
| 316 |
+
>
|
| 317 |
+
<div className="flex flex-col items-center space-y-1">
|
| 318 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 319 |
+
<i className={tab.icon}></i>
|
| 320 |
+
</div>
|
| 321 |
+
<span>{tab.name}</span>
|
| 322 |
+
</div>
|
| 323 |
+
</button>
|
| 324 |
+
))}
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
{/* 内容区域 */}
|
| 329 |
+
<div className="flex-1 overflow-y-auto p-4">
|
| 330 |
+
{selectedNode && activeTab === 'general' ? renderNodeSettings() :
|
| 331 |
+
activeTab === 'general' ? renderGeneralSettings() :
|
| 332 |
+
activeTab === 'triggers' ? renderTriggerSettings() :
|
| 333 |
+
activeTab === 'variables' ? renderVariableSettings() :
|
| 334 |
+
activeTab === 'security' ? renderSecuritySettings() : null}
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
);
|
| 338 |
+
}
|
app/workflows/create/page.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import WorkflowBuilder from './WorkflowBuilder';
|
| 6 |
+
|
| 7 |
+
export default function CreateWorkflowPage() {
|
| 8 |
+
return (
|
| 9 |
+
<div className="min-h-screen bg-gray-50">
|
| 10 |
+
<Header />
|
| 11 |
+
<div className="flex">
|
| 12 |
+
<Sidebar />
|
| 13 |
+
<main className="flex-1 p-8">
|
| 14 |
+
<div className="max-w-7xl mx-auto">
|
| 15 |
+
<div className="mb-8">
|
| 16 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">创建新工作流</h1>
|
| 17 |
+
<p className="text-gray-600">设计和配置您的自动化工作流程</p>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<WorkflowBuilder />
|
| 21 |
+
</div>
|
| 22 |
+
</main>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
);
|
| 26 |
+
}
|
app/workflows/page.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import WorkflowList from './WorkflowList';
|
| 6 |
+
import WorkflowFilters from './WorkflowFilters';
|
| 7 |
+
import Link from 'next/link';
|
| 8 |
+
|
| 9 |
+
export default function WorkflowsPage() {
|
| 10 |
+
return (
|
| 11 |
+
<div className="min-h-screen bg-gray-50">
|
| 12 |
+
<Header />
|
| 13 |
+
<div className="flex">
|
| 14 |
+
<Sidebar />
|
| 15 |
+
<main className="flex-1 p-8">
|
| 16 |
+
<div className="max-w-7xl mx-auto">
|
| 17 |
+
<div className="flex items-center justify-between mb-8">
|
| 18 |
+
<div>
|
| 19 |
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">工作流管理</h1>
|
| 20 |
+
<p className="text-gray-600">创建和管理自动化工作流程</p>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div className="flex items-center space-x-4">
|
| 24 |
+
<Link
|
| 25 |
+
href="/workflows/templates"
|
| 26 |
+
className="bg-white text-gray-700 border border-gray-300 px-6 py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap"
|
| 27 |
+
>
|
| 28 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 29 |
+
<i className="ri-template-line"></i>
|
| 30 |
+
</div>
|
| 31 |
+
模板库
|
| 32 |
+
</Link>
|
| 33 |
+
<Link
|
| 34 |
+
href="/workflows/create"
|
| 35 |
+
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 36 |
+
>
|
| 37 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 38 |
+
<i className="ri-add-line"></i>
|
| 39 |
+
</div>
|
| 40 |
+
创建工作流
|
| 41 |
+
</Link>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<WorkflowFilters />
|
| 46 |
+
<WorkflowList />
|
| 47 |
+
</div>
|
| 48 |
+
</main>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
);
|
| 52 |
+
}
|
app/workflows/templates/TemplateFilters.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
export default function TemplateFilters() {
|
| 6 |
+
const [activeCategory, setActiveCategory] = useState('all');
|
| 7 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 8 |
+
|
| 9 |
+
const categories = [
|
| 10 |
+
{ key: 'all', label: '全部模板', count: 48 },
|
| 11 |
+
{ key: 'data-processing', label: '数据处理', count: 16 },
|
| 12 |
+
{ key: 'content-creation', label: '内容创作', count: 12 },
|
| 13 |
+
{ key: 'automation', label: '任务自动化', count: 10 },
|
| 14 |
+
{ key: 'integration', label: '系统集成', count: 8 },
|
| 15 |
+
{ key: 'ai-workflow', label: 'AI工作流', count: 6 }
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
const popularTags = [
|
| 19 |
+
'API集成', '数据清洗', '内容生成', '邮件自动化', '报告生成',
|
| 20 |
+
'社交媒体', '客户服务', '电商运营', '财务管理', '项目管理'
|
| 21 |
+
];
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
| 25 |
+
<div className="flex flex-col gap-6">
|
| 26 |
+
{/* 分类筛选 */}
|
| 27 |
+
<div>
|
| 28 |
+
<h3 className="text-sm font-medium text-gray-900 mb-3">模板分类</h3>
|
| 29 |
+
<div className="flex flex-wrap gap-2">
|
| 30 |
+
{categories.map((category) => (
|
| 31 |
+
<button
|
| 32 |
+
key={category.key}
|
| 33 |
+
onClick={() => setActiveCategory(category.key)}
|
| 34 |
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors cursor-pointer whitespace-nowrap ${
|
| 35 |
+
activeCategory === category.key
|
| 36 |
+
? 'bg-blue-100 text-blue-700'
|
| 37 |
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
| 38 |
+
}`}
|
| 39 |
+
>
|
| 40 |
+
{category.label} ({category.count})
|
| 41 |
+
</button>
|
| 42 |
+
))}
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
{/* 搜索和排序 */}
|
| 47 |
+
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
| 48 |
+
<div className="relative flex-1 max-w-md">
|
| 49 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 50 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 51 |
+
<i className="ri-search-line text-gray-400"></i>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
<input
|
| 55 |
+
type="text"
|
| 56 |
+
placeholder="搜索模板..."
|
| 57 |
+
value={searchTerm}
|
| 58 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 59 |
+
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
| 60 |
+
/>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div className="flex items-center space-x-3">
|
| 64 |
+
<div className="relative">
|
| 65 |
+
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 66 |
+
热门程度
|
| 67 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block ml-2">
|
| 68 |
+
<i className="ri-arrow-down-s-line"></i>
|
| 69 |
+
</div>
|
| 70 |
+
</button>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div className="relative">
|
| 74 |
+
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap">
|
| 75 |
+
创建时间
|
| 76 |
+
<div className="w-4 h-4 flex items-center justify-center inline-block ml-2">
|
| 77 |
+
<i className="ri-arrow-down-s-line"></i>
|
| 78 |
+
</div>
|
| 79 |
+
</button>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
{/* 热门标签 */}
|
| 85 |
+
<div>
|
| 86 |
+
<h3 className="text-sm font-medium text-gray-900 mb-3">热门标签</h3>
|
| 87 |
+
<div className="flex flex-wrap gap-2">
|
| 88 |
+
{popularTags.map((tag, index) => (
|
| 89 |
+
<button
|
| 90 |
+
key={index}
|
| 91 |
+
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-gray-200 transition-colors cursor-pointer whitespace-nowrap"
|
| 92 |
+
>
|
| 93 |
+
{tag}
|
| 94 |
+
</button>
|
| 95 |
+
))}
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
);
|
| 101 |
+
}
|
app/workflows/templates/TemplateLibrary.tsx
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import Link from 'next/link';
|
| 5 |
+
|
| 6 |
+
export default function TemplateLibrary() {
|
| 7 |
+
const [selectedTemplate, setSelectedTemplate] = useState<number | null>(null);
|
| 8 |
+
|
| 9 |
+
const templates = [
|
| 10 |
+
{
|
| 11 |
+
id: 1,
|
| 12 |
+
name: '客户数据自动化处理',
|
| 13 |
+
description: '自动收集、清洗和分析客户数据,生成洞察报告',
|
| 14 |
+
category: '数据处理',
|
| 15 |
+
difficulty: '中级',
|
| 16 |
+
estimatedTime: '10-15分钟',
|
| 17 |
+
usageCount: 2156,
|
| 18 |
+
rating: 4.8,
|
| 19 |
+
tags: ['数据清洗', 'ETL', '客户分析', '报告生成'],
|
| 20 |
+
icon: '📊',
|
| 21 |
+
features: ['自动数据收集', '智能数据清洗', '实时分析', '可视化报表'],
|
| 22 |
+
preview: 'https://readdy.ai/api/search-image?query=modern%20data%20processing%20dashboard%20with%20clean%20charts%20and%20analytics%20visualization%2C%20blue%20and%20white%20interface%2C%20professional%20business%20intelligence%20design&width=400&height=240&seq=template_1&orientation=landscape'
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: 2,
|
| 26 |
+
name: 'AI内容创作助手',
|
| 27 |
+
description: '基于关键词自动生成高质量文章并发布到多个平台',
|
| 28 |
+
category: '内容创作',
|
| 29 |
+
difficulty: '简单',
|
| 30 |
+
estimatedTime: '5-10分钟',
|
| 31 |
+
usageCount: 1892,
|
| 32 |
+
rating: 4.9,
|
| 33 |
+
tags: ['内容生成', 'SEO优化', '多平台发布', 'AI写作'],
|
| 34 |
+
icon: '✍️',
|
| 35 |
+
features: ['AI智能写作', 'SEO优化', '一键发布', '内容调度'],
|
| 36 |
+
preview: 'https://readdy.ai/api/search-image?query=AI%20content%20creation%20interface%20with%20writing%20tools%20and%20publishing%20options%2C%20modern%20minimalist%20design%20with%20purple%20and%20blue%20accents&width=400&height=240&seq=template_2&orientation=landscape'
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
id: 3,
|
| 40 |
+
name: '邮件智能管理系统',
|
| 41 |
+
description: '自动分类邮件、生成智能回复并跟踪邮件状态',
|
| 42 |
+
category: '任务自动化',
|
| 43 |
+
difficulty: '中级',
|
| 44 |
+
estimatedTime: '8-12分钟',
|
| 45 |
+
usageCount: 1456,
|
| 46 |
+
rating: 4.7,
|
| 47 |
+
tags: ['邮件自动化', 'NLP处理', '智能回复', '状态跟踪'],
|
| 48 |
+
icon: '📧',
|
| 49 |
+
features: ['智能分类', '自动回复', '状态跟踪', '优先级管理'],
|
| 50 |
+
preview: 'https://readdy.ai/api/search-image?query=email%20management%20dashboard%20with%20organized%20inbox%2C%20smart%20filtering%20and%20automated%20responses%2C%20clean%20modern%20interface%20design&width=400&height=240&seq=template_3&orientation=landscape'
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
id: 4,
|
| 54 |
+
name: '电商订单自动化',
|
| 55 |
+
description: '从订单接收到发货的完整自动化处理流程',
|
| 56 |
+
category: '系统集成',
|
| 57 |
+
difficulty: '高级',
|
| 58 |
+
estimatedTime: '15-20分钟',
|
| 59 |
+
usageCount: 1234,
|
| 60 |
+
rating: 4.8,
|
| 61 |
+
tags: ['订单处理', 'ERP集成', '库存管理', '物流跟踪'],
|
| 62 |
+
icon: '📦',
|
| 63 |
+
features: ['订单自动处理', '库存同步', '物流跟踪', '异常处理'],
|
| 64 |
+
preview: 'https://readdy.ai/api/search-image?query=e-commerce%20order%20processing%20dashboard%20with%20inventory%20management%20and%20shipping%20tracking%2C%20professional%20blue%20and%20orange%20design&width=400&height=240&seq=template_4&orientation=landscape'
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
id: 5,
|
| 68 |
+
name: '社交媒体监控',
|
| 69 |
+
description: '监控品牌提及、分析情感倾向并生成营销洞察',
|
| 70 |
+
category: '数据处理',
|
| 71 |
+
difficulty: '中级',
|
| 72 |
+
estimatedTime: '12-18分钟',
|
| 73 |
+
usageCount: 987,
|
| 74 |
+
rating: 4.6,
|
| 75 |
+
tags: ['社媒监控', '情感分析', '品牌监测', '营销洞察'],
|
| 76 |
+
icon: '📱',
|
| 77 |
+
features: ['实时监控', '情感分析', '趋势预测', '竞品分析'],
|
| 78 |
+
preview: 'https://readdy.ai/api/search-image?query=social%20media%20monitoring%20dashboard%20with%20sentiment%20analysis%20charts%20and%20brand%20mention%20tracking%2C%20modern%20colorful%20interface&width=400&height=240&seq=template_5&orientation=landscape'
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
id: 6,
|
| 82 |
+
name: '财务报表自动化',
|
| 83 |
+
description: '自动收集财务数据,生成专业的财务报表和分析',
|
| 84 |
+
category: '数据处理',
|
| 85 |
+
difficulty: '高级',
|
| 86 |
+
estimatedTime: '20-25分钟',
|
| 87 |
+
usageCount: 756,
|
| 88 |
+
rating: 4.9,
|
| 89 |
+
tags: ['财务分析', '报表生成', '数据可视化', 'BI集成'],
|
| 90 |
+
icon: '💰',
|
| 91 |
+
features: ['多源数据整合', '自动报表', '财务分析', '预算跟踪'],
|
| 92 |
+
preview: 'https://readdy.ai/api/search-image?query=financial%20reporting%20dashboard%20with%20charts%20graphs%20and%20financial%20analytics%2C%20professional%20green%20and%20blue%20business%20interface&width=400&height=240&seq=template_6&orientation=landscape'
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
id: 7,
|
| 96 |
+
name: '客户服务自动化',
|
| 97 |
+
description: '智能客服机器人,自动回复客户询问并转接人工',
|
| 98 |
+
category: 'AI工作流',
|
| 99 |
+
difficulty: '中级',
|
| 100 |
+
estimatedTime: '10-15分钟',
|
| 101 |
+
usageCount: 1678,
|
| 102 |
+
rating: 4.7,
|
| 103 |
+
tags: ['智能客服', '自动回复', '工单管理', '人工转接'],
|
| 104 |
+
icon: '🤖',
|
| 105 |
+
features: ['智能对话', '多渠道支持', '工单管理', '知识库'],
|
| 106 |
+
preview: 'https://readdy.ai/api/search-image?query=customer%20service%20chatbot%20interface%20with%20conversation%20flow%20and%20automated%20responses%2C%20friendly%20blue%20and%20white%20design&width=400&height=240&seq=template_7&orientation=landscape'
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
id: 8,
|
| 110 |
+
name: '数据备份与同步',
|
| 111 |
+
description: '定期备份重要数据并同步到多个云存储平台',
|
| 112 |
+
category: '系统集成',
|
| 113 |
+
difficulty: '简单',
|
| 114 |
+
estimatedTime: '5-8分钟',
|
| 115 |
+
usageCount: 1345,
|
| 116 |
+
rating: 4.8,
|
| 117 |
+
tags: ['数据备份', '云同步', '定时任务', '安全存储'],
|
| 118 |
+
icon: '☁️',
|
| 119 |
+
features: ['定时备份', '多云同步', '数据加密', '恢复管理'],
|
| 120 |
+
preview: 'https://readdy.ai/api/search-image?query=cloud%20backup%20and%20sync%20dashboard%20with%20progress%20indicators%20and%20storage%20management%2C%20clean%20modern%20tech%20interface&width=400&height=240&seq=template_8&orientation=landscape'
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
id: 9,
|
| 124 |
+
name: '项目进度跟踪',
|
| 125 |
+
description: '自动跟踪项目进度,生成进度报告并发送提醒',
|
| 126 |
+
category: '任务自动化',
|
| 127 |
+
difficulty: '中级',
|
| 128 |
+
estimatedTime: '12-15分钟',
|
| 129 |
+
usageCount: 892,
|
| 130 |
+
rating: 4.6,
|
| 131 |
+
tags: ['项目管理', '进度跟踪', '报告生成', '提醒通知'],
|
| 132 |
+
icon: '📋',
|
| 133 |
+
features: ['进度监控', '里程碑跟踪', '团队协作', '风险预警'],
|
| 134 |
+
preview: 'https://readdy.ai/api/search-image?query=project%20management%20dashboard%20with%20progress%20tracking%20and%20milestone%20indicators%2C%20professional%20purple%20and%20blue%20interface&width=400&height=240&seq=template_9&orientation=landscape'
|
| 135 |
+
}
|
| 136 |
+
];
|
| 137 |
+
|
| 138 |
+
const getDifficultyColor = (difficulty: string) => {
|
| 139 |
+
switch (difficulty) {
|
| 140 |
+
case '简单': return 'text-green-600 bg-green-100';
|
| 141 |
+
case '中级': return 'text-yellow-600 bg-yellow-100';
|
| 142 |
+
case '高级': return 'text-red-600 bg-red-100';
|
| 143 |
+
default: return 'text-gray-600 bg-gray-100';
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const handleUseTemplate = (templateId: number) => {
|
| 148 |
+
console.log('使用模板:', templateId);
|
| 149 |
+
// 这里可以跳转到创建页面并预填充模板数据
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
return (
|
| 153 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
| 154 |
+
{templates.map((template) => (
|
| 155 |
+
<div
|
| 156 |
+
key={template.id}
|
| 157 |
+
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow cursor-pointer"
|
| 158 |
+
onClick={() => setSelectedTemplate(selectedTemplate === template.id ? null : template.id)}
|
| 159 |
+
>
|
| 160 |
+
{/* 模板预览图 */}
|
| 161 |
+
<div className="relative h-48 bg-gray-100">
|
| 162 |
+
<img
|
| 163 |
+
src={template.preview}
|
| 164 |
+
alt={template.name}
|
| 165 |
+
className="w-full h-full object-cover object-top"
|
| 166 |
+
/>
|
| 167 |
+
<div className="absolute top-3 right-3 bg-white rounded-full p-2 shadow-md">
|
| 168 |
+
<span className="text-xl">{template.icon}</span>
|
| 169 |
+
</div>
|
| 170 |
+
<div className="absolute bottom-3 left-3 flex items-center space-x-2">
|
| 171 |
+
<span className={`px-2 py-1 text-xs rounded-full ${getDifficultyColor(template.difficulty)}`}>
|
| 172 |
+
{template.difficulty}
|
| 173 |
+
</span>
|
| 174 |
+
<span className="px-2 py-1 text-xs bg-gray-900 text-white rounded-full">
|
| 175 |
+
{template.estimatedTime}
|
| 176 |
+
</span>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
{/* 模板信息 */}
|
| 181 |
+
<div className="p-6">
|
| 182 |
+
<div className="flex items-start justify-between mb-3">
|
| 183 |
+
<div>
|
| 184 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-1">{template.name}</h3>
|
| 185 |
+
<span className="text-sm text-blue-600 font-medium">{template.category}</span>
|
| 186 |
+
</div>
|
| 187 |
+
<div className="flex items-center space-x-1">
|
| 188 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 189 |
+
<i className="ri-star-fill text-yellow-400"></i>
|
| 190 |
+
</div>
|
| 191 |
+
<span className="text-sm font-medium text-gray-700">{template.rating}</span>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<p className="text-gray-600 text-sm mb-4 leading-relaxed">
|
| 196 |
+
{template.description}
|
| 197 |
+
</p>
|
| 198 |
+
|
| 199 |
+
{/* 使用统计 */}
|
| 200 |
+
<div className="flex items-center text-sm text-gray-500 mb-4">
|
| 201 |
+
<div className="w-4 h-4 flex items-center justify-center mr-1">
|
| 202 |
+
<i className="ri-user-line"></i>
|
| 203 |
+
</div>
|
| 204 |
+
<span>{template.usageCount.toLocaleString()} 次使用</span>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
{/* 标签 */}
|
| 208 |
+
<div className="flex flex-wrap gap-1 mb-4">
|
| 209 |
+
{template.tags.slice(0, 3).map((tag, index) => (
|
| 210 |
+
<span
|
| 211 |
+
key={index}
|
| 212 |
+
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full"
|
| 213 |
+
>
|
| 214 |
+
{tag}
|
| 215 |
+
</span>
|
| 216 |
+
))}
|
| 217 |
+
{template.tags.length > 3 && (
|
| 218 |
+
<span className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full">
|
| 219 |
+
+{template.tags.length - 3}
|
| 220 |
+
</span>
|
| 221 |
+
)}
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
{/* 展开的详细信息 */}
|
| 225 |
+
{selectedTemplate === template.id && (
|
| 226 |
+
<div className="border-t border-gray-200 pt-4 mt-4">
|
| 227 |
+
<div className="mb-4">
|
| 228 |
+
<h4 className="text-sm font-medium text-gray-900 mb-2">主要功能</h4>
|
| 229 |
+
<ul className="space-y-1">
|
| 230 |
+
{template.features.map((feature, index) => (
|
| 231 |
+
<li key={index} className="text-sm text-gray-600 flex items-center">
|
| 232 |
+
<div className="w-4 h-4 flex items-center justify-center mr-2">
|
| 233 |
+
<i className="ri-check-line text-green-500"></i>
|
| 234 |
+
</div>
|
| 235 |
+
{feature}
|
| 236 |
+
</li>
|
| 237 |
+
))}
|
| 238 |
+
</ul>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div className="mb-4">
|
| 242 |
+
<h4 className="text-sm font-medium text-gray-900 mb-2">所有标签</h4>
|
| 243 |
+
<div className="flex flex-wrap gap-1">
|
| 244 |
+
{template.tags.map((tag, index) => (
|
| 245 |
+
<span
|
| 246 |
+
key={index}
|
| 247 |
+
className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-full"
|
| 248 |
+
>
|
| 249 |
+
{tag}
|
| 250 |
+
</span>
|
| 251 |
+
))}
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
)}
|
| 256 |
+
|
| 257 |
+
{/* 操作按钮 */}
|
| 258 |
+
<div className="flex items-center space-x-3">
|
| 259 |
+
<button
|
| 260 |
+
onClick={(e) => {
|
| 261 |
+
e.stopPropagation();
|
| 262 |
+
handleUseTemplate(template.id);
|
| 263 |
+
}}
|
| 264 |
+
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-700 transition-colors whitespace-nowrap"
|
| 265 |
+
>
|
| 266 |
+
使用模板
|
| 267 |
+
</button>
|
| 268 |
+
<button
|
| 269 |
+
onClick={(e) => e.stopPropagation()}
|
| 270 |
+
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
| 271 |
+
>
|
| 272 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 273 |
+
<i className="ri-eye-line"></i>
|
| 274 |
+
</div>
|
| 275 |
+
</button>
|
| 276 |
+
<button
|
| 277 |
+
onClick={(e) => e.stopPropagation()}
|
| 278 |
+
className="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
| 279 |
+
>
|
| 280 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 281 |
+
<i className="ri-heart-line"></i>
|
| 282 |
+
</div>
|
| 283 |
+
</button>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
))}
|
| 288 |
+
</div>
|
| 289 |
+
);
|
| 290 |
+
}
|
app/workflows/templates/page.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Header from '@/components/Header';
|
| 4 |
+
import Sidebar from '@/components/Sidebar';
|
| 5 |
+
import TemplateLibrary from './TemplateLibrary';
|
| 6 |
+
import TemplateFilters from './TemplateFilters';
|
| 7 |
+
import Link from 'next/link';
|
| 8 |
+
|
| 9 |
+
export default function WorkflowTemplatesPage() {
|
| 10 |
+
return (
|
| 11 |
+
<div className="min-h-screen bg-gray-50">
|
| 12 |
+
<Header />
|
| 13 |
+
<div className="flex">
|
| 14 |
+
<Sidebar />
|
| 15 |
+
<main className="flex-1 p-8">
|
| 16 |
+
<div className="max-w-7xl mx-auto">
|
| 17 |
+
<div className="flex items-center justify-between mb-8">
|
| 18 |
+
<div>
|
| 19 |
+
<div className="flex items-center space-x-3 mb-2">
|
| 20 |
+
<Link
|
| 21 |
+
href="/workflows"
|
| 22 |
+
className="text-gray-500 hover:text-gray-700 transition-colors cursor-pointer"
|
| 23 |
+
>
|
| 24 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 25 |
+
<i className="ri-arrow-left-line"></i>
|
| 26 |
+
</div>
|
| 27 |
+
</Link>
|
| 28 |
+
<h1 className="text-3xl font-bold text-gray-900">工作流模板库</h1>
|
| 29 |
+
</div>
|
| 30 |
+
<p className="text-gray-600">选择预设模板快速创建工作流</p>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div className="flex items-center space-x-4">
|
| 34 |
+
<Link
|
| 35 |
+
href="/workflows/create"
|
| 36 |
+
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"
|
| 37 |
+
>
|
| 38 |
+
<div className="w-5 h-5 flex items-center justify-center inline-block mr-2">
|
| 39 |
+
<i className="ri-add-line"></i>
|
| 40 |
+
</div>
|
| 41 |
+
自定义创建
|
| 42 |
+
</Link>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<TemplateFilters />
|
| 47 |
+
<TemplateLibrary />
|
| 48 |
+
</div>
|
| 49 |
+
</main>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
);
|
| 53 |
+
}
|
components/Header.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link';
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
|
| 6 |
+
export default function Header() {
|
| 7 |
+
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
| 11 |
+
<div className="flex items-center justify-between px-6 py-4">
|
| 12 |
+
<div className="flex items-center space-x-8">
|
| 13 |
+
<Link href="/" className="text-2xl font-bold text-blue-600" style={{ fontFamily: 'Pacifico, serif' }}>
|
| 14 |
+
AI Agent Studio
|
| 15 |
+
</Link>
|
| 16 |
+
<nav className="hidden md:flex items-center space-x-6">
|
| 17 |
+
<Link href="/dashboard" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 18 |
+
工作台
|
| 19 |
+
</Link>
|
| 20 |
+
<Link href="/agents" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 21 |
+
Agent管理
|
| 22 |
+
</Link>
|
| 23 |
+
<Link href="/workflows" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 24 |
+
工作流
|
| 25 |
+
</Link>
|
| 26 |
+
<Link href="/knowledge" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 27 |
+
知识库
|
| 28 |
+
</Link>
|
| 29 |
+
<Link href="/templates" className="text-gray-700 hover:text-blue-600 transition-colors">
|
| 30 |
+
模板市场
|
| 31 |
+
</Link>
|
| 32 |
+
</nav>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<div className="flex items-center space-x-4">
|
| 36 |
+
<div className="relative">
|
| 37 |
+
<div className="w-8 h-8 flex items-center justify-center">
|
| 38 |
+
<i className="ri-notification-line text-gray-600 text-xl"></i>
|
| 39 |
+
</div>
|
| 40 |
+
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full"></span>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div className="relative">
|
| 44 |
+
<button
|
| 45 |
+
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
|
| 46 |
+
className="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
| 47 |
+
>
|
| 48 |
+
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
|
| 49 |
+
<span className="text-white text-sm font-medium">张</span>
|
| 50 |
+
</div>
|
| 51 |
+
<div className="w-4 h-4 flex items-center justify-center">
|
| 52 |
+
<i className="ri-arrow-down-s-line text-gray-600"></i>
|
| 53 |
+
</div>
|
| 54 |
+
</button>
|
| 55 |
+
|
| 56 |
+
{isUserMenuOpen && (
|
| 57 |
+
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2">
|
| 58 |
+
<div className="px-4 py-2 border-b border-gray-100">
|
| 59 |
+
<p className="text-sm font-medium text-gray-900">张三</p>
|
| 60 |
+
<p className="text-xs text-gray-500">zhang@example.com</p>
|
| 61 |
+
</div>
|
| 62 |
+
<Link href="/profile" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
| 63 |
+
个人设置
|
| 64 |
+
</Link>
|
| 65 |
+
<Link href="/billing" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
| 66 |
+
账单管理
|
| 67 |
+
</Link>
|
| 68 |
+
<div className="border-t border-gray-100 mt-2">
|
| 69 |
+
<button className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
| 70 |
+
退出登录
|
| 71 |
+
</button>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
)}
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</header>
|
| 79 |
+
);
|
| 80 |
+
}
|
components/Sidebar.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use client';
|
| 3 |
+
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
import Link from 'next/link';
|
| 6 |
+
import { usePathname } from 'next/navigation';
|
| 7 |
+
|
| 8 |
+
export default function Sidebar() {
|
| 9 |
+
const pathname = usePathname();
|
| 10 |
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
| 11 |
+
|
| 12 |
+
const menuItems = [
|
| 13 |
+
{ name: '工作台', href: '/dashboard', icon: 'ri-dashboard-line' },
|
| 14 |
+
{ name: 'Agent管理', href: '/agents', icon: 'ri-robot-line' },
|
| 15 |
+
{ name: '工作流', href: '/workflows', icon: 'ri-flow-chart' },
|
| 16 |
+
{ name: '知识库', href: '/knowledge', icon: 'ri-book-line' },
|
| 17 |
+
{ name: '模板市场', href: '/templates', icon: 'ri-store-line' },
|
| 18 |
+
{ name: '数据分析', href: '/analytics', icon: 'ri-bar-chart-line' },
|
| 19 |
+
{ name: '设置', href: '/settings', icon: 'ri-settings-line' }
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className={`bg-white border-r border-gray-200 flex-shrink-0 transition-all duration-300 ${
|
| 24 |
+
isCollapsed ? 'w-16' : 'w-64'
|
| 25 |
+
}`}>
|
| 26 |
+
<div className="p-6">
|
| 27 |
+
<h2 className="text-lg font-semibold text-gray-900 mb-6">导航菜单</h2>
|
| 28 |
+
<nav className="space-y-2">
|
| 29 |
+
{menuItems.map((item) => (
|
| 30 |
+
<Link
|
| 31 |
+
key={item.href}
|
| 32 |
+
href={item.href}
|
| 33 |
+
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors cursor-pointer ${
|
| 34 |
+
pathname === item.href
|
| 35 |
+
? 'bg-blue-50 text-blue-600 border-l-4 border-blue-600'
|
| 36 |
+
: 'text-gray-700 hover:bg-gray-50'
|
| 37 |
+
}`}
|
| 38 |
+
>
|
| 39 |
+
<div className="w-5 h-5 flex items-center justify-center">
|
| 40 |
+
<i className={`${item.icon} text-lg`}></i>
|
| 41 |
+
</div>
|
| 42 |
+
<span className="font-medium">{item.name}</span>
|
| 43 |
+
</Link>
|
| 44 |
+
))}
|
| 45 |
+
</nav>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
);
|
| 49 |
+
}
|
eslint.config.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { dirname } from "path";
|
| 2 |
+
import { fileURLToPath } from "url";
|
| 3 |
+
import { FlatCompat } from "@eslint/eslintrc";
|
| 4 |
+
|
| 5 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
+
const __dirname = dirname(__filename);
|
| 7 |
+
|
| 8 |
+
const compat = new FlatCompat({
|
| 9 |
+
baseDirectory: __dirname,
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
const eslintConfig = [
|
| 13 |
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
| 14 |
+
{
|
| 15 |
+
rules: {
|
| 16 |
+
"@typescript-eslint/no-explicit-any": "off"
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
export default eslintConfig;
|
index.html
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html>
|
| 3 |
-
<head>
|
| 4 |
-
<title>My app</title>
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<meta charset="utf-8">
|
| 7 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
-
</head>
|
| 9 |
-
<body class="flex justify-center items-center h-screen overflow-hidden bg-white font-sans text-center px-6">
|
| 10 |
-
<div class="w-full">
|
| 11 |
-
<span class="text-xs rounded-full mb-2 inline-block px-2 py-1 border border-amber-500/15 bg-amber-500/15 text-amber-500">🔥 New version dropped!</span>
|
| 12 |
-
<h1 class="text-4xl lg:text-6xl font-bold font-sans">
|
| 13 |
-
<span class="text-2xl lg:text-4xl text-gray-400 block font-medium">I'm ready to work,</span>
|
| 14 |
-
Ask me anything.
|
| 15 |
-
</h1>
|
| 16 |
-
</div>
|
| 17 |
-
<img src="https://enzostvs-deepsite.hf.space/arrow.svg" class="absolute bottom-8 left-0 w-[100px] transform rotate-[30deg]" />
|
| 18 |
-
<script></script>
|
| 19 |
-
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=daios007/test1" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
|
| 20 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
next.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
output: "export",
|
| 5 |
+
images: {
|
| 6 |
+
unoptimized: true,
|
| 7 |
+
},
|
| 8 |
+
typescript: {
|
| 9 |
+
// ignoreBuildErrors: true,
|
| 10 |
+
},
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
export default nextConfig;
|
package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "next-app",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "cross-env NODE_ENV=development next dev -H 0.0.0.0 -p 3000",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"lint": "next lint"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@react-google-maps/api": "^2.19.3",
|
| 12 |
+
"next": "15.3.2",
|
| 13 |
+
"react": "^19.0.0",
|
| 14 |
+
"react-dom": "^19.0.0",
|
| 15 |
+
"recharts": "^3.0.2"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@tailwindcss/postcss": "^4",
|
| 19 |
+
"@types/node": "^20",
|
| 20 |
+
"@types/react": "^19",
|
| 21 |
+
"@types/react-dom": "^19",
|
| 22 |
+
"autoprefixer": "^10.4.21",
|
| 23 |
+
"cross-env": "^7.0.3",
|
| 24 |
+
"postcss": "^8.5.5",
|
| 25 |
+
"tailwindcss": "^3.4.17",
|
| 26 |
+
"typescript": "^5"
|
| 27 |
+
}
|
| 28 |
+
}
|
postcss.config.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
}
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default config;
|
style.css
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
body {
|
| 2 |
-
padding: 2rem;
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
h1 {
|
| 7 |
-
font-size: 16px;
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|