Files
X-Agents/web/src/views/MCP.vue
DESKTOP-72TV0V4\caoxiaozhu 3c33f15f82 feat: 优化暗色主题样式,将原生select替换为el-select
- 优化el-select组件的暗色主题样式,包括输入框、下拉菜单、选中状态等
- 在Agents、MCP、ModelAPIs页面中将原生select替换为Element Plus el-select组件
- 提升UI一致性和用户体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:31:36 +08:00

1549 lines
58 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
interface MCPServer {
id: number
name: string
status: 'running' | 'stopped' | 'error'
type: string
port: number
createdAt: string
description: string
}
const mcpServers = ref<MCPServer[]>([
{ id: 1, name: 'linear-demo', status: 'running', type: 'Linear', port: 3001, createdAt: '2025-04-10', description: 'Linear API integration for project management' },
{ id: 2, name: 'google-maps', status: 'running', type: 'Google Maps', port: 3002, createdAt: '2025-04-08', description: 'Google Maps API for location services' },
{ id: 3, name: 'explorer-mcp', status: 'error', type: 'File System', port: 3003, createdAt: '2025-04-05', description: 'File system explorer and editor' },
{ id: 4, name: 'postgres-mcp', status: 'running', type: 'PostgreSQL', port: 3004, createdAt: '2025-04-12', description: 'PostgreSQL database operations' },
{ id: 5, name: 'github-mcp', status: 'stopped', type: 'GitHub', port: 3005, createdAt: '2025-04-11', description: 'GitHub API integration' },
])
const editingServer = ref<MCPServer | null>(null)
const isEditing = ref(false)
const searchQuery = ref('')
const filterStatus = ref<string>('all')
const editForm = ref({
name: '',
type: '',
port: 3000,
description: '',
})
const openEdit = (server: MCPServer) => {
editingServer.value = server
editForm.value = {
name: server.name,
type: server.type,
port: server.port,
description: server.description,
}
isEditing.value = true
}
const saveEdit = () => {
if (editingServer.value) {
const index = mcpServers.value.findIndex(s => s.id === editingServer.value!.id)
if (index !== -1) {
mcpServers.value[index] = {
...mcpServers.value[index],
...editForm.value,
}
}
}
isEditing.value = false
}
const cancelEdit = () => {
isEditing.value = false
editingServer.value = null
}
const toggleStatus = (server: MCPServer) => {
if (server.status === 'running') {
server.status = 'stopped'
} else if (server.status === 'stopped') {
server.status = 'running'
}
}
const deleteServer = (id: number) => {
mcpServers.value = mcpServers.value.filter(s => s.id !== id)
}
const filteredServers = () => {
return mcpServers.value.filter(server => {
const matchSearch = server.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
server.type.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || server.status === filterStatus.value
return matchSearch && matchStatus
})
}
// 新建 Skill
const isCreating = ref(false)
const createStep = ref(1) // 步骤1=基本信息, 2=配置页面
const newSkillForm = ref({
name: '',
type: 'API',
provider: 'Custom',
port: 3000,
description: '',
enabled: true,
model: 'gpt-4o',
})
const availableModels = [
{ name: 'gpt-4o', provider: 'OpenAI', icon: 'fa-openai' },
{ name: 'gpt-4o-mini', provider: 'OpenAI', icon: 'fa-openai' },
{ name: 'claude-3-5-sonnet', provider: 'Anthropic', icon: 'fa-robot' },
{ name: 'gemini-2.0-flash', provider: 'Google', icon: 'fa-google' },
{ name: 'gpt-5', provider: 'OpenAI', icon: 'fa-openai' },
]
const goToStep2 = () => {
if (!newSkillForm.value.name.trim()) return
createStep.value = 2
// 重置图谱状态
isGraphGenerated.value = false
isGeneratingGraph.value = false
}
const goBackToStep1 = () => {
createStep.value = 1
}
// 文件管理相关 - VS Code 风格
interface FileItem {
id: string
name: string
icon: string
content: string
type: 'file'
}
interface FolderItem {
id: string
name: string
icon: string
type: 'folder'
expanded: boolean
children: (FileItem | FolderItem)[]
}
// 默认文件内容
const skillMdContent = `# Skill Configuration
## Overview
This skill is designed to process user queries and generate appropriate responses.
## Data Sources
- user_data.csv
- config.json
## Scripts
- process.py
- transform.js
## Dependencies
- Python 3.8+
- Node.js 16+
`
const fileTree = ref<FolderItem[]>([
{
id: 'folder-1',
name: 'Data',
icon: 'fa-folder',
type: 'folder',
expanded: true,
children: [
{ id: 'file-1', name: 'user_data.csv', icon: 'fa-file', content: 'id,name,email,age\n1,John Doe,john@example.com,28\n2,Jane Smith,jane@example.com,32', type: 'file' },
{ id: 'file-2', name: 'config.json', icon: 'fa-code', content: '{\n "appName": "MyApp",\n "version": "1.0.0",\n "debug": true\n}', type: 'file' },
]
},
{
id: 'folder-2',
name: 'Script',
icon: 'fa-folder',
type: 'folder',
expanded: true,
children: [
{ id: 'file-3', name: 'process.py', icon: 'fa-code', content: 'import pandas as pd\n\ndef process_data(data):\n """Process the input data"""\n return data.apply(lambda x: x.strip())\n\nif __name__ == "__main__":\n df = pd.read_csv("user_data.csv")\n processed = process_data(df)\n processed.to_csv("output.csv", index=False)', type: 'file' },
{ id: 'file-4', name: 'transform.js', icon: 'fa-code', content: 'function transformData(input) {\n return input.map(item => ({\n ...item,\n processed: true,\n timestamp: Date.now()\n }));\n}\n\nmodule.exports = { transformData };', type: 'file' },
]
},
{
id: 'folder-3',
name: 'Reference',
icon: 'fa-folder',
type: 'folder',
expanded: true,
children: [
{ id: 'file-5', name: 'api_docs.md', icon: 'fa-code', content: '# API Documentation\n\n## Endpoints\n\n### GET /api/users\nReturns list of users.\n\n### POST /api/users\nCreate a new user.', type: 'file' },
]
},
])
// 根目录文件
const rootFiles = ref<FileItem[]>([
{ id: 'file-root-1', name: 'SKILL.md', icon: 'fa-file', content: skillMdContent, type: 'file' },
])
// 展开/收起文件夹
const toggleFolder = (folder: FolderItem) => {
folder.expanded = !folder.expanded
}
// 新建文件夹
const isCreatingFolder = ref(false)
const newFolderName = ref('')
const showNewFolderInput = () => {
newFolderName.value = ''
isCreatingFolder.value = true
}
const createFolder = () => {
if (!newFolderName.value.trim()) {
isCreatingFolder.value = false
return
}
fileTree.value.push({
id: 'folder-' + Date.now(),
name: newFolderName.value,
icon: 'fa-folder',
type: 'folder',
expanded: true,
children: []
})
isCreatingFolder.value = false
}
// 新建文件
const isCreatingFile = ref(false)
const newFileName = ref('')
const newFileParentFolder = ref<string | null>(null)
const showNewFileInput = (folderId: string | null = null) => {
newFileName.value = ''
newFileParentFolder.value = folderId
isCreatingFile.value = true
}
const createFile = () => {
if (!newFileName.value.trim()) {
isCreatingFile.value = false
return
}
const newFile: FileItem = {
id: 'file-' + Date.now(),
name: newFileName.value,
icon: getFileIconByName(newFileName.value),
content: '',
type: 'file'
}
if (newFileParentFolder.value) {
// 添加到指定文件夹
const folder = fileTree.value.find(f => f.id === newFileParentFolder.value)
if (folder && folder.children) {
folder.children.push(newFile)
}
} else {
// 添加到根目录
rootFiles.value.push(newFile)
}
isCreatingFile.value = false
}
// 根据文件名获取图标 - 使用通用图标
const getFileIconByName = (filename: string): string => {
const ext = filename.split('.').pop()?.toLowerCase()
const codeExtensions = ['js', 'ts', 'py', 'html', 'css', 'json', 'xml', 'yaml', 'yml', 'java', 'c', 'cpp', 'go', 'rs', 'php', 'rb', 'swift', 'kt', 'sql', 'sh', 'bash', 'md']
if (codeExtensions.includes(ext || '')) {
return 'fa-code'
}
return 'fa-file'
}
// 上传文件
const handleFileUpload = (event: Event, folderId: string | null = null) => {
const input = event.target as HTMLInputElement
if (!input.files?.length) return
Array.from(input.files).forEach(file => {
const reader = new FileReader()
reader.onload = (e) => {
const newFile: FileItem = {
id: 'file-' + Date.now() + Math.random(),
name: file.name,
icon: getFileIconByName(file.name),
content: e.target?.result as string || '',
type: 'file'
}
if (folderId) {
const folder = fileTree.value.find(f => f.id === folderId)
if (folder && folder.children) {
folder.children.push(newFile)
}
} else {
rootFiles.value.push(newFile)
}
}
reader.readAsText(file)
})
input.value = ''
}
// 递归查找文件
const findFile = (items: (FileItem | FolderItem)[], fileId: string): FileItem | null => {
for (const item of items) {
if (item.type === 'file' && item.id === fileId) {
return item
}
if (item.type === 'folder' && item.children) {
const found = findFile(item.children, fileId)
if (found) return found
}
}
return null
}
// 文件编辑相关
const isEditingFile = ref(false)
const isEditingFileClosing = ref(false)
// AI 对话面板
const isChatOpen = ref(false)
// 图谱生成相关
const isGeneratingGraph = ref(false)
const isGraphGenerated = ref(false)
const generateGraph = () => {
isGeneratingGraph.value = true
// 模拟生成图谱的等待过程
setTimeout(() => {
isGeneratingGraph.value = false
isGraphGenerated.value = true
nextTick(() => {
initGraphChart()
})
}, 2500)
}
const toggleChat = () => {
isChatOpen.value = !isChatOpen.value
}
const editingFile = ref<FileItem | null>(null)
const editingFileName = ref('')
const editingFileContent = ref('')
const editingFileType = ref('')
// 选中文件/文件夹状态
// 鼠标悬停状态
const hoveringItem = ref<string | null>(null)
// 删除文件
const deleteFile = (file: FileItem, parentId: string | null) => {
if (!parentId) {
// 根目录文件
rootFiles.value = rootFiles.value.filter(f => f.id !== file.id)
} else {
// 文件夹内文件
const folder = fileTree.value.find(f => f.id === parentId)
if (folder && folder.children) {
folder.children = folder.children.filter(c => (c as FileItem).id !== file.id)
}
}
}
// 删除文件夹
const deleteFolder = (folder: FolderItem) => {
fileTree.value = fileTree.value.filter(f => f.id !== folder.id)
}
// 打开文件编辑器
const openFile = (file: FileItem) => {
editingFile.value = file
editingFileName.value = file.name
editingFileContent.value = file.content
editingFileType.value = 'file'
isEditingFile.value = true
}
const saveFileEdit = () => {
if (!editingFile.value) return
// 更新文件内容
editingFile.value.content = editingFileContent.value
isEditingFileClosing.value = true
setTimeout(() => {
isEditingFile.value = false
isEditingFileClosing.value = false
editingFile.value = null
}, 300)
}
const cancelFileEdit = () => {
isEditingFileClosing.value = true
setTimeout(() => {
isEditingFile.value = false
isEditingFileClosing.value = false
editingFile.value = null
}, 300)
}
// 对话相关
const chatInput = ref('')
const chatMessages = ref<{ id: number; text: string }[]>([])
const sendMessage = () => {
if (!chatInput.value.trim()) return
chatMessages.value.push({
id: Date.now(),
text: chatInput.value
})
const userInput = chatInput.value
chatInput.value = ''
// 模拟 AI 回复
setTimeout(() => {
chatMessages.value.push({
id: Date.now(),
text: `I've updated the configuration based on your request: "${userInput}". The changes have been applied to your skill settings.`
})
}, 1000)
}
// 图谱相关
const graphChartRef = ref<HTMLElement | null>(null)
let chartInstance: echarts.ECharts | null = null
const initGraphChart = () => {
if (!graphChartRef.value) return
// 节点数据 - 更有科技感的节点,添加初始位置
const nodes = [
{ id: '1', name: '用户问题', category: 0, symbolSize: 70, x: 0, y: -200, desc: '用户提出的原始问题' },
{ id: '2', name: '问题分析', category: 1, symbolSize: 55, x: 0, y: -80, desc: '对问题进行拆解和分析' },
{ id: '3', name: '数据获取', category: 1, symbolSize: 50, x: -120, y: 20, desc: '从数据源获取相关信息' },
{ id: '4', name: '方案生成', category: 2, symbolSize: 55, x: 0, y: 80, desc: '基于分析结果生成解决方案' },
{ id: '5', name: '方案评估', category: 2, symbolSize: 45, x: 0, y: 180, desc: '评估方案的可行性和效果' },
{ id: '6', name: '最终输出', category: 3, symbolSize: 60, x: 0, y: 280, desc: '生成最终的问题解答' },
{ id: '7', name: '知识库', category: 0, symbolSize: 40, x: 150, y: 0, desc: '存储领域知识' },
{ id: '8', name: '规则引擎', category: 2, symbolSize: 40, x: 120, y: 120, desc: '业务规则处理' },
{ id: '9', name: 'AI模型', category: 3, symbolSize: 45, x: -100, y: 150, desc: '大语言模型调用' },
{ id: '10', name: '向量检索', category: 1, symbolSize: 38, x: -150, y: -20, desc: '相似度检索' },
]
const links = [
{ source: '1', target: '2', label: '分析' },
{ source: '2', target: '3', label: '获取' },
{ source: '2', target: '7', label: '查询' },
{ source: '2', target: '10', label: '检索' },
{ source: '3', target: '4', label: '生成' },
{ source: '10', target: '4', label: '增强' },
{ source: '4', target: '5', label: '评估' },
{ source: '4', target: '8', label: '校验' },
{ source: '4', target: '9', label: '调用' },
{ source: '5', target: '6', label: '输出' },
{ source: '7', target: '4', label: '支撑' },
]
// 深邃科技配色 - 更赛博朋克的感觉
const CAT_COLORS = [
{ main: '#00D9FF', glow: '#00D9FF', gradient: ['#0077B6', '#00D9FF'] }, // 青色 - 语义对象
{ main: '#10B981', glow: '#10B981', gradient: ['#059669', '#34D399'] }, // 绿色 - 对象行为
{ main: '#F59E0B', glow: '#F59E0B', gradient: ['#D97706', '#FBBF24'] }, // 琥珀色 - 约束规则
{ main: '#8B5CF6', glow: '#A78BFA', gradient: ['#6D28D9', '#A78BFA'] }, // 紫色 - 编排流程
]
const option = {
backgroundColor: 'transparent',
// 全局特效
graphic: [
{
type: 'group',
children: [
// 背景光晕1
{
type: 'circle',
shape: { cx: '30%', cy: '30%', r: 200 },
style: { fill: 'radialGradient', gradient: { type: 'radial', colorStops: [{ offset: 0, color: 'rgba(0, 217, 255, 0.15)' }, { offset: 1, color: 'transparent' }] } },
},
// 背景光晕2
{
type: 'circle',
shape: { cx: '70%', cy: '70%', r: 180 },
style: { fill: 'radialGradient', gradient: { type: 'radial', colorStops: [{ offset: 0, color: 'rgba(139, 92, 246, 0.12)' }, { offset: 1, color: 'transparent' }] } },
},
],
},
],
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(10, 14, 26, 0.95)',
borderColor: 'rgba(0, 217, 255, 0.3)',
borderWidth: 1,
textStyle: { color: '#e6edf3', fontSize: 12 },
extraCssText: 'box-shadow: 0 0 20px rgba(0, 217, 255, 0.2); border-radius: 8px;',
},
series: [{
type: 'graph',
layout: 'force',
// 持续动画 - 节点漂浮
animation: true,
animationDuration: 1500,
animationEasingUpdate: 'quinticInOut',
// 布局参数 - 修复边连接问题
force: {
repulsion: 350,
edgeLength: [80, 180],
gravity: 0.1,
layoutAnimation: true,
friction: 0.85,
alphaDecay: 0.02,
},
// 节点数据
data: nodes.map(n => {
const color = CAT_COLORS[n.category]
return {
id: n.id,
name: n.name,
category: n.category,
symbolSize: n.symbolSize,
x: n.x,
y: n.y,
// 渐变色节点
symbol: 'circle',
// 标签设置
label: {
show: true,
position: 'bottom',
distance: 8,
fontSize: 11,
fontWeight: 600,
color: '#E2E8F0',
textBorderColor: '#0a0e1a',
textBorderWidth: 3,
formatter: '{b}',
},
// 节点样式 - 科技感
itemStyle: {
color: {
type: 'radial',
x: 0.3,
y: 0.3,
r: 0.7,
colorStops: [
{ offset: 0, color: color.gradient[1] },
{ offset: 0.7, color: color.gradient[0] },
{ offset: 1, color: color.main },
],
},
shadowBlur: 25,
shadowColor: color.glow,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
},
// 呼吸效果通过 emphasis 实现
emphasis: {
focus: 'adjacency',
scale: 1.15,
itemStyle: {
shadowBlur: 40,
shadowColor: color.glow,
borderColor: '#fff',
borderWidth: 2,
},
},
// 原始数据
_data: n,
}
}),
// 边数据
links: links.map((l) => {
const sourceNode = nodes.find(n => n.id === l.source)
const color = sourceNode ? CAT_COLORS[sourceNode.category] : CAT_COLORS[0]
return {
source: l.source,
target: l.target,
// 边标签
label: {
show: true,
formatter: l.label,
fontSize: 9,
fontWeight: 500,
color: '#94A3B8',
backgroundColor: 'rgba(10, 14, 26, 0.8)',
padding: [2, 6],
borderRadius: 4,
textBorderColor: 'transparent',
},
// 边样式 - 科技感
lineStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 1, y2: 1,
colorStops: [
{ offset: 0, color: color.main + '40' },
{ offset: 0.5, color: color.main + '80' },
{ offset: 1, color: color.main + '40' },
],
},
width: 1.5,
curveness: 0.15,
opacity: 0.7,
type: 'solid',
},
// 箭头 - 修复连线不连接到节点的问题
edgeSymbol: 'none',
edgeSymbolSize: 0,
}
}),
// 允许拖拽和缩放
roam: true,
draggable: true,
// 边标签
edgeLabel: {
show: true,
fontSize: 10,
color: '#94A3B8',
},
// 连线特效
emphasis: {
focus: 'adjacency',
scale: true,
lineStyle: {
width: 3,
opacity: 1,
shadowBlur: 15,
shadowColor: '#00D9FF',
},
},
// 顶部图例(隐藏)
legend: { show: false },
}],
// 视觉映射
visualMap: {
show: false,
type: 'continuous',
dimension: 2,
inRange: {
color: ['#00D9FF', '#10B981', '#F59E0B', '#8B5CF6'],
},
},
}
chartInstance = echarts.init(graphChartRef.value)
chartInstance.setOption(option)
// 初始布局动画后停止 - 使用 disableLayoutAnimation 来停止持续抖动
setTimeout(() => {
if (chartInstance) {
chartInstance.setOption({
series: [{
force: {
layoutAnimation: false, // 关闭布局动画,停止抖动
},
}],
})
}
}, 3000)
// Handle resize
const handleResize = () => chartInstance?.resize()
window.addEventListener('resize', handleResize)
}
onMounted(() => {
// 图谱通过点击按钮生成,不再自动初始化
})
onUnmounted(() => {
chartInstance?.dispose()
})
const openCreate = () => {
newSkillForm.value = {
name: '',
type: 'API',
provider: 'Custom',
port: 3000,
description: '',
enabled: true,
model: 'gpt-4o',
}
createStep.value = 1
isCreating.value = true
}
const closeCreate = () => {
isCreating.value = false
createStep.value = 1
}
const saveNewSkill = () => {
const newId = Math.max(...mcpServers.value.map(s => s.id)) + 1
mcpServers.value.push({
id: newId,
name: newSkillForm.value.name || 'Untitled Skill',
status: 'stopped',
type: newSkillForm.value.type,
port: newSkillForm.value.port,
createdAt: new Date().toISOString().split('T')[0],
description: newSkillForm.value.description,
})
isCreating.value = false
}
const statusClass = (status: string) => {
switch (status) {
case 'running': return 'bg-primary-success'
case 'stopped': return 'bg-gray-500'
case 'error': return 'bg-primary-danger'
default: return 'bg-gray-500'
}
}
</script>
<style scoped>
/* 模态框进入动画 */
@keyframes modal-in {
0% {
opacity: 0;
transform: scale(0.95) translateY(20px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes fade-in {
0% { opacity: 0; transform: translateY(-5px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes scale-in {
0% { opacity: 0; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}
.animate-modal-in {
animation: modal-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes modal-out {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0.95); }
}
.animate-modal-out {
animation: modal-out 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes slide-in {
0% { transform: translateX(100%); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
@keyframes slide-out {
0% { transform: translateX(0); opacity: 1; }
100% { transform: translateX(100%); opacity: 0; }
}
.animate-slide-in {
animation: slide-in 0.3s ease-out forwards;
}
@keyframes progress-width {
0% { width: 0%; }
100% { width: 100%; }
}
.animate-progress-width {
animation: progress-width 2.5s ease-in-out forwards;
}
@keyframes spin-slow {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.animate-spin-slow {
animation: spin-slow 20s linear infinite;
}
@keyframes pulse-slow {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
}
.animate-pulse-slow {
animation: pulse-slow 2s ease-in-out infinite;
}
@keyframes float-particle {
0%, 100% { transform: translateY(0) scale(1); opacity: 0.8; }
50% { transform: translateY(-10px) scale(1.2); opacity: 1; }
}
.animate-float-particle {
animation: float-particle 3s ease-in-out infinite;
}
@keyframes scan {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
.animate-scan {
animation: scan 1.5s ease-in-out infinite;
}
.animate-fade-in {
animation: fade-in 0.3s ease-out forwards;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-scale-in {
animation: scale-in 0.5s ease-out forwards;
}
/* 图谱节点动画 */
.graph-node {
transition: transform 0.3s ease;
}
.graph-node:hover {
transform: scale(1.1);
}
.center-node {
transition: transform 0.3s ease;
}
.center-node:hover {
transform: scale(1.05);
}
.animate-pulse-slow {
animation: pulse-slow 3s ease-in-out infinite;
}
@keyframes pulse-slow {
0%, 100% { box-shadow: 0 0 20px rgba(30, 107, 249, 0.3); }
50% { box-shadow: 0 0 40px rgba(30, 107, 249, 0.5); }
}
@keyframes draw-line {
0% { stroke-dashoffset: 100; }
100% { stroke-dashoffset: 0; }
}
.animate-draw-line {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw-line 1.5s ease forwards;
}
</style>
<template>
<div class="p-6 min-h-screen">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-wand-magic-sparkles text-gray-400"></i>
<span class="font-medium">Skills</span>
</div>
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
<i class="fa-solid fa-plus"></i>
New Skill
</button>
</div>
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search skills..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<el-select v-model="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="Error" value="error" />
</el-select>
</div>
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Skill Name</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Type</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Port</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="server in filteredServers()" :key="server.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
<td class="px-5 py-4">
<div class="font-medium">{{ server.name }}</div>
<div class="text-sm text-gray-500">{{ server.description }}</div>
</td>
<td class="px-5 py-4">
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ server.type }}</span>
</td>
<td class="px-5 py-4 text-gray-300">{{ server.port }}</td>
<td class="px-5 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="statusClass(server.status)"></span>
<span class="capitalize text-sm">{{ server.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ server.createdAt }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button
@click="toggleStatus(server)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
:title="server.status === 'running' ? 'Stop' : 'Start'"
>
<i :class="['fa-solid', server.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
</button>
<button
@click="openEdit(server)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Edit"
>
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button
@click="deleteServer(server.id)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="filteredServers().length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-code text-4xl mb-3"></i>
<p>No skills found</p>
</div>
</div>
<Teleport to="body">
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="cancelEdit">
<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 Skill</h3>
<button @click="cancelEdit" 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">Skill Name</label>
<input
v-model="editForm.name"
type="text"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
<el-select v-model="editForm.type" placeholder="Select" class="w-full" size="large">
<el-option label="Linear" value="Linear" />
<el-option label="Google Maps" value="Google Maps" />
<el-option label="File System" value="File System" />
<el-option label="PostgreSQL" value="PostgreSQL" />
<el-option label="GitHub" value="GitHub" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
<input
v-model="editForm.port"
type="number"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white 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="editForm.description"
rows="3"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white 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="cancelEdit"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveEdit"
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"
>
Save Changes
</button>
</div>
</div>
</div>
</Teleport>
<!-- 新建 Skill 模态框 -->
<Teleport to="body">
<div v-if="isCreating" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-6" @click.self="createStep === 1 && closeCreate()">
<!-- 步骤1基本信息 -->
<div v-if="createStep === 1" class="bg-dark-800 rounded-2xl w-full max-w-lg border border-dark-600 shadow-2xl overflow-hidden animate-modal-in">
<!-- 头部 -->
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
<i class="fa-solid fa-wand-magic-sparkles text-white"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-white">Create New Skill</h3>
<p class="text-sm text-gray-400">Step 1 of 2 - Basic Info</p>
</div>
</div>
<button @click="closeCreate" class="text-gray-400 hover:text-white transition-all p-2 hover:bg-dark-600 rounded-lg">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<!-- 表单内容 -->
<div class="p-6 space-y-5">
<!-- Skill Name -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
<i class="fa-solid fa-tag mr-2 text-primary-orange"></i>Skill Name
</label>
<input
v-model="newSkillForm.name"
type="text"
placeholder="Enter skill name..."
class="w-full bg-dark-600 border border-dark-500 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange transition-colors"
>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
<i class="fa-solid fa-align-left mr-2 text-gray-400"></i>Description
</label>
<textarea
v-model="newSkillForm.description"
rows="3"
placeholder="Describe your skill's purpose..."
class="w-full bg-dark-600 border border-dark-500 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange transition-colors resize-none"
></textarea>
</div>
<!-- Model Selection -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
<i class="fa-solid fa-brain mr-2 text-primary-cyan"></i>Select Model
</label>
<div class="grid grid-cols-2 gap-3">
<div
v-for="model in availableModels"
:key="model.name"
@click="newSkillForm.model = model.name"
class="p-3 rounded-xl border-2 cursor-pointer transition-all hover:scale-105"
:class="newSkillForm.model === model.name
? 'border-primary-orange bg-dark-600 shadow-lg shadow-primary-orange/20'
: 'border-dark-500 bg-dark-700 hover:border-gray-500'"
>
<div class="flex items-center gap-2">
<i :class="['fa-solid', model.icon, 'text-lg']"></i>
<div>
<div class="font-medium text-white text-sm">{{ model.name }}</div>
<div class="text-xs text-gray-500">{{ model.provider }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
<button
@click="closeCreate"
class="px-5 py-2.5 rounded-xl bg-dark-600 text-gray-300 hover:bg-dark-500 border border-dark-500 transition-all"
>
Cancel
</button>
<button
@click="goToStep2"
:disabled="!newSkillForm.name.trim()"
class="px-6 py-2.5 rounded-xl 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"
>
Next Step
<i class="fa-solid fa-arrow-right"></i>
</button>
</div>
</div>
<!-- 步骤2三栏配置界面 -->
<div v-else class="bg-dark-800 rounded-2xl w-full max-w-[95vw] h-[90vh] border border-dark-600 shadow-2xl overflow-hidden flex animate-modal-in relative">
<!-- 顶部进度指示 -->
<div class="absolute top-0 left-0 right-0 h-1 bg-dark-700">
<div class="h-full bg-gradient-to-r from-primary-orange to-red-500 w-full"></div>
</div>
<!-- 左侧边栏VS Code 风格文件管理器 -->
<div class="w-64 bg-dark-900 border-r border-dark-600 flex flex-col">
<!-- 头部 -->
<div class="p-3 border-b border-dark-600 flex items-center justify-between">
<button
@click="goBackToStep1"
class="flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
>
<i class="fa-solid fa-arrow-left text-sm"></i>
<span class="font-semibold text-white text-sm">Explorer</span>
</button>
<div class="flex items-center gap-1">
<button @click="showNewFolderInput" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New Folder">
<i class="fa-solid fa-folder-plus text-sm"></i>
</button>
<button @click="showNewFileInput(null)" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New File">
<i class="fa-solid fa-file-circle-plus text-sm"></i>
</button>
<label class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors cursor-pointer" title="Upload File">
<i class="fa-solid fa-upload text-sm"></i>
<input type="file" class="hidden" multiple @change="handleFileUpload($event, null)">
</label>
</div>
</div>
<!-- VS Code 风格文件树 -->
<div class="flex-1 overflow-y-auto py-2">
<!-- 根目录文件 -->
<div class="px-2">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-2 py-1">Workspace</div>
<div
v-for="file in rootFiles"
:key="file.id"
@click="openFile(file)"
@mouseenter="hoveringItem = file.id"
@mouseleave="hoveringItem = null"
class="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer hover:bg-dark-700 group transition-colors"
:class="{ 'bg-dark-700': hoveringItem === file.id }"
>
<i :class="['fa-solid', file.icon, 'text-primary-cyan text-sm']"></i>
<span class="text-sm text-gray-300 group-hover:text-white truncate flex-1">{{ file.name }}</span>
<button
@click.stop="deleteFile(file, null)"
class="text-red-400 hover:text-red-300 p-1 rounded hover:bg-dark-600 transition-colors opacity-0 group-hover:opacity-100"
title="Delete File"
>
<i class="fa-solid fa-trash text-xs"></i>
</button>
</div>
</div>
<!-- 文件夹列表 -->
<div class="mt-2">
<div
v-for="folder in fileTree"
:key="folder.id"
class=""
>
<!-- 文件夹标题 -->
<div
@mouseenter="hoveringItem = folder.id"
@mouseleave="hoveringItem = null"
class="flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-dark-700 group transition-colors"
:class="{ 'bg-dark-700': hoveringItem === folder.id }"
>
<i
@click.stop="toggleFolder(folder)"
:class="['fa-solid', folder.expanded ? 'fa-chevron-down' : 'fa-chevron-right', 'text-gray-500 text-xs']"
></i>
<i
@click.stop="toggleFolder(folder)"
:class="['fa-solid', folder.expanded ? 'fa-folder-open' : 'fa-folder', 'text-yellow-400 text-sm']"
></i>
<span
class="text-sm text-gray-300 group-hover:text-white font-medium flex-1"
>{{ folder.name }}</span>
<button
@click.stop="deleteFolder(folder)"
class="text-red-400 hover:text-red-300 p-1 rounded hover:bg-dark-600 transition-colors opacity-0 group-hover:opacity-100"
title="Delete Folder"
>
<i class="fa-solid fa-trash text-xs"></i>
</button>
</div>
<!-- 文件夹内的文件 -->
<div v-if="folder.expanded" class="ml-4">
<div
v-for="child in folder.children"
:key="child.id"
@click="openFile(child as FileItem)"
@mouseenter="hoveringItem = child.id"
@mouseleave="hoveringItem = null"
class="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer hover:bg-dark-700 group transition-colors"
:class="{ 'bg-dark-700': hoveringItem === child.id }"
>
<i :class="['fa-solid', (child as FileItem).icon, 'text-gray-400 text-sm']"></i>
<span class="text-sm text-gray-300 group-hover:text-white truncate flex-1">{{ child.name }}</span>
<button
@click.stop="deleteFile(child as FileItem, folder.id)"
class="text-red-400 hover:text-red-300 p-1 rounded hover:bg-dark-600 transition-colors opacity-0 group-hover:opacity-100"
title="Delete File"
>
<i class="fa-solid fa-trash text-xs"></i>
</button>
</div>
<!-- 文件夹内新建文件/上传按钮 -->
<div class="flex items-center gap-2 px-2 py-1">
<button
@click.stop="showNewFileInput(folder.id)"
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors opacity-0 group-hover:opacity-100"
>
<i class="fa-solid fa-plus"></i>
<span>New File</span>
</button>
<label
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors cursor-pointer opacity-0 group-hover:opacity-100"
title="Upload File"
>
<i class="fa-solid fa-upload"></i>
<input type="file" class="hidden" multiple @change="handleFileUpload($event, folder.id)">
</label>
</div>
</div>
</div>
</div>
</div>
<!-- 新建文件夹输入框 -->
<div v-if="isCreatingFolder" class="p-2 border-t border-dark-600 bg-dark-800">
<div class="flex items-center gap-2">
<i class="fa-solid fa-folder text-yellow-400 text-sm"></i>
<input
v-model="newFolderName"
type="text"
placeholder="Folder name..."
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-1 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
@keyup.enter="createFolder"
@blur="createFolder"
autofocus
>
</div>
</div>
<!-- 新建文件输入框 -->
<div v-if="isCreatingFile" class="p-2 border-t border-dark-600 bg-dark-800">
<div class="flex items-center gap-2">
<i class="fa-solid fa-file text-gray-400 text-sm"></i>
<input
v-model="newFileName"
type="text"
placeholder="File name..."
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-1 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
@keyup.enter="createFile"
@blur="createFile"
autofocus
>
</div>
</div>
<!-- 底部操作 -->
<div class="p-3 border-t border-dark-600">
<button
@click="generateGraph"
:disabled="isGeneratingGraph || isGraphGenerated"
class="w-full bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white font-medium py-2 px-3 rounded-lg transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<i :class="['fa-solid fa-project-diagram', isGraphGenerated ? 'text-green-400' : '']"></i>
{{ isGraphGenerated ? '图谱已生成' : (isGeneratingGraph ? '生成中...' : '生成流程图谱') }}
</button>
</div>
</div>
<!-- 中间图谱视图 -->
<div class="flex-1 relative overflow-hidden" style="background: linear-gradient(135deg, #0a0e1a 0%, #0f172a 50%, #0a0e1a 100%);">
<!-- 未生成图谱时的提示 -->
<div v-if="!isGraphGenerated && !isGeneratingGraph" class="absolute inset-0 flex flex-col items-center justify-center z-10">
<div class="w-24 h-24 rounded-2xl bg-dark-700 flex items-center justify-center mb-4">
<i class="fa-solid fa-project-diagram text-4xl text-gray-600"></i>
</div>
<p class="text-gray-500 text-sm">点击左侧按钮生成流程图谱</p>
</div>
<!-- 生成中的加载动画 -->
<div v-else-if="isGeneratingGraph" class="absolute inset-0 flex flex-col items-center justify-center z-10">
<!-- 科幻风格加载动画 -->
<div class="relative">
<!-- 外层六边形 -->
<div class="w-40 h-40 relative animate-spin-slow">
<svg viewBox="0 0 100 100" class="w-full h-full">
<!-- 外圈 -->
<polygon points="50,2 95,27 95,73 50,98 5,73 5,27" fill="none" stroke="rgba(0,217,255,0.3)" stroke-width="1"/>
<polygon points="50,8 90,29 90,71 50,92 10,71 10,29" fill="none" stroke="rgba(0,217,255,0.5)" stroke-width="1.5"/>
<!-- 内圈 -->
<polygon points="50,15 82,33 82,67 50,85 18,67 18,33" fill="none" stroke="rgba(139,92,246,0.4)" stroke-width="1"/>
<!-- 旋转的连接线 -->
<line x1="50" y1="2" x2="50" y2="20" stroke="rgba(0,217,255,0.8)" stroke-width="2" class="animate-pulse"/>
<line x1="98" y1="50" x2="80" y2="50" stroke="rgba(139,92,246,0.8)" stroke-width="2" class="animate-pulse"/>
<line x1="50" y1="98" x2="50" y2="80" stroke="rgba(0,217,255,0.8)" stroke-width="2" class="animate-pulse"/>
<line x1="2" y1="50" x2="20" y2="50" stroke="rgba(139,92,246,0.8)" stroke-width="2" class="animate-pulse"/>
</svg>
</div>
<!-- 中心核心 -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-dark-900 to-dark-800 border-2 border-primary-cyan/50 flex items-center justify-center shadow-[0_0_30px_rgba(0,217,255,0.3)]">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-cyan to-purple-500 animate-pulse-slow flex items-center justify-center">
<i class="fa-solid fa-brain text-white text-lg"></i>
</div>
</div>
</div>
<!-- 浮动粒子 -->
<div class="absolute top-2 left-1/2 w-2 h-2 rounded-full bg-primary-cyan animate-float-particle"></div>
<div class="absolute bottom-4 right-2 w-1.5 h-1.5 rounded-full bg-purple-400 animate-float-particle" style="animation-delay: 0.5s"></div>
<div class="absolute top-1/3 left-2 w-1 h-1 rounded-full bg-cyan-300 animate-float-particle" style="animation-delay: 1s"></div>
</div>
<!-- 文字信息 -->
<div class="mt-8 text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<span class="w-2 h-2 rounded-full bg-primary-cyan animate-ping"></span>
<span class="text-white font-medium tracking-wider">正在构建知识图谱</span>
</div>
<p class="text-gray-400 text-sm">Neural Network Analysis</p>
</div>
<!-- 科幻进度条 -->
<div class="mt-8 w-64">
<div class="relative h-1 bg-dark-800 rounded-full overflow-hidden border border-dark-600">
<!-- 扫描线 -->
<div class="absolute inset-0">
<div class="h-full bg-gradient-to-r from-transparent via-primary-cyan/30 to-transparent animate-scan"></div>
</div>
<!-- 进度 -->
<div class="h-full bg-gradient-to-r from-primary-cyan via-purple-400 to-primary-cyan animate-progress-width bg-[length:200%_100%]"></div>
</div>
<div class="flex justify-between mt-2 text-xs text-gray-500 font-mono">
<span>INITIALIZING</span>
<span>0%</span>
</div>
</div>
</div>
<!-- 背景光效层 -->
<div v-show="isGraphGenerated" class="absolute inset-0 overflow-hidden">
<!-- 青色光晕 -->
<div class="absolute top-0 left-0 w-[500px] h-[500px] rounded-full opacity-20" style="background: radial-gradient(circle, rgba(0, 217, 255, 0.3) 0%, transparent 70%); filter: blur(60px);"></div>
<!-- 紫色光晕 -->
<div class="absolute bottom-0 right-0 w-[400px] h-[400px] rounded-full opacity-15" style="background: radial-gradient(circle, rgba(139, 92, 246, 0.3) 0%, transparent 70%); filter: blur(60px);"></div>
<!-- 网格线 -->
<div class="absolute inset-0 opacity-10" style="background-image: linear-gradient(rgba(0, 217, 255, 0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 217, 255, 0.3) 1px, transparent 1px); background-size: 50px 50px;"></div>
</div>
<!-- ECharts 图谱容器 -->
<div v-show="isGraphGenerated" ref="graphChartRef" class="w-full h-full relative z-10"></div>
<!-- 底部提示 -->
<div v-show="isGraphGenerated" class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-dark-700/80 backdrop-blur-sm border border-dark-500/50 rounded-lg px-4 py-2 text-xs text-gray-400 shadow-lg">
<i class="fa-solid fa-info-circle mr-1 text-primary-cyan"></i>
拖拽节点 · 滚轮缩放 · 点击查看详情
</div>
</div>
<!-- 右侧对话面板 -->
<!-- 悬浮图标按钮 -->
<button
v-if="!isChatOpen"
@click="toggleChat"
class="absolute right-4 bottom-4 w-14 h-14 rounded-full bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center shadow-lg shadow-purple-500/30 hover:scale-110 transition-transform z-20"
>
<i class="fa-solid fa-robot text-white text-xl animate-bounce"></i>
</button>
<!-- 展开的对话面板 -->
<div
v-else
class="w-80 bg-dark-800 border-l border-dark-600 flex flex-col animate-slide-in"
>
<!-- 头部 -->
<div class="p-4 border-b border-dark-600 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center">
<i class="fa-solid fa-robot text-white text-sm"></i>
</div>
<div>
<div class="font-medium text-white text-sm">AI Assistant</div>
<div class="text-xs text-gray-500">Configure your skill</div>
</div>
</div>
<button @click="toggleChat" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
<!-- 对话内容 -->
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<!-- AI 消息 -->
<div class="flex gap-3">
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-robot text-white text-xs"></i>
</div>
<div class="bg-dark-700 rounded-2xl rounded-tl-none px-4 py-3 text-sm text-gray-300">
<p>Hello! I'm your AI assistant. I can help you configure your skill by:</p>
<ul class="mt-2 space-y-1 text-gray-400">
<li>• Managing data files</li>
<li>• Adding reference documents</li>
<li>• Writing scripts</li>
</ul>
<p class="mt-2">What would you like to do?</p>
</div>
</div>
<!-- 用户消息 -->
<div v-for="msg in chatMessages" :key="msg.id" class="flex gap-3 flex-row-reverse">
<div class="w-7 h-7 rounded-lg bg-dark-600 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-user text-gray-400 text-xs"></i>
</div>
<div class="bg-dark-600 rounded-2xl rounded-tr-none px-4 py-3 text-sm text-white max-w-[80%]">
{{ msg.text }}
</div>
</div>
</div>
<!-- 输入框 -->
<div class="p-4 border-t border-dark-600">
<div class="relative">
<input
v-model="chatInput"
type="text"
placeholder="Ask AI to help..."
class="w-full bg-dark-700 border border-dark-500 rounded-xl px-4 py-3 pr-12 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
@keyup.enter="sendMessage"
>
<button
@click="sendMessage"
class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-lg bg-primary-orange text-white flex items-center justify-center hover:from-orange-500 hover:to-red-600 transition-all"
>
<i class="fa-solid fa-paper-plane text-xs"></i>
</button>
</div>
</div>
<!-- 底部按钮 -->
<div class="p-3 border-t border-dark-600 flex gap-2">
<button
@click="closeCreate"
class="flex-1 py-2 rounded-lg bg-dark-700 text-gray-300 hover:bg-dark-600 text-sm transition-colors"
>
Cancel
</button>
<button
@click="saveNewSkill"
class="flex-1 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 text-sm transition-all flex items-center justify-center gap-2"
>
<i class="fa-solid fa-check"></i>
Create
</button>
</div>
</div>
</div>
</div>
</Teleport>
<!-- 文件编辑弹窗 -->
<Teleport to="body">
<div v-if="isEditingFile" class="fixed inset-0 bg-black/80 flex items-center justify-center z-[60]">
<div :class="['bg-dark-800 rounded-2xl w-full max-w-5xl h-[85vh] border border-dark-600 shadow-2xl overflow-hidden flex flex-col', isEditingFileClosing ? 'animate-modal-out' : 'animate-modal-in']">
<!-- 头部 -->
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center">
<i class="fa-solid fa-file-edit text-white"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-white">Edit File</h3>
<p class="text-sm text-gray-400">{{ editingFileName }}</p>
</div>
</div>
<button @click="cancelFileEdit" class="text-gray-400 hover:text-white transition-all p-2 hover:bg-dark-600 rounded-lg">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<!-- 编辑器内容 -->
<div class="flex-1 p-5 overflow-hidden">
<textarea
v-model="editingFileContent"
class="w-full h-full bg-dark-900 border border-dark-500 rounded-xl p-4 text-sm text-gray-300 font-mono focus:outline-none focus:border-primary-cyan resize-none"
placeholder="File content..."
></textarea>
</div>
<!-- 底部按钮 -->
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
<button
@click="cancelFileEdit"
class="px-5 py-2.5 rounded-xl bg-dark-600 text-gray-300 hover:bg-dark-500 border border-dark-500 transition-all"
>
Cancel
</button>
<button
@click="saveFileEdit"
class="px-6 py-2.5 rounded-xl bg-gradient-to-r from-primary-cyan to-purple-500 text-white hover:from-cyan-500 hover:to-purple-600 transition-all flex items-center gap-2"
>
<i class="fa-solid fa-save"></i>
Save Changes
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>