feat: 优化前端页面和组件

- 重构 Agents 页面
- 优化 Knowledge 页面
- 更新侧边栏导航
- 添加前端依赖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 15:42:21 +08:00
parent 0a9f6e278e
commit cac05b4297
6 changed files with 513 additions and 642 deletions

29
web/package-lock.json generated
View File

@@ -20,6 +20,7 @@
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/papaparse": "^5.5.2",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@@ -982,6 +983,27 @@
"@types/lodash": "*" "@types/lodash": "*"
} }
}, },
"node_modules/@types/node": {
"version": "25.3.5",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.3.5.tgz",
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/papaparse": {
"version": "5.5.2",
"resolved": "https://registry.npmmirror.com/@types/papaparse/-/papaparse-5.5.2.tgz",
"integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.20", "version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -2624,6 +2646,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",

View File

@@ -21,6 +21,7 @@
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/papaparse": "^5.5.2",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",

View File

@@ -39,42 +39,45 @@ interface MenuItem {
path?: string path?: string
} }
const mainMenu = computed<MenuItem[]>(() => [ // 第1组: Chat, Agents
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' }, const group1 = computed(() => [
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' }, { name: 'Chat', icon: 'fa-robot', path: '/agents' },
{ name: 'Script', icon: 'fa-code', path: '/script' }, { name: 'Agents', icon: 'fa-users', badge: 3, path: '/agents' },
])
// 第2组: Database, Knowledge
const group2 = computed(() => [
{ name: 'Database', icon: 'fa-database', path: '/database', badge: databaseCount.value }, { name: 'Database', icon: 'fa-database', path: '/database', badge: databaseCount.value },
{ name: 'Knowledge', icon: 'fa-brain', path: '/knowledge', badge: knowledgeCount.value }, { name: 'Knowledge', icon: 'fa-brain', path: '/knowledge', badge: knowledgeCount.value },
]) ])
const middleMenu: MenuItem[] = [ // 第3组: Skills, Tools, Script
const group3 = computed(() => [
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' }, { name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' }, { name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
] { name: 'Script', icon: 'fa-code', path: '/script' },
])
const bottomMenu: MenuItem[] = [ // 第4组: Dashboard, Account, Settings
{ name: 'Settings', icon: 'fa-gear', path: '/settings' }, const group4 = computed(() => [
] { name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
const bottomMenu2: MenuItem[] = [
{ name: 'Account', icon: 'fa-user', path: '/account' }, { name: 'Account', icon: 'fa-user', path: '/account' },
] { name: 'Settings', icon: 'fa-gear', path: '/settings' },
])
const activeMenu = computed(() => { const activeMenu = computed(() => {
const currentPath = route.path const currentPath = route.path
// Check main menu
const menuItem = mainMenu.value.find(item => item.path === currentPath) // Special case for /agents - prioritize Chat over Agents
if (menuItem) return menuItem.name if (currentPath === '/agents') {
// Check middle menu (Skills, Tools) return 'Chat'
const middleItem = middleMenu.find(item => item.path === currentPath) }
if (middleItem) return middleItem.name
// Check bottom menu (Settings) // Check all groups
const bottomItem = bottomMenu.find(item => item.path === currentPath) const allGroups = [...group1.value, ...group2.value, ...group3.value, ...group4.value]
if (bottomItem) return bottomItem.name const item = allGroups.find(item => item.path === currentPath)
// Check bottomMenu2 (Account) if (item) return item.name
const bottomItem2 = bottomMenu2.find(item => item.path === currentPath) return 'Chat'
if (bottomItem2) return bottomItem2.name
return 'Dashboard'
}) })
const navigateTo = (item: MenuItem) => { const navigateTo = (item: MenuItem) => {
@@ -129,8 +132,8 @@ const handleUserCommand = (command: string) => {
<!-- 导航菜单 --> <!-- 导航菜单 -->
<nav class="flex-1 px-3 py-2"> <nav class="flex-1 px-3 py-2">
<ul class="space-y-1"> <ul class="space-y-1">
<!-- Dashboard, Agents --> <!-- 第1组: Chat, Agents -->
<li v-for="item in mainMenu.slice(0, 2)" :key="item.name"> <li v-for="item in group1" :key="item.name">
<a <a
href="#" href="#"
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm" class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
@@ -148,8 +151,8 @@ const handleUserCommand = (command: string) => {
<!-- 分隔线1 --> <!-- 分隔线1 -->
<li class="my-4 border-t border-dark-500"></li> <li class="my-4 border-t border-dark-500"></li>
<!-- Database, Knowledge --> <!-- 第2组: Database, Knowledge -->
<li v-for="item in mainMenu.slice(2)" :key="item.name"> <li v-for="item in group2" :key="item.name">
<a <a
href="#" href="#"
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm" class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
@@ -167,8 +170,8 @@ const handleUserCommand = (command: string) => {
<!-- 分隔线2 --> <!-- 分隔线2 -->
<li class="my-4 border-t border-dark-500"></li> <li class="my-4 border-t border-dark-500"></li>
<!-- Skills & Tools --> <!-- 第3组: Skills, Tools, Script -->
<li v-for="item in middleMenu" :key="item.name"> <li v-for="item in group3" :key="item.name">
<a <a
href="#" href="#"
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm" class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
@@ -183,8 +186,11 @@ const handleUserCommand = (command: string) => {
</a> </a>
</li> </li>
<!-- Settings --> <!-- 分隔线3 -->
<li v-for="item in bottomMenu" :key="item.name"> <li class="my-4 border-t border-dark-500"></li>
<!-- 第4组: Dashboard, Account, Settings -->
<li v-for="item in group4" :key="item.name">
<a <a
href="#" href="#"
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm" class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
@@ -202,24 +208,6 @@ const handleUserCommand = (command: string) => {
</div> </div>
</a> </a>
</li> </li>
<!-- 分隔线 -->
<li class="my-4 border-t border-dark-500"></li>
<!-- Account -->
<li v-for="item in bottomMenu2" :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>
</a>
</li>
</ul> </ul>
</nav> </nav>

View File

@@ -1,645 +1,429 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, nextTick } from 'vue'
interface ChatMessage {
id: number
role: 'user' | 'assistant'
content: string
timestamp: Date
isStreaming?: boolean
}
interface Agent { interface Agent {
id: number id: number
name: string name: string
framework: string avatar: string
status: 'running' | 'stopped' | 'error'
mcpServers: number
model: string
createdAt: string
description: string description: string
accentColor: string
gradient: string
} }
// Agents 数据 // AI 助手配置
const agents = ref<Agent[]>([ 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: 1, name: 'Claude', avatar: '🧠', description: 'Anthropic AI', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20' },
{ 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: 2, name: 'Gemini', avatar: '✨', description: 'Google DeepMind', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20' },
{ 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: 3, name: 'ChatGPT', avatar: '💬', description: 'OpenAI', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20' },
{ 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: 4, name: 'DeepSeek', avatar: '🔮', description: 'DeepSeek AI', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20' },
{ 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' }, { id: 5, name: 'Kimi', avatar: '🌙', description: 'Moonshot AI', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20' },
{ id: 6, name: '文心一言', avatar: '🐉', description: 'Baidu', accentColor: '#ef4444', gradient: 'from-red-500/20 to-orange-500/20' },
{ id: 7, name: '通义千问', avatar: '☁️', description: 'Alibaba', accentColor: '#06b6d4', gradient: 'from-cyan-500/20 to-sky-500/20' },
]) ])
// 编辑状态 // 当前选中的助手
const editingAgent = ref<Agent | null>(null) const selectedAgent = ref<Agent | null>(agents.value[0])
const isEditing = ref(false) const sidebarCollapsed = ref(false)
const isCreating = ref(false)
const searchQuery = ref('')
const filterStatus = ref<string>('all')
// 新建 Agent 表单 // 聊天消息
const newAgentForm = ref({ const messages = ref<ChatMessage[]>([
name: '', { id: 1, role: 'assistant', content: '你好!我是 Claude你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() },
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' }, const inputMessage = ref('')
{ name: 'OpenAI', icon: 'fa-openai', color: 'from-green-500 to-green-600' }, const isLoading = ref(false)
{ name: 'PydanticAI', icon: 'fa-robot', color: 'from-purple-500 to-purple-600' }, const messagesContainer = ref<HTMLElement | null>(null)
{ 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'] }, const sendMessage = async () => {
{ name: 'OpenAI', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'] }, if (!inputMessage.value.trim() || isLoading.value) return
{ 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 = [ const userContent = inputMessage.value.trim()
{ name: 'linear-demo', icon: 'fa-check-circle', status: 'connected' }, inputMessage.value = ''
{ 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 userMessage: ChatMessage = {
const openCreate = () => { id: Date.now(),
newAgentForm.value = { role: 'user',
name: '', content: userContent,
framework: 'Google ADK', timestamp: new Date()
model: 'gemini-2.0-flash',
description: '',
mcpServers: [],
}
isCreating.value = true
} }
messages.value.push(userMessage)
// 关闭新建弹窗 const aiMessage: ChatMessage = {
const closeCreate = () => { id: Date.now() + 1,
isCreating.value = false role: 'assistant',
content: '',
timestamp: new Date(),
isStreaming: true
} }
messages.value.push(aiMessage)
// 保存新建 nextTick(() => scrollToBottom())
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 服务器 isLoading.value = true
const toggleMCPServer = (serverName: string) => { const fullResponse = `我理解你发送了消息: "${userContent}"
const index = newAgentForm.value.mcpServers.indexOf(serverName)
if (index === -1) { 作为 AI 助手,我可以帮助你:
newAgentForm.value.mcpServers.push(serverName) • 回答各种问题
• 编写代码和调试
• 分析和处理数据
• 翻译和写作
• 头脑风暴和创意建议
请告诉我你需要什么帮助?`
let currentIndex = 0
const words = fullResponse.split('')
const streamInterval = setInterval(() => {
if (currentIndex < words.length) {
aiMessage.content += words[currentIndex]
currentIndex++
nextTick(() => scrollToBottom())
} else { } else {
newAgentForm.value.mcpServers.splice(index, 1) clearInterval(streamInterval)
aiMessage.isStreaming = false
isLoading.value = false
}
}, 30)
}
// 滚动到底部
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
} }
} }
// 编辑表单数据 // 复制消息
const editForm = ref({ const copyMessage = (content: string) => {
name: '', navigator.clipboard.writeText(content)
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 = () => { const selectAgent = (agent: Agent) => {
if (editingAgent.value) { selectedAgent.value = agent
const index = agents.value.findIndex(a => a.id === editingAgent.value!.id) messages.value = [
if (index !== -1) { { id: 1, role: 'assistant', content: `你好!我是 ${agent.name}。有什么我可以帮助你的吗?`, timestamp: new Date() }
agents.value[index] = { ]
...agents.value[index],
...editForm.value,
}
}
}
isEditing.value = false
} }
// 取消编辑 // 新建聊天
const cancelEdit = () => { const newChat = () => {
isEditing.value = false messages.value = [
editingAgent.value = null { id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value?.name || 'Claude'}。有什么我可以帮助你的吗?`, timestamp: new Date() }
]
} }
// 切换状态 // 格式化时间
const toggleStatus = (agent: Agent) => { const formatTime = (date: Date) => {
if (agent.status === 'running') { return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
agent.status = 'stopped' }
} else if (agent.status === 'stopped') {
agent.status = 'running' // 回车发送
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
sendMessage()
} }
} }
// 删除 Agent // 调整输入框高度
const deleteAgent = (id: number) => { const autoResize = (e: Event) => {
agents.value = agents.value.filter(a => a.id !== id) const target = e.target as HTMLTextAreaElement
target.style.height = 'auto'
target.style.height = Math.min(target.scrollHeight, 160) + 'px'
} }
// 过滤后的 Agents // 切换侧边栏
const filteredAgents = () => { const toggleSidebar = () => {
return agents.value.filter(agent => { sidebarCollapsed.value = !sidebarCollapsed.value
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || setTimeout(() => {
agent.framework.toLowerCase().includes(searchQuery.value.toLowerCase()) scrollToBottom()
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value }, 350)
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> </script>
<style scoped> <style scoped>
/* 模态框进入动画 */ ::-webkit-scrollbar {
@keyframes modal-in { width: 4px;
0% { }
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
@keyframes messageSlideIn {
from {
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(20px); transform: translateY(12px);
} }
100% { to {
opacity: 1; opacity: 1;
transform: scale(1) translateY(0); transform: translateY(0);
} }
} }
@keyframes fade-in { .message-enter {
0% { opacity: 0; transform: translateY(-5px); } animation: messageSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
100% { opacity: 1; transform: translateY(0); }
} }
@keyframes float { @keyframes blink {
0%, 100% { transform: translateY(0); } 0%, 100% { opacity: 1; }
50% { transform: translateY(-8px); } 50% { opacity: 0; }
} }
@keyframes scale-in { .cursor-blink {
0% { opacity: 0; transform: scale(0.9); } animation: blink 1s step-end infinite;
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> </style>
<template> <template>
<!-- 主内容区域 --> <div class="h-screen flex bg-[#0a0a0f]">
<div class="p-6 min-h-screen"> <!-- 主聊天区域 -->
<!-- 顶部导航 --> <div class="flex-1 flex flex-col bg-[#0a0a0f]">
<div class="flex justify-between items-center mb-6"> <!-- 顶部栏 -->
<div class="h-14 px-6 flex items-center justify-between border-b border-white/5 bg-[#0d0d12]/50 backdrop-blur-sm">
<!-- 左侧当前AI信息 -->
<div class="flex items-center gap-3">
<div v-if="selectedAgent" class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg shadow-lg"
:style="{ backgroundColor: selectedAgent.accentColor + '20', color: selectedAgent.accentColor }"
>
{{ selectedAgent.avatar }}
</div>
<div>
<div class="text-sm font-medium text-white">{{ selectedAgent?.name || 'Chat' }}</div>
<div class="text-[11px] flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="text-white/40">Online</span>
</div>
</div>
</div>
</div>
<!-- 右上角操作 -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<i class="fa-solid fa-robot text-gray-400"></i> <button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
<span class="font-medium">Agents</span> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
<button @click="openCreate" class="btn-primary"> </svg>
<i class="fa-solid fa-plus"></i>
New Agent
</button> </button>
</div> <button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<!-- 搜索和筛选 --> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
<div class="flex gap-4 mb-6"> </svg>
<div class="flex-1 relative"> </button>
<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="search-input w-full"
>
</div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
<el-option label="All Status" value="all" />
<el-option label="Running" value="running" />
<el-option label="Stopped" value="stopped" />
<el-option label="Error" value="error" />
</el-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="table-row">
<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 <button
@click="toggleStatus(agent)" v-if="sidebarCollapsed"
class="btn-icon" @click="toggleSidebar"
:title="agent.status === 'running' ? 'Stop' : 'Start'" class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors"
title="Show AI assistants"
> >
<i :class="['fa-solid', agent.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400']"></i> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button> </button>
</div>
</div>
<!-- 消息区域 -->
<div ref="messagesContainer" class="flex-1 overflow-y-auto px-6 py-6">
<div class="max-w-3xl mx-auto space-y-6">
<div
v-for="message in messages"
:key="message.id"
class="message-enter flex gap-4"
:class="message.role === 'user' ? 'flex-row-reverse' : ''"
>
<!-- 头像 -->
<div
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 shadow-lg"
:class="message.role === 'user' ? 'bg-gradient-to-br from-emerald-500 to-teal-600' : ''"
:style="message.role === 'assistant' && selectedAgent ? {
backgroundColor: selectedAgent.accentColor + '20',
color: selectedAgent.accentColor
} : {}"
>
<span v-if="message.role === 'user'" class="text-white text-sm">👤</span>
<span v-else class="text-lg">{{ selectedAgent?.avatar || '🧠' }}</span>
</div>
<!-- 消息内容 -->
<div
class="max-w-[75%] rounded-2xl px-4 py-3"
:class="message.role === 'user' ? 'bg-[#1e1e28] text-white' : 'bg-transparent'"
>
<div class="text-sm leading-relaxed whitespace-pre-wrap text-white/90">{{ message.content }}
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-violet-400 cursor-blink align-middle"></span>
</div>
<!-- 消息底部 -->
<div class="flex items-center justify-end mt-2 gap-3">
<span class="text-[10px] text-white/25">{{ formatTime(message.timestamp) }}</span>
<button <button
@click="openEdit(agent)" v-if="message.role === 'assistant' && !message.isStreaming"
class="btn-icon" @click="copyMessage(message.content)"
title="Edit" class="text-white/25 hover:text-violet-400 transition-colors"
title="Copy"
> >
<i class="fa-solid fa-pen text-gray-400"></i> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</button> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
<button </svg>
@click="deleteAgent(agent.id)"
class="btn-icon"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
</button> </button>
</div> </div>
</td> </div>
</tr> </div>
</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>
</div> </div>
<!-- 编辑弹窗 --> <!-- 输入区域 -->
<Teleport to="body"> <div class="p-4 border-t border-white/5 bg-[#0d0d12]/50">
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"> <div class="max-w-3xl mx-auto">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl"> <div class="relative bg-[#12121a] rounded-2xl border border-white/8 focus-within:border-violet-500/40 focus-within:shadow-lg focus-within:shadow-violet-500/10 transition-all duration-300">
<!-- 弹窗头部 --> <!-- 附件按钮 -->
<div class="flex items-center justify-between p-5 border-b border-dark-500"> <button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors p-1">
<h3 class="text-lg font-semibold">Edit Agent</h3> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
<i class="fa-solid fa-xmark text-xl"></i> </svg>
</button> </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="input-field"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Framework</label>
<el-select v-model="editForm.framework" placeholder="Select" class="w-full" size="large">
<el-option label="Google ADK" value="Google ADK" />
<el-option label="OpenAI" value="OpenAI" />
<el-option label="PydanticAI" value="PydanticAI" />
<el-option label="LangChain" value="LangChain" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
<el-select v-model="editForm.model" placeholder="Select" class="w-full" size="large">
<el-option label="gemini-2.0-flash" value="gemini-2.0-flash" />
<el-option label="gpt-4o" value="gpt-4o" />
<el-option label="gpt-4o-mini" value="gpt-4o-mini" />
<el-option label="claude-3-5-sonnet" value="claude-3-5-sonnet" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea <textarea
v-model="editForm.description" v-model="inputMessage"
rows="3" @keydown="handleKeydown"
class="input-field resize-none" @input="autoResize"
placeholder="Send a message..."
rows="1"
class="w-full bg-transparent text-white placeholder-white/30 py-3.5 pl-12 pr-24 resize-none focus:outline-none text-sm"
></textarea> ></textarea>
</div>
</div>
<!-- 弹窗底部 --> <!-- 发送按钮 -->
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button <button
@click="cancelEdit" @click="sendMessage"
class="btn-secondary" :disabled="!inputMessage.trim() || isLoading"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-violet-500 hover:bg-violet-400 disabled:bg-white/8 disabled:text-white/20 text-white transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/25"
> >
Cancel <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</button> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
<button </svg>
@click="saveEdit"
class="btn-primary"
>
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">
<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> </button>
</div> </div>
<!-- 三栏布局主体 --> <!-- 提示 -->
<div class="flex-1 flex overflow-hidden"> <div class="text-center mt-2.5">
<!-- 左侧框架选择 --> <span class="text-[10px] text-white/20">AI can make mistakes. Please verify important information.</span>
<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>
<div class="space-y-3"> </div>
<div </div>
v-for="fw in frameworks" </div>
: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" <transition
:class="newAgentForm.framework === fw.name enter-active-class="transition-all duration-300 ease-out"
? 'border-primary-orange bg-dark-600 shadow-lg shadow-primary-orange/20' enter-from-class="opacity-0 translate-x-4"
: 'border-dark-500 bg-dark-700 hover:border-gray-500'" enter-to-class="opacity-100 translate-x-0"
> leave-active-class="transition-all duration-250 ease-in"
<div class="flex items-center gap-3"> leave-from-class="opacity-100 translate-x-0"
<div :class="['w-10 h-10 rounded-lg bg-gradient-to-br flex items-center justify-center', fw.color]"> leave-to-class="opacity-0 translate-x-4"
<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>
<el-select v-model="newAgentForm.model" placeholder="Select" class="w-full" size="large">
<el-option
v-for="model in models.find(m => m.name === newAgentForm.framework)?.models"
:key="model"
:label="model"
:value="model"
/>
</el-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 v-show="!sidebarCollapsed" class="w-72 bg-[#0d0d12] border-l border-white/5 flex flex-col">
<!-- Logo -->
<div class="p-4 border-b border-white/5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-dark-500 flex items-center justify-center"> <div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-500/25">
<i :class="['fa-solid', server.icon, server.status === 'connected' ? 'text-green-400' : 'text-gray-500']"></i> <span class="text-white text-lg">🤖</span>
</div> </div>
<div> <span class="text-lg font-semibold text-white tracking-tight">AI Hub</span>
<div class="font-medium text-white">{{ server.name }}</div> </div>
<div class="text-xs flex items-center gap-1" :class="server.status === 'connected' ? 'text-green-400' : 'text-gray-500'"> <button
<span class="w-1.5 h-1.5 rounded-full" :class="server.status === 'connected' ? 'bg-green-400' : 'bg-gray-500'"></span> @click="toggleSidebar"
{{ server.status }} class="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white/60 transition-colors"
title="Hide sidebar"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div> </div>
</div> </div>
<!-- 新建聊天按钮 -->
<div class="p-4">
<button
@click="newChat"
class="w-full py-2.5 px-4 bg-[#1a1a24] hover:bg-[#22222e] border border-white/8 hover:border-violet-500/30 rounded-xl text-white/90 text-sm flex items-center justify-center gap-2 transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/10 group"
>
<svg class="w-4 h-4 text-violet-400 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
<span class="font-medium">New Chat</span>
</button>
</div> </div>
<!-- AI 助手列表 -->
<div class="flex-1 overflow-y-auto px-3 py-2">
<div class="text-[11px] font-medium text-white/30 uppercase tracking-wider px-3 mb-3">AI Assistants</div>
<div class="space-y-1">
<div <div
class="w-6 h-6 rounded-md flex items-center justify-center transition-all" v-for="agent in agents"
:class="newAgentForm.mcpServers.includes(server.name) ? 'bg-green-500' : 'bg-dark-500'" :key="agent.id"
@click="selectAgent(agent)"
class="group px-3 py-2.5 rounded-xl cursor-pointer transition-all duration-200"
:class="selectedAgent?.id === agent.id
? 'bg-gradient-to-r ' + agent.gradient + ' border-l-2'
: 'hover:bg-white/[0.03] border-l-2 border-transparent'"
:style="selectedAgent?.id === agent.id ? `border-left-color: ${agent.accentColor}` : ''"
> >
<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"> <div class="flex items-center gap-3">
<button <div
@click="closeCreate" class="w-8 h-8 rounded-lg flex items-center justify-center text-lg transition-transform duration-200 group-hover:scale-110"
class="btn-secondary px-6 py-2.5" :class="selectedAgent?.id === agent.id ? 'shadow-lg' : ''"
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
> >
Cancel {{ agent.avatar }}
</button> </div>
<button <div class="flex-1 min-w-0">
@click="saveNewAgent" <div class="text-sm font-medium text-white/90 truncate">{{ agent.name }}</div>
class="btn-primary px-6 py-2.5" <div class="text-[11px] text-white/40 truncate">{{ agent.description }}</div>
> </div>
<i class="fa-solid fa-plus"></i> </div>
Create Agent </div>
</div>
</div>
<!-- 底部设置 -->
<div class="p-4 border-t border-white/5">
<button class="w-full py-2.5 rounded-xl bg-white/[0.02] hover:bg-white/[0.05] text-white/50 hover:text-white/80 text-sm flex items-center justify-center gap-2 transition-all duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Settings</span>
</button> </button>
</div> </div>
</div> </div>
</div> </transition>
</div>
</Teleport>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings' import { useModelSettings } from './settings/useModelSettings'
import { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge' import { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge'
import VueOfficeDocx from '@vue-office/docx' import VueOfficeDocx from '@vue-office/docx'
@@ -42,6 +42,11 @@ const embeddingModels = computed(() => {
return models.value.filter((m: any) => m.model_type === 'embedding') return models.value.filter((m: any) => m.model_type === 'embedding')
}) })
// 筛选 VLM 模型
const vlmModels = computed(() => {
return models.value.filter((m: any) => m.model_type === 'vlm')
})
// 步骤验证 // 步骤验证
const step1Valid = computed(() => !!newKbForm.value.name.trim()) const step1Valid = computed(() => !!newKbForm.value.name.trim())
const step2Valid = computed(() => !!modelConfig.value.llmModelId && !!modelConfig.value.embeddingModelId) const step2Valid = computed(() => !!modelConfig.value.llmModelId && !!modelConfig.value.embeddingModelId)
@@ -188,6 +193,7 @@ const newKbForm = ref({
const modelConfig = ref({ const modelConfig = ref({
llmModelId: '', llmModelId: '',
embeddingModelId: '', embeddingModelId: '',
vlmModelId: '',
}) })
const parsingConfig = ref({ const parsingConfig = ref({
@@ -212,7 +218,11 @@ const storageConfig = ref({
const openCreateDialog = () => { const openCreateDialog = () => {
createStep.value = 1 createStep.value = 1
newKbForm.value = { name: '', description: '' } newKbForm.value = { name: '', description: '' }
modelConfig.value = { llmModelId: '', embeddingModelId: '' } modelConfig.value = {
llmModelId: '',
embeddingModelId: '',
vlmModelId: '',
}
parsingConfig.value = { parsingConfig.value = {
enablePdf: true, enablePdf: true,
engine: 'markitdown', engine: 'markitdown',
@@ -222,13 +232,23 @@ const openCreateDialog = () => {
highRes: false, highRes: false,
fileSizeLimit: '5242880', fileSizeLimit: '5242880',
} }
storageConfig.value = { type: 'local' } storageConfig.value = {
type: 'local',
endpoint: '',
accessKeyId: '',
secretAccessKey: '',
bucket: '',
}
showCreateDialog.value = true showCreateDialog.value = true
} }
const cancelCreate = () => { const cancelCreate = () => {
newKbForm.value = { name: '', description: '' } newKbForm.value = { name: '', description: '' }
modelConfig.value = { llmModelId: '', embeddingModelId: '' } modelConfig.value = {
llmModelId: '',
embeddingModelId: '',
vlmModelId: '',
}
parsingConfig.value = { parsingConfig.value = {
enablePdf: true, enablePdf: true,
engine: 'markitdown', engine: 'markitdown',
@@ -238,7 +258,13 @@ const cancelCreate = () => {
highRes: false, highRes: false,
fileSizeLimit: '5242880', fileSizeLimit: '5242880',
} }
storageConfig.value = { type: 'local' } storageConfig.value = {
type: 'local',
endpoint: '',
accessKeyId: '',
secretAccessKey: '',
bucket: '',
}
showCreateDialog.value = false showCreateDialog.value = false
} }
@@ -254,6 +280,10 @@ const createKnowledgeBase = async () => {
enable_pdf: parsingConfig.value.enablePdf, enable_pdf: parsingConfig.value.enablePdf,
pandoc: parsingConfig.value.pandoc, pandoc: parsingConfig.value.pandoc,
}, },
vlm_config: modelConfig.value.vlmModelId ? {
enabled: true,
model_id: modelConfig.value.vlmModelId,
} : undefined,
storage_config: { storage_config: {
type: storageConfig.value.type, type: storageConfig.value.type,
endpoint: storageConfig.value.type === 'minio' ? storageConfig.value.endpoint : undefined, endpoint: storageConfig.value.type === 'minio' ? storageConfig.value.endpoint : undefined,
@@ -266,7 +296,11 @@ const createKnowledgeBase = async () => {
if (result.success) { if (result.success) {
await fetchKbList() await fetchKbList()
newKbForm.value = { name: '', description: '' } newKbForm.value = { name: '', description: '' }
modelConfig.value = { llmModelId: '', embeddingModelId: '' } modelConfig.value = {
llmModelId: '',
embeddingModelId: '',
vlmModelId: '',
}
parsingConfig.value = { parsingConfig.value = {
enablePdf: true, enablePdf: true,
engine: 'markitdown', engine: 'markitdown',
@@ -316,6 +350,17 @@ const cancelEdit = () => {
// 删除知识库 // 删除知识库
const deleteKb = async (id: string) => { const deleteKb = async (id: string) => {
try {
await ElMessageBox.confirm(
'Are you sure you want to delete this knowledge base? This action cannot be undone.',
'Delete Knowledge Base',
{
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
const result = await apiDeleteKnowledgeBase(id) const result = await apiDeleteKnowledgeBase(id)
if (result.success) { if (result.success) {
await fetchKbList() await fetchKbList()
@@ -323,6 +368,9 @@ const deleteKb = async (id: string) => {
} else { } else {
ElMessage.error(result.message || 'Failed to delete knowledge base') ElMessage.error(result.message || 'Failed to delete knowledge base')
} }
} catch {
// User cancelled
}
} }
// 辅助函数:格式化文件大小 // 辅助函数:格式化文件大小
@@ -890,6 +938,23 @@ const deleteDocument = async (docId: string) => {
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- VLM Configuration -->
<el-form-item label="VLM Model (Optional)">
<el-select v-model="modelConfig.vlmModelId" placeholder="Select a VLM model" class="w-full" popper-class="dark-select-dropdown" clearable>
<el-option
v-for="model in vlmModels"
:key="model.id"
:label="model.name"
:value="model.id"
>
<div class="model-option">
<span class="model-name">{{ model.name }}</span>
<span class="model-info">{{ model.provider }} - {{ model.model }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form> </el-form>
</div> </div>

View File

@@ -77,6 +77,10 @@ export const createKnowledgeBase = async (params: {
enable_pdf?: boolean enable_pdf?: boolean
pandoc?: boolean pandoc?: boolean
} }
vlm_config?: {
enabled: boolean
model_id: string
}
storage_config?: { storage_config?: {
type: string type: string
endpoint?: string endpoint?: string
@@ -195,7 +199,7 @@ export const reparseDocument = async (kbId: string, docId: string): Promise<{ su
} }
// 获取文档预览内容 // 获取文档预览内容
export const getDocumentPreview = async (kbId: string, docId: string, page: number = 1): Promise<{ success: boolean; data?: { total_pages: number; current_page: number; content: string }; message?: string }> => { export const getDocumentPreview = async (kbId: string, docId: string, page: number = 1): Promise<{ success: boolean; data?: { total_pages: number; current_page: number; content: string; content_type?: string }; message?: string }> => {
try { try {
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}/preview?page=${page}`) const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}/preview?page=${page}`)
const data = await response.json() const data = await response.json()