Files
X-Agents/src/views/MCP.vue

1555 lines
58 KiB
Vue
Raw Normal View History

2026-03-05 09:28:32 +08:00
<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>
<select
v-model="filterStatus"
class="bg-dark-600 border border-dark-500 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-primary-orange"
>
<option value="all">All Status</option>
<option value="running">Running</option>
<option value="stopped">Stopped</option>
<option value="error">Error</option>
</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>
<select
v-model="editForm.type"
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"
>
<option value="Linear">Linear</option>
<option value="Google Maps">Google Maps</option>
<option value="File System">File System</option>
<option value="PostgreSQL">PostgreSQL</option>
<option value="GitHub">GitHub</option>
</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>