- 新增 web/src/views/skill/skill.ts skill视图组件 - 更新 Agents.vue 页面 - 更新 Skill.vue 页面 - 移除旧的 useSkills.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1033 lines
31 KiB
Vue
1033 lines
31 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
|
||
const API_BASE = 'http://localhost:8082'
|
||
|
||
// Skills 接口
|
||
interface Skill {
|
||
id: string
|
||
skill_name: string
|
||
skill_type: string
|
||
skill_desc: string
|
||
path: string
|
||
status: string
|
||
}
|
||
|
||
// 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)
|
||
const newAgent = ref({
|
||
name: '',
|
||
description: '',
|
||
skillsMode: 'all' as 'all' | 'include' | 'exclude',
|
||
selectedSkills: [] as string[],
|
||
knowledge: '',
|
||
prompt: '',
|
||
avatar: '🤖',
|
||
})
|
||
|
||
// Skills 选择器状态
|
||
const showSkillsDropdown = ref(false)
|
||
const showSubSkillsDropdown = ref(false)
|
||
|
||
// Skills 模式选项
|
||
const skillsModeOptions = [
|
||
{ value: 'all', label: 'All Skills', desc: 'Use all available skills' },
|
||
{ value: 'include', label: 'Include Skills', desc: 'Select specific skills to use' },
|
||
{ value: 'exclude', label: 'Exclude Skills', desc: 'Select skills to exclude' },
|
||
]
|
||
|
||
// 切换技能模式下拉框
|
||
const toggleSkillsMode = () => {
|
||
showSkillsDropdown.value = !showSkillsDropdown.value
|
||
showSubSkillsDropdown.value = false
|
||
}
|
||
|
||
// 选择技能模式
|
||
const selectSkillsMode = (mode: 'all' | 'include' | 'exclude') => {
|
||
newAgent.value.skillsMode = mode
|
||
if (mode === 'all') {
|
||
newAgent.value.selectedSkills = []
|
||
}
|
||
showSkillsDropdown.value = false
|
||
if (mode === 'include' || mode === 'exclude') {
|
||
showSubSkillsDropdown.value = true
|
||
}
|
||
}
|
||
|
||
// 处理技能模式点击(用于模板)
|
||
const handleSkillsModeClick = (mode: 'all' | 'include' | 'exclude') => {
|
||
selectSkillsMode(mode)
|
||
}
|
||
|
||
// 切换子下拉框
|
||
const toggleSubSkillsDropdown = () => {
|
||
showSubSkillsDropdown.value = !showSubSkillsDropdown.value
|
||
}
|
||
|
||
// 关闭所有下拉框
|
||
const closeAllDropdowns = () => {
|
||
showSkillsDropdown.value = false
|
||
showSubSkillsDropdown.value = false
|
||
}
|
||
|
||
// 获取显示文本
|
||
const getSkillsDisplayText = () => {
|
||
if (newAgent.value.skillsMode === 'all') {
|
||
return 'All Skills'
|
||
}
|
||
const count = newAgent.value.selectedSkills.length
|
||
if (count === 0) {
|
||
return newAgent.value.skillsMode === 'include' ? 'Select skills to include...' : 'Select skills to exclude...'
|
||
}
|
||
return `${count} skill${count > 1 ? 's' : ''} ${newAgent.value.skillsMode === 'include' ? 'included' : 'excluded'}`
|
||
}
|
||
|
||
// 切换子下拉框中的技能选择
|
||
const toggleSkillSelection = (skillId: string) => {
|
||
const index = newAgent.value.selectedSkills.indexOf(skillId)
|
||
if (index > -1) {
|
||
newAgent.value.selectedSkills.splice(index, 1)
|
||
} else {
|
||
newAgent.value.selectedSkills.push(skillId)
|
||
}
|
||
}
|
||
|
||
// 头像选项
|
||
const avatarOptions = [
|
||
'🤖', '🧠', '💻', '📊', '🔬', '🎧', '✨', '💬', '🔮', '🌙',
|
||
'🐉', '☁️', '🎨', '🎯', '🚀', '⚡', '🔥', '💡', '🎭', '🎪'
|
||
]
|
||
|
||
// Skills 列表
|
||
const skillsList = ref<Skill[]>([])
|
||
const skillsLoading = ref(false)
|
||
|
||
// 获取 skills 列表
|
||
const fetchSkills = async () => {
|
||
skillsLoading.value = true
|
||
try {
|
||
const response = await fetch(`${API_BASE}/skill/list`)
|
||
const result = await response.json()
|
||
if (result.list) {
|
||
skillsList.value = result.list
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch skills:', error)
|
||
} finally {
|
||
skillsLoading.value = false
|
||
}
|
||
}
|
||
|
||
// Skills 选项(从真实列表生成)
|
||
const skillsOptions = computed(() => {
|
||
return skillsList.value.map(skill => ({
|
||
value: skill.id,
|
||
label: skill.skill_name,
|
||
desc: skill.skill_desc,
|
||
}))
|
||
})
|
||
|
||
// Knowledge 选项
|
||
const knowledgeOptions = [
|
||
{ value: 'general', label: 'General Knowledge' },
|
||
{ value: 'codebase', label: 'Codebase' },
|
||
{ value: 'docs', label: 'Documentation' },
|
||
{ value: 'api', label: 'API Reference' },
|
||
]
|
||
|
||
// Skills 选择器状态
|
||
const skillsSearch = ref('')
|
||
|
||
// 获取 skill 标签
|
||
const getSkillLabel = (id: string) => {
|
||
return skillsOptions.value.find(s => s.value === id)?.label || id
|
||
}
|
||
|
||
// 过滤后的 skills
|
||
const filteredSkills = computed(() => {
|
||
if (!skillsSearch.value) return skillsOptions.value
|
||
const search = skillsSearch.value.toLowerCase()
|
||
return skillsOptions.value.filter(s =>
|
||
s.label.toLowerCase().includes(search) ||
|
||
(s.desc && s.desc.toLowerCase().includes(search))
|
||
)
|
||
})
|
||
|
||
// 是否全选
|
||
const isAllSelected = computed(() => {
|
||
return skillsOptions.value.length > 0 && newAgent.value.selectedSkills.length === skillsOptions.value.length
|
||
})
|
||
|
||
// 是否半选
|
||
const isIndeterminate = computed(() => {
|
||
return newAgent.value.selectedSkills.length > 0 && newAgent.value.selectedSkills.length < skillsOptions.value.length
|
||
})
|
||
|
||
// 切换下拉框
|
||
const toggleSkillsDropdown = () => {
|
||
showSkillsDropdown.value = !showSkillsDropdown.value
|
||
if (!showSkillsDropdown.value) {
|
||
skillsSearch.value = ''
|
||
}
|
||
}
|
||
|
||
// 关闭下拉框
|
||
const closeSkillsDropdown = () => {
|
||
showSkillsDropdown.value = false
|
||
skillsSearch.value = ''
|
||
}
|
||
|
||
// 全选 skills
|
||
const selectAllSkills = () => {
|
||
newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value)
|
||
}
|
||
|
||
// 清空 skills
|
||
const clearSkills = () => {
|
||
newAgent.value.selectedSkills = []
|
||
}
|
||
|
||
// 切换全选
|
||
const toggleSelectAll = () => {
|
||
if (isAllSelected.value) {
|
||
newAgent.value.selectedSkills = []
|
||
} else {
|
||
newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value)
|
||
}
|
||
}
|
||
|
||
// 点击外部关闭下拉框
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
const target = e.target as HTMLElement
|
||
if (!target.closest('.skills-selector')) {
|
||
closeSkillsDropdown()
|
||
}
|
||
}
|
||
|
||
// 页面加载时获取 skills
|
||
onMounted(() => {
|
||
fetchSkills()
|
||
document.addEventListener('click', handleClickOutside)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
document.removeEventListener('click', handleClickOutside)
|
||
})
|
||
|
||
// 打开创建弹窗
|
||
const openCreateModal = () => {
|
||
newAgent.value = {
|
||
name: '',
|
||
description: '',
|
||
skillsMode: 'all' as const,
|
||
selectedSkills: [],
|
||
knowledge: '',
|
||
prompt: '',
|
||
avatar: '🤖'
|
||
}
|
||
showCreateModal.value = true
|
||
}
|
||
|
||
// 创建智能体
|
||
const createAgent = async () => {
|
||
if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0) || !newAgent.value.knowledge) {
|
||
return
|
||
}
|
||
|
||
isCreating.value = true
|
||
try {
|
||
// 处理 skills 标签
|
||
const skillsLabels = newAgent.value.selectedSkills.map((id: string) => getSkillLabel(id)).join(', ')
|
||
|
||
// 模拟创建
|
||
const newId = Math.max(...agents.value.map(a => a.id)) + 1
|
||
agents.value.unshift({
|
||
id: newId,
|
||
name: newAgent.value.name,
|
||
avatar: newAgent.value.avatar,
|
||
description: newAgent.value.description,
|
||
accentColor: '#f97316',
|
||
gradient: 'from-orange-500/20 to-amber-500/20',
|
||
status: 'stopped',
|
||
framework: skillsLabels || 'None',
|
||
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
|
||
}
|
||
}
|
||
|
||
const searchQuery = ref('')
|
||
const filterStatus = ref('all')
|
||
|
||
// 过滤后的 agents
|
||
const filteredAgents = computed(() => {
|
||
return agents.value.filter(agent => {
|
||
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||
agent.framework.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
|
||
return matchSearch && matchStatus
|
||
})
|
||
})
|
||
|
||
// 统计数据
|
||
const stats = computed(() => ({
|
||
total: agents.value.length,
|
||
running: agents.value.filter(a => a.status === 'running').length,
|
||
stopped: agents.value.filter(a => a.status === 'stopped').length,
|
||
}))
|
||
|
||
// 状态颜色
|
||
const statusClass = (status: string) => {
|
||
switch (status) {
|
||
case 'running': return 'bg-primary-success'
|
||
case 'stopped': return 'bg-gray-500'
|
||
default: return 'bg-gray-500'
|
||
}
|
||
}
|
||
|
||
// 切换状态
|
||
const toggleStatus = (agent: any) => {
|
||
agent.status = agent.status === 'running' ? 'stopped' : 'running'
|
||
}
|
||
|
||
// 删除 Agent
|
||
const deleteAgent = (id: number) => {
|
||
agents.value = agents.value.filter(a => a.id !== id)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 主内容区域 -->
|
||
<div class="p-6 min-h-screen">
|
||
<!-- 顶部导航 -->
|
||
<div class="flex justify-between items-center mb-6">
|
||
<div class="flex items-center gap-2">
|
||
<i class="fa-solid fa-robot text-orange-500"></i>
|
||
<span class="font-medium">Agents</span>
|
||
</div>
|
||
<button @click="openCreateModal" class="btn-primary">
|
||
<i class="fa-solid fa-plus"></i>
|
||
New Agent
|
||
</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="searchQuery"
|
||
type="text"
|
||
placeholder="Search agents by name or framework..."
|
||
class="search-input w-full"
|
||
>
|
||
</div>
|
||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||
<el-option label="All Status" value="all" />
|
||
<el-option label="Running" value="running" />
|
||
<el-option label="Stopped" value="stopped" />
|
||
</el-select>
|
||
</div>
|
||
|
||
<!-- Agents 列表 -->
|
||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||
<table class="w-full">
|
||
<thead class="bg-dark-600">
|
||
<tr>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Agent Name</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Framework</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">MCP</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="agent in filteredAgents" :key="agent.id" class="table-row">
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center gap-3">
|
||
<div
|
||
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg"
|
||
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
|
||
>
|
||
{{ agent.avatar }}
|
||
</div>
|
||
<div>
|
||
<div class="font-medium text-white">{{ agent.name }}</div>
|
||
<div class="text-xs text-gray-500">{{ agent.description }}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="px-5 py-4">
|
||
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.framework }}</span>
|
||
</td>
|
||
<td class="px-5 py-4 text-gray-300">{{ agent.model }}</td>
|
||
<td class="px-5 py-4 text-center">
|
||
<span class="text-primary-cyan">{{ agent.mcpServers }}</span>
|
||
</td>
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-2 h-2 rounded-full" :class="statusClass(agent.status)"></span>
|
||
<span class="capitalize text-sm text-gray-300">{{ agent.status }}</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-5 py-4 text-gray-400 text-sm">{{ agent.createdAt }}</td>
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center justify-end gap-2">
|
||
<button
|
||
@click="toggleStatus(agent)"
|
||
class="btn-icon"
|
||
:title="agent.status === 'running' ? 'Stop' : 'Start'"
|
||
>
|
||
<i :class="['fa-solid', agent.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400']"></i>
|
||
</button>
|
||
<button class="btn-icon" title="Edit">
|
||
<i class="fa-solid fa-pen text-gray-400"></i>
|
||
</button>
|
||
<button class="btn-icon" title="Settings">
|
||
<i class="fa-solid fa-gear text-gray-400"></i>
|
||
</button>
|
||
<button
|
||
@click="deleteAgent(agent.id)"
|
||
class="btn-icon"
|
||
title="Delete"
|
||
>
|
||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="filteredAgents.length === 0" class="py-12 text-center text-gray-500">
|
||
<i class="fa-solid fa-robot text-4xl mb-3"></i>
|
||
<p>No agents found</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建智能体弹窗 -->
|
||
<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">Avatar</label>
|
||
<div class="flex flex-wrap gap-2">
|
||
<button
|
||
v-for="avatar in avatarOptions"
|
||
:key="avatar"
|
||
type="button"
|
||
@click="newAgent.avatar = avatar"
|
||
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg transition-all"
|
||
:class="newAgent.avatar === avatar ? 'bg-primary-orange text-white ring-2 ring-orange-400' : 'bg-dark-600 text-gray-300 hover:bg-dark-500'"
|
||
>
|
||
{{ avatar }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
|
||
<!-- 技能模式选择 - 两级下拉框 -->
|
||
<div class="skills-selector">
|
||
<!-- 已选 tags 显示区域 -->
|
||
<div class="selected-tags" @click="toggleSkillsDropdown">
|
||
<div v-if="newAgent.skillsMode === 'all'" class="placeholder">
|
||
All Skills
|
||
</div>
|
||
<div v-else class="tags-container">
|
||
<span
|
||
v-for="skillId in newAgent.selectedSkills.slice(0, 3)"
|
||
:key="skillId"
|
||
class="selected-tag"
|
||
>
|
||
{{ getSkillLabel(skillId) }}
|
||
</span>
|
||
<span v-if="newAgent.selectedSkills.length > 3" class="more-tag">
|
||
+{{ newAgent.selectedSkills.length - 3 }}
|
||
</span>
|
||
</div>
|
||
<svg class="dropdown-icon" :class="{ 'rotate-180': showSkillsDropdown }" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m6 9 6 6 6-6"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<!-- 第一级下拉面板:模式选择 -->
|
||
<Transition name="dropdown">
|
||
<div v-if="showSkillsDropdown" class="dropdown-panel">
|
||
<div class="skills-mode-options">
|
||
<div
|
||
v-for="option in skillsModeOptions"
|
||
:key="option.value"
|
||
class="skills-mode-item"
|
||
:class="{ 'active': newAgent.skillsMode === option.value }"
|
||
@click="handleSkillsModeClick(option.value as 'all' | 'include' | 'exclude')"
|
||
>
|
||
<div class="mode-radio">
|
||
<div v-if="newAgent.skillsMode === option.value" class="radio-dot"></div>
|
||
</div>
|
||
<div class="mode-content">
|
||
<span class="mode-label">{{ option.label }}</span>
|
||
<span class="mode-desc">{{ option.desc }}</span>
|
||
</div>
|
||
<!-- 显示展开图标 -->
|
||
<svg v-if="option.value !== 'all'" class="sub-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m9 18 6-6-6-6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
|
||
<!-- 第二级下拉面板:技能列表(悬浮窗形式) -->
|
||
<Transition name="dropdown">
|
||
<div v-if="showSubSkillsDropdown" class="sub-dropdown-panel" @click.stop>
|
||
<!-- 标题 -->
|
||
<div class="sub-dropdown-header">
|
||
<span class="sub-dropdown-title">
|
||
{{ newAgent.skillsMode === 'include' ? 'Select skills to include' : 'Select skills to exclude' }}
|
||
</span>
|
||
<button @click="showSubSkillsDropdown = false" class="sub-dropdown-close">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 搜索框 -->
|
||
<div class="search-box">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="search-icon">
|
||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
||
</svg>
|
||
<input
|
||
v-model="skillsSearch"
|
||
type="text"
|
||
placeholder="Search skills..."
|
||
class="search-input"
|
||
>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-bar">
|
||
<label class="checkbox-label cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
:checked="isAllSelected"
|
||
:indeterminate="isIndeterminate"
|
||
@change="toggleSelectAll"
|
||
class="checkbox"
|
||
>
|
||
<span class="checkbox-text">Select All</span>
|
||
</label>
|
||
<button
|
||
v-if="newAgent.selectedSkills.length > 0"
|
||
type="button"
|
||
@click="clearSkills"
|
||
class="clear-btn"
|
||
>
|
||
Clear
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 技能列表 -->
|
||
<div class="options-list">
|
||
<template v-if="filteredSkills.length > 0">
|
||
<label
|
||
v-for="skill in filteredSkills"
|
||
:key="skill.value"
|
||
class="option-item"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
:value="skill.value"
|
||
v-model="newAgent.selectedSkills"
|
||
class="checkbox"
|
||
>
|
||
<div class="option-content">
|
||
<span class="option-label">{{ skill.label }}</span>
|
||
<span v-if="skill.desc" class="option-desc">{{ skill.desc }}</span>
|
||
</div>
|
||
</label>
|
||
</template>
|
||
<div v-else class="no-results">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="no-results-icon">
|
||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
|
||
<path d="M21 3v5h-5"/>
|
||
</svg>
|
||
<span>No skills available</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
</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.skillsMode !== 'all' && newAgent.selectedSkills.length === 0) || !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>
|
||
|
||
<style scoped>
|
||
/* Skills Selector Styles */
|
||
.skills-selector {
|
||
position: relative;
|
||
}
|
||
|
||
/* Skills Mode Options */
|
||
.skills-mode-options {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #4b5563;
|
||
}
|
||
|
||
.skills-mode-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.skills-mode-item:hover {
|
||
background: #374151;
|
||
}
|
||
|
||
.skills-mode-item.active {
|
||
background: rgba(249, 115, 22, 0.1);
|
||
}
|
||
|
||
.mode-radio {
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 2px solid #6b7280;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.skills-mode-item.active .mode-radio {
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.radio-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
background: #f97316;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.mode-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.mode-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #f3f4f6;
|
||
}
|
||
|
||
.mode-desc {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
/* 子下拉框箭头 */
|
||
.sub-arrow {
|
||
margin-left: auto;
|
||
color: #9ca3af;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.skills-mode-item:hover .sub-arrow {
|
||
color: #f97316;
|
||
}
|
||
|
||
/* 子下拉面板 */
|
||
.sub-dropdown-panel {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 100%;
|
||
margin-left: 8px;
|
||
width: 280px;
|
||
background: #374151;
|
||
border: 1px solid #4b5563;
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
||
z-index: 100;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sub-dropdown-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #4b5563;
|
||
background: #4b5563;
|
||
}
|
||
|
||
.sub-dropdown-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #f3f4f6;
|
||
}
|
||
|
||
.sub-dropdown-close {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 6px;
|
||
color: #9ca3af;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.sub-dropdown-close:hover {
|
||
background: #374151;
|
||
color: #f3f4f6;
|
||
}
|
||
|
||
/* 无结果样式 */
|
||
.no-results {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24px;
|
||
color: #6b7280;
|
||
gap: 8px;
|
||
}
|
||
|
||
.no-results-icon {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.selected-tags {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 44px;
|
||
padding: 0 12px;
|
||
background: #374151;
|
||
border: 1px solid #4b5563;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.selected-tags:hover {
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.selected-tags .placeholder {
|
||
color: #9ca3af;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tags-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.selected-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 8px;
|
||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||
color: white;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
border-radius: 4px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.more-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 8px;
|
||
background: #374151;
|
||
color: #d1d5db;
|
||
font-size: 12px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.dropdown-icon {
|
||
color: #9ca3af;
|
||
transition: transform 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.dropdown-icon.rotate-180 {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
/* Dropdown Panel */
|
||
.dropdown-panel {
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
left: 0;
|
||
right: 0;
|
||
background: #374151;
|
||
border: 1px solid #4b5563;
|
||
border-radius: 12px;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||
z-index: 100;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Search Box */
|
||
.search-box {
|
||
position: relative;
|
||
padding: 12px;
|
||
border-bottom: 1px solid #4b5563;
|
||
}
|
||
|
||
.search-icon {
|
||
position: absolute;
|
||
left: 24px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
padding: 8px 12px 8px 36px;
|
||
background: #111827;
|
||
border: 1px solid #374151;
|
||
border-radius: 6px;
|
||
color: white;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
|
||
.search-input:focus {
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.search-input::placeholder {
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* Action Bar */
|
||
.action-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 12px;
|
||
background: #111827;
|
||
border-bottom: 1px solid #374151;
|
||
}
|
||
|
||
.checkbox-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.checkbox {
|
||
width: 16px;
|
||
height: 16px;
|
||
accent-color: #f97316;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.checkbox-text {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #f97316;
|
||
}
|
||
|
||
.clear-btn {
|
||
padding: 4px 10px;
|
||
background: transparent;
|
||
border: 1px solid #4b5563;
|
||
border-radius: 4px;
|
||
color: #9ca3af;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.clear-btn:hover {
|
||
background: #374151;
|
||
color: white;
|
||
}
|
||
|
||
/* Options List */
|
||
.options-list {
|
||
max-height: 240px;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
}
|
||
|
||
.options-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.options-list::-webkit-scrollbar-track {
|
||
background: #374151;
|
||
}
|
||
|
||
.options-list::-webkit-scrollbar-thumb {
|
||
background: #4b5563;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.option-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background 0.15s ease;
|
||
}
|
||
|
||
.option-item:hover {
|
||
background: #374151;
|
||
}
|
||
|
||
.option-item .checkbox {
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.option-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.option-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: white;
|
||
}
|
||
|
||
.option-desc {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.no-results {
|
||
padding: 20px;
|
||
text-align: center;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Dropdown Transition */
|
||
.dropdown-enter-active,
|
||
.dropdown-leave-active {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.dropdown-enter-from,
|
||
.dropdown-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-8px);
|
||
}
|
||
|
||
/* Indeterminate checkbox */
|
||
input[type="checkbox"]:indeterminate {
|
||
accent-color: #f97316;
|
||
}
|
||
</style>
|