feat: 更新前端页面

- Agents.vue: 大幅更新agent管理界面
- App.vue: 更新应用布局
- 各页面: 更新Account、Database、Knowledge、Memory、Script、Skill、Tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 23:19:04 +08:00
parent 5dc2e403e9
commit a07cc4498d
10 changed files with 489 additions and 112 deletions

View File

@@ -16,26 +16,8 @@ const showSidebar = computed(() => route.path !== '/login' && route.path !== '/'
<!-- 右侧内容区 -->
<main :class="showSidebar ? 'ml-64' : ''" class="flex-1 min-h-screen">
<router-view class="page-content" />
<router-view />
</main>
</div>
</ElConfigProvider>
</template>
<style>
/* 页面进入动画 */
.page-content {
animation: page-enter 0.3s ease-out;
}
@keyframes page-enter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuth } from '@/composables/useAuth'
import { formatDate } from '@/utils/format'
import './database/database.css'
const { getCurrentUser, logout } = useAuth()
@@ -261,7 +262,7 @@ const statusClass = (status: string) => {
</div>
<div class="mt-4 text-sm text-gray-500">
Member since: {{ currentUser?.created_at ? new Date(currentUser.created_at).toLocaleDateString() : 'N/A' }}
Member since: {{ currentUser?.created_at ? formatDate(currentUser.created_at, 'YYYY/MM/DD HH:mm') : 'N/A' }}
</div>
</div>
</div>
@@ -341,7 +342,7 @@ const statusClass = (status: string) => {
<span class="capitalize text-sm">{{ user.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ user.createdAt }}</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ formatDate(user.createdAt, 'YYYY/MM/DD HH:mm') }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/format'
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
const API_BASE = 'http://localhost:8082'
@@ -33,11 +35,10 @@ const fetchAgents = async () => {
description: agent.description || '',
accentColor: '#f97316',
gradient: 'from-orange-500/20 to-amber-500/20',
status: 'stopped' as const,
framework: agent.skills?.length > 0 ? agent.skills.join(', ') : 'None',
model: agent.model_name || 'claude-sonnet-4-20250514',
mcpServers: 0,
createdAt: new Date().toISOString().split('T')[0],
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'),
}))
} catch (error) {
console.error('Failed to fetch agents:', error)
@@ -56,6 +57,7 @@ const newAgent = ref({
knowledge: '',
prompt: '',
avatar: '🤖',
modelId: '',
})
// Skills 选择器状态
@@ -94,6 +96,20 @@ const handleSkillsModeClick = (mode: string) => {
}
}
// 处理编辑弹窗中的技能模式点击
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
}
}
}
// 切换子下拉框
const toggleSubSkillsDropdown = () => {
showSubSkillsDropdown.value = !showSubSkillsDropdown.value
@@ -137,6 +153,37 @@ const avatarOptions = [
const skillsList = ref<Skill[]>([])
const skillsLoading = ref(false)
// 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
}
}
// 获取 skills 列表
const fetchSkills = async () => {
skillsLoading.value = true
@@ -242,6 +289,7 @@ const handleClickOutside = (e: MouseEvent) => {
// 页面加载时获取 skills 和 agents
onMounted(() => {
fetchSkills()
fetchModels()
fetchAgents()
document.addEventListener('click', handleClickOutside)
})
@@ -259,17 +307,21 @@ const openCreateModal = () => {
selectedSkills: [],
knowledge: '',
prompt: '',
avatar: '🤖'
avatar: '🤖',
modelId: ''
}
showCreateModal.value = true
}
// 创建智能体
const createAgent = async () => {
if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0) || !newAgent.value.knowledge) {
if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0)) {
return
}
// 获取选中的模型信息
const selectedModel = modelsList.value.find(m => m.id === newAgent.value.modelId)
isCreating.value = true
try {
// 调用后端 API 创建智能体
@@ -282,10 +334,11 @@ const createAgent = async () => {
name: newAgent.value.name,
description: newAgent.value.description,
avatar: newAgent.value.avatar,
skills_mode: newAgent.value.skillsMode,
skillsMode: newAgent.value.skillsMode,
skills: newAgent.value.selectedSkills,
knowledge: newAgent.value.knowledge,
prompt: newAgent.value.prompt,
model_provider: selectedModel?.provider || '',
model_name: selectedModel?.model || '',
}),
})
@@ -307,10 +360,9 @@ const createAgent = async () => {
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],
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'),
})
showCreateModal.value = false
@@ -330,7 +382,7 @@ const filterStatus = ref('all')
const filteredAgents = computed(() => {
return agents.value.filter(agent => {
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
agent.framework.toLowerCase().includes(searchQuery.value.toLowerCase())
agent.skills.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
return matchSearch && matchStatus
})
@@ -339,27 +391,158 @@ const filteredAgents = computed(() => {
// 统计数据
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,
running: agents.value.filter(a => a.status === 'active').length,
stopped: agents.value.filter(a => a.status === 'inactive').length,
}))
// 状态颜色
const statusClass = (status: string) => {
switch (status) {
case 'running': return 'bg-primary-success'
case 'stopped': return 'bg-gray-500'
case 'active': return 'bg-green-500'
case 'inactive': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
// 切换状态
const toggleStatus = (agent: any) => {
agent.status = agent.status === 'running' ? 'stopped' : 'running'
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')
}
}
// 删除 Agent
const deleteAgent = (id: number) => {
agents.value = agents.value.filter(a => a.id !== id)
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
}
}
</script>
@@ -385,14 +568,14 @@ const deleteAgent = (id: number) => {
<input
v-model="searchQuery"
type="text"
placeholder="Search agents by name or framework..."
placeholder="Search agents by name or skills..."
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-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
</el-select>
</div>
@@ -401,18 +584,17 @@ const deleteAgent = (id: number) => {
<table v-if="filteredAgents.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">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>
<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>
</tr>
</thead>
<tbody>
<tr v-for="agent in filteredAgents" :key="agent.id" class="table-row">
<td class="px-5 py-4">
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg"
@@ -426,41 +608,38 @@ const deleteAgent = (id: number) => {
</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 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>
</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 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>
</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 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>
</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">
<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">
<button
@click="toggleStatus(agent)"
class="btn-icon"
:title="agent.status === 'running' ? 'Stop' : 'Start'"
:title="agent.status === 'active' ? 'Deactivate' : 'Activate'"
>
<i :class="['fa-solid', agent.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400']"></i>
<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" />
</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 @click="openEdit(agent)" class="btn-icon" title="Edit">
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
</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>
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
</button>
</div>
</td>
@@ -527,6 +706,29 @@ const deleteAgent = (id: number) => {
</div>
</div>
<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>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
<!-- 技能模式选择 - 两级下拉框 -->
@@ -663,13 +865,6 @@ const deleteAgent = (id: number) => {
</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
@@ -690,7 +885,7 @@ const deleteAgent = (id: number) => {
</button>
<button
@click="createAgent"
:disabled="isCreating || !newAgent.name || (newAgent.skillsMode !== 'all' && newAgent.selectedSkills.length === 0) || !newAgent.knowledge"
:disabled="isCreating || !newAgent.name || (newAgent.skillsMode !== 'all' && newAgent.selectedSkills.length === 0)"
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>
@@ -700,6 +895,206 @@ const deleteAgent = (id: number) => {
</div>
</div>
</Teleport>
<!-- 编辑智能体弹窗 -->
<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>
</template>
<style scoped>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useDatabase } from './database/useDatabase'
import { formatDate } from '@/utils/format'
import './database/database.css'
const {
@@ -111,7 +112,7 @@ const {
<td class="px-5 py-4 text-center">
<span class="text-primary-cyan">{{ db.table_count }}</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ db.created_at?.split('T')[0] }}</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ formatDate(db.created_at, 'YYYY/MM/DD HH:mm') }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
<button

View File

@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge'
import { formatDate } from '@/utils/format'
import VueOfficeDocx from '@vue-office/docx'
import VueOfficeExcel from '@vue-office/excel'
import Papa from 'papaparse'
@@ -382,13 +383,6 @@ const formatFileSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
// 辅助函数:格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN')
}
// 辅助函数:获取状态图标
const getStatusIcon = (status: string) => {
switch (status) {
@@ -776,7 +770,7 @@ const deleteDocument = async (docId: string) => {
</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">
{{ kb.created_at?.split('T')[0] }}
{{ formatDate(kb.created_at, 'YYYY/MM/DD HH:mm') }}
</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
@@ -1166,7 +1160,7 @@ const deleteDocument = async (docId: string) => {
<div class="file-item-name">{{ doc.name }}</div>
<div class="file-item-meta">
<span>{{ formatFileSize(doc.file_size) }}</span>
<span>{{ formatDate(doc.uploaded_at) }}</span>
<span>{{ formatDate(doc.uploaded_at, 'YYYY/MM/DD HH:mm') }}</span>
</div>
</div>
<div class="file-item-status" :class="doc.status">
@@ -1192,7 +1186,7 @@ const deleteDocument = async (docId: string) => {
</div>
<div class="preview-header-info">
<span class="info-tag">{{ formatFileSize(selectedDocument?.file_size) }}</span>
<span class="info-tag">{{ formatDate(selectedDocument?.uploaded_at) }}</span>
<span class="info-tag">{{ formatDate(selectedDocument?.uploaded_at, 'YYYY/MM/DD HH:mm') }}</span>
<span class="info-tag" :class="selectedDocument?.status">{{ selectedDocument?.status === 'parsed' ? 'Parsed' : selectedDocument?.status === 'parsing' ? 'Parsing' : selectedDocument?.status === 'failed' ? 'Failed' : 'Pending' }}</span>
<span class="info-tag">{{ selectedDocument?.chunk_count || 0 }} chunks</span>
</div>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { formatDate } from '@/utils/format'
import './database/database.css'
interface MemoryItem {
@@ -22,7 +23,7 @@ const memories = ref<MemoryItem[]>([
subject: '外部信息获取规范',
attribute: '工具调用原则',
score: 0.95,
createdAt: '3/10 15:35',
createdAt: '2026-03-10T15:35',
selected: false
},
{
@@ -32,7 +33,7 @@ const memories = ref<MemoryItem[]>([
subject: '工具调用',
attribute: '错误教训',
score: 0.95,
createdAt: '3/10 12:34',
createdAt: '2026-03-10T12:34',
selected: false
},
{
@@ -42,7 +43,7 @@ const memories = ref<MemoryItem[]>([
subject: '任务执行效率优化',
attribute: '迭代策略原则',
score: 0.92,
createdAt: '3/10 15:35',
createdAt: '2026-03-10T15:35',
selected: false
},
{
@@ -52,7 +53,7 @@ const memories = ref<MemoryItem[]>([
subject: '执行路径监控',
attribute: '质量控制机制',
score: 0.88,
createdAt: '3/10 15:35',
createdAt: '2026-03-10T15:35',
selected: false
},
{
@@ -62,7 +63,7 @@ const memories = ref<MemoryItem[]>([
subject: '迭代效率',
attribute: '错误教训',
score: 0.85,
createdAt: '3/10 12:34',
createdAt: '2026-03-10T12:34',
selected: false
},
{
@@ -72,7 +73,7 @@ const memories = ref<MemoryItem[]>([
subject: '代码生成策略',
attribute: '错误教训',
score: 0.80,
createdAt: '3/10 12:34',
createdAt: '2026-03-10T12:34',
selected: false
},
])
@@ -252,7 +253,7 @@ const getScoreColor = (score: number) => {
<td class="px-5 py-4">
<span :class="['font-medium', getScoreColor(memory.score)]">{{ memory.score }}</span>
</td>
<td class="px-5 py-4 text-sm text-gray-400">{{ memory.createdAt }}</td>
<td class="px-5 py-4 text-sm text-gray-400">{{ formatDate(memory.createdAt, 'YYYY/MM/DD HH:mm') }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
<button

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { formatDate } from '@/utils/format'
import * as monaco from 'monaco-editor'
interface Script {
@@ -204,7 +205,7 @@ const saveNewScript = () => {
description: newScriptForm.value.description,
code: newScriptForm.value.code,
status: 'stopped',
createdAt: new Date().toISOString().split('T')[0],
createdAt: formatDate(new Date(), 'YYYY/MM/DD HH:mm'),
})
isCreatingCode.value = false
}
@@ -269,7 +270,7 @@ const saveNewScript = () => {
<span class="capitalize text-sm">{{ script.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ script.createdAt }}</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ formatDate(script.createdAt, 'YYYY/MM/DD HH:mm') }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useSkills } from './skill/skill'
import { formatDate } from '@/utils/format'
import { Edit, Trash2, Wand2, Plus, Search, X, FolderInput, Play, Pause } from 'lucide-vue-next'
import '@/views/database/database.css'
@@ -146,20 +147,20 @@ onMounted(() => {
<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="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 class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="skill.status === 1 ? '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 === 1 ? 'bg-green-500' : 'bg-gray-400'"></span>
<span class="capitalize">{{ skill.status === 1 ? 'active' : 'inactive' }}</span>
</span>
</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 text-center text-gray-400 text-sm">{{ skill.created_at ? formatDate(skill.created_at, 'YYYY/MM/DD HH:mm') : '-' }}</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 === 'active' ? 'Pause' : 'Start'"
:title="skill.status === 1 ? 'Pause' : 'Start'"
>
<Pause v-if="skill.status === 'active'" class="w-4 h-4 text-gray-400 hover:text-yellow-400" />
<Pause v-if="skill.status === 1" 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" />
</button>
<button @click="openEdit(skill)" class="btn-icon" title="Edit">

View File

@@ -25,6 +25,7 @@ import {
X,
} from 'lucide-vue-next'
import { useTools } from './tools/useTools'
import { formatDate } from '@/utils/format'
import '@/views/database/database.css'
// 使用工具 composable
@@ -330,7 +331,7 @@ const submitMcp = async () => {
<span class="capitalize">{{ tool.status }}</span>
</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.created_at || '-' }}</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.created_at ? formatDate(tool.created_at, 'YYYY/MM/DD HH:mm') : '-' }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
<!-- 激活/停用按钮 - 所有工具都有 -->

View File

@@ -9,7 +9,7 @@ export interface Skill {
skill_type: string
skill_desc: string
path: string
status: string
status: number
created_at?: string
updated_at?: string
}
@@ -364,7 +364,7 @@ Example 1:
// 切换状态
const toggleStatus = async (skill: Skill) => {
const newStatus = skill.status === 'active' ? 'inactive' : 'active'
const newStatus = skill.status === 1 ? 0 : 1
try {
await updateSkill(skill.id, { status: newStatus })
skill.status = newStatus