2026-03-05 10:49:46 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-03-12 15:23:13 +08:00
|
|
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
2026-03-12 17:17:08 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-03-12 23:19:04 +08:00
|
|
|
|
import { formatDate } from '@/utils/format'
|
|
|
|
|
|
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
const API_BASE = 'http://localhost:8082'
|
|
|
|
|
|
|
|
|
|
|
|
// Skills 接口
|
|
|
|
|
|
interface Skill {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
skill_name: string
|
|
|
|
|
|
skill_type: string
|
|
|
|
|
|
skill_desc: string
|
|
|
|
|
|
path: string
|
|
|
|
|
|
status: string
|
|
|
|
|
|
}
|
2026-03-05 10:49:46 +08:00
|
|
|
|
|
2026-03-11 16:26:10 +08:00
|
|
|
|
// Agents 数据
|
2026-03-12 17:17:08 +08:00
|
|
|
|
const agents = ref<any[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
// 获取智能体列表
|
|
|
|
|
|
const fetchAgents = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('http://localhost:8082/api/agent/list')
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch agents')
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
|
|
|
|
|
|
// 转换后端数据为前端格式
|
|
|
|
|
|
agents.value = (data.agents || []).map((agent: any) => ({
|
|
|
|
|
|
id: agent.id,
|
|
|
|
|
|
name: agent.name,
|
|
|
|
|
|
avatar: agent.avatar || '🤖',
|
|
|
|
|
|
description: agent.description || '',
|
|
|
|
|
|
accentColor: '#f97316',
|
|
|
|
|
|
gradient: 'from-orange-500/20 to-amber-500/20',
|
2026-03-12 23:19:04 +08:00
|
|
|
|
status: agent.is_active ? 'active' : 'inactive',
|
|
|
|
|
|
skills: agent.skills?.length > 0 ? agent.skills.join(', ') : 'None',
|
|
|
|
|
|
model: agent.model_name || 'None',
|
|
|
|
|
|
createdAt: agent.created_at ? formatDate(agent.created_at, 'YYYY/MM/DD HH:mm') : formatDate(new Date(), 'YYYY/MM/DD HH:mm'),
|
2026-03-12 17:17:08 +08:00
|
|
|
|
}))
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch agents:', error)
|
|
|
|
|
|
// 保持空数组,不使用假数据
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 16:26:10 +08:00
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
// 创建智能体弹窗状态
|
|
|
|
|
|
const showCreateModal = ref(false)
|
|
|
|
|
|
const isCreating = ref(false)
|
|
|
|
|
|
const newAgent = ref({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
description: '',
|
2026-03-12 15:23:13 +08:00
|
|
|
|
skillsMode: 'all' as 'all' | 'include' | 'exclude',
|
|
|
|
|
|
selectedSkills: [] as string[],
|
2026-03-11 14:26:25 +08:00
|
|
|
|
knowledge: '',
|
|
|
|
|
|
prompt: '',
|
2026-03-12 10:49:44 +08:00
|
|
|
|
avatar: '🤖',
|
2026-03-12 23:19:04 +08:00
|
|
|
|
modelId: '',
|
2026-03-11 14:26:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理技能模式点击(用于模板)
|
2026-03-12 17:17:08 +08:00
|
|
|
|
const handleSkillsModeClick = (mode: string) => {
|
|
|
|
|
|
if (mode === 'all' || mode === 'include' || mode === 'exclude') {
|
|
|
|
|
|
selectSkillsMode(mode)
|
|
|
|
|
|
}
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 23:19:04 +08:00
|
|
|
|
// 处理编辑弹窗中的技能模式点击
|
|
|
|
|
|
const handleSkillsModeClickEdit = (mode: string) => {
|
|
|
|
|
|
if (mode === 'all' || mode === 'include' || mode === 'exclude') {
|
|
|
|
|
|
editingAgent.value.skillsMode = mode
|
|
|
|
|
|
if (mode === 'all') {
|
|
|
|
|
|
editingAgent.value.selectedSkills = []
|
|
|
|
|
|
}
|
|
|
|
|
|
showSkillsDropdown.value = false
|
|
|
|
|
|
if (mode === 'include' || mode === 'exclude') {
|
|
|
|
|
|
showSubSkillsDropdown.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// 切换子下拉框
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:49:44 +08:00
|
|
|
|
// 头像选项
|
|
|
|
|
|
const avatarOptions = [
|
|
|
|
|
|
'🤖', '🧠', '💻', '📊', '🔬', '🎧', '✨', '💬', '🔮', '🌙',
|
|
|
|
|
|
'🐉', '☁️', '🎨', '🎯', '🚀', '⚡', '🔥', '💡', '🎭', '🎪'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// Skills 列表
|
|
|
|
|
|
const skillsList = ref<Skill[]>([])
|
|
|
|
|
|
const skillsLoading = ref(false)
|
|
|
|
|
|
|
2026-03-12 23:19:04 +08:00
|
|
|
|
// Models 列表
|
|
|
|
|
|
interface ModelOption {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
provider: string
|
|
|
|
|
|
model: string
|
|
|
|
|
|
}
|
|
|
|
|
|
const modelsList = ref<ModelOption[]>([])
|
|
|
|
|
|
const modelsLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 models 列表
|
|
|
|
|
|
const fetchModels = async () => {
|
|
|
|
|
|
modelsLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/model/list`)
|
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
|
if (result.list) {
|
|
|
|
|
|
modelsList.value = result.list.map((m: any) => ({
|
|
|
|
|
|
id: m.id,
|
|
|
|
|
|
name: m.name,
|
|
|
|
|
|
provider: m.provider,
|
|
|
|
|
|
model: m.model
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch models:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modelsLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// 获取 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,
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
2026-03-11 14:26:25 +08:00
|
|
|
|
|
|
|
|
|
|
// Knowledge 选项
|
|
|
|
|
|
const knowledgeOptions = [
|
|
|
|
|
|
{ value: 'general', label: 'General Knowledge' },
|
|
|
|
|
|
{ value: 'codebase', label: 'Codebase' },
|
|
|
|
|
|
{ value: 'docs', label: 'Documentation' },
|
|
|
|
|
|
{ value: 'api', label: 'API Reference' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 17:17:08 +08:00
|
|
|
|
// 页面加载时获取 skills 和 agents
|
2026-03-12 15:23:13 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchSkills()
|
2026-03-12 23:19:04 +08:00
|
|
|
|
fetchModels()
|
2026-03-12 17:17:08 +08:00
|
|
|
|
fetchAgents()
|
2026-03-12 15:23:13 +08:00
|
|
|
|
document.addEventListener('click', handleClickOutside)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
document.removeEventListener('click', handleClickOutside)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
// 打开创建弹窗
|
|
|
|
|
|
const openCreateModal = () => {
|
2026-03-12 15:23:13 +08:00
|
|
|
|
newAgent.value = {
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
skillsMode: 'all' as const,
|
|
|
|
|
|
selectedSkills: [],
|
|
|
|
|
|
knowledge: '',
|
|
|
|
|
|
prompt: '',
|
2026-03-12 23:19:04 +08:00
|
|
|
|
avatar: '🤖',
|
|
|
|
|
|
modelId: ''
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
2026-03-11 14:26:25 +08:00
|
|
|
|
showCreateModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建智能体
|
|
|
|
|
|
const createAgent = async () => {
|
2026-03-12 23:19:04 +08:00
|
|
|
|
if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0)) {
|
2026-03-11 14:26:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 23:19:04 +08:00
|
|
|
|
// 获取选中的模型信息
|
|
|
|
|
|
const selectedModel = modelsList.value.find(m => m.id === newAgent.value.modelId)
|
|
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
isCreating.value = true
|
|
|
|
|
|
try {
|
2026-03-12 17:17:08 +08:00
|
|
|
|
// 调用后端 API 创建智能体
|
|
|
|
|
|
const response = await fetch('http://localhost:8082/api/agent/create', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
name: newAgent.value.name,
|
|
|
|
|
|
description: newAgent.value.description,
|
|
|
|
|
|
avatar: newAgent.value.avatar,
|
2026-03-12 23:19:04 +08:00
|
|
|
|
skillsMode: newAgent.value.skillsMode,
|
2026-03-12 17:17:08 +08:00
|
|
|
|
skills: newAgent.value.selectedSkills,
|
|
|
|
|
|
prompt: newAgent.value.prompt,
|
2026-03-12 23:19:04 +08:00
|
|
|
|
model_provider: selectedModel?.provider || '',
|
|
|
|
|
|
model_name: selectedModel?.model || '',
|
2026-03-12 17:17:08 +08:00
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Failed to create agent: ${response.status}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// 处理 skills 标签
|
|
|
|
|
|
const skillsLabels = newAgent.value.selectedSkills.map((id: string) => getSkillLabel(id)).join(', ')
|
|
|
|
|
|
|
2026-03-12 17:17:08 +08:00
|
|
|
|
// 添加到列表
|
2026-03-11 14:26:25 +08:00
|
|
|
|
agents.value.unshift({
|
2026-03-12 17:17:08 +08:00
|
|
|
|
id: result.agent_id,
|
2026-03-11 14:26:25 +08:00
|
|
|
|
name: newAgent.value.name,
|
2026-03-12 10:49:44 +08:00
|
|
|
|
avatar: newAgent.value.avatar,
|
2026-03-11 14:26:25 +08:00
|
|
|
|
description: newAgent.value.description,
|
|
|
|
|
|
accentColor: '#f97316',
|
|
|
|
|
|
gradient: 'from-orange-500/20 to-amber-500/20',
|
|
|
|
|
|
status: 'stopped',
|
2026-03-12 23:19:04 +08:00
|
|
|
|
skills: skillsLabels || 'None',
|
|
|
|
|
|
knowledge: knowledgeOptions.find(k => k.value === newAgent.value.knowledge)?.label || newAgent.value.knowledge,
|
|
|
|
|
|
createdAt: formatDate(new Date(), 'YYYY/MM/DD HH:mm'),
|
2026-03-11 14:26:25 +08:00
|
|
|
|
})
|
2026-03-12 17:17:08 +08:00
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
showCreateModal.value = false
|
2026-03-12 17:17:08 +08:00
|
|
|
|
ElMessage.success('Agent created successfully')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to create agent:', error)
|
|
|
|
|
|
ElMessage.error('Failed to create agent')
|
2026-03-11 14:26:25 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
isCreating.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
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()) ||
|
2026-03-12 23:19:04 +08:00
|
|
|
|
agent.skills.toLowerCase().includes(searchQuery.value.toLowerCase())
|
2026-03-10 16:09:09 +08:00
|
|
|
|
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
|
|
|
|
|
|
return matchSearch && matchStatus
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 统计数据
|
|
|
|
|
|
const stats = computed(() => ({
|
|
|
|
|
|
total: agents.value.length,
|
2026-03-12 23:19:04 +08:00
|
|
|
|
running: agents.value.filter(a => a.status === 'active').length,
|
|
|
|
|
|
stopped: agents.value.filter(a => a.status === 'inactive').length,
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
// 状态颜色
|
|
|
|
|
|
const statusClass = (status: string) => {
|
|
|
|
|
|
switch (status) {
|
2026-03-12 23:19:04 +08:00
|
|
|
|
case 'active': return 'bg-green-500'
|
|
|
|
|
|
case 'inactive': return 'bg-gray-500'
|
2026-03-10 16:09:09 +08:00
|
|
|
|
default: return 'bg-gray-500'
|
2026-03-05 10:49:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 15:42:21 +08:00
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
// 切换状态
|
2026-03-12 23:19:04 +08:00
|
|
|
|
const toggleStatus = async (agent: any) => {
|
|
|
|
|
|
const newStatus = agent.status === 'active' ? false : true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/agent/${agent.id}/status`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ is_active: newStatus })
|
|
|
|
|
|
})
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
agent.status = newStatus ? 'active' : 'inactive'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error('Failed to update status')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to update status:', error)
|
|
|
|
|
|
ElMessage.error('Failed to update status')
|
|
|
|
|
|
}
|
2026-03-10 15:42:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
// 删除 Agent
|
2026-03-12 23:19:04 +08:00
|
|
|
|
const deleteAgent = async (id: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/agent/${id}`, {
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
})
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
agents.value = agents.value.filter(a => a.id !== id)
|
|
|
|
|
|
ElMessage.success('Agent deleted successfully')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error('Failed to delete agent')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to delete agent:', error)
|
|
|
|
|
|
ElMessage.error('Failed to delete agent')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑弹窗状态
|
|
|
|
|
|
const showEditModal = ref(false)
|
|
|
|
|
|
const isEditing = ref(false)
|
|
|
|
|
|
const editingAgent = ref({
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
avatar: '🤖',
|
|
|
|
|
|
skillsMode: 'all' as 'all' | 'include' | 'exclude',
|
|
|
|
|
|
selectedSkills: [] as string[],
|
|
|
|
|
|
modelId: '',
|
|
|
|
|
|
prompt: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 打开编辑弹窗
|
|
|
|
|
|
const openEdit = (agent: any) => {
|
|
|
|
|
|
// 解析 skills
|
|
|
|
|
|
let selectedSkills: string[] = []
|
|
|
|
|
|
let skillsMode: 'all' | 'include' | 'exclude' = 'all'
|
|
|
|
|
|
if (agent.skills === '*') {
|
|
|
|
|
|
skillsMode = 'all'
|
|
|
|
|
|
selectedSkills = []
|
|
|
|
|
|
} else if (agent.skills && agent.skills !== 'None') {
|
|
|
|
|
|
skillsMode = 'include'
|
|
|
|
|
|
selectedSkills = agent.skills.split(',').map((s: string) => s.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找对应的 modelId
|
|
|
|
|
|
const model = modelsList.value.find(m => m.name === agent.model)
|
|
|
|
|
|
|
|
|
|
|
|
editingAgent.value = {
|
|
|
|
|
|
id: agent.id,
|
|
|
|
|
|
name: agent.name,
|
|
|
|
|
|
description: agent.description || '',
|
|
|
|
|
|
avatar: agent.avatar || '🤖',
|
|
|
|
|
|
skillsMode,
|
|
|
|
|
|
selectedSkills,
|
|
|
|
|
|
modelId: model?.id || '',
|
|
|
|
|
|
prompt: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
showEditModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存编辑
|
|
|
|
|
|
const saveEdit = async () => {
|
|
|
|
|
|
if (!editingAgent.value.name) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 skills
|
|
|
|
|
|
let skills: string[] = []
|
|
|
|
|
|
if (editingAgent.value.skillsMode === 'all') {
|
|
|
|
|
|
skills = ['*']
|
|
|
|
|
|
} else {
|
|
|
|
|
|
skills = editingAgent.value.selectedSkills
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取选中的模型信息
|
|
|
|
|
|
const selectedModel = modelsList.value.find(m => m.id === editingAgent.value.modelId)
|
|
|
|
|
|
|
|
|
|
|
|
isEditing.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/agent/${editingAgent.value.id}`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
name: editingAgent.value.name,
|
|
|
|
|
|
description: editingAgent.value.description,
|
|
|
|
|
|
skills: skills,
|
|
|
|
|
|
role_description: editingAgent.value.prompt,
|
|
|
|
|
|
model_provider: selectedModel?.provider || '',
|
|
|
|
|
|
model_name: selectedModel?.name || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
// 更新本地数据
|
|
|
|
|
|
const agent = agents.value.find(a => a.id === editingAgent.value.id)
|
|
|
|
|
|
if (agent) {
|
|
|
|
|
|
agent.name = editingAgent.value.name
|
|
|
|
|
|
agent.description = editingAgent.value.description
|
|
|
|
|
|
// 更新 skills 显示
|
|
|
|
|
|
if (editingAgent.value.skillsMode === 'all') {
|
|
|
|
|
|
agent.skills = '*'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
agent.skills = editingAgent.value.selectedSkills.join(', ')
|
|
|
|
|
|
}
|
|
|
|
|
|
// 更新 model 显示
|
|
|
|
|
|
agent.model = selectedModel?.name || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
showEditModal.value = false
|
|
|
|
|
|
ElMessage.success('Agent updated successfully')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error('Failed to update agent')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to update agent:', error)
|
|
|
|
|
|
ElMessage.error('Failed to update agent')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
isEditing.value = false
|
|
|
|
|
|
}
|
2026-03-10 15:42:21 +08:00
|
|
|
|
}
|
2026-03-05 10:49:46 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<!-- 主内容区域 -->
|
|
|
|
|
|
<div class="p-6 min-h-screen">
|
|
|
|
|
|
<!-- 顶部导航 -->
|
|
|
|
|
|
<div class="flex justify-between items-center mb-6">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
2026-03-10 17:39:10 +08:00
|
|
|
|
<i class="fa-solid fa-robot text-orange-500"></i>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<span class="font-medium">Agents</span>
|
|
|
|
|
|
</div>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<button @click="openCreateModal" class="btn-primary">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<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"
|
2026-03-12 23:19:04 +08:00
|
|
|
|
placeholder="Search agents by name or skills..."
|
2026-03-10 16:09:09 +08:00
|
|
|
|
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" />
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<el-option label="Active" value="active" />
|
|
|
|
|
|
<el-option label="Inactive" value="inactive" />
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
2026-03-05 10:49:46 +08:00
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<!-- Agents 列表 -->
|
|
|
|
|
|
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
2026-03-12 17:17:08 +08:00
|
|
|
|
<table v-if="filteredAgents.length > 0" class="w-full">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<thead class="bg-dark-600">
|
|
|
|
|
|
<tr>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<th class="text-left px-4 py-3 text-sm font-medium text-gray-400">Agent Name</th>
|
|
|
|
|
|
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Skills</th>
|
|
|
|
|
|
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Model</th>
|
|
|
|
|
|
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Status</th>
|
|
|
|
|
|
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Created</th>
|
|
|
|
|
|
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Actions</th>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-for="agent in filteredAgents" :key="agent.id" class="table-row">
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<td class="px-4 py-3">
|
2026-03-10 15:42:21 +08:00
|
|
|
|
<div class="flex items-center gap-3">
|
2026-03-05 10:49:46 +08:00
|
|
|
|
<div
|
2026-03-10 16:09:09 +08:00
|
|
|
|
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg"
|
2026-03-10 15:42:21 +08:00
|
|
|
|
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
|
2026-03-05 10:49:46 +08:00
|
|
|
|
>
|
2026-03-10 15:42:21 +08:00
|
|
|
|
{{ agent.avatar }}
|
2026-03-05 10:49:46 +08:00
|
|
|
|
</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium text-white">{{ agent.name }}</div>
|
|
|
|
|
|
<div class="text-xs text-gray-500">{{ agent.description }}</div>
|
2026-03-05 10:49:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</td>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<td class="px-4 py-3 text-center">
|
|
|
|
|
|
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.skills }}</span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</td>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<td class="px-4 py-3 text-center">
|
|
|
|
|
|
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.model }}</span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</td>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<td class="px-4 py-3 text-center">
|
|
|
|
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="agent.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="agent.status === 'active' ? 'bg-green-500' : 'bg-gray-400'"></span>
|
|
|
|
|
|
<span class="capitalize">{{ agent.status }}</span>
|
|
|
|
|
|
</span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</td>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<td class="px-4 py-3 text-center text-gray-400 text-sm">{{ agent.createdAt }}</td>
|
|
|
|
|
|
<td class="px-4 py-3">
|
|
|
|
|
|
<div class="flex items-center justify-center gap-2">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<button
|
|
|
|
|
|
@click="toggleStatus(agent)"
|
|
|
|
|
|
class="btn-icon"
|
2026-03-12 23:19:04 +08:00
|
|
|
|
:title="agent.status === 'active' ? 'Deactivate' : 'Activate'"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<Pause v-if="agent.status === 'active'" class="w-4 h-4 text-gray-400 hover:text-yellow-400" />
|
|
|
|
|
|
<Play v-else class="w-4 h-4 text-gray-400 hover:text-green-400" />
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</button>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<button @click="openEdit(agent)" class="btn-icon" title="Edit">
|
|
|
|
|
|
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="deleteAgent(agent.id)"
|
|
|
|
|
|
class="btn-icon"
|
|
|
|
|
|
title="Delete"
|
|
|
|
|
|
>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
2026-03-12 17:17:08 +08:00
|
|
|
|
<div v-if="filteredAgents.length === 0" class="empty-box">
|
|
|
|
|
|
<div class="empty-icon">
|
|
|
|
|
|
<i class="fa-solid fa-robot"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="empty-text">No agents found</p>
|
|
|
|
|
|
<p class="empty-tip">Click "New Agent" to create one</p>
|
2026-03-05 10:49:46 +08:00
|
|
|
|
</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
2026-03-05 10:49:46 +08:00
|
|
|
|
</div>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 创建智能体弹窗 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-12 10:49:44 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-12 23:19:04 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Preferred Model</label>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="newAgent.modelId"
|
|
|
|
|
|
placeholder="Select a model..."
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="model in modelsList"
|
|
|
|
|
|
:key="model.id"
|
|
|
|
|
|
:label="model.name"
|
|
|
|
|
|
:value="model.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<span>{{ model.name }}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500">{{ model.provider }} - {{ model.model }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
|
2026-03-12 15:23:13 +08:00
|
|
|
|
<!-- 技能模式选择 - 两级下拉框 -->
|
|
|
|
|
|
<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 }"
|
2026-03-12 17:17:08 +08:00
|
|
|
|
@click="handleSkillsModeClick(option.value)"
|
2026-03-12 15:23:13 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
</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"
|
2026-03-12 23:19:04 +08:00
|
|
|
|
:disabled="isCreating || !newAgent.name || (newAgent.skillsMode !== 'all' && newAgent.selectedSkills.length === 0)"
|
2026-03-11 14:26:25 +08:00
|
|
|
|
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>
|
2026-03-12 23:19:04 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 编辑智能体弹窗 -->
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<div v-if="showEditModal" 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">Edit Agent</h3>
|
|
|
|
|
|
<button @click="showEditModal = 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="editingAgent.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="editingAgent.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="editingAgent.avatar = avatar"
|
|
|
|
|
|
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg transition-all"
|
|
|
|
|
|
:class="editingAgent.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">Preferred Model</label>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="editingAgent.modelId"
|
|
|
|
|
|
placeholder="Select a model..."
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="model in modelsList"
|
|
|
|
|
|
:key="model.id"
|
|
|
|
|
|
:label="model.name"
|
|
|
|
|
|
:value="model.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<span>{{ model.name }}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500">{{ model.provider }} - {{ model.model }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
|
|
|
|
|
|
<!-- 技能模式选择 -->
|
|
|
|
|
|
<div class="skills-selector">
|
|
|
|
|
|
<div class="selected-tags" @click="toggleSkillsDropdown">
|
|
|
|
|
|
<div v-if="editingAgent.skillsMode === 'all'" class="placeholder">
|
|
|
|
|
|
All Skills
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="tags-container">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="skillId in editingAgent.selectedSkills.slice(0, 3)"
|
|
|
|
|
|
:key="skillId"
|
|
|
|
|
|
class="selected-tag"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ getSkillLabel(skillId) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="editingAgent.selectedSkills.length > 3" class="more-tag">
|
|
|
|
|
|
+{{ editingAgent.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': editingAgent.skillsMode === option.value }"
|
|
|
|
|
|
@click="handleSkillsModeClickEdit(option.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="mode-radio">
|
|
|
|
|
|
<div v-if="editingAgent.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">
|
|
|
|
|
|
{{ editingAgent.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="editingAgent.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="editingAgent.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">Prompt</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="editingAgent.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="showEditModal = false"
|
|
|
|
|
|
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
Cancel
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="saveEdit"
|
|
|
|
|
|
:disabled="isEditing || !editingAgent.name"
|
|
|
|
|
|
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="isEditing" class="fa-solid fa-circle-notch fa-spin"></i>
|
|
|
|
|
|
{{ isEditing ? 'Saving...' : 'Save' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Teleport>
|
2026-03-05 10:49:46 +08:00
|
|
|
|
</template>
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* Skills Selector Styles */
|
|
|
|
|
|
.skills-selector {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Skills Mode Options */
|
|
|
|
|
|
.skills-mode-options {
|
|
|
|
|
|
padding: 8px;
|
2026-03-12 17:17:08 +08:00
|
|
|
|
border-bottom: 1px solid #2a2c36;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2026-03-12 17:17:08 +08:00
|
|
|
|
background: #1a1c25;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-03-12 17:17:08 +08:00
|
|
|
|
background: #171922;
|
|
|
|
|
|
border: 1px solid #2a2c36;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
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;
|
2026-03-12 17:17:08 +08:00
|
|
|
|
border-bottom: 1px solid #2a2c36;
|
|
|
|
|
|
background: #1a1c25;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-03-12 17:17:08 +08:00
|
|
|
|
background: #171922;
|
|
|
|
|
|
border: 1px solid #2a2c36;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
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;
|
2026-03-12 17:17:08 +08:00
|
|
|
|
background: #171922;
|
|
|
|
|
|
border: 1px solid #2a2c36;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
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;
|
2026-03-12 17:17:08 +08:00
|
|
|
|
border-bottom: 1px solid #2a2c36;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2026-03-12 17:17:08 +08:00
|
|
|
|
background: #171922;
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-03-12 17:17:08 +08:00
|
|
|
|
|
|
|
|
|
|
/* Empty Box */
|
|
|
|
|
|
.empty-box {
|
|
|
|
|
|
min-height: 340px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-icon {
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
height: 100px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: linear-gradient(135deg, #1f2937, #111827);
|
|
|
|
|
|
border-radius: 24px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-icon i {
|
|
|
|
|
|
font-size: 40px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-text {
|
|
|
|
|
|
color: #d1d5db;
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-tip {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
2026-03-12 15:23:13 +08:00
|
|
|
|
</style>
|