Add Database page with new connection feature
- Reorganize project structure: move frontend to web/ directory - Add Database page with connection list (name, type, subtables, status, created, actions) - Integrate Element Plus for UI components with dark theme support - Add Quicksand font for rounded UI design - Configure root package.json to run frontend from web/ directory Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
47
README.md
47
README.md
@@ -1,8 +1,31 @@
|
||||
# X-Agents
|
||||
|
||||
Vue 3 + Vite + TypeScript + Tailwind CSS 项目
|
||||
前后端分离项目,前端使用 Vue 3 + Vite,后端支持 Python/Java
|
||||
|
||||
## 技术栈
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
X-Agents/
|
||||
├── web/ # 前端项目 (Vue 3 + Vite + TypeScript + Tailwind CSS)
|
||||
│ ├── src/ # 前端源代码
|
||||
│ │ ├── components/ # Vue 组件
|
||||
│ │ ├── views/ # 页面视图
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── App.vue # 根组件
|
||||
│ │ ├── main.ts # 入口文件
|
||||
│ │ └── style.css # 全局样式
|
||||
│ ├── public/ # 静态资源
|
||||
│ ├── index.html # 入口 HTML
|
||||
│ ├── package.json # 前端依赖
|
||||
│ ├── vite.config.ts # Vite 配置
|
||||
│ ├── tailwind.config.js # Tailwind 配置
|
||||
│ └── tsconfig.json # TypeScript 配置
|
||||
│
|
||||
└── src/ # 后端代码 (Python/Java)
|
||||
└── (待开发)
|
||||
```
|
||||
|
||||
## 前端技术栈
|
||||
|
||||
- Vue 3
|
||||
- Vite
|
||||
@@ -11,8 +34,15 @@ Vue 3 + Vite + TypeScript + Tailwind CSS 项目
|
||||
- Pinia (状态管理)
|
||||
- Vue Router
|
||||
- ECharts
|
||||
- Font Awesome
|
||||
|
||||
## 基础操作
|
||||
## 前端操作
|
||||
|
||||
### 进入前端目录
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
|
||||
@@ -40,13 +70,6 @@ npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
## 后端开发
|
||||
|
||||
```
|
||||
├── src/ # 源代码
|
||||
├── web/ # Web 资源
|
||||
├── index.html # 入口 HTML
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── tailwind.config.js # Tailwind 配置
|
||||
└── tsconfig.json # TypeScript 配置
|
||||
```
|
||||
后端代码请放在 `src/` 目录下,支持 Python 或 Java。
|
||||
|
||||
25
package.json
25
package.json
@@ -1,26 +1,11 @@
|
||||
{
|
||||
"name": "x-agent-dashboard",
|
||||
"name": "x-agents",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "^2.0.7"
|
||||
"dev": "cd web && npm run dev",
|
||||
"build": "cd web && npm run build",
|
||||
"preview": "cd web && npm run preview",
|
||||
"install:web": "cd web && npm install"
|
||||
}
|
||||
}
|
||||
|
||||
17
web/index.html
Normal file
17
web/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>X-Agent Dashboard</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2766
web/package-lock.json
generated
Normal file
2766
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
web/package.json
Normal file
27
web/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "x-agent-dashboard",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.13.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
41
web/src/App.vue
Normal file
41
web/src/App.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const showSidebar = computed(() => route.path !== '/')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :z-index="3000">
|
||||
<div class="flex min-h-screen bg-dark-900">
|
||||
<!-- 左侧侧边栏 (除了登录页) -->
|
||||
<Sidebar v-if="showSidebar" />
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main :class="showSidebar ? 'ml-64' : ''" class="flex-1 min-h-screen">
|
||||
<router-view class="page-content" />
|
||||
</main>
|
||||
</div>
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 页面进入动画 */
|
||||
.page-content {
|
||||
animation: page-enter 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes page-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
web/src/components/Sidebar.vue
Normal file
139
web/src/components/Sidebar.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
interface MenuItem {
|
||||
name: string
|
||||
icon: string
|
||||
badge?: string | number
|
||||
children?: MenuItem[]
|
||||
path?: string
|
||||
}
|
||||
|
||||
const mainMenu: MenuItem[] = [
|
||||
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
||||
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' },
|
||||
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
||||
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
||||
{ name: 'Database', icon: 'fa-database', path: '/database' },
|
||||
{ name: 'Knowledge', icon: 'fa-book' },
|
||||
]
|
||||
|
||||
const bottomMenu: MenuItem[] = [
|
||||
{ name: 'API Keys', icon: 'fa-key' },
|
||||
{ name: 'Settings', icon: 'fa-gear' },
|
||||
{ name: 'Team', icon: 'fa-users' },
|
||||
{ name: 'Service Accounts', icon: 'fa-user-shield' },
|
||||
{ name: 'Integrations', icon: 'fa-plug' },
|
||||
]
|
||||
|
||||
const bottomMenu2: MenuItem[] = [
|
||||
{ name: 'Information', icon: 'fa-circle-info' },
|
||||
{ name: 'Account', icon: 'fa-user' },
|
||||
]
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const currentPath = route.path
|
||||
const menuItem = mainMenu.find(item => item.path === currentPath)
|
||||
return menuItem ? menuItem.name : 'Dashboard'
|
||||
})
|
||||
|
||||
const navigateTo = (item: MenuItem) => {
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="w-64 bg-dark-950 h-screen flex flex-col fixed left-0 top-0 overflow-y-auto scrollbar-hide z-10">
|
||||
<!-- 顶部Logo与组织信息 -->
|
||||
<div class="p-5 flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
|
||||
<i class="fa-solid fa-basketball text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-base flex items-center gap-1 cursor-pointer">
|
||||
Organization
|
||||
<i class="fa-solid fa-chevron-down text-xs text-gray-500"></i>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">Saasfactor</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="flex-1 px-3 py-2">
|
||||
<ul class="space-y-1">
|
||||
<!-- Dashboard 激活项 -->
|
||||
<li v-for="item in mainMenu" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
||||
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
@click="navigateTo(item)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<span v-if="item.badge" class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">{{ item.badge }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<li class="my-4 border-t border-dark-500"></li>
|
||||
|
||||
<!-- API Keys -->
|
||||
<li v-for="item in bottomMenu" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-400 hover:text-white transition-colors text-sm"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<div v-if="item.name === 'Settings'" class="flex gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||
</div>
|
||||
<div v-else-if="item.name === 'Team'" class="flex gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<li class="my-4 border-t border-dark-500"></li>
|
||||
|
||||
<!-- Information -->
|
||||
<li v-for="item in bottomMenu2" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-400 hover:text-white transition-colors text-sm"
|
||||
>
|
||||
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
||||
<span>{{ item.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 底部用户信息 -->
|
||||
<div class="p-4 border-t border-dark-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="https://picsum.photos/id/64/40/40" alt="User Avatar" class="w-8 h-8 rounded-full object-cover">
|
||||
<div>
|
||||
<div class="font-medium text-sm text-gray-300">Alex Smith</div>
|
||||
<div class="text-xs text-gray-500">alex@gmail.com</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
15
web/src/main.ts
Normal file
15
web/src/main.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
45
web/src/router/index.ts
Normal file
45
web/src/router/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import Login from '@/views/Login.vue'
|
||||
import Agents from '@/views/Agents.vue'
|
||||
import MCP from '@/views/MCP.vue'
|
||||
import ModelAPIs from '@/views/ModelAPIs.vue'
|
||||
import Database from '@/views/Database.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: '/agents',
|
||||
name: 'agents',
|
||||
component: Agents
|
||||
},
|
||||
{
|
||||
path: '/mcp',
|
||||
name: 'mcp',
|
||||
component: MCP
|
||||
},
|
||||
{
|
||||
path: '/model-apis',
|
||||
name: 'model-apis',
|
||||
component: ModelAPIs
|
||||
},
|
||||
{
|
||||
path: '/database',
|
||||
name: 'database',
|
||||
component: Database
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
117
web/src/style.css
Normal file
117
web/src/style.css
Normal file
@@ -0,0 +1,117 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-dark-900 text-gray-100 font-sans;
|
||||
}
|
||||
|
||||
/* Element Plus 暗色主题 */
|
||||
html.dark {
|
||||
--el-bg-color: #171922;
|
||||
--el-bg-color-overlay: #171922;
|
||||
--el-text-color-primary: #ffffff;
|
||||
--el-text-color-regular: #a1a1aa;
|
||||
--el-border-color: #2a2c36;
|
||||
--el-border-color-light: #2a2c36;
|
||||
--el-fill-color-blank: #171922;
|
||||
--el-color-primary: #ff9500;
|
||||
}
|
||||
|
||||
html.dark .el-select {
|
||||
--el-select-input-focus-border-color: #ff9500;
|
||||
}
|
||||
|
||||
html.dark .el-select .el-input__wrapper {
|
||||
background-color: #171922;
|
||||
box-shadow: 0 0 0 1px #2a2c36 inset;
|
||||
}
|
||||
|
||||
html.dark .el-select .el-input__wrapper:hover {
|
||||
box-shadow: 0 0 0 1px #ff9500 inset;
|
||||
}
|
||||
|
||||
html.dark .el-select .el-input__wrapper.is-focus {
|
||||
box-shadow: 0 0 0 1px #ff9500 inset;
|
||||
}
|
||||
|
||||
html.dark .el-select .el-input__inner {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark .el-select-dropdown {
|
||||
background-color: #171922;
|
||||
border: 1px solid #2a2c36;
|
||||
}
|
||||
|
||||
html.dark .el-select-dropdown__item {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark .el-select-dropdown__item:hover {
|
||||
background-color: #1a1c25;
|
||||
}
|
||||
|
||||
html.dark .el-select-dropdown__item.is-selected {
|
||||
color: #ff9500;
|
||||
font-weight: 600;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
html.dark .el-popper.is-light {
|
||||
background: #171922;
|
||||
border: 1px solid #2a2c36;
|
||||
}
|
||||
|
||||
html.dark .el-popper.is-light .el-popper__arrow::before {
|
||||
background: #171922;
|
||||
border-color: #2a2c36;
|
||||
}
|
||||
|
||||
/* 柱状图增长动画 */
|
||||
@keyframes bar-grow {
|
||||
from {
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
animation: bar-grow 2.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
/* 进度条增长动画 */
|
||||
@keyframes progress-grow {
|
||||
from {
|
||||
width: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
width: var(--target-width);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
animation: progress-grow 2.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
654
web/src/views/Agents.vue
Normal file
654
web/src/views/Agents.vue
Normal file
@@ -0,0 +1,654 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
name: string
|
||||
framework: string
|
||||
status: 'running' | 'stopped' | 'error'
|
||||
mcpServers: number
|
||||
model: string
|
||||
createdAt: string
|
||||
description: string
|
||||
}
|
||||
|
||||
// Agents 数据
|
||||
const agents = ref<Agent[]>([
|
||||
{ id: 1, name: 'template-google-adk-api', framework: 'Google ADK', status: 'running', mcpServers: 2, model: 'gemini-2.0-flash', createdAt: '2025-04-10', description: 'Google ADK template for agent deployment' },
|
||||
{ id: 2, name: 'mcp-google-adk-api', framework: 'Google ADK', status: 'error', mcpServers: 1, model: 'gemini-2.0-flash', createdAt: '2025-04-08', description: 'MCP-enabled Google ADK agent' },
|
||||
{ id: 3, name: 'template-openai-api', framework: 'OpenAI', status: 'stopped', mcpServers: 3, model: 'gpt-4o', createdAt: '2025-04-05', description: 'OpenAI API template agent' },
|
||||
{ id: 4, name: 'pydantic-ai-agent', framework: 'PydanticAI', status: 'running', mcpServers: 2, model: 'gpt-4o-mini', createdAt: '2025-04-12', description: 'PydanticAI framework agent' },
|
||||
{ id: 5, name: 'langchain-agent', framework: 'LangChain', status: 'running', mcpServers: 4, model: 'claude-3-5-sonnet', createdAt: '2025-04-11', description: 'LangChain based agent with tools' },
|
||||
])
|
||||
|
||||
// 编辑状态
|
||||
const editingAgent = ref<Agent | null>(null)
|
||||
const isEditing = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref<string>('all')
|
||||
|
||||
// 新建 Agent 表单
|
||||
const newAgentForm = ref({
|
||||
name: '',
|
||||
framework: 'Google ADK',
|
||||
model: 'gemini-2.0-flash',
|
||||
description: '',
|
||||
mcpServers: [] as string[],
|
||||
})
|
||||
|
||||
const frameworks = [
|
||||
{ name: 'Google ADK', icon: 'fa-google', color: 'from-blue-500 to-blue-600' },
|
||||
{ name: 'OpenAI', icon: 'fa-openai', color: 'from-green-500 to-green-600' },
|
||||
{ name: 'PydanticAI', icon: 'fa-robot', color: 'from-purple-500 to-purple-600' },
|
||||
{ name: 'LangChain', icon: 'fa-link', color: 'from-orange-500 to-orange-600' },
|
||||
]
|
||||
|
||||
const models = [
|
||||
{ name: 'Google ADK', models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-pro'] },
|
||||
{ name: 'OpenAI', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'] },
|
||||
{ name: 'PydanticAI', models: ['gpt-4o', 'gpt-4o-mini', 'claude-3-5-sonnet'] },
|
||||
{ name: 'LangChain', models: ['claude-3-5-sonnet', 'gpt-4o', 'gpt-4o-mini'] },
|
||||
]
|
||||
|
||||
const availableMCPServers = [
|
||||
{ name: 'linear-demo', icon: 'fa-check-circle', status: 'connected' },
|
||||
{ name: 'google-maps', icon: 'fa-map-marker-alt', status: 'connected' },
|
||||
{ name: 'explorer-mcp', icon: 'fa-folder', status: 'connected' },
|
||||
{ name: 'postgres-mcp', icon: 'fa-database', status: 'disconnected' },
|
||||
{ name: 'github-mcp', icon: 'fa-github', status: 'disconnected' },
|
||||
]
|
||||
|
||||
// 打开新建弹窗
|
||||
const openCreate = () => {
|
||||
newAgentForm.value = {
|
||||
name: '',
|
||||
framework: 'Google ADK',
|
||||
model: 'gemini-2.0-flash',
|
||||
description: '',
|
||||
mcpServers: [],
|
||||
}
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
// 关闭新建弹窗
|
||||
const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 保存新建
|
||||
const saveNewAgent = () => {
|
||||
const newId = Math.max(...agents.value.map(a => a.id)) + 1
|
||||
agents.value.push({
|
||||
id: newId,
|
||||
name: newAgentForm.value.name || 'Untitled Agent',
|
||||
framework: newAgentForm.value.framework,
|
||||
status: 'stopped',
|
||||
mcpServers: newAgentForm.value.mcpServers.length,
|
||||
model: newAgentForm.value.model,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
description: newAgentForm.value.description,
|
||||
})
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 切换 MCP 服务器
|
||||
const toggleMCPServer = (serverName: string) => {
|
||||
const index = newAgentForm.value.mcpServers.indexOf(serverName)
|
||||
if (index === -1) {
|
||||
newAgentForm.value.mcpServers.push(serverName)
|
||||
} else {
|
||||
newAgentForm.value.mcpServers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑表单数据
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
framework: '',
|
||||
model: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (agent: Agent) => {
|
||||
editingAgent.value = agent
|
||||
editForm.value = {
|
||||
name: agent.name,
|
||||
framework: agent.framework,
|
||||
model: agent.model,
|
||||
description: agent.description,
|
||||
}
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = () => {
|
||||
if (editingAgent.value) {
|
||||
const index = agents.value.findIndex(a => a.id === editingAgent.value!.id)
|
||||
if (index !== -1) {
|
||||
agents.value[index] = {
|
||||
...agents.value[index],
|
||||
...editForm.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
editingAgent.value = null
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const toggleStatus = (agent: Agent) => {
|
||||
if (agent.status === 'running') {
|
||||
agent.status = 'stopped'
|
||||
} else if (agent.status === 'stopped') {
|
||||
agent.status = 'running'
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 Agent
|
||||
const deleteAgent = (id: number) => {
|
||||
agents.value = agents.value.filter(a => a.id !== id)
|
||||
}
|
||||
|
||||
// 过滤后的 Agents
|
||||
const filteredAgents = () => {
|
||||
return agents.value.filter(agent => {
|
||||
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
agent.framework.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
}
|
||||
|
||||
// 状态颜色
|
||||
const statusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running': return 'bg-primary-success'
|
||||
case 'stopped': return 'bg-gray-500'
|
||||
case 'error': return 'bg-primary-danger'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 模态框进入动画 */
|
||||
@keyframes modal-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; transform: translateY(-5px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
0% { opacity: 0; transform: scale(0.9); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.animate-modal-in {
|
||||
animation: modal-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scale-in 0.5s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<!-- 主内容区域 -->
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-robot text-gray-400"></i>
|
||||
<span class="font-medium">Agents</span>
|
||||
</div>
|
||||
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search agents..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<select
|
||||
v-model="filterStatus"
|
||||
class="bg-dark-600 border border-dark-500 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="running">Running</option>
|
||||
<option value="stopped">Stopped</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Agents 列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Agent Name</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Framework</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">MCP Servers</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="agent in filteredAgents()" :key="agent.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
|
||||
<td class="px-5 py-4">
|
||||
<div class="font-medium">{{ agent.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ agent.description }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ agent.framework }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-300">{{ agent.model }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="text-primary-cyan">{{ agent.mcpServers }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusClass(agent.status)"></span>
|
||||
<span class="capitalize text-sm">{{ agent.status }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ agent.createdAt }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="toggleStatus(agent)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
:title="agent.status === 'running' ? 'Stop' : 'Start'"
|
||||
>
|
||||
<i :class="['fa-solid', agent.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="openEdit(agent)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteAgent(agent.id)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredAgents().length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-robot text-4xl mb-3"></i>
|
||||
<p>No agents found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="cancelEdit">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Agent</h3>
|
||||
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Agent Name</label>
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
type="text"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Framework</label>
|
||||
<select
|
||||
v-model="editForm.framework"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
<option value="Google ADK">Google ADK</option>
|
||||
<option value="OpenAI">OpenAI</option>
|
||||
<option value="PydanticAI">PydanticAI</option>
|
||||
<option value="LangChain">LangChain</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
|
||||
<select
|
||||
v-model="editForm.model"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
<option value="gemini-2.0-flash">gemini-2.0-flash</option>
|
||||
<option value="gpt-4o">gpt-4o</option>
|
||||
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
||||
<option value="claude-3-5-sonnet">claude-3-5-sonnet</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="editForm.description"
|
||||
rows="3"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗底部 -->
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="cancelEdit"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveEdit"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 新建 Agent 模态框 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isCreating" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4" @click.self="closeCreate">
|
||||
<div class="bg-dark-800 rounded-2xl w-full max-w-6xl h-[85vh] border border-dark-600 shadow-2xl overflow-hidden flex flex-col animate-modal-in">
|
||||
<!-- 模态框头部 -->
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center animate-pulse">
|
||||
<i class="fa-solid fa-robot text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-white">Create New Agent</h3>
|
||||
<p class="text-sm text-gray-400">Configure your agent workflow</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="closeCreate" class="text-gray-400 hover:text-white transition-all p-2 hover:bg-dark-600 rounded-lg">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 三栏布局主体 -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- 左侧:框架选择 -->
|
||||
<div class="w-72 bg-dark-700/50 border-r border-dark-600 p-5 overflow-y-auto">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<i class="fa-solid fa-layer-group text-primary-orange"></i>
|
||||
<h4 class="font-medium text-white">Framework</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="fw in frameworks"
|
||||
:key="fw.name"
|
||||
@click="newAgentForm.framework = fw.name; newAgentForm.model = models.find(m => m.name === fw.name)?.models[0] || ''"
|
||||
class="p-4 rounded-xl border-2 cursor-pointer transition-all duration-300 hover:scale-105"
|
||||
:class="newAgentForm.framework === fw.name
|
||||
? 'border-primary-orange bg-dark-600 shadow-lg shadow-primary-orange/20'
|
||||
: 'border-dark-500 bg-dark-700 hover:border-gray-500'"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div :class="['w-10 h-10 rounded-lg bg-gradient-to-br flex items-center justify-center', fw.color]">
|
||||
<i :class="['fa-solid', fw.icon, 'text-white text-lg']"></i>
|
||||
</div>
|
||||
<span class="font-medium text-white">{{ fw.name }}</span>
|
||||
</div>
|
||||
<div v-if="newAgentForm.framework === fw.name" class="mt-2 flex items-center gap-1 text-primary-orange text-sm animate-fade-in">
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
<span>Selected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型选择 -->
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fa-solid fa-brain text-primary-cyan"></i>
|
||||
<h4 class="font-medium text-white">Model</h4>
|
||||
</div>
|
||||
<select
|
||||
v-model="newAgentForm.model"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-primary-orange transition-colors cursor-pointer"
|
||||
>
|
||||
<option v-for="model in models.find(m => m.name === newAgentForm.framework)?.models" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Agent 名称 -->
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fa-solid fa-tag text-primary-purple"></i>
|
||||
<h4 class="font-medium text-white">Agent Name</h4>
|
||||
</div>
|
||||
<input
|
||||
v-model="newAgentForm.name"
|
||||
type="text"
|
||||
placeholder="Enter agent name..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange transition-colors"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:流程画布 -->
|
||||
<div class="flex-1 bg-dark-900 relative overflow-hidden">
|
||||
<!-- 背景网格 -->
|
||||
<div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(circle, #1E6BF9 1px, transparent 1px); background-size: 30px 30px;"></div>
|
||||
|
||||
<!-- 流程节点 -->
|
||||
<div class="h-full flex flex-col items-center justify-center p-8 relative z-10">
|
||||
<!-- 开始节点 -->
|
||||
<div class="node bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl w-64 p-4 shadow-lg shadow-blue-500/30 animate-float">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-play text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-white">Start</div>
|
||||
<div class="text-xs text-blue-200">Agent begins</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<div class="h-8 w-0.5 bg-gradient-to-b from-blue-500 to-primary-orange animate-pulse"></div>
|
||||
|
||||
<!-- 框架节点 -->
|
||||
<div class="node bg-dark-700 border-2 border-primary-orange rounded-xl w-64 p-4 shadow-lg shadow-primary-orange/20 animate-scale-in">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
|
||||
<i :class="['fa-solid', frameworks.find(f => f.name === newAgentForm.framework)?.icon || 'fa-robot', 'text-white']"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-white">{{ newAgentForm.framework }}</div>
|
||||
<div class="text-xs text-gray-400">{{ newAgentForm.model }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<div class="h-8 w-0.5 bg-gradient-to-b from-primary-orange to-purple-500 animate-pulse"></div>
|
||||
|
||||
<!-- MCP 服务器节点 -->
|
||||
<div class="node bg-dark-700 border-2 border-purple-500 rounded-xl w-64 p-4 shadow-lg shadow-purple-500/20 animate-scale-in" style="animation-delay: 0.2s">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-server text-purple-400"></i>
|
||||
<span class="font-medium text-white">MCP Servers</span>
|
||||
</div>
|
||||
<span class="bg-purple-500/30 text-purple-300 text-xs px-2 py-0.5 rounded">{{ newAgentForm.mcpServers.length }} connected</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span v-for="mcp in newAgentForm.mcpServers" :key="mcp" class="bg-dark-600 text-gray-300 text-xs px-2 py-1 rounded flex items-center gap-1">
|
||||
<i class="fa-solid fa-check-circle text-green-400"></i>
|
||||
{{ mcp }}
|
||||
</span>
|
||||
<span v-if="newAgentForm.mcpServers.length === 0" class="text-gray-500 text-xs">No servers selected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<div class="h-8 w-0.5 bg-gradient-to-b from-purple-500 to-green-500 animate-pulse"></div>
|
||||
|
||||
<!-- 结束节点 -->
|
||||
<div class="node bg-gradient-to-r from-green-500 to-emerald-600 rounded-xl w-64 p-4 shadow-lg shadow-green-500/30 animate-float" style="animation-delay: 0.5s">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-check text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-white">Ready</div>
|
||||
<div class="text-xs text-green-200">Agent configured</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 装饰性光效 -->
|
||||
<div class="absolute top-1/4 -left-20 w-40 h-40 bg-primary-orange/10 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div class="absolute bottom-1/4 -right-20 w-40 h-40 bg-purple-500/10 rounded-full blur-3xl animate-pulse" style="animation-delay: 0.5s"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:MCP 服务器选择 -->
|
||||
<div class="w-80 bg-dark-700/50 border-l border-dark-600 p-5 overflow-y-auto">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<i class="fa-solid fa-plug text-primary-success"></i>
|
||||
<h4 class="font-medium text-white">MCP Servers</h4>
|
||||
<span class="text-xs text-gray-500">({{ newAgentForm.mcpServers.length }} selected)</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="server in availableMCPServers"
|
||||
:key="server.name"
|
||||
@click="toggleMCPServer(server.name)"
|
||||
class="p-4 rounded-xl border-2 cursor-pointer transition-all duration-300 hover:scale-105"
|
||||
:class="newAgentForm.mcpServers.includes(server.name)
|
||||
? 'border-green-500 bg-dark-600 shadow-lg shadow-green-500/20'
|
||||
: 'border-dark-500 bg-dark-700 hover:border-gray-500'"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-dark-500 flex items-center justify-center">
|
||||
<i :class="['fa-solid', server.icon, server.status === 'connected' ? 'text-green-400' : 'text-gray-500']"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-white">{{ server.name }}</div>
|
||||
<div class="text-xs flex items-center gap-1" :class="server.status === 'connected' ? 'text-green-400' : 'text-gray-500'">
|
||||
<span class="w-1.5 h-1.5 rounded-full" :class="server.status === 'connected' ? 'bg-green-400' : 'bg-gray-500'"></span>
|
||||
{{ server.status }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-6 h-6 rounded-md flex items-center justify-center transition-all"
|
||||
:class="newAgentForm.mcpServers.includes(server.name) ? 'bg-green-500' : 'bg-dark-500'"
|
||||
>
|
||||
<i v-if="newAgentForm.mcpServers.includes(server.name)" class="fa-solid fa-check text-white text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fa-solid fa-align-left text-gray-400"></i>
|
||||
<h4 class="font-medium text-white">Description</h4>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="newAgentForm.description"
|
||||
rows="4"
|
||||
placeholder="Describe your agent's purpose..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange transition-colors resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="flex items-center justify-between p-5 border-t border-dark-600 bg-dark-700/50">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<i class="fa-solid fa-circle-info"></i>
|
||||
<span>Configure your agent settings</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="closeCreate"
|
||||
class="px-6 py-2.5 rounded-xl bg-dark-600 text-gray-300 hover:bg-dark-500 border border-dark-500 transition-all hover:scale-105"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveNewAgent"
|
||||
class="px-6 py-2.5 rounded-xl bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all hover:scale-105 shadow-lg shadow-primary-orange/30 flex items-center gap-2"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Create Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
439
web/src/views/Dashboard.vue
Normal file
439
web/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 显示的数字(用于动画)
|
||||
const displayStats = ref({
|
||||
activeAgents: 0,
|
||||
activeMCP: 0,
|
||||
activeModels: 0,
|
||||
requests: 0,
|
||||
agentsCalls: 0,
|
||||
mcpCalls: 0,
|
||||
modelRequests: 0,
|
||||
})
|
||||
|
||||
// 目标数字
|
||||
const targetStats = {
|
||||
activeAgents: 3,
|
||||
activeMCP: 21,
|
||||
activeModels: 13,
|
||||
requests: 36,
|
||||
agentsCalls: 3,
|
||||
mcpCalls: 21,
|
||||
modelRequests: 13,
|
||||
}
|
||||
|
||||
// 数字滚动动画函数
|
||||
const animateNumber = (key: keyof typeof displayStats.value, target: number) => {
|
||||
const duration = 2000
|
||||
const startTime = Date.now()
|
||||
const startValue = 0
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
// 使用easeOut曲线
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
displayStats.value[key] = Math.floor(startValue + (target - startValue) * eased)
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
} else {
|
||||
displayStats.value[key] = target
|
||||
}
|
||||
}
|
||||
animate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 页面加载后依次动画每个数字
|
||||
setTimeout(() => animateNumber('activeAgents', targetStats.activeAgents), 0)
|
||||
setTimeout(() => animateNumber('activeMCP', targetStats.activeMCP), 300)
|
||||
setTimeout(() => animateNumber('activeModels', targetStats.activeModels), 600)
|
||||
// deployment insights 数字动画(延迟900ms,在顶部stats之后)
|
||||
setTimeout(() => animateNumber('requests', targetStats.requests), 900)
|
||||
setTimeout(() => animateNumber('agentsCalls', targetStats.agentsCalls), 1000)
|
||||
setTimeout(() => animateNumber('mcpCalls', targetStats.mcpCalls), 1100)
|
||||
setTimeout(() => animateNumber('modelRequests', targetStats.modelRequests), 1200)
|
||||
})
|
||||
|
||||
// Agents列表
|
||||
const agents = ref([
|
||||
{ name: 'template-google-adk-api', count: 1, color: 'bg-primary-yellow', status: 'success' },
|
||||
{ name: 'mcp-google-adk-api', count: 1, color: 'bg-primary-cyan', status: 'error' },
|
||||
{ name: 'template-openai-api', count: 1, color: 'bg-primary-purple', status: 'error' },
|
||||
])
|
||||
|
||||
// MCP Servers列表
|
||||
const mcpServers = ref([
|
||||
{ name: 'linear-demo', count: 15, color: 'bg-primary-yellow' },
|
||||
{ name: 'google-maps', count: 4, color: 'bg-primary-cyan' },
|
||||
{ name: 'explorer-mcp', count: 2, color: 'bg-primary-purple' },
|
||||
])
|
||||
|
||||
// Models列表
|
||||
const models = ref([
|
||||
{ name: 'gpt-40-2024-08-12', count: 2, color: 'bg-primary-yellow' },
|
||||
{ name: 'cerebras-sandbox', count: 6, color: 'bg-primary-cyan' },
|
||||
{ name: 'sandbox-openai', count: 5, color: 'bg-primary-purple' },
|
||||
])
|
||||
|
||||
// 图表数据
|
||||
const chartData = ref([
|
||||
{ time: '3:02 PM', agents: 1, mcp: 2, models: 1.5 },
|
||||
{ time: '3:07 PM', agents: 2, mcp: 2.5, models: 2 },
|
||||
{ time: '3:12 PM', agents: 2.5, mcp: 5, models: 4 },
|
||||
{ time: '3:17 PM', agents: 2.5, mcp: 3, models: 2 },
|
||||
{ time: '3:22 PM', agents: 1.5, mcp: 2.5, models: 1.5 },
|
||||
{ time: '3:27 PM', agents: 1.5, mcp: 3, models: 2.5 },
|
||||
{ time: '3:32 PM', agents: 1, mcp: 2, models: 2.5 },
|
||||
{ time: '3:37 PM', agents: 2.5, mcp: 8, models: 3 },
|
||||
{ time: '3:42 PM', agents: 1, mcp: 5, models: 2.5 },
|
||||
{ time: '3:47 PM', agents: 2.5, mcp: 3, models: 2 },
|
||||
])
|
||||
|
||||
// Top 10请求
|
||||
const topRequests = ref([
|
||||
{ name: 'gpt-40-2024-08-12', type: 'cube', count: 7 },
|
||||
{ name: 'google-maps', type: 'code', count: 4 },
|
||||
{ name: 'explorer-mcp', type: 'code', count: 2 },
|
||||
{ name: 'template-google-adk-api', type: 'cube', count: 4 },
|
||||
{ name: 'linear-demo', type: 'cube', count: 2 },
|
||||
{ name: 'cerebras-sandbox', type: 'code', count: 1 },
|
||||
{ name: 'sandbox-openai', type: 'cube', count: 2 },
|
||||
])
|
||||
|
||||
// What's new
|
||||
const whatsNew = ref([
|
||||
{ title: 'New framework supported: PydanticAI', desc: 'Added support for PydanticAI framework', date: '2025-04-12' },
|
||||
{ title: 'New framework supported: Google ADK', desc: 'Added support for Google ADK (Agent Development Kit) framework', date: '2025-04-07' },
|
||||
{ title: 'Improved Analytics Dashboard', desc: 'Enhanced real-time monitoring with faster data refresh', date: '2025-04-15' },
|
||||
])
|
||||
|
||||
// Recent requests
|
||||
const recentRequests = ref([
|
||||
{ name: 'linear-demo', type: 'cube', time: '21 hours', status: 'success' },
|
||||
{ name: 'myagent', type: 'robot', time: '21 hours', status: 'success' },
|
||||
{ name: 'linear-demo', type: 'cube', time: '21 hours', status: 'success' },
|
||||
{ name: 'gpt-40', type: 'code', time: '21 hours', status: 'success' },
|
||||
{ name: 'linear-demo', type: 'cube', time: '21 hours', status: 'success' },
|
||||
])
|
||||
|
||||
// 开关状态
|
||||
const agentErrorEnabled = ref(true)
|
||||
const mcpErrorEnabled = ref(false)
|
||||
const modelErrorEnabled = ref(false)
|
||||
|
||||
// Top requests标签切换
|
||||
const topRequestsTab = ref<'general' | 'errors'>('general')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 主内容区域 -->
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航与日期选择区 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-gauge text-gray-400"></i>
|
||||
<span class="font-medium">Dashboard</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-gray-400">Date Range</span>
|
||||
<div class="flex items-center gap-2 bg-dark-600 rounded-lg px-3 py-2">
|
||||
<span>10 December</span>
|
||||
<span class="text-gray-400">To</span>
|
||||
<span>12 December</span>
|
||||
<i class="fa-solid fa-calendar text-gray-400"></i>
|
||||
</div>
|
||||
<button class="text-primary-orange hover:text-orange-400 transition-colors">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片网格布局 -->
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
<!-- 第一行:3个状态卡片 -->
|
||||
<!-- Active Agents 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Active Agents</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">Errors</span>
|
||||
<!-- 开启状态开关 -->
|
||||
<div
|
||||
class="w-10 h-5 rounded-full relative cursor-pointer transition-colors"
|
||||
:class="agentErrorEnabled ? 'bg-primary-orange' : 'bg-dark-500'"
|
||||
@click="agentErrorEnabled = !agentErrorEnabled"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform"
|
||||
:class="agentErrorEnabled ? 'right-0.5' : 'left-0.5'"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-4xl font-bold mb-4">{{ displayStats.activeAgents }}</div>
|
||||
<!-- 进度条 - 三个同时动画 -->
|
||||
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4 relative">
|
||||
<!-- 黄色 -->
|
||||
<div class="absolute left-0 top-0 h-full bg-primary-yellow progress-bar" style="--target-width: 33%"></div>
|
||||
<!-- 蓝色 -->
|
||||
<div class="absolute top-0 h-full bg-primary-cyan progress-bar" style="left: 33%; --target-width: 33%"></div>
|
||||
<!-- 紫色 -->
|
||||
<div class="absolute top-0 h-full bg-primary-purple progress-bar" style="left: 66%; --target-width: 34%"></div>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<ul class="space-y-2">
|
||||
<li v-for="agent in agents" :key="agent.name" class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm" :class="agent.color"></span>
|
||||
<span class="text-sm text-gray-300">{{ agent.name }}</span>
|
||||
<span v-if="agent.status === 'error'" class="bg-primary-danger text-white text-xs px-1.5 py-0.5 rounded">Error</span>
|
||||
</div>
|
||||
<span class="text-sm">{{ agent.count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Active MCP Servers 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Active MCP Servers</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">Errors</span>
|
||||
<div
|
||||
class="w-10 h-5 rounded-full relative cursor-pointer transition-colors"
|
||||
:class="mcpErrorEnabled ? 'bg-primary-orange' : 'bg-dark-500'"
|
||||
@click="mcpErrorEnabled = !mcpErrorEnabled"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0.5 w-4 h-4 rounded-full transition-transform"
|
||||
:class="mcpErrorEnabled ? 'right-0.5 bg-white' : 'left-0.5 bg-gray-400'"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-4xl font-bold mb-4">{{ displayStats.activeMCP }}</div>
|
||||
<!-- 进度条 - 三个同时动画 -->
|
||||
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4 relative">
|
||||
<div class="absolute left-0 top-0 h-full bg-primary-yellow progress-bar" style="--target-width: 71%"></div>
|
||||
<div class="absolute top-0 h-full bg-primary-cyan progress-bar" style="left: 71%; --target-width: 19%"></div>
|
||||
<div class="absolute top-0 h-full bg-primary-purple progress-bar" style="left: 90%; --target-width: 10%"></div>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<ul class="space-y-2">
|
||||
<li v-for="server in mcpServers" :key="server.name" class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm" :class="server.color"></span>
|
||||
<span class="text-sm text-gray-300">{{ server.name }}</span>
|
||||
</div>
|
||||
<span class="text-sm">{{ server.count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Active Models 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Active Models</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">Errors</span>
|
||||
<div
|
||||
class="w-10 h-5 rounded-full relative cursor-pointer transition-colors"
|
||||
:class="modelErrorEnabled ? 'bg-primary-orange' : 'bg-dark-500'"
|
||||
@click="modelErrorEnabled = !modelErrorEnabled"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0.5 w-4 h-4 rounded-full transition-transform"
|
||||
:class="modelErrorEnabled ? 'right-0.5 bg-white' : 'left-0.5 bg-gray-400'"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-4xl font-bold mb-4">{{ displayStats.activeModels }}</div>
|
||||
<!-- 进度条 - 三个同时动画 -->
|
||||
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4 relative">
|
||||
<div class="absolute left-0 top-0 h-full bg-primary-yellow progress-bar" style="--target-width: 46%"></div>
|
||||
<div class="absolute top-0 h-full bg-primary-cyan progress-bar" style="left: 46%; --target-width: 15%"></div>
|
||||
<div class="absolute top-0 h-full bg-primary-purple progress-bar" style="left: 61%; --target-width: 39%"></div>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<ul class="space-y-2">
|
||||
<li v-for="model in models" :key="model.name" class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm" :class="model.color"></span>
|
||||
<span class="text-sm text-gray-300">{{ model.name }}</span>
|
||||
</div>
|
||||
<span class="text-sm">{{ model.count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 第二行 -->
|
||||
<!-- All deployment request Insights 卡片(跨2列) -->
|
||||
<div class="bg-dark-700 rounded-xl p-5 col-span-2">
|
||||
<h3 class="font-semibold text-lg mb-4">All deployment request Insights</h3>
|
||||
<!-- 数据概览 -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">Requests</div>
|
||||
<div class="text-2xl font-bold">{{ displayStats.requests }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">Agents calls</div>
|
||||
<div class="text-2xl font-bold">{{ displayStats.agentsCalls }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">MCP servers calls</div>
|
||||
<div class="text-2xl font-bold">{{ displayStats.mcpCalls }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">Models requests</div>
|
||||
<div class="text-2xl font-bold">{{ displayStats.modelRequests }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 纯CSS模拟柱状图 -->
|
||||
<div class="relative h-52 w-full">
|
||||
<!-- 绘图区(网格线+Y轴+柱子) 预留底部20px给时间标签 -->
|
||||
<div class="relative h-[calc(100%-20px)] w-full">
|
||||
<!-- 横向网格线 -->
|
||||
<div class="absolute left-0 top-0 w-full h-full z-0">
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-0"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[25%]"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[50%]"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[75%]"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[100%]"></div>
|
||||
</div>
|
||||
|
||||
<!-- Y轴刻度 -->
|
||||
<div class="absolute left-0 top-0 h-full flex flex-col justify-between text-xs text-gray-500 z-10">
|
||||
<span>8</span>
|
||||
<span>6</span>
|
||||
<span>4</span>
|
||||
<span>2</span>
|
||||
<span>0</span>
|
||||
</div>
|
||||
|
||||
<!-- 柱状图容器 柱子底部对齐0网格线 -->
|
||||
<div class="ml-6 h-full flex items-end justify-between gap-1 z-10 relative">
|
||||
<div
|
||||
v-for="item in chartData"
|
||||
:key="item.time"
|
||||
class="flex items-end gap-1 h-full w-full justify-center"
|
||||
>
|
||||
<!-- 所有柱子同时动画 -->
|
||||
<div
|
||||
class="w-3 bg-primary-yellow rounded-t-sm chart-bar"
|
||||
:style="{ height: (item.mcp / 8 * 100) + '%' }"
|
||||
></div>
|
||||
<div
|
||||
class="w-3 bg-primary-cyan rounded-t-sm chart-bar"
|
||||
:style="{ height: (item.models / 8 * 100) + '%' }"
|
||||
></div>
|
||||
<div
|
||||
class="w-3 bg-primary-purple rounded-t-sm chart-bar"
|
||||
:style="{ height: (item.agents / 8 * 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间标签区 -->
|
||||
<div class="ml-6 w-full flex justify-between gap-1 mt-1">
|
||||
<span v-for="item in chartData" :key="item.time" class="text-xs text-gray-500 w-full text-center">{{ item.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图例 -->
|
||||
<div class="flex justify-center gap-6 mt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
|
||||
<span class="text-xs text-gray-400">Agents calls</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
|
||||
<span class="text-xs text-gray-400">MCP servers calls</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
|
||||
<span class="text-xs text-gray-400">Models requests</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top 10 requests 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<h3 class="font-semibold text-lg mb-4">Top 10 requests</h3>
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex mb-4 border border-dark-500 rounded-lg overflow-hidden">
|
||||
<button
|
||||
class="flex-1 py-2 text-sm font-medium transition-colors"
|
||||
:class="topRequestsTab === 'general' ? 'bg-dark-500 text-white' : 'bg-dark-600 text-gray-400 hover:text-white'"
|
||||
@click="topRequestsTab = 'general'"
|
||||
>
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2 text-sm font-medium transition-colors"
|
||||
:class="topRequestsTab === 'errors' ? 'bg-dark-500 text-white' : 'bg-dark-600 text-gray-400 hover:text-white'"
|
||||
@click="topRequestsTab = 'errors'"
|
||||
>
|
||||
Errors
|
||||
</button>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<ul class="space-y-3">
|
||||
<li v-for="req in topRequests" :key="req.name" class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i :class="['fa-solid', req.type === 'cube' ? 'fa-cube' : 'fa-code', 'text-gray-400']"></i>
|
||||
<span class="text-sm text-gray-300">{{ req.name }}</span>
|
||||
</div>
|
||||
<span class="text-sm">{{ req.count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 第三行 -->
|
||||
<!-- What's new 卡片(跨2列) -->
|
||||
<div class="bg-dark-700 rounded-xl p-5 col-span-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">What's new</h3>
|
||||
<a href="#" class="text-primary-orange text-sm flex items-center gap-1 hover:text-orange-400 transition-colors">
|
||||
Full change log
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-4">Stay up to date with our latest feature and improvements</p>
|
||||
<!-- 更新列表 -->
|
||||
<ul class="space-y-4">
|
||||
<li v-for="item in whatsNew" :key="item.title" class="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 class="font-medium mb-1">{{ item.title }}</h4>
|
||||
<p class="text-sm text-gray-400">{{ item.desc }}</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{{ item.date }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Recent requests (10) 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<h3 class="font-semibold text-lg mb-4">Recent requests (10)</h3>
|
||||
<!-- 列表 -->
|
||||
<ul class="space-y-3">
|
||||
<li v-for="(req, index) in recentRequests" :key="index" class="flex items-center gap-3">
|
||||
<i :class="['fa-solid', req.type === 'cube' ? 'fa-cube' : req.type === 'robot' ? 'fa-robot' : 'fa-code', 'text-gray-400']"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">{{ req.name }}</span>
|
||||
<span class="text-xs text-gray-500">in {{ req.time }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-primary-success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>{{ req.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
378
web/src/views/Database.vue
Normal file
378
web/src/views/Database.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Database {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
subtables: number
|
||||
status: 'connected' | 'disconnected' | 'error'
|
||||
createdAt: string
|
||||
description: string
|
||||
}
|
||||
|
||||
// 数据库数据
|
||||
const databases = ref<Database[]>([
|
||||
{ id: 1, name: 'production-db', type: 'MySQL', subtables: 24, status: 'connected', createdAt: '2025-04-10', description: 'Main production database' },
|
||||
{ id: 2, name: 'staging-db', type: 'MySQL', subtables: 18, status: 'connected', createdAt: '2025-04-08', description: 'Staging environment database' },
|
||||
{ id: 3, name: 'analytics-db', type: 'ClickHouse', subtables: 56, status: 'connected', createdAt: '2025-04-05', description: 'Analytics and reporting database' },
|
||||
{ id: 4, name: 'cache-db', type: 'Redis', subtables: 8, status: 'connected', createdAt: '2025-04-12', description: 'Cache layer database' },
|
||||
{ id: 5, name: 'test-db', type: 'MySQL', subtables: 12, status: 'disconnected', createdAt: '2025-04-11', description: 'Testing database' },
|
||||
])
|
||||
|
||||
// 编辑状态
|
||||
const editingDb = ref<Database | null>(null)
|
||||
const isEditing = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref<string>('all')
|
||||
|
||||
const dbTypes = ['MySQL']
|
||||
|
||||
// 新建 Database 表单
|
||||
const newDbForm = ref({
|
||||
name: '',
|
||||
type: 'MySQL',
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 打开新建弹窗
|
||||
const openCreate = () => {
|
||||
newDbForm.value = {
|
||||
name: '',
|
||||
type: 'MySQL',
|
||||
description: '',
|
||||
}
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
// 关闭新建弹窗
|
||||
const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 保存新建
|
||||
const saveNewDb = () => {
|
||||
const newId = Math.max(...databases.value.map(d => d.id)) + 1
|
||||
databases.value.push({
|
||||
id: newId,
|
||||
name: newDbForm.value.name || 'Untitled Database',
|
||||
type: newDbForm.value.type,
|
||||
subtables: 0,
|
||||
status: 'disconnected',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
description: newDbForm.value.description,
|
||||
})
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 编辑表单数据
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
type: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (db: Database) => {
|
||||
editingDb.value = db
|
||||
editForm.value = {
|
||||
name: db.name,
|
||||
type: db.type,
|
||||
description: db.description,
|
||||
}
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = () => {
|
||||
if (editingDb.value) {
|
||||
const index = databases.value.findIndex(d => d.id === editingDb.value!.id)
|
||||
if (index !== -1) {
|
||||
databases.value[index] = {
|
||||
...databases.value[index],
|
||||
...editForm.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
editingDb.value = null
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const toggleStatus = (db: Database) => {
|
||||
if (db.status === 'connected') {
|
||||
db.status = 'disconnected'
|
||||
} else if (db.status === 'disconnected') {
|
||||
db.status = 'connected'
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 Database
|
||||
const deleteDb = (id: number) => {
|
||||
databases.value = databases.value.filter(d => d.id !== id)
|
||||
}
|
||||
|
||||
// 过滤后的 Databases
|
||||
const filteredDatabases = () => {
|
||||
return databases.value.filter(db => {
|
||||
const matchSearch = db.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
db.type.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || db.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
}
|
||||
|
||||
// 状态颜色
|
||||
const statusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'connected': return 'bg-primary-success'
|
||||
case 'disconnected': return 'bg-gray-500'
|
||||
case 'error': return 'bg-primary-danger'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 主内容区域 -->
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-database text-gray-400"></i>
|
||||
<span class="font-medium">Database</span>
|
||||
</div>
|
||||
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Connection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search databases..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||
<el-option label="All Status" value="all" />
|
||||
<el-option label="Connected" value="connected" />
|
||||
<el-option label="Disconnected" value="disconnected" />
|
||||
<el-option label="Error" value="error" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- Database 列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Database Name</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Type</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Subtables</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="db in filteredDatabases()" :key="db.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
|
||||
<td class="px-5 py-4">
|
||||
<div class="font-medium">{{ db.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ db.description }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ db.type }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="text-primary-cyan">{{ db.subtables }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span
|
||||
class="px-3 py-1 rounded-full text-xs capitalize"
|
||||
:class="{
|
||||
'bg-green-500/20 text-green-400': db.status === 'connected',
|
||||
'bg-gray-500/20 text-gray-400': db.status === 'disconnected',
|
||||
'bg-red-500/20 text-red-400': db.status === 'error'
|
||||
}"
|
||||
>{{ db.status }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ db.createdAt }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button
|
||||
@click="toggleStatus(db)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
:title="db.status === 'connected' ? 'Disconnect' : 'Connect'"
|
||||
>
|
||||
<i :class="['fa-solid', db.status === 'connected' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="openEdit(db)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteDb(db.id)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredDatabases().length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-database text-4xl mb-3"></i>
|
||||
<p>No databases found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="cancelEdit">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Database</h3>
|
||||
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Database Name</label>
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
type="text"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||||
<el-select v-model="editForm.type" placeholder="Select" class="w-full" size="large">
|
||||
<el-option v-for="type in dbTypes" :key="type" :label="type" :value="type" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="editForm.description"
|
||||
rows="3"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗底部 -->
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="cancelEdit"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveEdit"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 新建 Database 弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isCreating" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4" @click.self="closeCreate">
|
||||
<div class="bg-dark-800 rounded-2xl w-full max-w-lg border border-dark-600 shadow-2xl overflow-hidden animate-modal-in">
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
|
||||
<i class="fa-solid fa-database text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-white">Create New Connection</h3>
|
||||
<p class="text-sm text-gray-400">Configure your database connection</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="closeCreate" class="text-gray-400 hover:text-white transition-all p-2 hover:bg-dark-600 rounded-lg">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Database Name</label>
|
||||
<input
|
||||
v-model="newDbForm.name"
|
||||
type="text"
|
||||
placeholder="Enter database name..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||||
<el-select v-model="newDbForm.type" placeholder="Select" class="w-full" size="large">
|
||||
<el-option v-for="type in dbTypes" :key="type" :label="type" :value="type" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="newDbForm.description"
|
||||
rows="3"
|
||||
placeholder="Describe this database..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||||
<button
|
||||
@click="closeCreate"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveNewDb"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all flex items-center gap-2"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Create Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
156
web/src/views/Login.vue
Normal file
156
web/src/views/Login.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 表单数据
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const rememberMe = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
// 模拟登录验证
|
||||
const handleLogin = () => {
|
||||
errorMsg.value = ''
|
||||
|
||||
if (username.value !== 'admin' || password.value !== 'admin') {
|
||||
errorMsg.value = 'Invalid username or password'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
// 模拟登录
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
// 登录成功,跳转到 Dashboard
|
||||
router.push('/dashboard')
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-dark-900 flex items-center justify-center p-4">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div class="absolute top-1/4 -left-20 w-80 h-80 bg-primary-orange/10 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-1/4 -right-20 w-80 h-80 bg-primary-purple/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<div class="w-full max-w-md relative">
|
||||
<!-- Logo -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center mx-auto mb-4 shadow-lg shadow-primary-orange/20">
|
||||
<i class="fa-solid fa-basketball text-white text-2xl"></i>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white">Welcome back</h1>
|
||||
<p class="text-gray-400 mt-2">Sign in to your account to continue</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="space-y-5">
|
||||
<!-- 用户名 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Username</label>
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-user absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="Enter username (admin)"
|
||||
required
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-xl py-3 pl-12 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange focus:ring-1 focus:ring-primary-orange transition-colors"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-xl py-3 pl-12 pr-12 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange focus:ring-1 focus:ring-primary-orange transition-colors"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<i :class="['fa-solid', showPassword ? 'fa-eye-slash' : 'fa-eye']"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 记住我 & 忘记密码 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="rememberMe"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
>
|
||||
<div class="w-5 h-5 border-2 border-dark-400 rounded-md peer-checked:border-primary-orange peer-checked:bg-primary-orange transition-colors flex items-center justify-center">
|
||||
<i v-if="rememberMe" class="fa-solid fa-check text-white text-xs"></i>
|
||||
</div>
|
||||
<span class="ml-2 text-sm text-gray-400">Remember me</span>
|
||||
</label>
|
||||
<a href="#" class="text-sm text-primary-orange hover:text-orange-400 transition-colors">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading"
|
||||
class="w-full bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white font-semibold py-3 rounded-xl transition-all duration-300 shadow-lg shadow-primary-orange/20 hover:shadow-primary-orange/40 flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<i v-if="isLoading" class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
<span>{{ isLoading ? 'Signing in...' : 'Sign in' }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-dark-500"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-4 bg-dark-700 text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="flex items-center justify-center gap-2 bg-dark-600 hover:bg-dark-500 border border-dark-500 rounded-xl py-2.5 text-gray-300 font-medium transition-colors">
|
||||
<i class="fa-brands fa-google"></i>
|
||||
<span>Google</span>
|
||||
</button>
|
||||
<button class="flex items-center justify-center gap-2 bg-dark-600 hover:bg-dark-500 border border-dark-500 rounded-xl py-2.5 text-gray-300 font-medium transition-colors">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<p class="text-center mt-6 text-gray-400">
|
||||
Don't have an account?
|
||||
<a href="#" class="text-primary-orange hover:text-orange-400 font-medium transition-colors">Sign up</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
1554
web/src/views/MCP.vue
Normal file
1554
web/src/views/MCP.vue
Normal file
File diff suppressed because it is too large
Load Diff
293
web/src/views/ModelAPIs.vue
Normal file
293
web/src/views/ModelAPIs.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface ModelAPI {
|
||||
id: number
|
||||
name: string
|
||||
provider: string
|
||||
status: 'active' | 'inactive' | 'error'
|
||||
model: string
|
||||
requests: number
|
||||
latency: string
|
||||
createdAt: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const modelAPIs = ref<ModelAPI[]>([
|
||||
{ id: 1, name: 'OpenAI Primary', provider: 'OpenAI', status: 'active', model: 'gpt-4o', requests: 1250, latency: '45ms', createdAt: '2025-04-10', description: 'Primary OpenAI API endpoint' },
|
||||
{ id: 2, name: 'OpenAI Backup', provider: 'OpenAI', status: 'inactive', model: 'gpt-4o-mini', requests: 0, latency: '0ms', createdAt: '2025-04-08', description: 'Backup OpenAI API endpoint' },
|
||||
{ id: 3, name: 'Google Gemini', provider: 'Google', status: 'active', model: 'gemini-2.0-flash', requests: 890, latency: '32ms', createdAt: '2025-04-05', description: 'Google Gemini API integration' },
|
||||
{ id: 4, name: 'Cerebras Fast', provider: 'Cerebras', status: 'active', model: 'cerebras-sandbox', requests: 2100, latency: '12ms', createdAt: '2025-04-12', description: 'Cerebras high-speed inference' },
|
||||
{ id: 5, name: 'Anthropic Claude', provider: 'Anthropic', status: 'error', model: 'claude-3-5-sonnet', requests: 450, latency: '0ms', createdAt: '2025-04-11', description: 'Anthropic Claude API' },
|
||||
{ id: 6, name: 'Azure OpenAI', provider: 'Microsoft', status: 'active', model: 'gpt-4', requests: 680, latency: '55ms', createdAt: '2025-04-09', description: 'Azure-hosted OpenAI models' },
|
||||
])
|
||||
|
||||
const editingModel = ref<ModelAPI | null>(null)
|
||||
const isEditing = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref<string>('all')
|
||||
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
provider: '',
|
||||
model: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const openEdit = (model: ModelAPI) => {
|
||||
editingModel.value = model
|
||||
editForm.value = {
|
||||
name: model.name,
|
||||
provider: model.provider,
|
||||
model: model.model,
|
||||
description: model.description,
|
||||
}
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
if (editingModel.value) {
|
||||
const index = modelAPIs.value.findIndex(m => m.id === editingModel.value!.id)
|
||||
if (index !== -1) {
|
||||
modelAPIs.value[index] = {
|
||||
...modelAPIs.value[index],
|
||||
...editForm.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
editingModel.value = null
|
||||
}
|
||||
|
||||
const toggleStatus = (model: ModelAPI) => {
|
||||
if (model.status === 'active') {
|
||||
model.status = 'inactive'
|
||||
} else if (model.status === 'inactive') {
|
||||
model.status = 'active'
|
||||
}
|
||||
}
|
||||
|
||||
const deleteModel = (id: number) => {
|
||||
modelAPIs.value = modelAPIs.value.filter(m => m.id !== id)
|
||||
}
|
||||
|
||||
const filteredModels = () => {
|
||||
return modelAPIs.value.filter(model => {
|
||||
const matchSearch = model.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
model.provider.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
model.model.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || model.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
}
|
||||
|
||||
const statusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'bg-primary-success'
|
||||
case 'inactive': return 'bg-gray-500'
|
||||
case 'error': return 'bg-primary-danger'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
const providerIcon = (provider: string) => {
|
||||
switch (provider) {
|
||||
case 'OpenAI': return 'fa-openai'
|
||||
case 'Google': return 'fa-google'
|
||||
case 'Cerebras': return 'fa-microchip'
|
||||
case 'Anthropic': return 'fa-robot'
|
||||
case 'Microsoft': return 'fa-microsoft'
|
||||
default: return 'fa-cube'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<span class="font-medium">Model APIs</span>
|
||||
</div>
|
||||
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Model API
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search model APIs..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<select
|
||||
v-model="filterStatus"
|
||||
class="bg-dark-600 border border-dark-500 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">API Name</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Requests</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Latency</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="model in filteredModels()" :key="model.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
|
||||
<td class="px-5 py-4">
|
||||
<div class="font-medium">{{ model.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ model.description }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i :class="['fa-brands', providerIcon(model.provider), 'text-lg']"></i>
|
||||
<span>{{ model.provider }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ model.model }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-300">{{ model.requests.toLocaleString() }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<span :class="model.latency === '0ms' ? 'text-gray-500' : 'text-primary-cyan'">{{ model.latency }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusClass(model.status)"></span>
|
||||
<span class="capitalize text-sm">{{ model.status }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="toggleStatus(model)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
:title="model.status === 'active' ? 'Deactivate' : 'Activate'"
|
||||
>
|
||||
<i :class="['fa-solid', model.status === 'active' ? 'fa-pause' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="openEdit(model)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteModel(model.id)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="filteredModels().length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-cube text-4xl mb-3"></i>
|
||||
<p>No model APIs found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="cancelEdit">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Model API</h3>
|
||||
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">API Name</label>
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
type="text"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
||||
<select
|
||||
v-model="editForm.provider"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
<option value="OpenAI">OpenAI</option>
|
||||
<option value="Google">Google</option>
|
||||
<option value="Cerebras">Cerebras</option>
|
||||
<option value="Anthropic">Anthropic</option>
|
||||
<option value="Microsoft">Microsoft</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
|
||||
<select
|
||||
v-model="editForm.model"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
<option value="gpt-4o">gpt-4o</option>
|
||||
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
||||
<option value="gpt-4">gpt-4</option>
|
||||
<option value="gemini-2.0-flash">gemini-2.0-flash</option>
|
||||
<option value="cerebras-sandbox">cerebras-sandbox</option>
|
||||
<option value="claude-3-5-sonnet">claude-3-5-sonnet</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="editForm.description"
|
||||
rows="3"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="cancelEdit"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveEdit"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
7
web/src/vite-env.d.ts
vendored
Normal file
7
web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
33
web/tailwind.config.js
Normal file
33
web/tailwind.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
950: '#0a0c10',
|
||||
900: '#0f1115',
|
||||
800: '#121419',
|
||||
700: '#171922',
|
||||
600: '#1a1c25',
|
||||
500: '#2a2c36',
|
||||
},
|
||||
primary: {
|
||||
orange: '#ff9500',
|
||||
yellow: '#ffc247',
|
||||
cyan: '#36bffa',
|
||||
purple: '#a78bfa',
|
||||
danger: '#ef4444',
|
||||
success: '#10b981',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Quicksand', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
25
web/tsconfig.json
Normal file
25
web/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
web/tsconfig.node.json
Normal file
10
web/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
12
web/vite.config.ts
Normal file
12
web/vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user