2026-03-05 10:49:46 +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 ( 20 px ) ;
}
100 % {
opacity : 1 ;
transform : scale ( 1 ) translateY ( 0 ) ;
}
}
@ keyframes fade - in {
0 % { opacity : 0 ; transform : translateY ( - 5 px ) ; }
100 % { opacity : 1 ; transform : translateY ( 0 ) ; }
}
@ keyframes float {
0 % , 100 % { transform : translateY ( 0 ) ; }
50 % { transform : translateY ( - 8 px ) ; }
}
@ keyframes scale - in {
0 % { opacity : 0 ; transform : scale ( 0.9 ) ; }
100 % { opacity : 1 ; transform : scale ( 1 ) ; }
}
. animate - modal - in {
animation : modal - in 0.4 s 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.3 s 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.3 s ease - out forwards ;
}
@ keyframes progress - width {
0 % { width : 0 % ; }
100 % { width : 100 % ; }
}
. animate - progress - width {
animation : progress - width 2.5 s ease - in - out forwards ;
}
@ keyframes spin - slow {
0 % { transform : rotate ( 0 deg ) ; }
100 % { transform : rotate ( 360 deg ) ; }
}
. animate - spin - slow {
animation : spin - slow 20 s 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 2 s ease - in - out infinite ;
}
@ keyframes float - particle {
0 % , 100 % { transform : translateY ( 0 ) scale ( 1 ) ; opacity : 0.8 ; }
50 % { transform : translateY ( - 10 px ) scale ( 1.2 ) ; opacity : 1 ; }
}
. animate - float - particle {
animation : float - particle 3 s ease - in - out infinite ;
}
@ keyframes scan {
0 % { transform : translateX ( - 100 % ) ; }
100 % { transform : translateX ( 200 % ) ; }
}
. animate - scan {
animation : scan 1.5 s ease - in - out infinite ;
}
. animate - fade - in {
animation : fade - in 0.3 s ease - out forwards ;
}
. animate - float {
animation : float 3 s ease - in - out infinite ;
}
. animate - scale - in {
animation : scale - in 0.5 s ease - out forwards ;
}
/* 图谱节点动画 */
. graph - node {
transition : transform 0.3 s ease ;
}
. graph - node : hover {
transform : scale ( 1.1 ) ;
}
. center - node {
transition : transform 0.3 s ease ;
}
. center - node : hover {
transform : scale ( 1.05 ) ;
}
. animate - pulse - slow {
animation : pulse - slow 3 s ease - in - out infinite ;
}
@ keyframes pulse - slow {
0 % , 100 % { box - shadow : 0 0 20 px rgba ( 30 , 107 , 249 , 0.3 ) ; }
50 % { box - shadow : 0 0 40 px 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.5 s 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 >
2026-03-05 11:31:36 +08:00
< 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 >
2026-03-05 10:49:46 +08:00
< / 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 >
2026-03-05 11:31:36 +08:00
< 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 >
2026-03-05 10:49:46 +08:00
< / 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 >