feat: 更新前端页面

- Agents, Chat, Skill 页面
- useSkills composable
- package.json 更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 16:26:10 +08:00
parent 715dc14b38
commit 243a190124
6 changed files with 541 additions and 260 deletions

View File

@@ -1,6 +1,15 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
// Agents 数据
const agents = ref([
{ id: 1, name: 'Claude Agent', avatar: '🧠', description: 'General purpose AI assistant', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'running' as const, framework: 'Google ADK', model: 'gemini-2.0-flash', mcpServers: 2, createdAt: '2025-04-10' },
{ id: 2, name: 'Code Assistant', avatar: '💻', description: 'Specialized in code generation', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'running' as const, framework: 'OpenAI', model: 'gpt-4o', mcpServers: 1, createdAt: '2025-04-08' },
{ id: 3, name: 'Data Analyst', avatar: '📊', description: 'Data analysis and visualization', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'stopped' as const, framework: 'PydanticAI', model: 'gpt-4o-mini', mcpServers: 3, createdAt: '2025-04-05' },
{ id: 4, name: 'Research Bot', avatar: '🔬', description: 'Academic research assistant', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'running' as const, framework: 'LangChain', model: 'claude-3-5-sonnet', mcpServers: 2, createdAt: '2025-04-12' },
{ id: 5, name: '客服助手', avatar: '🎧', description: 'Customer support agent', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'running' as const, framework: 'Google ADK', model: 'gemini-1.5-pro', mcpServers: 4, createdAt: '2025-04-11' },
])
// 创建智能体弹窗状态
const showCreateModal = ref(false)
const isCreating = ref(false)
@@ -65,29 +74,6 @@ const createAgent = async () => {
}
}
interface Agent {
id: number
name: string
avatar: string
description: string
accentColor: string
gradient: string
status: 'running' | 'stopped'
framework: string
model: string
mcpServers: number
createdAt: string
}
// 管理页面的 agents
const agents = ref<Agent[]>([
{ id: 1, name: 'Claude Agent', avatar: '🧠', description: 'General purpose AI assistant', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'running', framework: 'Google ADK', model: 'gemini-2.0-flash', mcpServers: 2, createdAt: '2025-04-10' },
{ id: 2, name: 'Code Assistant', avatar: '💻', description: 'Specialized in code generation', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'running', framework: 'OpenAI', model: 'gpt-4o', mcpServers: 1, createdAt: '2025-04-08' },
{ id: 3, name: 'Data Analyst', avatar: '📊', description: 'Data analysis and visualization', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'stopped', framework: 'PydanticAI', model: 'gpt-4o-mini', mcpServers: 3, createdAt: '2025-04-05' },
{ id: 4, name: 'Research Bot', avatar: '🔬', description: 'Academic research assistant', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'running', framework: 'LangChain', model: 'claude-3-5-sonnet', mcpServers: 2, createdAt: '2025-04-12' },
{ id: 5, name: '客服助手', avatar: '🎧', description: 'Customer support agent', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'running', framework: 'Google ADK', model: 'gemini-1.5-pro', mcpServers: 4, createdAt: '2025-04-11' },
])
const searchQuery = ref('')
const filterStatus = ref('all')
@@ -118,7 +104,7 @@ const statusClass = (status: string) => {
}
// 切换状态
const toggleStatus = (agent: Agent) => {
const toggleStatus = (agent: any) => {
agent.status = agent.status === 'running' ? 'stopped' : 'running'
}
@@ -143,54 +129,6 @@ const deleteAgent = (id: number) => {
</button>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-dark-600 flex items-center justify-center">
<i class="fa-solid fa-robot text-gray-400"></i>
</div>
<div>
<div class="text-2xl font-bold text-white">{{ stats.total }}</div>
<div class="text-xs text-gray-400">Total Agents</div>
</div>
</div>
</div>
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary-success/20 flex items-center justify-center">
<i class="fa-solid fa-circle-check text-primary-success"></i>
</div>
<div>
<div class="text-2xl font-bold text-primary-success">{{ stats.running }}</div>
<div class="text-xs text-gray-400">Running</div>
</div>
</div>
</div>
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-gray-500/20 flex items-center justify-center">
<i class="fa-solid fa-circle-stop text-gray-400"></i>
</div>
<div>
<div class="text-2xl font-bold text-gray-400">{{ stats.stopped }}</div>
<div class="text-xs text-gray-400">Stopped</div>
</div>
</div>
</div>
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary-cyan/20 flex items-center justify-center">
<i class="fa-solid fa-plug text-primary-cyan"></i>
</div>
<div>
<div class="text-2xl font-bold text-primary-cyan">{{ agents.reduce((sum, a) => sum + a.mcpServers, 0) }}</div>
<div class="text-xs text-gray-400">MCP Servers</div>
</div>
</div>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const API_BASE = 'http://localhost:8082'
interface ChatMessage {
id: number
role: 'user' | 'assistant'
@@ -158,31 +160,43 @@ const sendMessage = async () => {
nextTick(() => scrollToBottom())
isLoading.value = true
const fullResponse = `我理解你发送了消息: "${userContent}"
作为 AI 助手,我可以帮助你:
• 回答各种问题
• 编写代码和调试
• 分析和处理数据
• 翻译和写作
• 头脑风暴和创意建议
try {
// 调用后端 API
const response = await fetch(`${API_BASE}/api/agent/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
agent_id: selectedAgent.value?.id || 1,
message: userContent,
}),
})
请告诉我你需要什么帮助?`
const data = await response.json()
const fullResponse = data.reply || data.response || 'No response'
let currentIndex = 0
const words = fullResponse.split('')
// 流式显示回复
let currentIndex = 0
const words = fullResponse.split('')
const streamInterval = setInterval(() => {
if (currentIndex < words.length) {
aiMessage.content += words[currentIndex]
currentIndex++
nextTick(() => scrollToBottom())
} else {
clearInterval(streamInterval)
aiMessage.isStreaming = false
isLoading.value = false
}
}, 30)
const streamInterval = setInterval(() => {
if (currentIndex < words.length) {
aiMessage.content += words[currentIndex]
currentIndex++
nextTick(() => scrollToBottom())
} else {
clearInterval(streamInterval)
aiMessage.isStreaming = false
isLoading.value = false
}
}, 30)
} catch (error: any) {
aiMessage.content = `Error: ${error.message || 'Failed to send message'}`
aiMessage.isStreaming = false
isLoading.value = false
}
}
// 滚动到底部

View File

@@ -1,28 +1,32 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useSkills } from './skill/useSkills'
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
import { Edit, Trash2, Wand2, Plus, Search, X } from 'lucide-vue-next'
import '@/views/database/database.css'
const {
skillsLoading,
searchQuery,
filterStatus,
isEditing,
isCreating,
editForm,
newSkillForm,
categories,
types,
filteredSkills,
statusClass,
fetchSkills,
openCreate,
closeCreate,
saveNewSkill,
openEdit,
closeEdit,
saveEdit,
toggleStatus,
deleteSkill,
} = useSkills()
// 页面加载时获取技能列表
onMounted(() => {
fetchSkills()
})
</script>
<template>
@@ -30,11 +34,11 @@ const {
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-wand-magic-sparkles text-orange-500"></i>
<Wand2 class="w-5 h-5 text-orange-500" />
<span class="font-medium">Skills</span>
</div>
<button @click="openCreate" class="btn-primary">
<i class="fa-solid fa-plus"></i>
<Plus class="w-4 h-4" />
New Skill
</button>
</div>
@@ -42,7 +46,7 @@ const {
<!-- 搜索和筛选 -->
<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"
@@ -59,15 +63,12 @@ const {
</div>
<!-- Skills 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<div class="bg-dark-700 rounded-xl overflow-hidden shadow-lg">
<table v-if="filteredSkills.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">Skill Name</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Type</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400 w-1/3">Skill Name</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Category</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Port</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Tools</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
@@ -76,31 +77,28 @@ const {
<tbody>
<tr v-for="skill in filteredSkills" :key="skill.id" class="table-row">
<td class="px-5 py-4">
<div class="font-medium">{{ skill.name }}</div>
<div class="text-sm text-gray-500">{{ skill.description }}</div>
<div class="flex gap-3">
<div class="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center flex-shrink-0 self-center">
<Wand2 class="w-5 h-5 text-orange-400" />
</div>
<div class="min-w-0 self-center">
<div class="font-medium">{{ skill.skill_name }}</div>
<div class="text-sm text-gray-500 line-clamp-2 mt-0.5">{{ skill.skill_desc || '-' }}</div>
</div>
</div>
</td>
<td class="px-5 py-4 text-center">
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ skill.type }}</span>
<span class="text-gray-400 text-sm">{{ skill.skill_type === 'system' ? 'System' : 'User' }}</span>
</td>
<td class="px-5 py-4 text-center">
<span class="text-gray-400 text-sm capitalize">{{ skill.category }}</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ skill.port }}</td>
<td class="px-5 py-4 text-center">
<span class="text-primary-cyan">{{ skill.tools }}</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="skill.status === 'running' ? 'bg-green-500/20 text-green-400' : skill.status === 'error' ? 'bg-red-500/20 text-red-400' : 'bg-gray-500/20 text-gray-400'">
<span class="w-1.5 h-1.5 rounded-full" :class="statusClass(skill.status)"></span>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="skill.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
<span class="w-1.5 h-1.5 rounded-full" :class="skill.status === 'active' ? 'bg-green-500' : 'bg-gray-400'"></span>
<span class="capitalize">{{ skill.status }}</span>
</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ skill.createdAt }}</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ skill.created_at ? new Date(skill.created_at).toLocaleDateString() : '-' }}</td>
<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'">
<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">
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
</button>
@@ -116,7 +114,7 @@ const {
<!-- 空状态 -->
<div v-else class="empty-box">
<div class="empty-icon">
<i class="fa-solid fa-wand-magic-sparkles"></i>
<Wand2 class="w-12 h-12" />
</div>
<p class="empty-text">No skills found</p>
<p class="empty-tip">Click "New Skill" to add a skill</p>
@@ -130,39 +128,27 @@ const {
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit Skill</h3>
<button @click="closeEdit" class="btn-icon">
<i class="fa-solid fa-xmark text-xl"></i>
<X class="w-5 h-5" />
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
<input v-model="editForm.name" type="text" 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="editForm.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="editForm.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>
<input v-model="editForm.skill_name" type="text" class="input-field">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
<input v-model="editForm.port" type="number" class="input-field">
<label class="block text-sm font-medium text-gray-300 mb-2">Category</label>
<select v-model="editForm.skill_type" class="input-field">
<option value="user">User</option>
<option value="system">System</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea v-model="editForm.description" rows="2" class="input-field resize-none"></textarea>
<textarea v-model="editForm.skill_desc" rows="2" class="input-field resize-none"></textarea>
</div>
</div>
@@ -181,7 +167,7 @@ const {
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center">
<i class="fa-solid fa-wand-magic-sparkles text-white"></i>
<Wand2 class="w-5 h-5 text-white" />
</div>
<div>
<h3 class="text-xl font-semibold text-white">Create New Skill</h3>
@@ -189,30 +175,33 @@ const {
</div>
</div>
<button @click="closeCreate" class="btn-icon">
<i class="fa-solid fa-xmark text-xl"></i>
<X class="w-5 h-5" />
</button>
</div>
<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">
<input v-model="newSkillForm.skill_name" type="text" placeholder="Enter skill name..." class="input-field">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Category</label>
<select v-model="newSkillForm.skill_type" class="input-field">
<option value="user">User</option>
<option value="system">System</option>
</select>
</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>
<textarea v-model="newSkillForm.skill_desc" rows="2" placeholder="Describe this skill..." class="input-field resize-none"></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" :disabled="!newSkillForm.name || !newSkillForm.description || !newSkillForm.markdown" class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">Create Skill</button>
<button @click="saveNewSkill" :disabled="!newSkillForm.skill_name || !newSkillForm.skill_desc" class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">Create Skill</button>
</div>
</div>
</div>

View File

@@ -1,60 +1,144 @@
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { ElMessageBox, ElMessage } from 'element-plus'
const API_BASE = 'http://localhost:8082'
export interface Skill {
id: string
name: string
description: string
description_cn?: string
command: string
args?: string
env?: string
category: string
skill_name: string
skill_type: string
skill_desc: string
path: string
status: string
created_at?: string
updated_at?: string
}
export function useSkills() {
// 从 API 获取 MCP 列表
const fetchSkills = async () => {
const skills = ref<Skill[]>([])
const skillsLoading = ref(false)
// 获取技能列表
const fetchSkills = async (type?: string) => {
skillsLoading.value = true
try {
const response = await fetch(`${API_BASE}/mcp/list`)
let url = `${API_BASE}/skill/list`
if (type) {
url += `?type=${type}`
}
const response = await fetch(url)
const result = await response.json()
if (result && Array.isArray(result)) {
skills.value = result.map((mcp: any) => ({
id: mcp.id,
name: mcp.name,
description: mcp.description || '',
description_cn: mcp.description_cn || '',
command: mcp.command || '',
args: mcp.args || '',
env: mcp.env || '',
category: mcp.category || 'custom',
status: mcp.status || 'stopped',
created_at: mcp.created_at,
if (result.list) {
skills.value = result.list.map((skill: any) => ({
id: skill.id,
skill_name: skill.skill_name,
skill_type: skill.skill_type,
skill_desc: skill.skill_desc || '',
path: skill.path || '',
status: skill.status || 'active',
created_at: skill.created_at,
updated_at: skill.updated_at,
}))
}
return result.list || []
} catch (error) {
console.error('Failed to fetch MCP list:', error)
console.error('Failed to fetch skills:', error)
return []
} finally {
skillsLoading.value = false
}
}
onMounted(() => {
fetchSkills()
// 同步技能
const syncSkills = async () => {
try {
const response = await fetch(`${API_BASE}/skill/sync`)
const data = await response.json()
await fetchSkills()
return data
} catch (error) {
console.error('Failed to sync skills:', error)
throw error
}
}
// 获取技能详情
const fetchSkillById = async (id: string) => {
try {
const response = await fetch(`${API_BASE}/skill/${id}`)
const data = await response.json()
return data.skill
} catch (error) {
console.error('Failed to fetch skill:', error)
throw error
}
}
// 创建技能
const createSkill = async (skill: Partial<Skill>) => {
try {
const response = await fetch(`${API_BASE}/skill/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(skill),
})
const data = await response.json()
await fetchSkills()
return data
} catch (error) {
console.error('Failed to create skill:', error)
throw error
}
}
// 更新技能
const updateSkill = async (id: string, skill: Partial<Skill>) => {
try {
const response = await fetch(`${API_BASE}/skill/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(skill),
})
const data = await response.json()
await fetchSkills()
return data
} catch (error) {
console.error('Failed to update skill:', error)
throw error
}
}
// 删除技能
const deleteSkill = async (id: string) => {
try {
const response = await fetch(`${API_BASE}/skill/${id}`, {
method: 'DELETE',
})
const data = await response.json()
await fetchSkills()
return data
} catch (error) {
console.error('Failed to delete skill:', error)
throw error
}
}
// 按类型获取技能
const getSkillsByType = (type: string) => {
return skills.value.filter(skill => skill.skill_type === type)
}
// 获取所有分类
const categories = computed(() => {
const cats = new Set(skills.value.map(skill => skill.skill_type))
return Array.from(cats)
})
// Skill 数据
const skills = ref<Skill[]>([
{ id: 1, name: 'Linear', description: 'Linear API integration for project management', type: 'API', category: 'api', status: 'running', port: 3001, createdAt: '2025-04-10', tools: 12 },
{ id: 2, name: 'Google Maps', description: 'Google Maps API for location services', type: 'Google Maps', category: 'api', status: 'running', port: 3002, createdAt: '2025-04-08', tools: 8 },
{ id: 3, name: 'File Explorer', description: 'File system explorer and editor', type: 'File System', category: 'filesystem', status: 'error', port: 3003, createdAt: '2025-04-05', tools: 15 },
{ id: 4, name: 'PostgreSQL', description: 'PostgreSQL database operations', type: 'Database', category: 'database', status: 'running', port: 3004, createdAt: '2025-04-12', tools: 10 },
{ id: 5, name: 'GitHub', description: 'GitHub API integration', type: 'GitHub', category: 'communication', status: 'stopped', port: 3005, createdAt: '2025-04-11', tools: 20 },
{ id: 6, name: 'Slack', description: 'Slack messaging integration', type: 'Communication', category: 'communication', status: 'running', port: 3006, createdAt: '2025-04-09', tools: 6 },
{ id: 7, name: 'OpenAI', description: 'OpenAI GPT models for AI conversations', type: 'AI/ML', category: 'ai', status: 'running', port: 3007, createdAt: '2025-04-07', tools: 5 },
{ id: 8, name: 'MySQL', description: 'MySQL database operations', type: 'Database', category: 'database', status: 'stopped', port: 3008, createdAt: '2025-04-06', tools: 10 },
])
// 搜索和筛选
const searchQuery = ref('')
@@ -67,35 +151,22 @@ export function useSkills() {
// 表单
const editForm = ref({
name: '',
type: '',
category: '',
port: 3000,
description: '',
skill_name: '',
skill_desc: '',
skill_type: 'user',
})
const newSkillForm = ref({
name: '',
description: '',
markdown: '',
skill_name: '',
skill_desc: '',
skill_type: 'user',
})
// 分类选项
const categories = [
{ value: 'api', label: 'API' },
{ value: 'database', label: 'Database' },
{ value: 'filesystem', label: 'File System' },
{ value: 'communication', label: 'Communication' },
{ value: 'ai', label: 'AI/ML' },
]
const types = ['API', 'Database', 'File System', 'Communication', 'AI/ML']
// 筛选
const filteredSkills = computed(() => {
return skills.value.filter(skill => {
const matchSearch = skill.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
skill.description.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchSearch = skill.skill_name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
(skill.skill_desc && skill.skill_desc.toLowerCase().includes(searchQuery.value.toLowerCase()))
const matchStatus = filterStatus.value === 'all' || skill.status === filterStatus.value
return matchSearch && matchStatus
})
@@ -104,8 +175,8 @@ export function useSkills() {
// 状态样式
const statusClass = (status: string) => {
switch (status) {
case 'running': return 'bg-green-500'
case 'stopped': return 'bg-gray-500'
case 'active': return 'bg-green-500'
case 'inactive': return 'bg-gray-500'
case 'error': return 'bg-red-500'
default: return 'bg-gray-500'
}
@@ -113,7 +184,7 @@ export function useSkills() {
// 打开创建弹窗
const openCreate = () => {
newSkillForm.value = { name: '', description: '', markdown: '' }
newSkillForm.value = { skill_name: '', skill_desc: '', skill_type: 'user' }
isCreating.value = true
}
@@ -123,31 +194,28 @@ export function useSkills() {
}
// 保存新技能
const saveNewSkill = () => {
const newId = Math.max(...skills.value.map(s => s.id)) + 1
skills.value.push({
id: newId,
name: newSkillForm.value.name,
description: newSkillForm.value.description,
type: 'Custom',
category: 'custom',
status: 'stopped',
port: 3000,
createdAt: new Date().toISOString().split('T')[0],
tools: 0,
})
isCreating.value = false
const saveNewSkill = async () => {
try {
await createSkill({
skill_name: newSkillForm.value.skill_name,
skill_desc: newSkillForm.value.skill_desc,
skill_type: newSkillForm.value.skill_type,
status: 'active',
})
ElMessage.success('Skill created successfully')
isCreating.value = false
} catch (error) {
ElMessage.error('Failed to create skill')
}
}
// 打开编辑弹窗
const openEdit = (skill: Skill) => {
editingSkill.value = skill
editForm.value = {
name: skill.name,
type: skill.type,
category: skill.category,
port: skill.port,
description: skill.description,
skill_name: skill.skill_name,
skill_desc: skill.skill_desc,
skill_type: skill.skill_type,
}
isEditing.value = true
}
@@ -159,41 +227,54 @@ export function useSkills() {
}
// 保存编辑
const saveEdit = () => {
const index = skills.value.findIndex(s => s.id === editingSkill.value!.id)
if (index !== -1) {
skills.value[index] = {
...skills.value[index],
name: editForm.value.name,
type: editForm.value.type,
category: editForm.value.category,
port: editForm.value.port,
description: editForm.value.description,
}
const saveEdit = async () => {
try {
await updateSkill(editingSkill.value!.id, {
skill_name: editForm.value.skill_name,
skill_desc: editForm.value.skill_desc,
skill_type: editForm.value.skill_type,
})
ElMessage.success('Skill updated successfully')
isEditing.value = false
} catch (error) {
ElMessage.error('Failed to update skill')
}
isEditing.value = false
}
// 切换状态
const toggleStatus = (skill: Skill) => {
if (skill.status === 'running') skill.status = 'stopped'
else if (skill.status === 'stopped') skill.status = 'running'
const toggleStatus = async (skill: Skill) => {
const newStatus = skill.status === 'active' ? 'inactive' : 'active'
try {
await updateSkill(skill.id, { status: newStatus })
skill.status = newStatus
} catch (error) {
ElMessage.error('Failed to update status')
}
}
// 删除技能
const deleteSkill = (id: number) => {
ElMessageBox.confirm('Are you sure you want to delete this skill?', 'Confirm Delete', {
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning',
}).then(() => {
skills.value = skills.value.filter(s => s.id !== id)
}).catch(() => {})
const handleDeleteSkill = async (id: string) => {
try {
await ElMessageBox.confirm('Are you sure you want to delete this skill?', 'Confirm Delete', {
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning',
})
await deleteSkill(id)
ElMessage.success('Skill deleted successfully')
} catch (error: any) {
if (error !== 'cancel') {
console.error('Failed to delete skill:', error)
ElMessage.error('Failed to delete skill')
}
}
}
return {
// State
skills,
skillsLoading,
searchQuery,
filterStatus,
isEditing,
@@ -202,10 +283,11 @@ export function useSkills() {
editForm,
newSkillForm,
categories,
types,
// Computed
filteredSkills,
// Methods
fetchSkills,
syncSkills,
statusClass,
openCreate,
closeCreate,
@@ -214,6 +296,6 @@ export function useSkills() {
closeEdit,
saveEdit,
toggleStatus,
deleteSkill,
deleteSkill: handleDeleteSkill,
}
}