feat: 更新前端页面
- Agents, Chat, Settings, Skill, Tools - Account, Plan, Script - useSkills composable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import './database/database.css'
|
||||
|
||||
const { getCurrentUser, logout } = useAuth()
|
||||
|
||||
// 菜单类型
|
||||
type MenuKey = 'users' | 'roles' | 'permissions'
|
||||
type MenuKey = 'profile' | 'users' | 'roles' | 'permissions'
|
||||
|
||||
// 当前选中的菜单
|
||||
const activeMenu = ref<MenuKey>('users')
|
||||
const activeMenu = ref<MenuKey>('profile')
|
||||
|
||||
// 当前登录用户信息
|
||||
const currentUser = ref<any>(null)
|
||||
const userLoading = ref(false)
|
||||
const userError = ref('')
|
||||
|
||||
// 获取当前用户
|
||||
const fetchCurrentUser = async () => {
|
||||
userLoading.value = true
|
||||
userError.value = ''
|
||||
try {
|
||||
currentUser.value = await getCurrentUser()
|
||||
} catch (error: any) {
|
||||
userError.value = error.message
|
||||
} finally {
|
||||
userLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCurrentUser()
|
||||
})
|
||||
|
||||
// 菜单列表
|
||||
const menuItems = [
|
||||
{ key: 'profile', label: 'Profile', icon: 'fa-user' },
|
||||
{ key: 'users', label: 'Users', icon: 'fa-users' },
|
||||
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
|
||||
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
|
||||
@@ -193,6 +225,61 @@ const statusClass = (status: string) => {
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex-1">
|
||||
<!-- Profile -->
|
||||
<div v-if="activeMenu === 'profile'" class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">My Profile</h2>
|
||||
|
||||
<div v-if="userLoading" class="flex items-center justify-center py-12">
|
||||
<i class="fa-solid fa-circle-notch fa-spin text-2xl text-primary-orange"></i>
|
||||
</div>
|
||||
|
||||
<div v-else-if="userError" class="bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-400">
|
||||
{{ userError }}
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-dark-700 rounded-xl p-6 border border-dark-500">
|
||||
<div class="flex items-start gap-6">
|
||||
<!-- 头像 -->
|
||||
<div class="w-20 h-20 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center text-white text-3xl font-bold">
|
||||
{{ currentUser?.username?.charAt(0)?.toUpperCase() || 'U' }}
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-semibold">{{ currentUser?.username || 'User' }}</h3>
|
||||
<p class="text-gray-400">{{ currentUser?.email || 'No email' }}</p>
|
||||
|
||||
<div class="mt-4 flex gap-4">
|
||||
<div class="bg-dark-600 px-4 py-2 rounded-lg">
|
||||
<div class="text-sm text-gray-400">Role</div>
|
||||
<div class="font-medium">{{ currentUser?.role_id || 'User' }}</div>
|
||||
</div>
|
||||
<div class="bg-dark-600 px-4 py-2 rounded-lg">
|
||||
<div class="text-sm text-gray-400">Status</div>
|
||||
<div class="font-medium text-green-400">{{ currentUser?.is_active ? 'Active' : 'Inactive' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
Member since: {{ currentUser?.created_at ? new Date(currentUser.created_at).toLocaleDateString() : 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-6 pt-6 border-t border-dark-500 flex gap-3">
|
||||
<button 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">
|
||||
<i class="fa-solid fa-pen mr-2"></i>
|
||||
Edit Profile
|
||||
</button>
|
||||
<button @click="handleLogout" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors">
|
||||
<i class="fa-solid fa-sign-out-alt mr-2"></i>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div v-if="activeMenu === 'users'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -1,6 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 创建智能体弹窗状态
|
||||
const showCreateModal = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const newAgent = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
skills: '',
|
||||
knowledge: '',
|
||||
prompt: '',
|
||||
})
|
||||
|
||||
// Skills 选项
|
||||
const skillsOptions = [
|
||||
{ value: 'research', label: 'Research' },
|
||||
{ value: 'coder', label: 'Coder' },
|
||||
{ value: 'review', label: 'Code Review' },
|
||||
{ value: 'writer', label: 'Writer' },
|
||||
{ value: 'analyst', label: 'Analyst' },
|
||||
{ value: 'assistant', label: 'Assistant' },
|
||||
]
|
||||
|
||||
// Knowledge 选项
|
||||
const knowledgeOptions = [
|
||||
{ value: 'general', label: 'General Knowledge' },
|
||||
{ value: 'codebase', label: 'Codebase' },
|
||||
{ value: 'docs', label: 'Documentation' },
|
||||
{ value: 'api', label: 'API Reference' },
|
||||
]
|
||||
|
||||
// 打开创建弹窗
|
||||
const openCreateModal = () => {
|
||||
newAgent.value = { name: '', description: '', skills: '', knowledge: '', prompt: '' }
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
// 创建智能体
|
||||
const createAgent = async () => {
|
||||
if (!newAgent.value.name || !newAgent.value.skills || !newAgent.value.knowledge) {
|
||||
return
|
||||
}
|
||||
|
||||
isCreating.value = true
|
||||
try {
|
||||
// 模拟创建
|
||||
const newId = Math.max(...agents.value.map(a => a.id)) + 1
|
||||
agents.value.unshift({
|
||||
id: newId,
|
||||
name: newAgent.value.name,
|
||||
avatar: '🤖',
|
||||
description: newAgent.value.description,
|
||||
accentColor: '#f97316',
|
||||
gradient: 'from-orange-500/20 to-amber-500/20',
|
||||
status: 'stopped',
|
||||
framework: skillsOptions.find(f => f.value === newAgent.value.skills)?.label || newAgent.value.skills,
|
||||
model: knowledgeOptions.find(k => k.value === newAgent.value.knowledge)?.label || newAgent.value.knowledge,
|
||||
mcpServers: 0,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
})
|
||||
showCreateModal.value = false
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
name: string
|
||||
@@ -73,7 +137,7 @@ const deleteAgent = (id: number) => {
|
||||
<i class="fa-solid fa-robot text-orange-500"></i>
|
||||
<span class="font-medium">Agents</span>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<button @click="openCreateModal" class="btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Agent
|
||||
</button>
|
||||
@@ -224,4 +288,81 @@ const deleteAgent = (id: number) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建智能体弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showCreateModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<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">Create New Agent</h3>
|
||||
<button @click="showCreateModal = false" 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="newAgent.name"
|
||||
type="text"
|
||||
placeholder="Enter agent name..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="newAgent.description"
|
||||
rows="3"
|
||||
placeholder="Describe what this agent does..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
|
||||
<el-select v-model="newAgent.skills" placeholder="Select skills" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="s in skillsOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Knowledge *</label>
|
||||
<el-select v-model="newAgent.knowledge" placeholder="Select knowledge" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="k in knowledgeOptions" :key="k.value" :label="k.label" :value="k.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Custom Prompt</label>
|
||||
<textarea
|
||||
v-model="newAgent.prompt"
|
||||
rows="4"
|
||||
placeholder="Define the agent's behavior and instructions..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="showCreateModal = false"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="createAgent"
|
||||
:disabled="isCreating || !newAgent.name || !newAgent.skills || !newAgent.knowledge"
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<i v-if="isCreating" class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
{{ isCreating ? 'Creating...' : 'Create Agent' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +53,76 @@ const chatSessions = ref<ChatSession[]>([
|
||||
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) },
|
||||
])
|
||||
|
||||
// 群聊数据
|
||||
interface GroupChat {
|
||||
id: number
|
||||
name: string
|
||||
members: string[]
|
||||
lastMessage: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
const groupChats = ref<GroupChat[]>([
|
||||
{ id: 1, name: 'AI 讨论组', members: ['Claude', 'GPT-4', 'Gemini'], lastMessage: '我们来讨论一下...', timestamp: new Date(Date.now() - 1800000) },
|
||||
{ id: 2, name: '编程助手', members: ['Claude', 'DeepSeek'], lastMessage: '这段代码有问题吗?', timestamp: new Date(Date.now() - 3600000) },
|
||||
{ id: 3, name: '创意头脑风暴', members: ['GPT-4', 'Claude', 'Kimi'], lastMessage: '有个新想法...', timestamp: new Date(Date.now() - 7200000) },
|
||||
])
|
||||
|
||||
// 智能体选择弹窗状态
|
||||
const showAgentSelector = ref(false)
|
||||
const selectMode = ref<'single' | 'group'>('single')
|
||||
const selectedAgents = ref<Agent[]>([])
|
||||
const groupChatName = ref('')
|
||||
|
||||
// 打开智能体选择器
|
||||
const openAgentSelector = (mode: 'single' | 'group') => {
|
||||
selectMode.value = mode
|
||||
selectedAgents.value = []
|
||||
groupChatName.value = ''
|
||||
showAgentSelector.value = true
|
||||
}
|
||||
|
||||
// 切换智能体选择(群聊模式)
|
||||
const toggleAgentSelection = (agent: Agent) => {
|
||||
const index = selectedAgents.value.findIndex(a => a.id === agent.id)
|
||||
if (index > -1) {
|
||||
selectedAgents.value.splice(index, 1)
|
||||
} else {
|
||||
selectedAgents.value.push(agent)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const confirmAgentSelection = () => {
|
||||
if (selectMode.value === 'single') {
|
||||
// 单聊模式:选择一个智能体开始对话
|
||||
if (selectedAgents.value.length > 0) {
|
||||
selectedAgent.value = selectedAgents.value[0]
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value.name},你的 AI 助手。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
} else {
|
||||
// 群聊模式:选择多个智能体
|
||||
const name = groupChatName.value.trim() || `群聊 (${selectedAgents.value.length}人)`
|
||||
console.log('创建群聊:', { name, members: selectedAgents.value })
|
||||
// 添加到群聊列表
|
||||
groupChats.value.unshift({
|
||||
id: Date.now(),
|
||||
name: name,
|
||||
members: selectedAgents.value.map(a => a.name),
|
||||
lastMessage: 'New group created',
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
showAgentSelector.value = false
|
||||
}
|
||||
|
||||
// 取消选择
|
||||
const cancelAgentSelection = () => {
|
||||
showAgentSelector.value = false
|
||||
}
|
||||
|
||||
// 侧边栏展开/收起状态
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
@@ -398,15 +468,26 @@ const toggleSidebar = () => {
|
||||
|
||||
<!-- 新建对话按钮 -->
|
||||
<div class="p-3">
|
||||
<button
|
||||
@click="newChat"
|
||||
class="w-full flex items-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium 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="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>新建对话</span>
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="openAgentSelector('single')"
|
||||
class="flex-1 flex items-center justify-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium 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="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>新建对话</span>
|
||||
</button>
|
||||
<button
|
||||
@click="openAgentSelector('group')"
|
||||
class="flex-1 flex items-center justify-center gap-2 px-3 py-2.5 bg-dark-700 hover:bg-dark-600 border border-dark-500 rounded-lg text-white/80 text-sm font-medium 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="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span>新建群聊</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手选择 -->
|
||||
@@ -432,27 +513,115 @@ const toggleSidebar = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史对话列表 -->
|
||||
<!-- 群聊列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 pb-3">
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">最近对话</div>
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">群聊</div>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="session in chatSessions"
|
||||
:key="session.id"
|
||||
@click="selectSession(session)"
|
||||
v-for="group in groupChats"
|
||||
:key="group.id"
|
||||
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-white/30 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ session.title }}</span>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ group.name }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-white/30 mt-1 pl-6">{{ formatRelativeTime(session.timestamp) }}</div>
|
||||
<div class="text-xs text-white/30 mt-1 pl-6">{{ group.members.length }} members</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能体选择弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showAgentSelector" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="cancelAgentSelection">
|
||||
<div class="bg-dark-800 rounded-xl w-full max-w-md border border-dark-600 shadow-2xl" @click.stop>
|
||||
<div class="p-4 border-b border-dark-600">
|
||||
<h3 class="text-lg font-semibold text-white">
|
||||
{{ selectMode === 'single' ? '选择智能体' : '选择群聊成员' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
{{ selectMode === 'single' ? '选择一个智能体开始对话' : '选择多个智能体创建群聊' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 max-h-80 overflow-y-auto">
|
||||
<!-- 群聊名称输入框 -->
|
||||
<div v-if="selectMode === 'group'" class="mb-4">
|
||||
<input
|
||||
v-model="groupChatName"
|
||||
type="text"
|
||||
placeholder="Enter group name..."
|
||||
class="w-full bg-dark-700 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@click="selectMode === 'group' ? toggleAgentSelection(agent) : (selectedAgents = [agent], confirmAgentSelection())"
|
||||
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200"
|
||||
:class="selectedAgents.some(a => a.id === agent.id)
|
||||
? 'bg-orange-500/20 border border-orange-500/50'
|
||||
: 'bg-dark-700 hover:bg-dark-600 border border-transparent'"
|
||||
>
|
||||
<span class="text-xl">{{ agent.avatar }}</span>
|
||||
<div class="flex-1 text-left">
|
||||
<div class="text-white font-medium">{{ agent.name }}</div>
|
||||
<div class="text-xs text-gray-400">{{ agent.description }}</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="agent.status === 'online'"
|
||||
class="w-2 h-2 rounded-full bg-emerald-400"
|
||||
></span>
|
||||
<span
|
||||
v-if="selectMode === 'group' && selectedAgents.some(a => a.id === agent.id)"
|
||||
class="w-5 h-5 rounded-full bg-orange-500 flex items-center justify-center"
|
||||
>
|
||||
<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-dark-600 flex gap-3">
|
||||
<button
|
||||
@click="cancelAgentSelection"
|
||||
class="flex-1 py-2.5 bg-dark-700 hover:bg-dark-600 text-gray-300 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="selectMode === 'group'"
|
||||
@click="confirmAgentSelection"
|
||||
:disabled="selectedAgents.length < 2"
|
||||
class="flex-1 py-2.5 bg-orange-500 hover:bg-orange-400 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Create Group ({{ selectedAgents.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,15 +41,13 @@ const tasks = ref([
|
||||
},
|
||||
])
|
||||
|
||||
const activeTab = ref('running') // running, completed, all
|
||||
const filterStatus = ref('all') // running, stopped, all
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
let result = tasks.value
|
||||
if (activeTab.value === 'running') {
|
||||
result = result.filter(t => t.status === 'running')
|
||||
} else if (activeTab.value === 'completed') {
|
||||
result = result.filter(t => t.status === 'stopped')
|
||||
if (filterStatus.value !== 'all') {
|
||||
result = result.filter(t => t.status === filterStatus.value)
|
||||
}
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
@@ -63,7 +61,7 @@ const filteredTasks = computed(() => {
|
||||
|
||||
const getTaskCount = (status: string) => {
|
||||
if (status === 'running') return tasks.value.filter(t => t.status === 'running').length
|
||||
if (status === 'completed') return tasks.value.filter(t => t.status === 'stopped').length
|
||||
if (status === 'stopped') return tasks.value.filter(t => t.status === 'stopped').length
|
||||
return tasks.value.length
|
||||
}
|
||||
|
||||
@@ -101,28 +99,11 @@ const getStatusClass = (status: string) => {
|
||||
class="search-input w-full"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="flex gap-6 mb-4">
|
||||
<button
|
||||
:class="['tab-item', { active: activeTab === 'running' }]"
|
||||
@click="activeTab = 'running'"
|
||||
>
|
||||
Running {{ getTaskCount('running') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-item', { active: activeTab === 'completed' }]"
|
||||
@click="activeTab = 'completed'"
|
||||
>
|
||||
Completed {{ getTaskCount('completed') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-item', { active: activeTab === 'all' }]"
|
||||
@click="activeTab = 'all'"
|
||||
>
|
||||
All {{ getTaskCount('all') }}
|
||||
</button>
|
||||
<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-select>
|
||||
</div>
|
||||
|
||||
<!-- Task List Table -->
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import * as monaco from 'monaco-editor'
|
||||
|
||||
interface Script {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
code: string
|
||||
status: 'running' | 'stopped'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 模拟脚本数据
|
||||
const scripts = ref<Script[]>([
|
||||
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', status: 'running', createdAt: '2025-04-10' },
|
||||
{ id: 2, name: 'Report Generator', type: 'Python', description: 'Generate weekly reports', status: 'stopped', createdAt: '2025-04-08' },
|
||||
{ id: 3, name: 'Backup Script', type: 'Shell', description: 'Database backup automation', status: 'running', createdAt: '2025-04-05' },
|
||||
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', status: 'stopped', createdAt: '2025-04-12' },
|
||||
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', code: 'print("hello")', status: 'running', createdAt: '2025-04-10' },
|
||||
{ id: 2, name: 'Report Generator', type: 'Python', description: 'Generate weekly reports', code: '', status: 'stopped', createdAt: '2025-04-08' },
|
||||
{ id: 3, name: 'Backup Script', type: 'Shell', description: 'Database backup automation', code: '', status: 'running', createdAt: '2025-04-05' },
|
||||
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', code: '', status: 'stopped', createdAt: '2025-04-12' },
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const isCreating = ref(false)
|
||||
const isCreatingCode = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const editingScript = ref<Script | null>(null)
|
||||
|
||||
@@ -28,6 +31,90 @@ const newScriptForm = ref({
|
||||
name: '',
|
||||
type: 'Python',
|
||||
description: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
// Monaco Editor 实例
|
||||
const codeEditorRef = ref<HTMLElement | null>(null)
|
||||
let codeEditor: monaco.editor.IStandaloneCodeEditor | null = null
|
||||
|
||||
// 定义自定义主题(匹配弹窗背景色)
|
||||
const defineCustomTheme = () => {
|
||||
monaco.editor.defineTheme('custom-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#1f2937', // dark-700
|
||||
'editor.lineHighlightBackground': '#374151',
|
||||
'editorLineNumber.foreground': '#6b7280',
|
||||
'editorLineNumber.activeForeground': '#9ca3af',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Monaco 语言映射
|
||||
const getMonacoLanguage = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'Python': 'python',
|
||||
'Shell': 'shell',
|
||||
'JavaScript': 'javascript',
|
||||
'Go': 'go',
|
||||
}
|
||||
return map[type] || 'plaintext'
|
||||
}
|
||||
|
||||
// 初始化代码编辑器
|
||||
const initCodeEditor = () => {
|
||||
if (!codeEditorRef.value) return
|
||||
|
||||
// 如果已存在,先销毁
|
||||
if (codeEditor) {
|
||||
codeEditor.dispose()
|
||||
}
|
||||
|
||||
// 定义自定义主题
|
||||
defineCustomTheme()
|
||||
|
||||
codeEditor = monaco.editor.create(codeEditorRef.value, {
|
||||
value: newScriptForm.value.code || '',
|
||||
language: getMonacoLanguage(newScriptForm.value.type),
|
||||
theme: 'custom-dark',
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'on',
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
roundedSelection: false,
|
||||
padding: { top: 16, bottom: 16 },
|
||||
})
|
||||
|
||||
// 监听内容变化
|
||||
codeEditor.onDidChangeModelContent(() => {
|
||||
newScriptForm.value.code = codeEditor?.getValue() || ''
|
||||
})
|
||||
}
|
||||
|
||||
// 监听脚本类型变化,更新语言
|
||||
watch(() => newScriptForm.value.type, (newType) => {
|
||||
if (codeEditor) {
|
||||
monaco.editor.setModelLanguage(codeEditor.getModel()!, getMonacoLanguage(newType))
|
||||
}
|
||||
})
|
||||
|
||||
// 监听弹窗打开
|
||||
watch(isCreatingCode, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
initCodeEditor()
|
||||
})
|
||||
} else {
|
||||
if (codeEditor) {
|
||||
codeEditor.dispose()
|
||||
codeEditor = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 过滤后的脚本
|
||||
@@ -86,6 +173,7 @@ const openCreate = () => {
|
||||
name: '',
|
||||
type: 'Python',
|
||||
description: '',
|
||||
code: '',
|
||||
}
|
||||
isCreating.value = true
|
||||
}
|
||||
@@ -94,6 +182,19 @@ const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 跳转到代码编辑
|
||||
const goToCodeEditor = () => {
|
||||
if (!newScriptForm.value.name || !newScriptForm.value.type) {
|
||||
return
|
||||
}
|
||||
isCreating.value = false
|
||||
isCreatingCode.value = true
|
||||
}
|
||||
|
||||
const closeCreateCode = () => {
|
||||
isCreatingCode.value = false
|
||||
}
|
||||
|
||||
const saveNewScript = () => {
|
||||
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
|
||||
scripts.value.push({
|
||||
@@ -101,10 +202,11 @@ const saveNewScript = () => {
|
||||
name: newScriptForm.value.name || 'Untitled Script',
|
||||
type: newScriptForm.value.type,
|
||||
description: newScriptForm.value.description,
|
||||
code: newScriptForm.value.code,
|
||||
status: 'stopped',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
})
|
||||
isCreating.value = false
|
||||
isCreatingCode.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -254,6 +356,43 @@ const saveNewScript = () => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="goToCodeEditor"
|
||||
:disabled="!newScriptForm.name || !newScriptForm.type"
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 代码编辑弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isCreatingCode" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-8">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-6xl h-[90vh] border border-dark-500 shadow-2xl flex flex-col">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Edit Code - {{ newScriptForm.name }}</h3>
|
||||
<p class="text-sm text-gray-400">{{ newScriptForm.type }}</p>
|
||||
</div>
|
||||
<button @click="closeCreateCode" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div ref="codeEditorRef" class="w-full h-full"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="closeCreateCode"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
@click="saveNewScript"
|
||||
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"
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted } from 'vue'
|
||||
import { useModelSettings } from './settings/useModelSettings'
|
||||
import FormDialog from '@/components/FormDialog.vue'
|
||||
|
||||
// 当前选中的设置菜单
|
||||
const activeMenu = ref('general')
|
||||
|
||||
// 导入 Model Settings 逻辑
|
||||
const {
|
||||
models,
|
||||
@@ -32,265 +28,43 @@ const {
|
||||
testConnectionEdit,
|
||||
} = useModelSettings()
|
||||
|
||||
// 监听菜单切换,获取模型列表
|
||||
watch(activeMenu, (newVal) => {
|
||||
if (newVal === 'modelSettings') {
|
||||
fetchModels()
|
||||
}
|
||||
// 页面加载时获取模型列表
|
||||
onMounted(() => {
|
||||
fetchModels()
|
||||
})
|
||||
|
||||
// 设置菜单列表
|
||||
const menuItems = [
|
||||
{ key: 'general', label: 'General', icon: 'fa-gear' },
|
||||
{ key: 'members', label: 'Members', icon: 'fa-users' },
|
||||
{ key: 'notifications', label: 'Notifications', icon: 'fa-bell' },
|
||||
{ key: 'modelSettings', label: 'Model Settings', icon: 'fa-brain' },
|
||||
{ key: 'logs', label: 'Logs', icon: 'fa-file-lines' },
|
||||
]
|
||||
|
||||
// General 设置表单
|
||||
const generalForm = ref({
|
||||
name: 'Alex Smith',
|
||||
email: 'alex@gmail.com',
|
||||
password: '********',
|
||||
language: 'English',
|
||||
timezone: 'UTC +08:00 Beijing',
|
||||
})
|
||||
|
||||
// 语言选项
|
||||
const languageOptions = [
|
||||
{ value: 'English', label: 'English' },
|
||||
{ value: 'Chinese', label: '中文' },
|
||||
{ value: 'Japanese', label: '日本語' },
|
||||
]
|
||||
|
||||
// 时区选项
|
||||
const timezoneOptions = [
|
||||
{ value: 'UTC +08:00 Beijing', label: 'UTC +08:00 Beijing' },
|
||||
{ value: 'UTC +00:00 London', label: 'UTC +00:00 London' },
|
||||
{ value: 'UTC -05:00 New York', label: 'UTC -05:00 New York' },
|
||||
{ value: 'UTC -08:00 Los Angeles', label: 'UTC -08:00 Los Angeles' },
|
||||
]
|
||||
|
||||
// 保存设置
|
||||
const saveChanges = () => {
|
||||
ElMessage.success('Settings saved successfully')
|
||||
}
|
||||
|
||||
// 显示密码修改弹窗
|
||||
const showChangePassword = () => {
|
||||
ElMessage.info('Password change dialog would open here')
|
||||
}
|
||||
|
||||
// ========== Logs 功能 ==========
|
||||
interface Log {
|
||||
id: number
|
||||
level: 'info' | 'warning' | 'error' | 'debug'
|
||||
source: string
|
||||
message: string
|
||||
timestamp: string
|
||||
user?: string
|
||||
}
|
||||
|
||||
const logs = ref<Log[]>([
|
||||
{ id: 1, level: 'info', source: 'System', message: 'User logged in successfully', timestamp: '2025-03-10 14:35:22', user: 'alex@example.com' },
|
||||
{ id: 2, level: 'warning', source: 'API', message: 'Rate limit approaching for API key', timestamp: '2025-03-10 14:32:15', user: 'john@example.com' },
|
||||
{ id: 3, level: 'error', source: 'Database', message: 'Connection timeout to primary database', timestamp: '2025-03-10 14:30:45' },
|
||||
{ id: 4, level: 'info', source: 'Skill', message: 'MCP Server started successfully', timestamp: '2025-03-10 14:28:10' },
|
||||
{ id: 5, level: 'debug', source: 'Auth', message: 'Token refresh initiated', timestamp: '2025-03-10 14:25:33', user: 'jane@example.com' },
|
||||
{ id: 6, level: 'error', source: 'Script', message: 'Failed to execute backup script', timestamp: '2025-03-10 14:20:18' },
|
||||
{ id: 7, level: 'info', source: 'Account', message: 'User role updated', timestamp: '2025-03-10 14:15:42', user: 'admin@example.com' },
|
||||
{ id: 8, level: 'warning', source: 'Memory', message: 'Memory usage exceeds 80% threshold', timestamp: '2025-03-10 14:10:55' },
|
||||
{ id: 9, level: 'info', source: 'Knowledge', message: 'Document indexed successfully', timestamp: '2025-03-10 14:05:30' },
|
||||
{ id: 10, level: 'error', source: 'API', message: 'Invalid API key provided', timestamp: '2025-03-10 14:00:12' },
|
||||
])
|
||||
|
||||
const logSearchQuery = ref('')
|
||||
const logFilterLevel = ref('')
|
||||
const logFilterSource = ref('')
|
||||
|
||||
const logLevelOptions = [
|
||||
{ value: '', label: 'All Levels' },
|
||||
{ value: 'info', label: 'Info' },
|
||||
{ value: 'warning', label: 'Warning' },
|
||||
{ value: 'error', label: 'Error' },
|
||||
{ value: 'debug', label: 'Debug' },
|
||||
]
|
||||
|
||||
const logSourceOptions = computed(() => {
|
||||
const sources = [...new Set(logs.value.map(l => l.source))]
|
||||
return [{ value: '', label: 'All Sources' }, ...sources.map(s => ({ value: s, label: s }))]
|
||||
})
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
return logs.value.filter(log => {
|
||||
const matchSearch = logSearchQuery.value === '' ||
|
||||
log.message.toLowerCase().includes(logSearchQuery.value.toLowerCase()) ||
|
||||
log.source.toLowerCase().includes(logSearchQuery.value.toLowerCase())
|
||||
const matchLevel = logFilterLevel.value === '' || log.level === logFilterLevel.value
|
||||
const matchSource = logFilterSource.value === '' || log.source === logFilterSource.value
|
||||
return matchSearch && matchLevel && matchSource
|
||||
})
|
||||
})
|
||||
|
||||
const logLevelClass = (level: string) => {
|
||||
switch (level) {
|
||||
case 'info': return 'bg-blue-500/20 text-blue-400'
|
||||
case 'warning': return 'bg-yellow-500/20 text-yellow-400'
|
||||
case 'error': return 'bg-red-500/20 text-red-400'
|
||||
case 'debug': return 'bg-gray-500/20 text-gray-400'
|
||||
default: return 'bg-gray-500/20 text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const selectedLog = ref<Log | null>(null)
|
||||
const showLogDetail = ref(false)
|
||||
|
||||
const viewLogDetail = (log: Log) => {
|
||||
selectedLog.value = log
|
||||
showLogDetail.value = true
|
||||
}
|
||||
|
||||
const closeLogDetail = () => {
|
||||
showLogDetail.value = false
|
||||
selectedLog.value = null
|
||||
}
|
||||
|
||||
const clearLogs = () => {
|
||||
logs.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<i class="fa-solid fa-gear text-orange-500"></i>
|
||||
<span class="font-medium">Settings</span>
|
||||
<i class="fa-solid fa-brain text-orange-500"></i>
|
||||
<span class="font-medium">Models</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6">
|
||||
<!-- 左侧菜单 -->
|
||||
<nav class="w-48 flex-shrink-0">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
@click="activeMenu = item.key"
|
||||
class="px-4 py-3 rounded-lg cursor-pointer transition-colors flex items-center gap-3"
|
||||
:class="activeMenu === item.key
|
||||
? 'bg-orange-500/10 text-orange-400'
|
||||
: 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
>
|
||||
<i :class="['fa-solid', item.icon]"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex-1 space-y-4">
|
||||
<!-- General 设置 -->
|
||||
<div v-if="activeMenu === 'general'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">General Settings</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Manage your personal information and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-700 rounded-xl p-6">
|
||||
<el-form :model="generalForm" label-position="top">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<el-form-item label="Name">
|
||||
<el-input v-model="generalForm.name" placeholder="Enter your name" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Email">
|
||||
<el-input v-model="generalForm.email" placeholder="Enter your email" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Password">
|
||||
<div class="flex gap-3">
|
||||
<el-input v-model="generalForm.password" type="password" disabled class="flex-1" />
|
||||
<el-button @click="showChangePassword">Change</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Language">
|
||||
<el-select v-model="generalForm.language" placeholder="Select language" class="w-full">
|
||||
<el-option
|
||||
v-for="lang in languageOptions"
|
||||
:key="lang.value"
|
||||
:label="lang.label"
|
||||
:value="lang.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Timezone">
|
||||
<el-select v-model="generalForm.timezone" placeholder="Select timezone" class="w-full">
|
||||
<el-option
|
||||
v-for="tz in timezoneOptions"
|
||||
:key="tz.value"
|
||||
:label="tz.label"
|
||||
:value="tz.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<el-button type="primary" @click="saveChanges">Save Changes</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="space-y-4">
|
||||
<!-- Models 内容 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<p class="text-sm text-gray-400 mt-1">Configure AI models</p>
|
||||
</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" @click="showAddModelForm = true">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add New Model
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Members 设置 -->
|
||||
<div v-if="activeMenu === 'members'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Members</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Manage team members</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications 设置 -->
|
||||
<div v-if="activeMenu === 'notifications'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Notifications</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Configure notification preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Settings 设置 -->
|
||||
<div v-if="activeMenu === 'modelSettings'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Model Settings</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Configure AI model settings</p>
|
||||
</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" @click="showAddModelForm = true">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add New Model
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
<div v-if="modelsLoading" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
||||
</div>
|
||||
<div v-else 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">Model Name</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
||||
<!-- 模型列表 -->
|
||||
<div v-if="modelsLoading" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
||||
</div>
|
||||
<div v-else 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">Model 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">Model Type</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Base URL</th>
|
||||
@@ -532,143 +306,6 @@ const clearLogs = () => {
|
||||
</button>
|
||||
</template>
|
||||
</FormDialog>
|
||||
</div>
|
||||
|
||||
<!-- Logs 设置 -->
|
||||
<div v-if="activeMenu === 'logs'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Logs</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">View system logs</p>
|
||||
</div>
|
||||
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
Clear Logs
|
||||
</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="logSearchQuery"
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="logFilterLevel" placeholder="All Levels" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="opt in logLevelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="logFilterSource" placeholder="All Sources" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="opt in logSourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-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">Level</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Source</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Message</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Timestamp</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in filteredLogs" :key="log.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
||||
<td class="px-5 py-4">
|
||||
<span :class="['px-2 py-1 rounded text-xs font-medium', logLevelClass(log.level)]">
|
||||
{{ log.level.toUpperCase() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-300">{{ log.source }}</td>
|
||||
<td class="px-5 py-4 text-gray-300 max-w-md">
|
||||
<div class="truncate">{{ log.message }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400">{{ log.user || '-' }}</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ log.timestamp }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<button
|
||||
@click="viewLogDetail(log)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="View Details"
|
||||
>
|
||||
<i class="fa-solid fa-eye text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredLogs.length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-file-lines text-4xl mb-3"></i>
|
||||
<p>No logs found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showLogDetail" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="closeLogDetail">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-2xl border border-dark-500 shadow-2xl" @click.stop>
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Log Details</h3>
|
||||
<button @click="closeLogDetail" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog" class="p-5 space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span :class="['px-3 py-1 rounded text-sm font-medium', logLevelClass(selectedLog.level)]">
|
||||
{{ selectedLog.level.toUpperCase() }}
|
||||
</span>
|
||||
<span class="text-gray-400">{{ selectedLog.timestamp }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Source</label>
|
||||
<div class="text-white">{{ selectedLog.source }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">User</label>
|
||||
<div class="text-white">{{ selectedLog.user || 'System' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
|
||||
<div class="bg-dark-800 rounded-lg p-4 text-gray-300 font-mono text-sm whitespace-pre-wrap">
|
||||
{{ selectedLog.message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Log ID</label>
|
||||
<div class="text-gray-500">#{{ selectedLog.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="closeLogDetail"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useSkills } from './skill/useSkills'
|
||||
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
|
||||
import '@/views/database/database.css'
|
||||
|
||||
const {
|
||||
@@ -98,13 +99,13 @@ const {
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button @click="toggleStatus(skill)" class="btn-icon" :title="skill.status === 'running' ? 'Stop' : 'Start'">
|
||||
<i :class="['fa-solid', skill.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
<component :is="skill.status === 'running' ? Pause : Play" class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="openEdit(skill)" class="btn-icon" title="Edit">
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="deleteSkill(skill.id)" class="btn-icon" title="Delete">
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -192,41 +193,26 @@ const {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
|
||||
<input v-model="newSkillForm.name" type="text" placeholder="Enter skill name..." class="input-field">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||||
<el-select v-model="newSkillForm.type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="type in types" :key="type" :label="type" :value="type" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Category</label>
|
||||
<el-select v-model="newSkillForm.category" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="cat in categories" :key="cat.value" :label="cat.label" :value="cat.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||||
<input v-model="newSkillForm.port" type="number" placeholder="3000" class="input-field">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea v-model="newSkillForm.description" rows="2" placeholder="Describe this skill..." class="input-field resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Markdown Content</label>
|
||||
<textarea v-model="newSkillForm.markdown" rows="10" placeholder="# Skill Documentation Describe your skill here using Markdown..." class="input-field resize-none font-mono text-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||||
<button @click="closeCreate" class="btn-secondary">Cancel</button>
|
||||
<button @click="saveNewSkill" class="btn-primary">Create Skill</button>
|
||||
<button @click="saveNewSkill" :disabled="!newSkillForm.name || !newSkillForm.description || !newSkillForm.markdown" class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">Create Skill</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
FileText,
|
||||
Globe,
|
||||
Calculator,
|
||||
Code,
|
||||
Braces,
|
||||
Github,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
Database,
|
||||
Folder,
|
||||
GitBranch,
|
||||
Box,
|
||||
Wrench,
|
||||
Server,
|
||||
Terminal,
|
||||
Search,
|
||||
Plus,
|
||||
Pause,
|
||||
Play,
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
import { useTools } from './tools/useTools'
|
||||
import '@/views/database/database.css'
|
||||
|
||||
interface Tool {
|
||||
id: number
|
||||
name: string
|
||||
type: 'built-in' | 'custom' | 'mcp'
|
||||
description: string
|
||||
status: 'active' | 'inactive'
|
||||
icon?: string
|
||||
provider?: string
|
||||
config?: object
|
||||
createdAt: string
|
||||
// 使用工具 composable
|
||||
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
|
||||
|
||||
// 图标组件映射
|
||||
const iconComponents: Record<string, any> = {
|
||||
FileText,
|
||||
Globe,
|
||||
Calculator,
|
||||
Code,
|
||||
Braces,
|
||||
Github,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
Database,
|
||||
Folder,
|
||||
GitBranch,
|
||||
Box,
|
||||
Wrench,
|
||||
Server,
|
||||
Terminal,
|
||||
}
|
||||
|
||||
// Mock data for tools
|
||||
const builtInTools = ref<Tool[]>([
|
||||
{ id: 1, name: 'File Reader', type: 'built-in', description: 'Read files from the filesystem', status: 'active', icon: 'fa-file-lines', createdAt: '2025-01-15' },
|
||||
{ id: 2, name: 'Web Search', type: 'built-in', description: 'Search the web for information', status: 'active', icon: 'fa-globe', createdAt: '2025-01-15' },
|
||||
{ id: 3, name: 'Calculator', type: 'built-in', description: 'Perform mathematical calculations', status: 'active', icon: 'fa-calculator', createdAt: '2025-01-15' },
|
||||
{ id: 4, name: 'Code Executor', type: 'built-in', description: 'Execute code in a sandbox', status: 'active', icon: 'fa-code', createdAt: '2025-01-15' },
|
||||
{ id: 5, name: 'JSON Parser', type: 'built-in', description: 'Parse and validate JSON data', status: 'active', icon: 'fa-brackets-curly', createdAt: '2025-01-15' },
|
||||
])
|
||||
const getIconComponent = (iconName?: string) => {
|
||||
if (!iconName) return Box
|
||||
return iconComponents[iconName] || Box
|
||||
}
|
||||
|
||||
const customTools = ref<Tool[]>([
|
||||
{ id: 101, name: 'GitHub API', type: 'custom', description: 'Interact with GitHub REST API', status: 'active', icon: 'fa-github', provider: 'Custom', createdAt: '2025-03-01' },
|
||||
{ id: 102, name: 'Slack Notifier', type: 'custom', description: 'Send notifications to Slack', status: 'inactive', icon: 'fa-slack', provider: 'Custom', createdAt: '2025-03-05' },
|
||||
{ id: 103, name: 'Email Sender', type: 'custom', description: 'Send emails via SMTP', status: 'active', icon: 'fa-envelope', provider: 'Custom', createdAt: '2025-03-08' },
|
||||
])
|
||||
interface Tool {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
provider: string
|
||||
security_level: string
|
||||
require_approval: boolean
|
||||
parameters: string
|
||||
status: string
|
||||
type: 'built-in' | 'mcp'
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
const mcpTools = ref<Tool[]>([
|
||||
{ id: 201, name: 'Puppeteer', type: 'mcp', description: 'Browser automation via Puppeteer', status: 'active', icon: 'fa-browser', provider: 'MCP Server', createdAt: '2025-02-10' },
|
||||
{ id: 202, name: 'SQL Database', type: 'mcp', description: 'Execute SQL queries on databases', status: 'active', icon: 'fa-database', provider: 'MCP Server', createdAt: '2025-02-12' },
|
||||
{ id: 203, name: 'Filesystem', type: 'mcp', description: 'File operations via MCP', status: 'active', icon: 'fa-folder', provider: 'MCP Server', createdAt: '2025-02-15' },
|
||||
{ id: 204, name: 'Git Operations', type: 'mcp', description: 'Git repository operations', status: 'inactive', icon: 'fa-git-alt', provider: 'MCP Server', createdAt: '2025-02-18' },
|
||||
])
|
||||
// 页面加载时获取工具列表
|
||||
onMounted(async () => {
|
||||
await fetchTools()
|
||||
})
|
||||
|
||||
const activeTab = ref<'built-in' | 'custom' | 'mcp'>('built-in')
|
||||
// 按类型分类工具
|
||||
const builtInTools = computed(() => {
|
||||
return tools.value.filter(t => t.provider === 'system')
|
||||
})
|
||||
|
||||
const mcpTools = computed(() => {
|
||||
return tools.value.filter(t => t.provider !== 'system')
|
||||
})
|
||||
|
||||
const activeTab = ref<'built-in' | 'mcp'>('built-in')
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const editingTool = ref<Tool | null>(null)
|
||||
@@ -50,21 +96,19 @@ const editForm = ref({
|
||||
|
||||
// Statistics
|
||||
const stats = computed(() => ({
|
||||
total: builtInTools.value.length + customTools.value.length + mcpTools.value.length,
|
||||
active: [...builtInTools.value, ...customTools.value, ...mcpTools.value].filter(t => t.status === 'active').length,
|
||||
total: tools.value.length,
|
||||
active: tools.value.filter(t => t.status === 'active').length,
|
||||
builtIn: builtInTools.value.length,
|
||||
custom: customTools.value.length,
|
||||
mcp: mcpTools.value.length
|
||||
}))
|
||||
|
||||
const currentTools = computed(() => {
|
||||
let tools: Tool[] = []
|
||||
let toolsList: Tool[] = []
|
||||
switch (activeTab.value) {
|
||||
case 'built-in': tools = builtInTools.value; break
|
||||
case 'custom': tools = customTools.value; break
|
||||
case 'mcp': tools = mcpTools.value; break
|
||||
case 'built-in': toolsList = builtInTools.value as Tool[]; break
|
||||
case 'mcp': toolsList = mcpTools.value as Tool[]; break
|
||||
}
|
||||
return tools.filter(tool => {
|
||||
return toolsList.filter(tool => {
|
||||
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
|
||||
@@ -74,64 +118,64 @@ const currentTools = computed(() => {
|
||||
|
||||
const tabCounts = computed(() => ({
|
||||
'built-in': builtInTools.value.length,
|
||||
'custom': customTools.value.length,
|
||||
'mcp': mcpTools.value.length,
|
||||
}))
|
||||
|
||||
const openEdit = (tool: Tool) => {
|
||||
const openEdit = (tool: any) => {
|
||||
editingTool.value = tool
|
||||
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
if (editingTool.value) {
|
||||
const targetArray = editingTool.value.type === 'built-in' ? builtInTools.value :
|
||||
editingTool.value.type === 'custom' ? customTools.value : mcpTools.value
|
||||
const index = targetArray.findIndex(t => t.id === editingTool.value!.id)
|
||||
if (index !== -1) targetArray[index] = { ...targetArray[index], ...editForm.value }
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
||||
|
||||
const toggleStatus = (tool: Tool) => {
|
||||
const targetArray = tool.type === 'built-in' ? builtInTools.value :
|
||||
tool.type === 'custom' ? customTools.value : mcpTools.value
|
||||
const found = targetArray.find(t => t.id === tool.id)
|
||||
if (found) found.status = found.status === 'active' ? 'inactive' : 'active'
|
||||
const toggleStatus = async (tool: any) => {
|
||||
const newStatus = tool.status === 'active' ? 'inactive' : 'active'
|
||||
// TODO: 调用 API 更新状态
|
||||
}
|
||||
|
||||
const deleteTool = (id: number) => {
|
||||
switch (activeTab.value) {
|
||||
case 'built-in': builtInTools.value = builtInTools.value.filter(t => t.id !== id); break
|
||||
case 'custom': customTools.value = customTools.value.filter(t => t.id !== id); break
|
||||
case 'mcp': mcpTools.value = mcpTools.value.filter(t => t.id !== id); break
|
||||
const handleDeleteTool = async (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this tool?')) {
|
||||
await deleteToolApi(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 同步工具
|
||||
const handleSyncTools = async () => {
|
||||
await syncTools()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex justify-between items-center mb-6 h-10">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-tools text-orange-500"></i>
|
||||
<Wrench class="w-5 h-5 text-orange-500" />
|
||||
<span class="font-medium">Tools</span>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Tool
|
||||
</button>
|
||||
<div class="h-10 flex gap-2">
|
||||
<button v-if="activeTab === 'built-in'" @click="handleSyncTools" class="btn-secondary">
|
||||
<i class="fa-solid fa-sync mr-1"></i>
|
||||
Sync Tools
|
||||
</button>
|
||||
<button v-if="activeTab === 'mcp'" class="btn-primary">
|
||||
<Plus class="w-4 h-4 mr-1" />
|
||||
Add MCP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<button
|
||||
v-for="(label, tab) in { 'built-in': 'Built-in', 'custom': 'Custom', 'mcp': 'MCP Servers' }"
|
||||
v-for="(label, tab) in { 'built-in': 'Built-in', 'mcp': 'MCP Servers' }"
|
||||
:key="tab"
|
||||
@click="activeTab = tab as 'built-in' | 'custom' | 'mcp'"
|
||||
@click="activeTab = tab as 'built-in' | 'mcp'"
|
||||
class="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
|
||||
:class="activeTab === tab
|
||||
? 'bg-orange-500 text-white'
|
||||
@@ -147,12 +191,12 @@ const deleteTool = (id: number) => {
|
||||
<!-- 搜索和筛选 -->
|
||||
<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>
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search tools by name or description..."
|
||||
class="search-input w-full"
|
||||
class="search-input w-full pl-10"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||
@@ -164,7 +208,11 @@ const deleteTool = (id: number) => {
|
||||
|
||||
<!-- Tools 列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table v-if="currentTools.length > 0" class="w-full">
|
||||
<!-- Loading -->
|
||||
<div v-if="toolsLoading" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
||||
</div>
|
||||
<table v-else-if="currentTools.length > 0" class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th>
|
||||
@@ -178,8 +226,8 @@ const deleteTool = (id: number) => {
|
||||
<tr v-for="tool in currentTools" :key="tool.id" class="table-row">
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : tool.type === 'custom' ? 'bg-blue-500/20' : 'bg-emerald-500/20'">
|
||||
<i :class="['fa-solid', tool.icon || 'fa-cube', 'text-lg', tool.type === 'built-in' ? 'text-orange-400' : tool.type === 'custom' ? 'text-blue-400' : 'text-emerald-400']"></i>
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : 'bg-emerald-500/20'">
|
||||
<component :is="getIconComponent(tool.icon)" class="w-5 h-5" :class="tool.type === 'built-in' ? 'text-orange-400' : 'text-emerald-400'" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ tool.name }}</div>
|
||||
@@ -188,7 +236,7 @@ const deleteTool = (id: number) => {
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'Custom') }}</span>
|
||||
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'MCP Server') }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="tool.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
|
||||
@@ -196,17 +244,17 @@ const deleteTool = (id: number) => {
|
||||
<span class="capitalize">{{ tool.status }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.createdAt }}</td>
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.created_at || '-' }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
||||
<i :class="['fa-solid', tool.status === 'active' ? 'fa-pause' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
<component :is="tool.status === 'active' ? Pause : Play" class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="deleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
<button @click="handleDeleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -217,10 +265,10 @@ const deleteTool = (id: number) => {
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-box">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-tools"></i>
|
||||
<Wrench class="w-12 h-12" />
|
||||
</div>
|
||||
<p class="empty-text">No tools found</p>
|
||||
<p class="empty-tip">Click "New Tool" to add a tool</p>
|
||||
<p class="empty-tip">Click "Add MCP" to add a new MCP server</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +279,7 @@ const deleteTool = (id: number) => {
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Tool</h3>
|
||||
<button @click="cancelEdit" class="btn-icon">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,10 +46,8 @@ export function useSkills() {
|
||||
|
||||
const newSkillForm = ref({
|
||||
name: '',
|
||||
type: 'API',
|
||||
category: 'api',
|
||||
port: 3000,
|
||||
description: '',
|
||||
markdown: '',
|
||||
})
|
||||
|
||||
// 分类选项
|
||||
@@ -85,7 +83,7 @@ export function useSkills() {
|
||||
|
||||
// 打开创建弹窗
|
||||
const openCreate = () => {
|
||||
newSkillForm.value = { name: '', type: 'API', category: 'api', port: 3000, description: '' }
|
||||
newSkillForm.value = { name: '', description: '', markdown: '' }
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
@@ -101,10 +99,10 @@ export function useSkills() {
|
||||
id: newId,
|
||||
name: newSkillForm.value.name,
|
||||
description: newSkillForm.value.description,
|
||||
type: newSkillForm.value.type,
|
||||
category: newSkillForm.value.category,
|
||||
type: 'Custom',
|
||||
category: 'custom',
|
||||
status: 'stopped',
|
||||
port: newSkillForm.value.port,
|
||||
port: 3000,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
tools: 0,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user