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">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import './database/database.css'
|
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 = [
|
const menuItems = [
|
||||||
|
{ key: 'profile', label: 'Profile', icon: 'fa-user' },
|
||||||
{ key: 'users', label: 'Users', icon: 'fa-users' },
|
{ key: 'users', label: 'Users', icon: 'fa-users' },
|
||||||
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
|
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
|
||||||
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
|
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
|
||||||
@@ -193,6 +225,61 @@ const statusClass = (status: string) => {
|
|||||||
|
|
||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="flex-1">
|
<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 -->
|
<!-- Users -->
|
||||||
<div v-if="activeMenu === 'users'" class="space-y-4">
|
<div v-if="activeMenu === 'users'" class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,6 +1,70 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
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 {
|
interface Agent {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -73,7 +137,7 @@ const deleteAgent = (id: number) => {
|
|||||||
<i class="fa-solid fa-robot text-orange-500"></i>
|
<i class="fa-solid fa-robot text-orange-500"></i>
|
||||||
<span class="font-medium">Agents</span>
|
<span class="font-medium">Agents</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary">
|
<button @click="openCreateModal" class="btn-primary">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
New Agent
|
New Agent
|
||||||
</button>
|
</button>
|
||||||
@@ -224,4 +288,81 @@ const deleteAgent = (id: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|||||||
@@ -53,6 +53,76 @@ const chatSessions = ref<ChatSession[]>([
|
|||||||
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) },
|
{ 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)
|
const sidebarCollapsed = ref(false)
|
||||||
|
|
||||||
@@ -398,15 +468,26 @@ const toggleSidebar = () => {
|
|||||||
|
|
||||||
<!-- 新建对话按钮 -->
|
<!-- 新建对话按钮 -->
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<button
|
<div class="flex gap-2">
|
||||||
@click="newChat"
|
<button
|
||||||
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"
|
@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 class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||||
<span>新建对话</span>
|
</svg>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- AI 助手选择 -->
|
<!-- AI 助手选择 -->
|
||||||
@@ -432,27 +513,115 @@ const toggleSidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 历史对话列表 -->
|
<!-- 群聊列表 -->
|
||||||
<div class="flex-1 overflow-y-auto px-3 pb-3">
|
<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">
|
<div class="space-y-1">
|
||||||
<button
|
<button
|
||||||
v-for="session in chatSessions"
|
v-for="group in groupChats"
|
||||||
:key="session.id"
|
:key="group.id"
|
||||||
@click="selectSession(session)"
|
|
||||||
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
|
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">
|
<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">
|
<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>
|
</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>
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</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 searchQuery = ref('')
|
||||||
|
|
||||||
const filteredTasks = computed(() => {
|
const filteredTasks = computed(() => {
|
||||||
let result = tasks.value
|
let result = tasks.value
|
||||||
if (activeTab.value === 'running') {
|
if (filterStatus.value !== 'all') {
|
||||||
result = result.filter(t => t.status === 'running')
|
result = result.filter(t => t.status === filterStatus.value)
|
||||||
} else if (activeTab.value === 'completed') {
|
|
||||||
result = result.filter(t => t.status === 'stopped')
|
|
||||||
}
|
}
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
const query = searchQuery.value.toLowerCase()
|
const query = searchQuery.value.toLowerCase()
|
||||||
@@ -63,7 +61,7 @@ const filteredTasks = computed(() => {
|
|||||||
|
|
||||||
const getTaskCount = (status: string) => {
|
const getTaskCount = (status: string) => {
|
||||||
if (status === 'running') return tasks.value.filter(t => t.status === 'running').length
|
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
|
return tasks.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,28 +99,11 @@ const getStatusClass = (status: string) => {
|
|||||||
class="search-input w-full"
|
class="search-input w-full"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||||
|
<el-option label="All Status" value="all" />
|
||||||
<!-- Tab Navigation -->
|
<el-option label="Running" value="running" />
|
||||||
<div class="flex gap-6 mb-4">
|
<el-option label="Stopped" value="stopped" />
|
||||||
<button
|
</el-select>
|
||||||
: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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Task List Table -->
|
<!-- Task List Table -->
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<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 {
|
interface Script {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
description: string
|
description: string
|
||||||
|
code: string
|
||||||
status: 'running' | 'stopped'
|
status: 'running' | 'stopped'
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟脚本数据
|
// 模拟脚本数据
|
||||||
const scripts = ref<Script[]>([
|
const scripts = ref<Script[]>([
|
||||||
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', status: 'running', createdAt: '2025-04-10' },
|
{ 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', status: 'stopped', createdAt: '2025-04-08' },
|
{ 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', status: 'running', createdAt: '2025-04-05' },
|
{ 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', status: 'stopped', createdAt: '2025-04-12' },
|
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', code: '', status: 'stopped', createdAt: '2025-04-12' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const filterStatus = ref('all')
|
const filterStatus = ref('all')
|
||||||
const isCreating = ref(false)
|
const isCreating = ref(false)
|
||||||
|
const isCreatingCode = ref(false)
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
const editingScript = ref<Script | null>(null)
|
const editingScript = ref<Script | null>(null)
|
||||||
|
|
||||||
@@ -28,6 +31,90 @@ const newScriptForm = ref({
|
|||||||
name: '',
|
name: '',
|
||||||
type: 'Python',
|
type: 'Python',
|
||||||
description: '',
|
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: '',
|
name: '',
|
||||||
type: 'Python',
|
type: 'Python',
|
||||||
description: '',
|
description: '',
|
||||||
|
code: '',
|
||||||
}
|
}
|
||||||
isCreating.value = true
|
isCreating.value = true
|
||||||
}
|
}
|
||||||
@@ -94,6 +182,19 @@ const closeCreate = () => {
|
|||||||
isCreating.value = false
|
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 saveNewScript = () => {
|
||||||
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
|
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
|
||||||
scripts.value.push({
|
scripts.value.push({
|
||||||
@@ -101,10 +202,11 @@ const saveNewScript = () => {
|
|||||||
name: newScriptForm.value.name || 'Untitled Script',
|
name: newScriptForm.value.name || 'Untitled Script',
|
||||||
type: newScriptForm.value.type,
|
type: newScriptForm.value.type,
|
||||||
description: newScriptForm.value.description,
|
description: newScriptForm.value.description,
|
||||||
|
code: newScriptForm.value.code,
|
||||||
status: 'stopped',
|
status: 'stopped',
|
||||||
createdAt: new Date().toISOString().split('T')[0],
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
})
|
})
|
||||||
isCreating.value = false
|
isCreatingCode.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -254,6 +356,43 @@ const saveNewScript = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</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
|
<button
|
||||||
@click="saveNewScript"
|
@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"
|
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">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { useModelSettings } from './settings/useModelSettings'
|
import { useModelSettings } from './settings/useModelSettings'
|
||||||
import FormDialog from '@/components/FormDialog.vue'
|
import FormDialog from '@/components/FormDialog.vue'
|
||||||
|
|
||||||
// 当前选中的设置菜单
|
|
||||||
const activeMenu = ref('general')
|
|
||||||
|
|
||||||
// 导入 Model Settings 逻辑
|
// 导入 Model Settings 逻辑
|
||||||
const {
|
const {
|
||||||
models,
|
models,
|
||||||
@@ -32,265 +28,43 @@ const {
|
|||||||
testConnectionEdit,
|
testConnectionEdit,
|
||||||
} = useModelSettings()
|
} = useModelSettings()
|
||||||
|
|
||||||
// 监听菜单切换,获取模型列表
|
// 页面加载时获取模型列表
|
||||||
watch(activeMenu, (newVal) => {
|
onMounted(() => {
|
||||||
if (newVal === 'modelSettings') {
|
fetchModels()
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 min-h-screen">
|
<div class="p-6 min-h-screen">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="flex items-center gap-2 mb-6">
|
<div class="flex items-center gap-2 mb-6">
|
||||||
<i class="fa-solid fa-gear text-orange-500"></i>
|
<i class="fa-solid fa-brain text-orange-500"></i>
|
||||||
<span class="font-medium">Settings</span>
|
<span class="font-medium">Models</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-6">
|
<!-- 内容区域 -->
|
||||||
<!-- 左侧菜单 -->
|
<div class="space-y-4">
|
||||||
<nav class="w-48 flex-shrink-0">
|
<!-- Models 内容 -->
|
||||||
<ul class="space-y-1">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<li
|
<div>
|
||||||
v-for="item in menuItems"
|
<p class="text-sm text-gray-400 mt-1">Configure AI models</p>
|
||||||
: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>
|
</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="modelsLoading" class="py-12 text-center text-gray-500">
|
||||||
<div v-if="activeMenu === 'members'">
|
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
||||||
<div class="flex items-center justify-between mb-6">
|
</div>
|
||||||
<div>
|
<div v-else class="bg-dark-700 rounded-xl overflow-hidden">
|
||||||
<h2 class="text-xl font-semibold">Members</h2>
|
<table class="w-full">
|
||||||
<p class="text-sm text-gray-400 mt-1">Manage team members</p>
|
<thead class="bg-dark-600">
|
||||||
</div>
|
<tr>
|
||||||
</div>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Name</th>
|
||||||
</div>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
<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</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">Model Type</th>
|
||||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Base URL</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>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</FormDialog>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSkills } from './skill/useSkills'
|
import { useSkills } from './skill/useSkills'
|
||||||
|
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
|
||||||
import '@/views/database/database.css'
|
import '@/views/database/database.css'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -98,13 +99,13 @@ const {
|
|||||||
<td class="px-5 py-4">
|
<td class="px-5 py-4">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
<button @click="toggleStatus(skill)" class="btn-icon" :title="skill.status === 'running' ? 'Stop' : 'Start'">
|
<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>
|
||||||
<button @click="openEdit(skill)" class="btn-icon" title="Edit">
|
<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>
|
||||||
<button @click="deleteSkill(skill.id)" class="btn-icon" title="Delete">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -192,41 +193,26 @@ const {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-5 space-y-4">
|
<div class="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
|
<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">
|
<input v-model="newSkillForm.name" type="text" placeholder="Enter skill name..." class="input-field">
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
<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>
|
<textarea v-model="newSkillForm.description" rows="2" placeholder="Describe this skill..." class="input-field resize-none"></textarea>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
<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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,42 +1,88 @@
|
|||||||
<script setup lang="ts">
|
<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'
|
import '@/views/database/database.css'
|
||||||
|
|
||||||
interface Tool {
|
// 使用工具 composable
|
||||||
id: number
|
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
|
||||||
name: string
|
|
||||||
type: 'built-in' | 'custom' | 'mcp'
|
// 图标组件映射
|
||||||
description: string
|
const iconComponents: Record<string, any> = {
|
||||||
status: 'active' | 'inactive'
|
FileText,
|
||||||
icon?: string
|
Globe,
|
||||||
provider?: string
|
Calculator,
|
||||||
config?: object
|
Code,
|
||||||
createdAt: string
|
Braces,
|
||||||
|
Github,
|
||||||
|
MessageSquare,
|
||||||
|
Mail,
|
||||||
|
Database,
|
||||||
|
Folder,
|
||||||
|
GitBranch,
|
||||||
|
Box,
|
||||||
|
Wrench,
|
||||||
|
Server,
|
||||||
|
Terminal,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock data for tools
|
const getIconComponent = (iconName?: string) => {
|
||||||
const builtInTools = ref<Tool[]>([
|
if (!iconName) return Box
|
||||||
{ id: 1, name: 'File Reader', type: 'built-in', description: 'Read files from the filesystem', status: 'active', icon: 'fa-file-lines', createdAt: '2025-01-15' },
|
return iconComponents[iconName] || Box
|
||||||
{ 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 customTools = ref<Tool[]>([
|
interface 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: string
|
||||||
{ id: 102, name: 'Slack Notifier', type: 'custom', description: 'Send notifications to Slack', status: 'inactive', icon: 'fa-slack', provider: 'Custom', createdAt: '2025-03-05' },
|
name: string
|
||||||
{ id: 103, name: 'Email Sender', type: 'custom', description: 'Send emails via SMTP', status: 'active', icon: 'fa-envelope', provider: 'Custom', createdAt: '2025-03-08' },
|
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' },
|
onMounted(async () => {
|
||||||
{ 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' },
|
await fetchTools()
|
||||||
{ 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' },
|
|
||||||
])
|
|
||||||
|
|
||||||
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 searchQuery = ref('')
|
||||||
const filterStatus = ref('all')
|
const filterStatus = ref('all')
|
||||||
const editingTool = ref<Tool | null>(null)
|
const editingTool = ref<Tool | null>(null)
|
||||||
@@ -50,21 +96,19 @@ const editForm = ref({
|
|||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
const stats = computed(() => ({
|
const stats = computed(() => ({
|
||||||
total: builtInTools.value.length + customTools.value.length + mcpTools.value.length,
|
total: tools.value.length,
|
||||||
active: [...builtInTools.value, ...customTools.value, ...mcpTools.value].filter(t => t.status === 'active').length,
|
active: tools.value.filter(t => t.status === 'active').length,
|
||||||
builtIn: builtInTools.value.length,
|
builtIn: builtInTools.value.length,
|
||||||
custom: customTools.value.length,
|
|
||||||
mcp: mcpTools.value.length
|
mcp: mcpTools.value.length
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const currentTools = computed(() => {
|
const currentTools = computed(() => {
|
||||||
let tools: Tool[] = []
|
let toolsList: Tool[] = []
|
||||||
switch (activeTab.value) {
|
switch (activeTab.value) {
|
||||||
case 'built-in': tools = builtInTools.value; break
|
case 'built-in': toolsList = builtInTools.value as Tool[]; break
|
||||||
case 'custom': tools = customTools.value; break
|
case 'mcp': toolsList = mcpTools.value as Tool[]; break
|
||||||
case 'mcp': tools = mcpTools.value; break
|
|
||||||
}
|
}
|
||||||
return tools.filter(tool => {
|
return toolsList.filter(tool => {
|
||||||
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||||
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
|
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
|
||||||
@@ -74,64 +118,64 @@ const currentTools = computed(() => {
|
|||||||
|
|
||||||
const tabCounts = computed(() => ({
|
const tabCounts = computed(() => ({
|
||||||
'built-in': builtInTools.value.length,
|
'built-in': builtInTools.value.length,
|
||||||
'custom': customTools.value.length,
|
|
||||||
'mcp': mcpTools.value.length,
|
'mcp': mcpTools.value.length,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const openEdit = (tool: Tool) => {
|
const openEdit = (tool: any) => {
|
||||||
editingTool.value = tool
|
editingTool.value = tool
|
||||||
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
||||||
isEditing.value = true
|
isEditing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEdit = () => {
|
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
|
isEditing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
||||||
|
|
||||||
const toggleStatus = (tool: Tool) => {
|
const toggleStatus = async (tool: any) => {
|
||||||
const targetArray = tool.type === 'built-in' ? builtInTools.value :
|
const newStatus = tool.status === 'active' ? 'inactive' : 'active'
|
||||||
tool.type === 'custom' ? customTools.value : mcpTools.value
|
// TODO: 调用 API 更新状态
|
||||||
const found = targetArray.find(t => t.id === tool.id)
|
|
||||||
if (found) found.status = found.status === 'active' ? 'inactive' : 'active'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteTool = (id: number) => {
|
const handleDeleteTool = async (id: string) => {
|
||||||
switch (activeTab.value) {
|
if (confirm('Are you sure you want to delete this tool?')) {
|
||||||
case 'built-in': builtInTools.value = builtInTools.value.filter(t => t.id !== id); break
|
await deleteToolApi(id)
|
||||||
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 handleSyncTools = async () => {
|
||||||
|
await syncTools()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 min-h-screen">
|
<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">
|
<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>
|
<span class="font-medium">Tools</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary">
|
<div class="h-10 flex gap-2">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<button v-if="activeTab === 'built-in'" @click="handleSyncTools" class="btn-secondary">
|
||||||
New Tool
|
<i class="fa-solid fa-sync mr-1"></i>
|
||||||
</button>
|
Sync Tools
|
||||||
|
</button>
|
||||||
|
<button v-if="activeTab === 'mcp'" class="btn-primary">
|
||||||
|
<Plus class="w-4 h-4 mr-1" />
|
||||||
|
Add MCP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 导航 -->
|
<!-- Tab 导航 -->
|
||||||
<div class="flex items-center gap-2 mb-6">
|
<div class="flex items-center gap-2 mb-6">
|
||||||
<button
|
<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"
|
: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="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
|
||||||
:class="activeTab === tab
|
:class="activeTab === tab
|
||||||
? 'bg-orange-500 text-white'
|
? 'bg-orange-500 text-white'
|
||||||
@@ -147,12 +191,12 @@ const deleteTool = (id: number) => {
|
|||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索和筛选 -->
|
||||||
<div class="flex gap-4 mb-6">
|
<div class="flex gap-4 mb-6">
|
||||||
<div class="flex-1 relative">
|
<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
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search tools by name or description..."
|
placeholder="Search tools by name or description..."
|
||||||
class="search-input w-full"
|
class="search-input w-full pl-10"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||||
@@ -164,7 +208,11 @@ const deleteTool = (id: number) => {
|
|||||||
|
|
||||||
<!-- Tools 列表 -->
|
<!-- Tools 列表 -->
|
||||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
<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">
|
<thead class="bg-dark-600">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th>
|
<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">
|
<tr v-for="tool in currentTools" :key="tool.id" class="table-row">
|
||||||
<td class="px-5 py-4">
|
<td class="px-5 py-4">
|
||||||
<div class="flex items-center gap-3">
|
<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'">
|
<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'">
|
||||||
<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>
|
<component :is="getIconComponent(tool.icon)" class="w-5 h-5" :class="tool.type === 'built-in' ? 'text-orange-400' : 'text-emerald-400'" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">{{ tool.name }}</div>
|
<div class="font-medium">{{ tool.name }}</div>
|
||||||
@@ -188,7 +236,7 @@ const deleteTool = (id: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-4 text-center">
|
<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>
|
||||||
<td class="px-5 py-4 text-center">
|
<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'">
|
<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 class="capitalize">{{ tool.status }}</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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">
|
<td class="px-5 py-4">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
<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>
|
||||||
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
<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>
|
||||||
<button @click="deleteTool(tool.id)" class="btn-icon" title="Delete">
|
<button @click="handleDeleteTool(tool.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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -217,10 +265,10 @@ const deleteTool = (id: number) => {
|
|||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="empty-box">
|
<div v-else class="empty-box">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<i class="fa-solid fa-tools"></i>
|
<Wrench class="w-12 h-12" />
|
||||||
</div>
|
</div>
|
||||||
<p class="empty-text">No tools found</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -231,7 +279,7 @@ const deleteTool = (id: number) => {
|
|||||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||||
<h3 class="text-lg font-semibold">Edit Tool</h3>
|
<h3 class="text-lg font-semibold">Edit Tool</h3>
|
||||||
<button @click="cancelEdit" class="btn-icon">
|
<button @click="cancelEdit" class="btn-icon">
|
||||||
<i class="fa-solid fa-xmark text-xl"></i>
|
<X class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,8 @@ export function useSkills() {
|
|||||||
|
|
||||||
const newSkillForm = ref({
|
const newSkillForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
type: 'API',
|
|
||||||
category: 'api',
|
|
||||||
port: 3000,
|
|
||||||
description: '',
|
description: '',
|
||||||
|
markdown: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分类选项
|
// 分类选项
|
||||||
@@ -85,7 +83,7 @@ export function useSkills() {
|
|||||||
|
|
||||||
// 打开创建弹窗
|
// 打开创建弹窗
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
newSkillForm.value = { name: '', type: 'API', category: 'api', port: 3000, description: '' }
|
newSkillForm.value = { name: '', description: '', markdown: '' }
|
||||||
isCreating.value = true
|
isCreating.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +99,10 @@ export function useSkills() {
|
|||||||
id: newId,
|
id: newId,
|
||||||
name: newSkillForm.value.name,
|
name: newSkillForm.value.name,
|
||||||
description: newSkillForm.value.description,
|
description: newSkillForm.value.description,
|
||||||
type: newSkillForm.value.type,
|
type: 'Custom',
|
||||||
category: newSkillForm.value.category,
|
category: 'custom',
|
||||||
status: 'stopped',
|
status: 'stopped',
|
||||||
port: newSkillForm.value.port,
|
port: 3000,
|
||||||
createdAt: new Date().toISOString().split('T')[0],
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
tools: 0,
|
tools: 0,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user