Files
X-Agents/web/src/views/Skill.vue
DESKTOP-72TV0V4\caoxiaozhu ac384ce10b fix: 优化各页面代码格式和样式
- 微调多个页面的样式和布局
- 保持功能不变

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:39:10 +08:00

1258 lines
57 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useModelSettings } from './settings/useModelSettings'
import { useSkillServers } from './skill/useSkillServers'
import { useFileTree } from './skill/useFileTree'
import { useSkillChat } from './skill/useSkillChat'
import { useWorkflow } from './skill/useWorkflow'
import type { FileItem } from './skill/types'
// MCP Server management
const {
mcpServers,
editingServer,
isEditing,
searchQuery,
filterStatus,
editForm,
openEdit,
saveEdit,
cancelEdit,
toggleStatus,
deleteServer,
filteredServers,
statusClass,
} = useSkillServers()
// File tree management
const {
fileTree,
rootFiles,
inlineNewFolderName,
inlineCreatingFolderId,
inlineNewFileName,
inlineCreatingFileId,
isEditingFile,
isEditingFileClosing,
editingFile,
editingFileName,
editingFileContent,
editingFileType,
hoveringItem,
selectedFolder,
toggleFolder,
showInlineNewFolderInput,
confirmInlineCreateFolder,
cancelInlineCreateFolder,
findFolderById,
showInlineNewFileInput,
confirmInlineCreateFile,
cancelInlineCreateFile,
getFileIconByName,
handleFileUpload,
findFile,
selectFolder,
deleteFile,
deleteFolder,
openFile,
saveFileEdit,
cancelFileEdit,
} = useFileTree()
// Chat functionality
const {
isChatOpen,
chatInput,
chatMessages,
toggleChat,
sendMessage,
} = useSkillChat()
// Workflow visualization
const {
workflowData,
isGeneratingGraph,
isGraphGenerated,
toggleNode,
renderWorkflow,
generateGraph,
} = useWorkflow()
// 新建 Skill
const isCreating = ref(false)
const createStep = ref(1)
const newSkillForm = ref({
name: '',
type: 'API',
provider: 'Custom',
port: 3000,
description: '',
enabled: true,
model: 'gpt-4o',
})
// 从 Model Settings 获取模型
const { models, fetchModels } = useModelSettings()
const availableModels = computed(() => {
return models.value
.filter((m: any) => m.model_type === 'chat')
.map((m: any) => ({
name: m.model,
provider: m.provider,
icon: 'fa-brain'
}))
})
onMounted(() => {
fetchModels()
})
const goToStep2 = () => {
if (!newSkillForm.value.name.trim()) return
createStep.value = 2
isGraphGenerated.value = false
isGeneratingGraph.value = false
}
const goBackToStep1 = () => {
createStep.value = 1
}
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
}
</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-orange-500"></i>
<span class="font-medium">Skills</span>
</div>
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
<i class="fa-solid fa-plus"></i>
New Skill
</button>
</div>
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search skills..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
<el-option label="All Status" value="all" />
<el-option label="Running" value="running" />
<el-option label="Stopped" value="stopped" />
<el-option label="Error" value="error" />
</el-select>
</div>
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Skill Name</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Type</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Port</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="server in filteredServers()" :key="server.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
<td class="px-5 py-4">
<div class="font-medium">{{ server.name }}</div>
<div class="text-sm text-gray-500">{{ server.description }}</div>
</td>
<td class="px-5 py-4">
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ server.type }}</span>
</td>
<td class="px-5 py-4 text-gray-300">{{ server.port }}</td>
<td class="px-5 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="statusClass(server.status)"></span>
<span class="capitalize text-sm">{{ server.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ server.createdAt }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button
@click="toggleStatus(server)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
:title="server.status === 'running' ? 'Stop' : 'Start'"
>
<i :class="['fa-solid', server.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
</button>
<button
@click="openEdit(server)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Edit"
>
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button
@click="deleteServer(server.id)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="filteredServers().length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-code text-4xl mb-3"></i>
<p>No skills found</p>
</div>
</div>
<Teleport to="body">
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit Skill</h3>
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
<input
v-model="editForm.name"
type="text"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
<el-select v-model="editForm.type" placeholder="Select" class="w-full" size="large">
<el-option label="Linear" value="Linear" />
<el-option label="Google Maps" value="Google Maps" />
<el-option label="File System" value="File System" />
<el-option label="PostgreSQL" value="PostgreSQL" />
<el-option label="GitHub" value="GitHub" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
<input
v-model="editForm.port"
type="number"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="editForm.description"
rows="3"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="cancelEdit"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveEdit"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
>
Save Changes
</button>
</div>
</div>
</div>
</Teleport>
<!-- 新建 Skill 模态框 -->
<Teleport to="body">
<div v-if="isCreating" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-6">
<!-- 步骤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>
<el-select
v-model="newSkillForm.model"
placeholder="Select a model"
class="w-full"
size="large"
popper-class="dark-select-dropdown"
>
<el-option
v-for="model in availableModels"
:key="model.name"
:label="`${model.name} (${model.provider})`"
:value="model.name"
>
<div class="flex items-center gap-2">
<i :class="['fa-solid', model.icon, 'text-primary-cyan']"></i>
<span class="text-white">{{ model.name }}</span>
<span class="text-gray-500 text-sm">({{ model.provider }})</span>
</div>
</el-option>
</el-select>
</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="showInlineNewFolderInput(null)" 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="showInlineNewFileInput(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 v-if="inlineCreatingFolderId === 'root'" class="flex items-center gap-2 px-2 py-1">
<i class="fa-solid fa-folder text-yellow-400 text-sm"></i>
<input
v-model="inlineNewFolderName"
type="text"
placeholder="Folder name"
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
@keyup.enter="confirmInlineCreateFolder"
@blur="confirmInlineCreateFolder"
@keyup.escape="cancelInlineCreateFolder"
autofocus
>
</div>
<div v-else-if="inlineCreatingFileId === 'root'" class="flex items-center gap-2 px-2 py-1">
<i class="fa-solid fa-file text-gray-400 text-sm"></i>
<input
v-model="inlineNewFileName"
type="text"
placeholder="File name"
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
@keyup.enter="confirmInlineCreateFile"
@blur="confirmInlineCreateFile"
@keyup.escape="cancelInlineCreateFile"
autofocus
>
</div>
<div v-else class="flex items-center gap-2 px-2 py-1">
<button
@click="showInlineNewFolderInput(null)"
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
>
<i class="fa-solid fa-folder-plus"></i>
<span>New Folder</span>
</button>
<button
@click="showInlineNewFileInput(null)"
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
>
<i class="fa-solid fa-file-circle-plus"></i>
<span>New File</span>
</button>
</div>
</div>
<!-- 文件夹列表 -->
<div class="mt-2">
<div
v-for="folder in fileTree"
:key="folder.id"
class=""
>
<!-- 文件夹标题 -->
<div
@click.stop="selectFolder(folder.id)"
@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 || selectedFolder === 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>
<!-- 选中状态指示器 -->
<span v-if="selectedFolder === folder.id" class="text-xs text-primary-cyan">Selected</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 || selectedFolder === folder.id" 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 v-if="inlineCreatingFolderId === folder.id" class="flex items-center gap-2 px-2 py-1">
<i class="fa-solid fa-folder text-yellow-400 text-xs"></i>
<input
v-model="inlineNewFolderName"
type="text"
placeholder="Folder name"
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
@keyup.enter="confirmInlineCreateFolder"
@blur="confirmInlineCreateFolder"
@keyup.escape="cancelInlineCreateFolder"
autofocus
>
</div>
<div v-else-if="inlineCreatingFileId === folder.id" class="flex items-center gap-2 px-2 py-1">
<i class="fa-solid fa-file text-gray-400 text-xs"></i>
<input
v-model="inlineNewFileName"
type="text"
placeholder="File name"
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
@keyup.enter="confirmInlineCreateFile"
@blur="confirmInlineCreateFile"
@keyup.escape="cancelInlineCreateFile"
autofocus
>
</div>
<div v-else class="flex items-center gap-2 px-2 py-1">
<button
@click.stop="showInlineNewFolderInput(folder.id)"
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
title="New Folder"
>
<i class="fa-solid fa-folder-plus"></i>
<span>New Folder</span>
</button>
<button
@click.stop="showInlineNewFileInput(folder.id)"
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
:class="selectedFolder === folder.id ? '' : '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"
:class="selectedFolder === folder.id ? '' : '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 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-sitemap', 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-sitemap 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="flex flex-col items-center">
<!-- 多层旋转环 + 脉冲点 -->
<div class="relative w-24 h-24 mb-8">
<!-- 外环 -->
<div class="absolute inset-0 border-2 border-primary-cyan/20 rounded-full"></div>
<div class="absolute inset-1 border-2 border-t-primary-cyan rounded-full loading-spin" style="animation-duration: 1.5s;"></div>
<!-- 中环 - 反向 -->
<div class="absolute inset-2 border-2 border-purple-500/30 rounded-full" style="animation: loading-spin 2s linear infinite reverse;"></div>
<!-- 内环 -->
<div class="absolute inset-4 border-2 border-primary-cyan/40 rounded-full" style="animation: loading-spin 1s linear infinite;"></div>
<!-- 中心脉冲 -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-3 h-3 bg-primary-cyan rounded-full animate-pulse"></div>
</div>
<!-- 周围脉冲点 -->
<div class="absolute inset-0 flex items-center justify-center" style="animation: loading-dots 1.4s ease-in-out infinite both;">
<span class="absolute w-2 h-2 bg-primary-cyan rounded-full" style="top: 10%;"></span>
<span class="absolute w-2 h-2 bg-purple-500 rounded-full" style="right: 10%;"></span>
<span class="absolute w-2 h-2 bg-primary-orange rounded-full" style="bottom: 10%;"></span>
<span class="absolute w-2 h-2 bg-green-500 rounded-full" style="left: 10%;"></span>
</div>
</div>
<!-- 文字 -->
<div class="text-center mb-6">
<p class="text-white font-medium text-lg mb-2">正在生成执行流程</p>
<p class="text-gray-400 text-sm">Building your workflow...</p>
</div>
<!-- 步骤指示器 -->
<div class="flex items-center gap-2 mb-6">
<div class="flex items-center gap-1.5">
<div class="w-2 h-2 rounded-full bg-primary-cyan loading-pulse"></div>
<span class="text-xs text-gray-500">分析</span>
</div>
<div class="w-8 h-px bg-dark-600"></div>
<div class="flex items-center gap-1.5">
<div class="w-2 h-2 rounded-full bg-gray-600"></div>
<span class="text-xs text-gray-500">规划</span>
</div>
<div class="w-8 h-px bg-dark-600"></div>
<div class="flex items-center gap-1.5">
<div class="w-2 h-2 rounded-full bg-gray-600"></div>
<span class="text-xs text-gray-500">执行</span>
</div>
<div class="w-8 h-px bg-dark-600"></div>
<div class="flex items-center gap-1.5">
<div class="w-2 h-2 rounded-full bg-gray-600"></div>
<span class="text-xs text-gray-500">完成</span>
</div>
</div>
<!-- 进度条 -->
<div class="w-64">
<div class="h-1 bg-dark-700 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary-cyan via-purple-500 to-primary-orange loading-progress-bar rounded-full"></div>
</div>
</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-30" style="background: radial-gradient(circle, rgba(0, 217, 255, 0.4) 0%, transparent 70%); filter: blur(80px);"></div>
<!-- 紫色光晕 - 更亮更柔和 -->
<div class="absolute bottom-0 right-0 w-[400px] h-[400px] rounded-full opacity-25" style="background: radial-gradient(circle, rgba(139, 92, 246, 0.4) 0%, transparent 70%); filter: blur(80px);"></div>
<!-- 中心补充光效 -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-15" style="background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); filter: blur(100px);"></div>
</div>
<!-- 金字塔/树形流程图展示 -->
<div v-show="isGraphGenerated" class="w-full h-full relative z-10 overflow-auto p-6">
<!-- 流程标题 -->
<div class="sticky top-0 bg-slate-900/80 backdrop-blur-sm px-4 py-3 border-b border-slate-700/50 mb-6 z-20">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<i class="fa-solid fa-sitemap text-primary-cyan"></i>
<span class="text-sm font-medium text-white">执行流程</span>
<span class="px-2 py-0.5 rounded-full bg-slate-700 text-xs text-slate-300">4</span>
</div>
<span class="text-xs text-slate-400">点击节点展开详情</span>
</div>
</div>
<!-- 树形结构 - 深邃版本 -->
<div class="flex flex-col items-center">
<!-- 递归渲染树节点 -->
<template v-for="node in workflowData" :key="node.id">
<div class="w-full flex flex-col items-center">
<!-- 根节点 - 深邃设计 -->
<div
@click="toggleNode(node)"
class="w-full max-w-lg cursor-pointer group"
>
<!-- 外层发光效果 -->
<div
class="relative p-1 rounded-2xl transition-all duration-500"
:class="node.expanded ? 'opacity-100' : 'opacity-80 hover:opacity-100'"
>
<!-- 内层背景 -->
<div
class="relative bg-slate-800/70 backdrop-blur-xl border rounded-2xl p-5 overflow-hidden"
:class="node.expanded ? `border-2 ${node.borderColor} shadow-lg shadow-${node.color.includes('green') ? 'green' : node.color.includes('orange') ? 'orange' : node.color.includes('purple') ? 'purple' : 'cyan'}-500/30` : 'border-slate-600/50'"
>
<!-- 背景光效 -->
<div
class="absolute inset-0 opacity-30 transition-opacity duration-500"
:class="node.expanded ? 'opacity-100' : 'opacity-0'"
:style="`background: linear-gradient(135deg, ${node.color.includes('green') ? 'rgba(34, 197, 94, 0.15)' : node.color.includes('orange') ? 'rgba(249, 115, 22, 0.15)' : node.color.includes('purple') ? 'rgba(168, 85, 247, 0.15)' : 'rgba(6, 182, 212, 0.15)'} 0%, transparent 100%);`"
></div>
<!-- 网格纹理 -->
<div class="absolute inset-0 opacity-10" style="background-image: linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px); background-size: 20px 20px;"></div>
<!-- 主内容 -->
<div class="relative flex items-center gap-4">
<!-- 图标容器 - 多层设计 -->
<div class="relative">
<!-- 外层光晕 -->
<div
class="absolute inset-0 rounded-2xl blur-xl opacity-40 transition-all duration-500"
:class="node.expanded ? 'scale-150 opacity-40' : 'scale-100 opacity-0'"
:style="`background: ${node.color.includes('green') ? '#22c55e' : node.color.includes('orange') ? '#f97316' : node.color.includes('purple') ? '#a855f7' : '#06b6d4'};`"
></div>
<!-- 图标框 -->
<div
class="relative w-16 h-16 rounded-2xl bg-gradient-to-br flex items-center justify-center border-2 backdrop-blur-sm"
:class="[node.bgColor, node.borderColor]"
>
<i :class="['fa-solid', node.icon, node.color, 'text-2xl']"></i>
</div>
<!-- 状态指示点 -->
<div
class="absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-slate-700 flex items-center justify-center"
:class="node.status === 'completed' ? 'bg-green-500' : node.status === 'processing' ? 'bg-yellow-500' : 'bg-gray-500'"
>
<i v-if="node.status === 'completed'" class="fa-solid fa-check text-[8px] text-white"></i>
</div>
</div>
<!-- 文字内容 -->
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<h4 class="text-white font-bold text-lg tracking-wide">{{ node.name }}</h4>
<span
class="px-2.5 py-1 rounded-full text-xs font-semibold uppercase tracking-wider backdrop-blur-sm"
:class="[
node.status === 'completed' ? 'bg-green-500/30 text-green-400 border border-green-500/50' :
node.status === 'processing' ? 'bg-yellow-500/30 text-yellow-400 border border-yellow-500/50' :
'bg-slate-500/30 text-slate-300 border border-slate-500/50'
]"
>
{{ node.status === 'completed' ? '✓ 完成' : node.status === 'processing' ? '◐ 进行中' : '○ 待处理' }}
</span>
</div>
<p class="text-slate-300 text-sm">{{ node.description }}</p>
<!-- 底部信息 -->
<div class="flex items-center gap-4 mt-2 text-xs text-slate-400">
<span class="flex items-center gap-1">
<i class="fa-solid fa-layer-group"></i>
Level 1
</span>
<span class="flex items-center gap-1">
<i class="fa-solid fa-code-branch"></i>
{{ node.children?.length || 0 }} 个子任务
</span>
</div>
</div>
<!-- 展开箭头 -->
<div
v-if="node.children && node.children.length > 0"
class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-700/50 border border-slate-600"
>
<i
:class="['fa-solid fa-chevron-down text-slate-300 transition-transform duration-300', node.expanded ? 'rotate-180' : '']"
></i>
</div>
</div>
</div>
</div>
</div>
<!-- 连接线 + 子节点 -->
<div v-if="node.expanded && node.children" class="w-full flex flex-col items-center mt-1">
<!-- 垂直连接线 - 复杂设计 -->
<div class="relative flex flex-col items-center">
<!-- 主线 -->
<div class="w-0.5 h-8 bg-gradient-to-b from-green-500/40 to-purple-500/40"></div>
<!-- 发光点 -->
<div class="absolute top-1/2 w-2 h-2 rounded-full bg-cyan-400 shadow-lg shadow-cyan-400/40"></div>
</div>
<!-- 子节点容器 -->
<div class="w-full flex justify-center gap-6">
<div v-for="(child, idx) in node.children" :key="child.id" class="flex-1 flex flex-col items-center max-w-md">
<!-- 水平连接线 -->
<div class="w-full h-0.5 bg-gradient-to-r from-transparent via-purple-500/40 to-transparent mb-3"></div>
<!-- 子节点 - 深邃设计 -->
<div
@click.stop="toggleNode(child)"
class="w-full cursor-pointer group"
>
<div
class="relative p-0.5 rounded-xl transition-all duration-300"
:class="child.expanded ? 'opacity-100' : 'opacity-80 hover:opacity-100'"
>
<div
class="relative bg-slate-800/60 backdrop-blur-xl border rounded-xl p-4"
:class="child.expanded ? `border-2 ${child.borderColor}` : 'border-slate-600/50'"
>
<!-- 背景微光 -->
<div
class="absolute inset-0 opacity-20 rounded-xl"
:style="`background: linear-gradient(135deg, ${child.color.includes('green') ? 'rgba(34, 197, 94, 0.2)' : child.color.includes('orange') ? 'rgba(249, 115, 22, 0.2)' : child.color.includes('yellow') ? 'rgba(234, 179, 8, 0.2)' : 'rgba(6, 182, 212, 0.2)'} 0%, transparent 100%);`"
></div>
<div class="relative flex items-center gap-3">
<!-- 图标 -->
<div class="relative">
<div
class="w-12 h-12 rounded-xl bg-gradient-to-br flex items-center justify-center border"
:class="[child.bgColor, child.borderColor]"
>
<i :class="['fa-solid', child.icon, child.color, 'text-xl']"></i>
</div>
<!-- 序号标签 -->
<div class="absolute -top-2 -left-2 w-5 h-5 rounded-full bg-slate-700 border border-slate-500 flex items-center justify-center text-[10px] text-slate-300 font-bold">
{{ idx + 1 }}
</div>
</div>
<!-- 文字 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-0.5">
<h5 class="text-white font-semibold">{{ child.name }}</h5>
<span
v-if="child.children && child.children.length > 0"
class="px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 text-xs"
>
{{ child.children.length }}
</span>
</div>
<p class="text-slate-400 text-xs truncate">{{ child.description }}</p>
</div>
<!-- 箭头 -->
<i
v-if="child.children && child.children.length > 0"
:class="['fa-solid fa-chevron-down text-slate-400 text-xs transition-transform duration-300', child.expanded ? 'rotate-180' : '']"
></i>
</div>
</div>
</div>
<!-- 孙子节点 -->
<div v-if="child.expanded && child.children" class="mt-2 ml-4 space-y-2">
<div
v-for="(grandchild, gidx) in child.children"
:key="grandchild.id"
class="relative pl-4 border-l-2 border-slate-600/50"
>
<!-- 连接点 -->
<div class="absolute -left-1 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-slate-500"></div>
<div class="flex items-center gap-3 p-2 rounded-lg bg-slate-700/30 hover:bg-slate-700/60 transition-colors cursor-pointer border border-transparent hover:border-slate-500">
<div
class="w-8 h-8 rounded-lg bg-gradient-to-br flex items-center justify-center border"
:class="[grandchild.bgColor, grandchild.borderColor]"
>
<i :class="['fa-solid', grandchild.icon, grandchild.color, 'text-sm']"></i>
</div>
<div class="flex-1 min-w-0">
<h6 class="text-white font-medium text-sm">{{ grandchild.name }}</h6>
<p class="text-slate-400 text-xs truncate">{{ grandchild.description }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- 底部提示 -->
<div v-show="isGraphGenerated" class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-slate-800/80 backdrop-blur-sm border border-slate-600/50 rounded-lg px-4 py-2 text-xs text-slate-300 shadow-lg">
<i class="fa-solid fa-sitemap mr-1 text-primary-cyan"></i>
金字塔结构 · 4层执行流程 · 点击展开
</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>