1 .first update

This commit is contained in:
2026-03-05 09:28:32 +08:00
parent f7207cc627
commit 7f5781d4f1
25 changed files with 8091 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(npm install)",
"Bash(npm run dev)",
"Bash(npm run build)",
"Bash(npm install echarts)"
]
}
}

3
.gitignore vendored
View File

@@ -179,6 +179,9 @@ cython_debug/
#
# Recommended template: Node.gitignore
# Node modules
node_modules/
# TODO: where does this rule come from?
docs/_book

View File

@@ -1,2 +1,52 @@
# X-Agents
Vue 3 + Vite + TypeScript + Tailwind CSS 项目
## 技术栈
- Vue 3
- Vite
- TypeScript
- Tailwind CSS
- Pinia (状态管理)
- Vue Router
- ECharts
## 基础操作
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev
```
启动开发服务器,默认访问 http://localhost:5173
### 构建生产版本
```bash
npm run build
```
### 预览生产版本
```bash
npm run preview
```
## 项目结构
```
├── src/ # 源代码
├── web/ # Web 资源
├── index.html # 入口 HTML
├── vite.config.ts # Vite 配置
├── tailwind.config.js # Tailwind 配置
└── tsconfig.json # TypeScript 配置
```

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<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">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2579
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"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",
"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
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

38
src/App.vue Normal file
View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import Sidebar from '@/components/Sidebar.vue'
const route = useRoute()
const showSidebar = computed(() => route.path !== '/')
</script>
<template>
<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>
</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>

138
src/components/Sidebar.vue Normal file
View File

@@ -0,0 +1,138 @@
<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: 'MCP', icon: 'fa-network-wired', badge: 1 },
]
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>

12
src/main.ts Normal file
View File

@@ -0,0 +1,12 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import './style.css'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

39
src/router/index.ts Normal file
View File

@@ -0,0 +1,39 @@
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'
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
}
]
})
export default router

55
src/style.css Normal file
View File

@@ -0,0 +1,55 @@
@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;
}
/* 柱状图增长动画 */
@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
src/views/Agents.vue Normal file
View 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
src/views/Dashboard.vue Normal file
View 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>

156
src/views/Login.vue Normal file
View 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
src/views/MCP.vue Normal file

File diff suppressed because it is too large Load Diff

293
src/views/ModelAPIs.vue Normal file
View 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
src/vite-env.d.ts vendored Normal file
View 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
tailwind.config.js Normal file
View 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: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}

25
tsconfig.json Normal file
View 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
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

12
vite.config.ts Normal file
View 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')
}
}
})

337
web/agents.html Normal file
View File

@@ -0,0 +1,337 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Remo MVP Design Interface</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 自定义Tailwind配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#1E6BF9',
figma: '#F24E1E',
action: '#000000',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.node-shadow {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.flow-line {
@apply bg-gray-300 w-0.5;
}
.flow-line-h {
@apply bg-gray-300 h-0.5;
}
}
</style>
</head>
<body class="bg-gray-100 font-sans h-screen flex flex-col overflow-hidden">
<!-- 顶部浏览器模拟栏 -->
<div class="bg-white border-b border-gray-200 px-4 py-2 flex items-center gap-2">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-400"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
<div class="w-3 h-3 rounded-full bg-green-400"></div>
</div>
<div class="flex-1 mx-4 bg-gray-100 rounded-md px-3 py-1 text-sm text-gray-500 flex items-center gap-2">
<i class="fa-solid fa-lock text-xs"></i>
<span>Remo MVP Design</span>
</div>
<div class="flex gap-2 text-gray-500">
<i class="fa-solid fa-chevron-left"></i>
<i class="fa-solid fa-chevron-right opacity-50"></i>
</div>
</div>
<!-- 主内容三栏布局 -->
<div class="flex flex-1 overflow-hidden">
<!-- 左侧边栏 -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full">
<!-- 顶部导航信息 -->
<div class="p-4 border-b border-gray-200">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-1">
<i class="fa-solid fa-th text-gray-600"></i>
<span class="font-medium text-gray-800">Designera</span>
<span class="text-gray-400">|</span>
<span class="text-gray-600 text-sm">Web Design</span>
</div>
<i class="fa-solid fa-window-maximize text-gray-500"></i>
</div>
<p class="text-xs text-gray-500">Drafts</p>
</div>
<!-- 文件/资产标签栏 -->
<div class="px-4 py-2 border-b border-gray-200">
<div class="flex items-center justify-between">
<div class="flex gap-4">
<button class="text-sm font-medium text-gray-800 border-b-2 border-primary pb-1">File</button>
<button class="text-sm font-medium text-gray-400 pb-1">Assets</button>
</div>
<i class="fa-solid fa-magnifying-glass text-gray-500 text-sm"></i>
</div>
</div>
<!-- Pages区域 -->
<div class="p-4 border-b border-gray-200">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Pages</span>
<button class="text-gray-500 hover:text-gray-800">
<i class="fa-solid fa-plus text-xs"></i>
</button>
</div>
<div class="flex items-center gap-2 bg-gray-100 rounded px-3 py-2">
<i class="fa-solid fa-palette text-sm text-gray-600"></i>
<span class="text-sm font-medium text-gray-800">Design</span>
</div>
</div>
<!-- Layers区域 -->
<div class="p-4 flex-1 overflow-y-auto">
<div class="flex items-center gap-2 mb-3">
<i class="fa-solid fa-layer-group text-xs text-gray-500"></i>
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Layers</span>
</div>
</div>
</aside>
<!-- 中间画布区域 -->
<main class="flex-1 bg-gray-50 relative overflow-auto">
<!-- 流程图容器 -->
<div class="min-h-full w-full flex flex-col items-center py-10 px-4">
<!-- 节点1 -->
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
<div class="flex items-center gap-1.5">
<i class="fa-brands fa-figma text-figma text-lg"></i>
<span class="font-medium text-gray-800">Figma</span>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500">
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
<i class="fa-solid fa-pen text-sm"></i>
</div>
</div>
<p class="text-gray-700 ml-8 mt-1">1. Start your project from sketch</p>
</div>
<!-- 连接线1 -->
<div class="flow-line h-6"></div>
<!-- 节点2 Action黑色节点 -->
<div class="node bg-action rounded-lg node-shadow w-72 p-3 relative z-10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-5 h-5 flex items-center justify-center">
<i class="fa-solid fa-bolt text-white text-sm"></i>
</div>
<div class="flex items-center gap-1.5">
<span class="font-medium text-white">Action</span>
</div>
</div>
<div class="flex items-center gap-2 text-white/70">
<i class="fa-solid fa-pen text-sm"></i>
</div>
</div>
<p class="text-white/90 ml-8 mt-1">2. Take a frame and start designing</p>
</div>
<!-- 连接线2 -->
<div class="flow-line h-6"></div>
<!-- 节点3 -->
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
<div class="flex items-center gap-1.5">
<i class="fa-brands fa-figma text-figma text-lg"></i>
<span class="font-medium text-gray-800">Figma</span>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500">
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
<i class="fa-solid fa-pen text-sm"></i>
</div>
</div>
<p class="text-gray-700 ml-8 mt-1">3. Add few shapes</p>
</div>
<!-- 连接线3 -->
<div class="flow-line h-6"></div>
<!-- 节点4 -->
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
<div class="flex items-center gap-1.5">
<i class="fa-brands fa-figma text-figma text-lg"></i>
<span class="font-medium text-gray-800">Figma</span>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500">
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
<i class="fa-solid fa-pen text-sm"></i>
</div>
</div>
<p class="text-gray-700 ml-8 mt-1">4. Split into paths</p>
</div>
<!-- 分支连接线 -->
<div class="w-full max-w-2xl relative flex justify-center">
<div class="flow-line h-6"></div>
<div class="absolute top-6 w-full flex justify-center">
<div class="flow-line-h w-2/3"></div>
</div>
<div class="absolute top-6 w-full flex justify-between px-[12.5%]">
<div class="flow-line h-6"></div>
<div class="flow-line h-6"></div>
</div>
</div>
<!-- 分支标签 -->
<div class="w-full max-w-2xl flex justify-between px-[12.5%] mb-6">
<span class="bg-primary text-white text-xs font-medium px-3 py-1 rounded-full">Path A</span>
<span class="bg-primary text-white text-xs font-medium px-3 py-1 rounded-full">Path B</span>
</div>
<!-- 双分支节点 -->
<div class="w-full max-w-2xl flex justify-between px-4">
<!-- 节点5 Path A -->
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
<div class="flex items-center gap-1.5">
<i class="fa-brands fa-figma text-figma text-lg"></i>
<span class="font-medium text-gray-800">Figma</span>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500">
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
<i class="fa-solid fa-pen text-sm"></i>
</div>
</div>
<p class="text-gray-700 ml-8 mt-1">5. Split into paths</p>
</div>
<!-- 节点6 Path B -->
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
<div class="flex items-center gap-1.5">
<i class="fa-brands fa-figma text-figma text-lg"></i>
<span class="font-medium text-gray-800">Figma</span>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500">
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
<i class="fa-solid fa-pen text-sm"></i>
</div>
</div>
<p class="text-gray-700 ml-8 mt-1">6. Split into paths</p>
</div>
</div>
</div>
<!-- 底部工具栏 -->
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white rounded-full shadow-lg px-4 py-2 flex items-center gap-4">
<button class="text-primary p-1.5 rounded hover:bg-gray-100">
<i class="fa-solid fa-cursor-pointer"></i>
</button>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-solid fa-hand"></i>
</button>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-solid fa-plus"></i>
</button>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-regular fa-square"></i>
</button>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-solid fa-pen"></i>
</button>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-solid fa-t"></i>
</button>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-regular fa-circle"></i>
</button>
<div class="w-px h-5 bg-gray-200"></div>
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
<i class="fa-solid fa-code"></i>
</button>
</div>
</main>
<!-- 右侧Copilot面板 -->
<aside class="w-96 bg-white border-l border-gray-200 flex flex-col h-full">
<!-- 顶部Copilot品牌区 -->
<div class="p-6 flex flex-col items-center text-center border-b border-gray-200">
<div class="w-16 h-16 rounded-xl bg-gradient-to-tr from-blue-500 via-purple-500 to-orange-400 mb-4 flex items-center justify-center">
<div class="w-10 h-10 bg-white/20 backdrop-blur-sm rounded-lg"></div>
</div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">Hey, what's on your mind today?</h2>
<button class="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium px-4 py-2 rounded-lg transition">
Get advice
</button>
</div>
<!-- 聊天对话区 -->
<div class="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
<!-- Copilot消息 -->
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 mb-1">
<div class="w-5 h-5 rounded bg-gradient-to-tr from-blue-500 to-purple-500 flex items-center justify-center">
<i class="fa-brands fa-microsoft text-white text-xs"></i>
</div>
<span class="text-sm font-medium text-gray-700">Copilot</span>
</div>
<div class="bg-gray-100 rounded-2xl rounded-tl-none px-4 py-3 text-gray-700 ml-7">
Advice on what, exactly? Whether it's life decisions, career choices, relationships, or something entirely random, I'm here to help. Let me know the topic, and we'll dive into it together!
</div>
</div>
<!-- 用户消息 -->
<div class="flex flex-col gap-2 items-end">
<div class="bg-gray-800 rounded-2xl rounded-tr-none px-4 py-3 text-white max-w-[80%]">
What are some common life decisions people seek advice on?
</div>
</div>
</div>
<!-- 底部输入框 -->
<div class="p-4 border-t border-gray-200">
<div class="relative">
<input type="text" placeholder="Ask anything..." class="w-full border border-gray-300 rounded-full px-4 py-3 pr-12 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<button class="absolute right-3 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center">
<i class="fa-solid fa-arrow-up"></i>
</button>
</div>
<div class="flex items-center gap-1 mt-2 ml-2 text-xs text-gray-500">
<i class="fa-brands fa-microsoft"></i>
<span>Copilot by Microsoft</span>
</div>
</div>
</aside>
</div>
</body>
</html>

692
web/dashboard.html Normal file
View File

@@ -0,0 +1,692 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Saasfactor Dashboard</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- 自定义Tailwind配置 -->
<script>
tailwind.config = {
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: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
</style>
</head>
<body class="bg-dark-900 text-gray-100 font-sans min-h-screen flex overflow-x-hidden">
<!-- 左侧侧边栏 -->
<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-lg flex items-center gap-1">
Organization
<i class="fa-solid fa-chevron-down text-xs text-gray-400"></i>
</div>
<div class="text-sm text-gray-400">Saasfactor</div>
</div>
</div>
<!-- 导航菜单 -->
<nav class="flex-1 px-3 py-2">
<ul class="space-y-1">
<!-- Dashboard 激活项 -->
<li>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-dark-600 text-white">
<i class="fa-solid fa-gauge w-5 text-center"></i>
<span>Dashboard</span>
</a>
</li>
<!-- Agents -->
<li>
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<div class="flex items-center gap-3">
<i class="fa-solid fa-robot w-5 text-center"></i>
<span>Agents</span>
</div>
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">3</span>
</a>
</li>
<!-- MCP -->
<li>
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<div class="flex items-center gap-3">
<i class="fa-solid fa-code w-5 text-center"></i>
<span>MCP</span>
</div>
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">21</span>
</a>
</li>
<!-- Model APIs -->
<li>
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<div class="flex items-center gap-3">
<i class="fa-solid fa-cube w-5 text-center"></i>
<span>Model APIs</span>
</div>
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">13</span>
</a>
</li>
<!-- Policies -->
<li>
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<div class="flex items-center gap-3">
<i class="fa-solid fa-shield-halved w-5 text-center"></i>
<span>Policies</span>
</div>
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">1</span>
</a>
</li>
<!-- 分隔线 -->
<li class="my-4 border-t border-dark-500"></li>
<!-- API Keys -->
<li>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<i class="fa-solid fa-key w-5 text-center"></i>
<span>API Keys</span>
</a>
</li>
<!-- Settings -->
<li>
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<div class="flex items-center gap-3">
<i class="fa-solid fa-gear w-5 text-center"></i>
<span>Settings</span>
</div>
<div 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>
</a>
</li>
<!-- Team -->
<li>
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<div class="flex items-center gap-3">
<i class="fa-solid fa-users w-5 text-center"></i>
<span>Team</span>
</div>
<div 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>
<!-- Service Accounts -->
<li>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<i class="fa-solid fa-user-shield w-5 text-center"></i>
<span>Service Accounts</span>
</a>
</li>
<!-- Integrations -->
<li>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<i class="fa-solid fa-plug w-5 text-center"></i>
<span>Integrations</span>
</a>
</li>
<!-- 分隔线 -->
<li class="my-4 border-t border-dark-500"></li>
<!-- Information -->
<li>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<i class="fa-solid fa-circle-info w-5 text-center"></i>
<span>Information</span>
</a>
</li>
<!-- Account -->
<li>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
<i class="fa-solid fa-user w-5 text-center"></i>
<span>Account</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">Alex Smith</div>
<div class="text-xs text-gray-400">alex@gmail.com</div>
</div>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="ml-64 flex-1 p-6">
<!-- 顶部导航与日期选择区 -->
<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 bg-primary-orange relative cursor-pointer">
<div class="absolute right-0.5 top-0.5 w-4 h-4 rounded-full bg-white"></div>
</div>
</div>
</div>
<div class="text-4xl font-bold mb-4">3</div>
<!-- 进度条 -->
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4">
<div class="flex h-full">
<div class="w-[33%] bg-primary-yellow"></div>
<div class="w-[33%] bg-primary-cyan"></div>
<div class="w-[34%] bg-primary-purple"></div>
</div>
</div>
<!-- 明细列表 -->
<ul class="space-y-2">
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
<span class="text-sm text-gray-300">template-google-adk-api</span>
</div>
<span class="text-sm">1</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
<span class="text-sm text-gray-300">mcp-google-adk-api</span>
<span class="bg-primary-danger text-white text-xs px-1.5 py-0.5 rounded">Error</span>
</div>
<span class="text-sm">1</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
<span class="text-sm text-gray-300">template-openai-api</span>
<span class="bg-primary-danger text-white text-xs px-1.5 py-0.5 rounded">Error</span>
</div>
<span class="text-sm">1</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 bg-dark-500 relative cursor-pointer">
<div class="absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-gray-400"></div>
</div>
</div>
</div>
<div class="text-4xl font-bold mb-4">21</div>
<!-- 进度条 -->
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4">
<div class="flex h-full">
<div class="w-[71%] bg-primary-yellow"></div>
<div class="w-[19%] bg-primary-cyan"></div>
<div class="w-[10%] bg-primary-purple"></div>
</div>
</div>
<!-- 明细列表 -->
<ul class="space-y-2">
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
<span class="text-sm text-gray-300">linear-demo</span>
</div>
<span class="text-sm">15</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
<span class="text-sm text-gray-300">google-maps</span>
</div>
<span class="text-sm">4</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
<span class="text-sm text-gray-300">explorer-mcp</span>
</div>
<span class="text-sm">2</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 bg-dark-500 relative cursor-pointer">
<div class="absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-gray-400"></div>
</div>
</div>
</div>
<div class="text-4xl font-bold mb-4">13</div>
<!-- 进度条 -->
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4">
<div class="flex h-full">
<div class="w-[46%] bg-primary-yellow"></div>
<div class="w-[15%] bg-primary-cyan"></div>
<div class="w-[39%] bg-primary-purple"></div>
</div>
</div>
<!-- 明细列表 -->
<ul class="space-y-2">
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
<span class="text-sm text-gray-300">gpt-40-2024-08-12</span>
</div>
<span class="text-sm">2</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
<span class="text-sm text-gray-300">cerebras-sandbox</span>
</div>
<span class="text-sm">6</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
<span class="text-sm text-gray-300">sandbox-openai</span>
</div>
<span class="text-sm">5</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">36</div>
</div>
<div>
<div class="text-sm text-gray-400 mb-1">Agents calls</div>
<div class="text-2xl font-bold">3</div>
</div>
<div>
<div class="text-sm text-gray-400 mb-1">MCP servers calls</div>
<div class="text-2xl font-bold">21</div>
</div>
<div>
<div class="text-sm text-gray-400 mb-1">Models requests</div>
<div class="text-2xl font-bold">13</div>
</div>
</div>
<!-- 【重构后】图表容器 完美对齐0刻度 -->
<div class="relative h-52 w-full">
<!-- 绘图区(网格线+Y轴+柱子) 预留底部20px给时间标签 -->
<div class="relative h-[calc(100%-20px)] w-full">
<!-- 横向网格线 与Y轴刻度完全对齐 0刻度线在绘图区最底部 -->
<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轴刻度 0刻度与绘图区最底部的0网格线完美垂直对齐 -->
<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">
<!-- 3:02 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 12.5%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 25%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 18.75%"></div>
</div>
<!-- 3:07 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 25%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 31.25%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 25%"></div>
</div>
<!-- 3:12 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 62.5%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 50%"></div>
</div>
<!-- 3:17 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 37.5%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 25%"></div>
</div>
<!-- 3:22 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 18.75%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 31.25%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 18.75%"></div>
</div>
<!-- 3:27 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 18.75%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 37.5%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 31.25%"></div>
</div>
<!-- 3:32 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 12.5%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 25%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 31.25%"></div>
</div>
<!-- 3:37 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 100%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 37.5%"></div>
</div>
<!-- 3:42 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 12.5%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 62.5%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 31.25%"></div>
</div>
<!-- 3:47 PM 柱子 -->
<div class="flex items-end gap-1 h-full w-full justify-center">
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 37.5%"></div>
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 25%"></div>
</div>
</div>
</div>
<!-- 时间标签区 统一放在0刻度线下方 与对应柱子居中对齐 -->
<div class="ml-6 w-full flex justify-between gap-1 mt-1">
<span class="text-xs text-gray-500 w-full text-center">3:02 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:07 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:12 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:17 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:22 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:27 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:32 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:37 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:42 PM</span>
<span class="text-xs text-gray-500 w-full text-center">3:47 PM</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">
<button class="flex-1 bg-dark-500 text-white py-1.5 rounded-l-lg text-sm">General</button>
<button class="flex-1 bg-transparent text-gray-400 py-1.5 rounded-r-lg text-sm hover:bg-dark-600 transition-colors">Errors</button>
</div>
<!-- 列表 -->
<ul class="space-y-3">
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-cube text-gray-400"></i>
<span class="text-sm text-gray-300">gpt-40-2024-08-12</span>
</div>
<span class="text-sm">7</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-code text-gray-400"></i>
<span class="text-sm text-gray-300">google-maps</span>
</div>
<span class="text-sm">4</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-code text-gray-400"></i>
<span class="text-sm text-gray-300">explorer-mcp</span>
</div>
<span class="text-sm">2</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-cube text-gray-400"></i>
<span class="text-sm text-gray-300">template-google-adk-api</span>
</div>
<span class="text-sm">4</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-cube text-gray-400"></i>
<span class="text-sm text-gray-300">linear-demo</span>
</div>
<span class="text-sm">2</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-code text-gray-400"></i>
<span class="text-sm text-gray-300">cerebras-sandbox</span>
</div>
<span class="text-sm">1</span>
</li>
<li class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-cube text-gray-400"></i>
<span class="text-sm text-gray-300">sandbox-openai</span>
</div>
<span class="text-sm">2</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 class="flex justify-between items-start">
<div>
<h4 class="font-medium mb-1">New framework supported: PydanticAI</h4>
<p class="text-sm text-gray-400">Added support for PydanticAI framework</p>
</div>
<span class="text-xs text-gray-500">2025-04-12</span>
</li>
<li class="flex justify-between items-start">
<div>
<h4 class="font-medium mb-1">New framework supported: Google ADK</h4>
<p class="text-sm text-gray-400">Added support for Google ADK (Agent Development Kit) framework</p>
</div>
<span class="text-xs text-gray-500">2025-04-07</span>
</li>
<li class="flex justify-between items-start">
<div>
<h4 class="font-medium mb-1">Improved Analytics Dashboard</h4>
<p class="text-sm text-gray-400">Enhanced real-time monitoring with faster data refresh</p>
</div>
<span class="text-xs text-gray-500">2025-04-15</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 class="flex items-center gap-3">
<i class="fa-solid fa-cube text-gray-400"></i>
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">linear-demo</span>
<span class="text-xs text-gray-500">in 21 hours</span>
</div>
<div class="flex items-center gap-1 text-xs text-primary-success">
<i class="fa-solid fa-circle-check"></i>
<span>Success</span>
</div>
</div>
</li>
<li class="flex items-center gap-3">
<i class="fa-solid fa-robot text-gray-400"></i>
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">myagent</span>
<span class="text-xs text-gray-500">in 21 hours</span>
</div>
<div class="flex items-center gap-1 text-xs text-primary-success">
<i class="fa-solid fa-circle-check"></i>
<span>Success</span>
</div>
</div>
</li>
<li class="flex items-center gap-3">
<i class="fa-solid fa-cube text-gray-400"></i>
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">linear-demo</span>
<span class="text-xs text-gray-500">in 21 hours</span>
</div>
<div class="flex items-center gap-1 text-xs text-primary-success">
<i class="fa-solid fa-circle-check"></i>
<span>Success</span>
</div>
</div>
</li>
<li class="flex items-center gap-3">
<i class="fa-solid fa-code text-gray-400"></i>
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">gpt-40</span>
<span class="text-xs text-gray-500">in 21 hours</span>
</div>
<div class="flex items-center gap-1 text-xs text-primary-success">
<i class="fa-solid fa-circle-check"></i>
<span>Success</span>
</div>
</div>
</li>
<li class="flex items-center gap-3">
<i class="fa-solid fa-cube text-gray-400"></i>
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">linear-demo</span>
<span class="text-xs text-gray-500">in 21 hours</span>
</div>
<div class="flex items-center gap-1 text-xs text-primary-success">
<i class="fa-solid fa-circle-check"></i>
<span>Success</span>
</div>
</div>
</li>
</ul>
</div>
</div>
</main>
</body>
</html>

909
web/graph.html Normal file
View File

@@ -0,0 +1,909 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>思维本体元模型 — 知识图谱</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="./graph-data.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Microsoft YaHei', 'PingFang SC', -apple-system, sans-serif;
background: #0a0e1a;
color: #e6edf3;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
#topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 52px;
background: linear-gradient(90deg, #0f1628, #1a1040);
border-bottom: 1px solid #2d2d55;
flex-shrink: 0;
z-index: 100;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
}
#topbar .title {
display: flex;
align-items: center;
gap: 10px;
}
#topbar .title .icon {
font-size: 22px;
}
#topbar .title h1 {
font-size: 18px;
font-weight: 600;
background: linear-gradient(90deg, #8B5CF6, #3B82F6, #10B981);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
#topbar .subtitle {
font-size: 12px;
color: #8b949e;
}
#topbar .controls {
display: flex;
gap: 8px;
align-items: center;
}
.ctrl-btn {
background: #1a1a2e;
border: 1px solid #2d2d55;
color: #c9d1d9;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all .2s;
}
.ctrl-btn:hover {
background: #2d2d55;
border-color: #8B5CF6;
color: #8B5CF6;
}
.ctrl-btn.active {
background: #4C1D95;
border-color: #8B5CF6;
color: #fff;
}
#main-layout {
display: flex;
flex: 1;
overflow: hidden;
}
#left-panel {
width: 230px;
background: #0f1628;
border-right: 1px solid #2d2d55;
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
}
.panel-section {
padding: 14px 14px 10px;
border-bottom: 1px solid #1a1a2e;
}
.panel-section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #8b949e;
margin-bottom: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
border-radius: 6px;
cursor: pointer;
transition: background .15s;
margin-bottom: 2px;
}
.legend-item:hover {
background: #1a1a2e;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 0 6px currentColor;
}
.legend-label {
font-size: 13px;
color: #c9d1d9;
}
.legend-count {
margin-left: auto;
font-size: 11px;
color: #8b949e;
background: #1a1a2e;
padding: 1px 6px;
border-radius: 10px;
}
.scenario-btn {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 10px;
margin-bottom: 4px;
background: #1a1a2e;
border: 1px solid #2d2d55;
border-radius: 8px;
cursor: pointer;
color: #c9d1d9;
font-size: 12px;
text-align: left;
transition: all .2s;
}
.scenario-btn:hover {
border-color: #8B5CF6;
color: #8B5CF6;
background: #1a1040;
}
.scenario-btn.active {
border-color: var(--sc-color);
color: var(--sc-color);
background: rgba(139, 92, 246, .08);
box-shadow: 0 0 8px rgba(139, 92, 246, .2);
}
.scenario-btn .sc-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--sc-color);
flex-shrink: 0;
}
.scenario-btn .sc-label {
flex: 1;
}
.scenario-btn .sc-count {
font-size: 10px;
color: #8b949e;
background: #0f1628;
padding: 1px 5px;
border-radius: 6px;
}
.rel-item {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 0;
font-size: 11px;
color: #8b949e;
}
.rel-line {
width: 28px;
height: 2px;
flex-shrink: 0;
}
#chart-container {
flex: 1;
position: relative;
}
#chart {
width: 100%;
height: 100%;
}
#graph-hint {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
background: rgba(15, 22, 40, 0.85);
border: 1px solid #2d2d55;
border-radius: 8px;
padding: 6px 14px;
font-size: 11px;
color: #8b949e;
pointer-events: none;
white-space: nowrap;
backdrop-filter: blur(8px);
}
#right-panel {
width: 260px;
background: #0f1628;
border-left: 1px solid #2d2d55;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
}
#detail-header {
padding: 14px 16px 10px;
border-bottom: 1px solid #1a1a2e;
}
#detail-type-badge {
display: inline-block;
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
margin-bottom: 6px;
font-weight: 600;
}
#detail-name {
font-size: 16px;
font-weight: 700;
color: #e6edf3;
margin-bottom: 2px;
}
#detail-tag {
font-size: 11px;
color: #8b949e;
}
#detail-body {
padding: 12px 16px;
flex: 1;
}
#detail-desc {
font-size: 12px;
color: #8b949e;
line-height: 1.7;
white-space: pre-wrap;
background: #0a0e1a;
border: 1px solid #1a1a2e;
border-radius: 6px;
padding: 10px;
}
#detail-connections {
margin-top: 12px;
}
.conn-title {
font-size: 11px;
color: #6e7681;
text-transform: uppercase;
letter-spacing: .5px;
margin-bottom: 6px;
font-weight: 600;
}
.conn-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 2px;
font-size: 11px;
color: #8b949e;
background: #0a0e1a;
border: 1px solid #1a1a2e;
cursor: pointer;
}
.conn-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.conn-text {
flex: 1;
}
.conn-rel {
font-size: 10px;
color: #6e7681;
}
#detail-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #484f58;
padding: 20px;
text-align: center;
}
#detail-placeholder .ph-icon {
font-size: 48px;
margin-bottom: 12px;
}
#detail-placeholder .ph-text {
font-size: 13px;
line-height: 1.6;
}
#stats-bar {
display: flex;
padding: 10px 16px;
gap: 10px;
border-top: 1px solid #1a1a2e;
flex-shrink: 0;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-num {
font-size: 18px;
font-weight: 700;
}
.stat-label {
font-size: 10px;
color: #8b949e;
}
/* 模拟控制栏 */
#sim-bar {
position: fixed;
bottom: -160px;
left: 50%;
transform: translateX(-50%);
width: min(820px, 94vw);
background: linear-gradient(135deg, #1a1040, #0f1628);
border: 1px solid #2d2d55;
border-bottom: none;
border-radius: 14px 14px 0 0;
box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.6);
z-index: 500;
transition: bottom .4s cubic-bezier(.4, 0, .2, 1);
overflow: hidden;
}
#sim-bar.visible {
bottom: 0;
}
#sim-header {
display: flex;
align-items: center;
padding: 10px 18px 8px;
gap: 10px;
border-bottom: 1px solid #1a1a2e;
}
#sim-header .sim-icon {
font-size: 16px;
}
#sim-title {
font-size: 13px;
font-weight: 700;
color: #c084fc;
flex: 1;
}
#sim-step-badge {
font-size: 11px;
background: #1a1a2e;
color: #8b949e;
padding: 3px 10px;
border-radius: 12px;
border: 1px solid #2d2d55;
font-family: monospace;
}
#sim-phase-badge {
font-size: 10px;
padding: 3px 9px;
border-radius: 10px;
font-weight: 700;
}
#sim-progress {
height: 3px;
background: #1a1a2e;
margin: 0 18px;
}
#sim-progress-bar {
height: 100%;
background: linear-gradient(90deg, #8B5CF6, #c084fc);
border-radius: 2px;
transition: width .4s ease;
}
#sim-body {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 18px 14px;
}
#sim-desc {
flex: 1;
font-size: 12px;
color: #c9d1d9;
line-height: 1.6;
}
#sim-desc .sim-step-title {
font-size: 13px;
font-weight: 700;
color: #e6edf3;
margin-bottom: 4px;
}
#sim-controls {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.sim-btn {
padding: 7px 14px;
border-radius: 8px;
border: 1px solid #2d2d55;
background: #1a1a2e;
color: #c9d1d9;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all .2s;
white-space: nowrap;
}
.sim-btn:hover {
background: #2d2d55;
color: #fff;
}
.sim-btn:disabled {
opacity: .35;
cursor: not-allowed;
}
.sim-btn.primary {
background: linear-gradient(135deg, #7C3AED, #8B5CF6);
border-color: #7C3AED;
color: #fff;
box-shadow: 0 0 12px rgba(124, 58, 237, .4);
}
.sim-btn.stop {
background: #2d1f1f;
border-color: #EF4444;
color: #EF4444;
}
.sim-btn.stop:hover {
background: #EF4444;
color: #fff;
}
#sim-auto-btn.playing {
background: linear-gradient(135deg, #059669, #10B981);
border-color: #10B981;
color: #fff;
}
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #2d2d55;
border-radius: 2px;
}
</style>
</head>
<body>
<div id="topbar">
<div class="title">
<span class="icon">🧠</span>
<div>
<h1>思维本体元模型 — 知识图谱</h1>
<div class="subtitle">Thinking Ontology Meta-Model · ECharts Force-Directed Graph</div>
</div>
</div>
<div class="controls">
<button class="ctrl-btn" id="btn-reset">↺ 复位</button>
<button class="ctrl-btn active" id="btn-labels">标签 ON</button>
<button class="ctrl-btn active" id="btn-rules">规则边</button>
<button class="ctrl-btn" id="btn-fullscreen">⛶ 全屏</button>
</div>
</div>
<div id="main-layout">
<div id="left-panel">
<div class="panel-section">
<div class="panel-section-title">节点类型图例</div>
<div class="legend-item" onclick="filterByCategory(0)">
<div class="legend-dot" style="background:#3B82F6;color:#3B82F6"></div>
<span class="legend-label">语义对象</span>
<span class="legend-count" id="cnt-0"></span>
</div>
<div class="legend-item" onclick="filterByCategory(1)">
<div class="legend-dot" style="background:#10B981;color:#10B981"></div>
<span class="legend-label">对象行为</span>
<span class="legend-count" id="cnt-1"></span>
</div>
<div class="legend-item" onclick="filterByCategory(2)">
<div class="legend-dot" style="background:#F59E0B;color:#F59E0B"></div>
<span class="legend-label">约束规则</span>
<span class="legend-count" id="cnt-2"></span>
</div>
<div class="legend-item" onclick="filterByCategory(3)">
<div class="legend-dot" style="background:#8B5CF6;color:#8B5CF6"></div>
<span class="legend-label">编排流程</span>
<span class="legend-count" id="cnt-3"></span>
</div>
</div>
<div class="panel-section">
<div class="panel-section-title">场景联动筛选</div>
<div id="scenario-buttons"></div>
<button class="ctrl-btn" style="width:100%;margin-top:6px;font-size:11px" onclick="clearScenario()">
清除筛选</button>
</div>
<div class="panel-section">
<div class="panel-section-title">关系类型</div>
<div class="rel-item">
<div class="rel-line" style="background:#3B82F6"></div><span>语义对象关系</span>
</div>
<div class="rel-item">
<div class="rel-line" style="background:#10B981"></div><span>行为操作关系</span>
</div>
<div class="rel-item">
<div class="rel-line"
style="background:repeating-linear-gradient(90deg,#F59E0B 0,#F59E0B 4px,transparent 4px,transparent 8px)">
</div><span>规则引用关系</span>
</div>
<div class="rel-item">
<div class="rel-line" style="background:#8B5CF6"></div><span>流程调用关系</span>
</div>
<div class="rel-item">
<div class="rel-line" style="background:#EF4444"></div><span>⭐ 核心关系</span>
</div>
</div>
<div id="stats-bar">
<div class="stat-item">
<div class="stat-num" style="color:#3B82F6" id="s-nodes"></div>
<div class="stat-label">节点</div>
</div>
<div class="stat-item">
<div class="stat-num" style="color:#10B981" id="s-links"></div>
<div class="stat-label">关系</div>
</div>
</div>
</div>
<div id="chart-container">
<div id="chart"></div>
<div id="graph-hint">🖱 拖拽节点 · 滚轮缩放 · 点击查看详情 · 双击流程触发场景</div>
<div id="sim-bar">
<div id="sim-header">
<span class="sim-icon">🎬</span>
<span id="sim-title">问题分析与解决 — 动态模拟</span>
<span id="sim-phase-badge"></span>
<span id="sim-step-badge">0 / 14</span>
</div>
<div id="sim-progress">
<div id="sim-progress-bar" style="width:0%"></div>
</div>
<div id="sim-body">
<div id="sim-desc">
<div class="sim-step-title" id="sim-step-title">点击「下一步」开始模拟</div>
<div id="sim-step-desc">逐步展示问题分析与解决的完整调用链路。</div>
</div>
<div id="sim-controls">
<button class="sim-btn" id="sim-prev-btn" disabled onclick="simStep(-1)">◀ 上一步</button>
<button class="sim-btn primary" id="sim-next-btn" onclick="simStep(1)">下一步 ▶</button>
<button class="sim-btn" id="sim-auto-btn" onclick="simToggleAuto()">⏵ 自动</button>
<button class="sim-btn stop" onclick="simStop()">■ 停止</button>
</div>
</div>
</div>
</div>
<div id="right-panel">
<div id="detail-placeholder">
<div class="ph-icon">🔍</div>
<div class="ph-text">点击图谱中任意节点<br>查看详细元数据信息</div>
</div>
<div id="detail-content" style="display:none;flex-direction:column;flex:1">
<div id="detail-header">
<div id="detail-type-badge"></div>
<div id="detail-name"></div>
<div id="detail-tag"></div>
</div>
<div id="detail-body">
<div class="conn-title">节点描述</div>
<div id="detail-desc"></div>
<div id="detail-connections">
<div class="conn-title" style="margin-top:14px">关联节点</div>
<div id="conn-list"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const { CATEGORIES, NODES, LINKS, SCENARIOS, SIMULATION_STEPS } = ONTOLOGY_DATA;
let chart = null, activeScenario = null, showLabels = true, showRuleEdges = true, catFilter = null;
const CAT_COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6'];
const CAT_NAMES = ['语义对象', '对象行为', '约束规则', '编排流程'];
const nodeMap = {}; NODES.forEach(n => nodeMap[n.id] = n);
document.getElementById('s-nodes').textContent = NODES.length;
document.getElementById('s-links').textContent = LINKS.length;
for (let i = 0; i < 4; i++) document.getElementById('cnt-' + i).textContent = NODES.filter(n => n.category === i).length;
// 场景按钮
const scColors = { 'WF.ProblemSolving': '#F59E0B', 'WF.ArticleGen': '#3B82F6', 'WF.KnowledgeUpdate': '#10B981', 'WF.CognitionUpgrade': '#EF4444' };
Object.entries(SCENARIOS).forEach(([key, sc]) => {
const btn = document.createElement('button');
btn.className = 'scenario-btn'; btn.dataset.key = key;
btn.style.setProperty('--sc-color', scColors[key] || '#8B5CF6');
btn.innerHTML = `<span class="sc-dot"></span><span class="sc-label">${sc.label}</span><span class="sc-count">${sc.nodes.length}节点</span>`;
btn.onclick = () => activateScenario(key);
document.getElementById('scenario-buttons').appendChild(btn);
});
// 构建 ECharts 选项
function buildOption(highlightIds = null) {
const filteredLinks = showRuleEdges ? LINKS : LINKS.filter(l => l.relType !== 'reference');
const nodes = NODES.map(n => {
const hi = highlightIds ? highlightIds.includes(n.id) : true;
const cv = catFilter === null || n.category === catFilter;
const op = (hi && cv) ? 1 : 0.08;
return {
id: n.id, name: n.name, category: n.category,
symbolSize: n.symbolSize * (hi ? 1 : 0.85),
label: { show: showLabels && hi && cv, fontSize: n.category === 3 ? 11 : (n.category === 0 ? 10 : 9), fontWeight: n.category <= 1 ? 'bold' : 'normal' },
itemStyle: {
color: CAT_COLORS[n.category], opacity: op,
shadowBlur: hi && n.tag && n.tag.includes('⭐') ? 20 : (hi ? 8 : 0),
shadowColor: hi ? CAT_COLORS[n.category] : 'transparent',
borderColor: hi && n.tag && n.tag.includes('⭐') ? '#FFD700' : CAT_COLORS[n.category],
borderWidth: hi && n.tag && n.tag.includes('⭐') ? 2.5 : 0,
}, _data: n,
};
});
const links = filteredLinks.map(l => {
const sv = highlightIds ? highlightIds.includes(l.source) : true;
const tv = highlightIds ? highlightIds.includes(l.target) : true;
const v = sv && tv;
return {
source: l.source, target: l.target,
label: { show: v && showLabels && l.relType !== 'reference', formatter: l.label, fontSize: 9, color: '#8b949e' },
lineStyle: { ...l.lineStyle, opacity: v ? (l.relType === 'reference' ? 0.5 : 0.75) : 0.03, curveness: 0.2, type: l.lineStyle.type || 'solid' },
symbol: ['none', 'arrow'], symbolSize: [0, 7],
};
});
return {
backgroundColor: '#0a0e1a',
tooltip: {
trigger: 'item', backgroundColor: 'rgba(15,22,40,0.95)', borderColor: '#2d2d55', textStyle: { color: '#e6edf3', fontSize: 12 },
formatter: p => { if (p.dataType !== 'node') return ''; const n = nodeMap[p.data.id]; if (!n) return ''; return `<div style="max-width:260px"><div style="color:${CAT_COLORS[n.category]};font-weight:bold;font-size:13px;margin-bottom:4px">${n.name.replace(/\n/g, ' ')}</div><div style="color:#8b949e;font-size:10px;margin-bottom:6px">[${CAT_NAMES[n.category]}] ${n.tag || ''}</div><div style="color:#c9d1d9;font-size:11px;line-height:1.6;white-space:pre-wrap">${n.desc || ''}</div></div>`; }
},
legend: { show: false },
series: [{
type: 'graph', layout: 'force', animation: true, animationDuration: 800, animationEasingUpdate: 'quinticInOut',
data: nodes, links: links,
categories: CATEGORIES.map((c, i) => ({ name: c.name, itemStyle: { color: CAT_COLORS[i] } })),
roam: true, draggable: true, focusNodeAdjacency: false,
symbol: (v, p) => { const c = p.data.category; return c === 3 ? 'diamond' : c === 2 ? 'roundRect' : 'circle'; },
label: { show: showLabels, position: 'bottom', formatter: '{b}', color: '#c9d1d9', textBorderColor: '#0a0e1a', textBorderWidth: 3 },
edgeLabel: { show: showLabels, fontSize: 9, color: '#8b949e' },
force: { repulsion: 320, edgeLength: [80, 200], gravity: 0.06, layoutAnimation: true, friction: 0.6 },
lineStyle: { curveness: 0.2 },
emphasis: { focus: 'adjacency', scale: true, itemStyle: { shadowBlur: 20 }, lineStyle: { width: 3, opacity: 1 }, label: { show: true } },
}]
};
}
function initChart() {
chart = echarts.init(document.getElementById('chart'), 'dark');
chart.setOption(buildOption());
chart.on('click', p => { if (p.dataType === 'node') showDetail(p.data.id); });
chart.on('dblclick', p => { if (p.dataType === 'node' && SCENARIOS[p.data.id]) activateScenario(p.data.id); });
window.addEventListener('resize', () => chart.resize());
}
function showDetail(nodeId) {
const n = nodeMap[nodeId]; if (!n) return;
document.getElementById('detail-placeholder').style.display = 'none';
document.getElementById('detail-content').style.display = 'flex';
const color = CAT_COLORS[n.category];
const badge = document.getElementById('detail-type-badge');
badge.textContent = CAT_NAMES[n.category]; badge.style.background = color + '22'; badge.style.color = color; badge.style.border = `1px solid ${color}55`;
document.getElementById('detail-name').textContent = n.name.replace(/\n/g, ' ');
document.getElementById('detail-name').style.color = color;
document.getElementById('detail-tag').textContent = n.tag || '';
document.getElementById('detail-desc').textContent = n.desc || '暂无描述';
const connList = document.getElementById('conn-list'); connList.innerHTML = '';
const rels = LINKS.filter(l => l.source === nodeId || l.target === nodeId);
if (!rels.length) { connList.innerHTML = '<div style="color:#484f58;font-size:11px">无直接关联</div>'; return; }
rels.slice(0, 12).forEach(l => {
const isOut = l.source === nodeId, otherId = isOut ? l.target : l.source, o = nodeMap[otherId];
if (!o) return;
const div = document.createElement('div'); div.className = 'conn-item';
div.innerHTML = `<div class="conn-dot" style="background:${CAT_COLORS[o.category]}"></div><div class="conn-text">${o.name.replace(/\n/g, ' ')}</div><div class="conn-rel">${isOut ? '→' : '←'} ${l.label}</div>`;
div.onclick = () => showDetail(otherId); connList.appendChild(div);
});
}
function activateScenario(key) {
activeScenario = key;
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.toggle('active', b.dataset.key === key));
chart.setOption(buildOption(SCENARIOS[key].nodes));
showDetail(key);
if (SIMULATION_STEPS[key]) simInit(key); else simHide();
}
function clearScenario() {
activeScenario = null; catFilter = null; simHide(); simClearTimer(); simCurrentStep = -1;
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.remove('active'));
chart.setOption(buildOption());
}
function filterByCategory(cat) {
catFilter = catFilter === cat ? null : cat;
chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null));
}
document.getElementById('btn-reset').onclick = () => { clearScenario(); chart.dispatchAction({ type: 'restore' }); };
document.getElementById('btn-labels').onclick = function () { showLabels = !showLabels; this.textContent = showLabels ? '标签 ON' : '标签 OFF'; this.classList.toggle('active', showLabels); chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null)); };
document.getElementById('btn-rules').onclick = function () { showRuleEdges = !showRuleEdges; this.textContent = showRuleEdges ? '规则边' : '规则边 OFF'; this.classList.toggle('active', showRuleEdges); chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null)); };
document.getElementById('btn-fullscreen').onclick = () => { if (!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); };
initChart();
// ================================================================
// 动态模拟系统
// ================================================================
let simCurrentStep = -1, simScenarioKey = null, simAutoTimer = null, simIsPlaying = false;
const PHASE_META = {
process: { color: '#8B5CF6', bg: 'rgba(139,92,246,0.15)', label: '🟣 流程启动' },
behavior: { color: '#10B981', bg: 'rgba(16,185,129,0.15)', label: '🟢 行为调用' },
rule: { color: '#F59E0B', bg: 'rgba(245,158,11,0.15)', label: '🟠 规则引用' },
entity: { color: '#3B82F6', bg: 'rgba(59,130,246,0.15)', label: '🔵 对象生成' },
complete: { color: '#FFD700', bg: 'rgba(255,215,0,0.15)', label: '✅ 完成' },
};
function simInit(key) {
simScenarioKey = key; simCurrentStep = -1; simClearTimer(); simIsPlaying = false;
document.getElementById('sim-auto-btn').classList.remove('playing');
document.getElementById('sim-auto-btn').textContent = '⏵ 自动';
document.getElementById('sim-title').textContent = (SCENARIOS[key] ? SCENARIOS[key].label : key) + ' — 动态模拟';
document.getElementById('sim-step-badge').textContent = '0 / ' + SIMULATION_STEPS[key].length;
document.getElementById('sim-step-title').textContent = '点击「下一步」开始模拟';
document.getElementById('sim-step-desc').textContent = '逐步展示问题分析与解决的完整调用链路。';
document.getElementById('sim-progress-bar').style.width = '0%';
document.getElementById('sim-phase-badge').textContent = '';
document.getElementById('sim-phase-badge').style.cssText = '';
document.getElementById('sim-prev-btn').disabled = true;
document.getElementById('sim-next-btn').disabled = false;
document.getElementById('sim-next-btn').textContent = '下一步 ▶';
document.getElementById('sim-bar').classList.add('visible');
}
function simHide() { document.getElementById('sim-bar').classList.remove('visible'); }
function simClearTimer() { if (simAutoTimer) { clearInterval(simAutoTimer); simAutoTimer = null; } }
function simStep(dir) {
if (!simScenarioKey) return;
const steps = SIMULATION_STEPS[simScenarioKey], next = simCurrentStep + dir;
if (next < 0 || next >= steps.length) return;
simCurrentStep = next; simRenderStep(steps[simCurrentStep]);
}
function simRenderStep(step) {
const steps = SIMULATION_STEPS[simScenarioKey], total = steps.length;
document.getElementById('sim-progress-bar').style.width = Math.round((step.stepNo / total) * 100) + '%';
document.getElementById('sim-step-badge').textContent = step.stepNo + ' / ' + total;
const pm = PHASE_META[step.phase] || PHASE_META.process;
const badge = document.getElementById('sim-phase-badge');
badge.textContent = pm.label; badge.style.cssText = `color:${pm.color};background:${pm.bg};border:1px solid ${pm.color}44;font-size:10px;padding:3px 9px;border-radius:10px;font-weight:700;`;
document.getElementById('sim-step-title').textContent = step.title;
document.getElementById('sim-step-desc').textContent = step.desc;
document.getElementById('sim-prev-btn').disabled = simCurrentStep <= 0;
const isLast = simCurrentStep >= total - 1;
document.getElementById('sim-next-btn').disabled = isLast;
document.getElementById('sim-next-btn').textContent = isLast ? '已完成 ✓' : '下一步 ▶';
if (isLast && simIsPlaying) simToggleAuto();
const allNodeIds = step.allNodes, newNodeIds = step.newNodes || [];
const activeEdgeSet = new Set(step.activeEdges.map(e => e.source + '__' + e.target));
const edgeSeqMap = {}; step.activeEdges.forEach(e => { edgeSeqMap[e.source + '__' + e.target] = e; });
const simNodes = NODES.map(n => {
const isActive = allNodeIds.includes(n.id), isNew = newNodeIds.includes(n.id) && !step.isComplete, isComp = step.isComplete;
let sb = 0, sc = 'transparent', bw = 0, bc = CAT_COLORS[n.category];
if (isNew) { sb = 30; sc = CAT_COLORS[n.category]; bw = 3; bc = '#fff'; }
else if (isActive && isComp) { sb = 16; sc = CAT_COLORS[n.category]; }
else if (isActive) { sb = 10; sc = CAT_COLORS[n.category]; }
return {
id: n.id, name: n.name, category: n.category,
symbolSize: n.symbolSize * (isNew ? 1.22 : (isActive ? 1 : 0.8)),
label: { show: isActive, fontSize: n.category === 3 ? 11 : (n.category === 0 ? 10 : 9), fontWeight: isNew ? 'bold' : 'normal', color: isNew ? '#fff' : '#c9d1d9', textBorderColor: '#0a0e1a', textBorderWidth: 3 },
itemStyle: { color: CAT_COLORS[n.category], opacity: isActive ? 1 : 0.05, shadowBlur: sb, shadowColor: sc, borderWidth: bw, borderColor: bc },
};
});
const simLinks = LINKS.map(l => {
const ek = l.source + '__' + l.target, ia = activeEdgeSet.has(ek), ei = edgeSeqMap[ek];
return {
source: l.source, target: l.target,
label: { show: ia && !!ei, formatter: ei ? ei.label : l.label, fontSize: 10, fontWeight: 'bold', color: '#FFD700', textBorderColor: '#0a0e1a', textBorderWidth: 2, backgroundColor: 'rgba(0,0,0,0.55)', padding: [2, 5], borderRadius: 4 },
lineStyle: { color: ia ? (pm.color || '#10B981') : l.lineStyle.color, width: ia ? (l.lineStyle.width || 1.5) + 1.5 : (l.lineStyle.width || 1.5), opacity: ia ? 0.92 : 0.03, curveness: 0.2, type: 'solid', shadowBlur: ia ? 12 : 0, shadowColor: ia ? (pm.color || '#10B981') : 'transparent' },
symbol: ['none', 'arrow'], symbolSize: ia ? [0, 9] : [0, 6],
};
});
chart.setOption({ series: [{ data: simNodes, links: simLinks, force: { repulsion: 320, edgeLength: [80, 200], gravity: 0.06, layoutAnimation: false } }] }, false);
}
function simToggleAuto() {
simIsPlaying = !simIsPlaying; const btn = document.getElementById('sim-auto-btn');
if (simIsPlaying) {
btn.classList.add('playing'); btn.textContent = '⏸ 暂停';
simAutoTimer = setInterval(() => { const s = SIMULATION_STEPS[simScenarioKey]; if (simCurrentStep >= s.length - 1) simToggleAuto(); else simStep(1); }, 2200);
} else { btn.classList.remove('playing'); btn.textContent = '⏵ 自动'; simClearTimer(); }
}
function simStop() {
simClearTimer(); simIsPlaying = false; simCurrentStep = -1; simScenarioKey = null; simHide();
if (activeScenario) chart.setOption(buildOption(SCENARIOS[activeScenario].nodes)); else chart.setOption(buildOption());
}
</script>
</body>
</html>