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:
2026-03-11 14:26:25 +08:00
parent 7791d198f1
commit 03540fb9e9
9 changed files with 742 additions and 556 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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"

View File

@@ -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>

View File

@@ -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&#10;&#10;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>

View File

@@ -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>

View File

@@ -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,
})