From 11c9ff2428e209061eea53375afce386d0bff85d Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Sun, 8 Mar 2026 20:34:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Skill=20=E5=92=8C?= =?UTF-8?q?=20Team=20=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Skill 技能管理页面 - 添加 Team 团队管理页面 - 更新侧边栏导航和路由配置 Co-Authored-By: Claude Opus 4.6 --- web/src/components/Sidebar.vue | 11 +- web/src/router/index.ts | 10 +- web/src/views/Skill.vue | 1257 ++++++++++++++++++++++++ web/src/views/Team.vue | 9 + web/src/views/skill/types.ts | 46 + web/src/views/skill/useFileTree.ts | 340 +++++++ web/src/views/skill/useSkillChat.ts | 46 + web/src/views/skill/useSkillServers.ts | 114 +++ web/src/views/skill/useWorkflow.ts | 136 +++ 9 files changed, 1959 insertions(+), 10 deletions(-) create mode 100644 web/src/views/Skill.vue create mode 100644 web/src/views/Team.vue create mode 100644 web/src/views/skill/types.ts create mode 100644 web/src/views/skill/useFileTree.ts create mode 100644 web/src/views/skill/useSkillChat.ts create mode 100644 web/src/views/skill/useSkillServers.ts create mode 100644 web/src/views/skill/useWorkflow.ts diff --git a/web/src/components/Sidebar.vue b/web/src/components/Sidebar.vue index c9000bd..437000d 100644 --- a/web/src/components/Sidebar.vue +++ b/web/src/components/Sidebar.vue @@ -20,6 +20,7 @@ interface MenuItem { const mainMenu: MenuItem[] = [ { name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' }, { name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' }, + { name: 'Team', icon: 'fa-users', path: '/team' }, { name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' }, { name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' }, { name: 'Database', icon: 'fa-database', path: '/database' }, @@ -28,7 +29,6 @@ const mainMenu: MenuItem[] = [ const bottomMenu: MenuItem[] = [ { name: 'Settings', icon: 'fa-gear', path: '/settings' }, - { name: 'Team', icon: 'fa-users', path: '/team' }, ] const bottomMenu2: MenuItem[] = [ @@ -117,7 +117,7 @@ const handleUserCommand = (command: string) => {
  • - +
  • { -
    - - - -
  • - +
  • +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 +} + + + + + diff --git a/web/src/views/Team.vue b/web/src/views/Team.vue new file mode 100644 index 0000000..9a8447f --- /dev/null +++ b/web/src/views/Team.vue @@ -0,0 +1,9 @@ + + + diff --git a/web/src/views/skill/types.ts b/web/src/views/skill/types.ts new file mode 100644 index 0000000..2d17789 --- /dev/null +++ b/web/src/views/skill/types.ts @@ -0,0 +1,46 @@ +// Skill page types + +export interface MCPServer { + id: number + name: string + status: 'running' | 'stopped' | 'error' + type: string + port: number + createdAt: string + description: string +} + +export interface FileItem { + id: string + name: string + icon?: string + content?: string + type: 'file' +} + +export interface FolderItem { + id: string + name: string + icon?: string + type: 'folder' + expanded?: boolean + children: (FileItem | FolderItem)[] +} + +export interface WorkflowNode { + id: string + name: string + description: string + icon: string + color: string + bgColor: string + borderColor: string + status: 'pending' | 'processing' | 'completed' + expanded: boolean + children?: WorkflowNode[] +} + +export interface ChatMessage { + id: number + text: string +} diff --git a/web/src/views/skill/useFileTree.ts b/web/src/views/skill/useFileTree.ts new file mode 100644 index 0000000..0eeefda --- /dev/null +++ b/web/src/views/skill/useFileTree.ts @@ -0,0 +1,340 @@ +import { ref } from 'vue' +import type { FileItem, FolderItem } from './types' + +// 默认的 SKILL.md 内容 +const DEFAULT_SKILL_CONTENT = `# Skill Configuration + +## Overview +This skill is configured to process data and generate outputs. + +## Dependencies +- Python 3.8+ +- Node.js 16+ +` + +export function useFileTree() { + // 文件树数据 + const fileTree = ref([ + { + 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([ + { id: 'file-root-1', name: 'SKILL.md', icon: 'fa-file', content: DEFAULT_SKILL_CONTENT, type: 'file' }, + ]) + + // 展开/收起文件夹 + const toggleFolder = (folder: FolderItem) => { + folder.expanded = !folder.expanded + } + + // 新建文件夹 - 内联输入模式 + const inlineNewFolderName = ref('') + const inlineCreatingFolderId = ref(null) + + const showInlineNewFolderInput = (parentId: string | null = null) => { + inlineNewFolderName.value = '' + inlineCreatingFolderId.value = parentId === null ? 'root' : parentId + } + + const confirmInlineCreateFolder = () => { + if (!inlineNewFolderName.value.trim()) { + inlineCreatingFolderId.value = null + return + } + + const newFolder: FolderItem = { + id: 'folder-' + Date.now(), + name: inlineNewFolderName.value, + icon: 'fa-folder', + type: 'folder', + expanded: true, + children: [] + } + + if (inlineCreatingFolderId.value && inlineCreatingFolderId.value !== 'root') { + const parentFolder = findFolderById(fileTree.value, inlineCreatingFolderId.value) + if (parentFolder && parentFolder.children) { + parentFolder.children.push(newFolder) + // 选中新创建的文件夹 + selectedFolder.value = newFolder.id + parentFolder.expanded = true + } + } else { + fileTree.value.push(newFolder) + // 选中新创建的文件夹 + selectedFolder.value = newFolder.id + } + inlineCreatingFolderId.value = null + inlineNewFolderName.value = '' + } + + const cancelInlineCreateFolder = () => { + inlineCreatingFolderId.value = null + inlineNewFolderName.value = '' + // 恢复选中之前选中的文件夹 + if (selectedFolder.value) { + const folder = findFolderById(fileTree.value, selectedFolder.value) + if (folder) { + folder.expanded = true + } + } + } + + // 递归查找文件夹 + const findFolderById = (folders: (FileItem | FolderItem)[], id: string): FolderItem | null => { + for (const folder of folders) { + if ('children' in folder && folder.id === id) return folder as FolderItem + if ('children' in folder && folder.children) { + const found = findFolderById(folder.children, id) + if (found) return found + } + } + return null + } + + // 新建文件 - 内联输入模式 + const inlineNewFileName = ref('') + const inlineCreatingFileId = ref(null) + + const showInlineNewFileInput = (folderId: string | null = null) => { + inlineNewFileName.value = '' + inlineCreatingFileId.value = folderId === null ? 'root' : folderId + } + + const confirmInlineCreateFile = () => { + if (!inlineNewFileName.value.trim()) { + inlineCreatingFileId.value = null + return + } + + const newFile: FileItem = { + id: 'file-' + Date.now(), + name: inlineNewFileName.value, + icon: getFileIconByName(inlineNewFileName.value), + content: '', + type: 'file' + } + + if (inlineCreatingFileId.value && inlineCreatingFileId.value !== 'root') { + const folder = findFolderById(fileTree.value, inlineCreatingFileId.value) + if (folder && folder.children) { + folder.children.push(newFile) + // 选中父文件夹 + selectedFolder.value = folder.id + folder.expanded = true + } + } else { + rootFiles.value.push(newFile) + } + + inlineCreatingFileId.value = null + inlineNewFileName.value = '' + } + + const cancelInlineCreateFile = () => { + inlineCreatingFileId.value = null + inlineNewFileName.value = '' + // 恢复选中之前选中的文件夹 + if (selectedFolder.value) { + const folder = findFolderById(fileTree.value, selectedFolder.value) + if (folder) { + folder.expanded = true + } + } + } + + // 根据文件名获取图标 + 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 = findFolderById(fileTree.value, 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) + const editingFile = ref(null) + const editingFileName = ref('') + const editingFileContent = ref('') + const editingFileType = ref('') + + // 选中文件/文件夹状态 + const hoveringItem = ref(null) + const selectedFolder = ref(null) + + // 选中文件夹 + const selectFolder = (folderId: string) => { + if (selectedFolder.value === folderId) { + selectedFolder.value = null + } else { + selectedFolder.value = folderId + const folder = findFolderById(fileTree.value, folderId) + if (folder) { + folder.expanded = true + } + } + } + + // 删除文件 + 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) + } + + return { + // State + fileTree, + rootFiles, + inlineNewFolderName, + inlineCreatingFolderId, + inlineNewFileName, + inlineCreatingFileId, + isEditingFile, + isEditingFileClosing, + editingFile, + editingFileName, + editingFileContent, + editingFileType, + hoveringItem, + selectedFolder, + + // Methods + toggleFolder, + showInlineNewFolderInput, + confirmInlineCreateFolder, + cancelInlineCreateFolder, + findFolderById, + showInlineNewFileInput, + confirmInlineCreateFile, + cancelInlineCreateFile, + getFileIconByName, + handleFileUpload, + findFile, + selectFolder, + deleteFile, + deleteFolder, + openFile, + saveFileEdit, + cancelFileEdit, + } +} diff --git a/web/src/views/skill/useSkillChat.ts b/web/src/views/skill/useSkillChat.ts new file mode 100644 index 0000000..d89f953 --- /dev/null +++ b/web/src/views/skill/useSkillChat.ts @@ -0,0 +1,46 @@ +import { ref } from 'vue' +import type { ChatMessage } from './types' + +export function useSkillChat() { + // AI 对话面板 + const isChatOpen = ref(false) + + // 对话相关 + const chatInput = ref('') + const chatMessages = ref([]) + + const toggleChat = () => { + isChatOpen.value = !isChatOpen.value + } + + 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) + } + + return { + // State + isChatOpen, + chatInput, + chatMessages, + + // Methods + toggleChat, + sendMessage, + } +} diff --git a/web/src/views/skill/useSkillServers.ts b/web/src/views/skill/useSkillServers.ts new file mode 100644 index 0000000..eaacab3 --- /dev/null +++ b/web/src/views/skill/useSkillServers.ts @@ -0,0 +1,114 @@ +import { ref } from 'vue' +import type { MCPServer } from './types' + +export function useSkillServers() { + // MCP Server 列表 + const mcpServers = ref([ + { 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(null) + const isEditing = ref(false) + + // 搜索和筛选 + const searchQuery = ref('') + const filterStatus = ref('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 + }) + } + + // 状态样式 + 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' + } + } + + return { + // State + mcpServers, + editingServer, + isEditing, + searchQuery, + filterStatus, + editForm, + + // Methods + openEdit, + saveEdit, + cancelEdit, + toggleStatus, + deleteServer, + filteredServers, + statusClass, + } +} diff --git a/web/src/views/skill/useWorkflow.ts b/web/src/views/skill/useWorkflow.ts new file mode 100644 index 0000000..4d90bf7 --- /dev/null +++ b/web/src/views/skill/useWorkflow.ts @@ -0,0 +1,136 @@ +import { ref } from 'vue' + +interface WorkflowNode { + id: string + name: string + description: string + icon: string + color: string + bgColor: string + borderColor: string + status: 'pending' | 'processing' | 'completed' + expanded: boolean + children?: WorkflowNode[] +} + +export function useWorkflow() { + // 流程生成相关 + const isGeneratingGraph = ref(false) + const isGraphGenerated = ref(false) + + const workflowData = ref([ + { + id: 'output', + name: '最终输出', + description: '生成最终结果返回给用户', + icon: 'fa-check-circle', + color: 'text-green-400', + bgColor: 'from-green-500/20 to-green-500/5', + borderColor: 'border-green-500/30', + status: 'pending', + expanded: false, + children: [ + { + id: 'generate', + name: '方案生成', + description: '基于分析结果构建解决方案', + icon: 'fa-lightbulb', + color: 'text-primary-orange', + bgColor: 'from-primary-orange/20 to-primary-orange/5', + borderColor: 'border-primary-orange/30', + status: 'pending', + expanded: false, + children: [ + { + id: 'analyze', + name: '问题分析', + description: '拆解问题、提取关键信息、理解用户意图', + icon: 'fa-microscope', + color: 'text-purple-400', + bgColor: 'from-purple-500/20 to-purple-500/5', + borderColor: 'border-purple-500/30', + status: 'pending', + expanded: false, + children: [ + { + id: 'question', + name: '用户问题', + description: '输入原始需求和问题描述', + icon: 'fa-question', + color: 'text-primary-cyan', + bgColor: 'from-primary-cyan/20 to-primary-cyan/5', + borderColor: 'border-primary-cyan/30', + status: 'pending', + expanded: false, + } + ] + }, + { + id: 'data', + name: '数据获取', + description: '从知识库、向量数据库获取相关信息', + icon: 'fa-database', + color: 'text-green-400', + bgColor: 'from-green-500/20 to-green-500/5', + borderColor: 'border-green-500/30', + status: 'pending', + expanded: false, + } + ] + }, + { + id: 'evaluate', + name: '方案评估', + description: '验证方案可行性和效果', + icon: 'fa-clipboard-check', + color: 'text-yellow-400', + bgColor: 'from-yellow-500/20 to-yellow-500/5', + borderColor: 'border-yellow-500/30', + status: 'pending', + expanded: false, + } + ] + } + ]) + + // 展开/收起节点 + const toggleNode = (node: WorkflowNode) => { + node.expanded = !node.expanded + } + + // 递归渲染节点 + const renderWorkflow = (nodes: WorkflowNode[], level: number = 0) => { + return nodes + } + + const generateGraph = () => { + isGeneratingGraph.value = true + setTimeout(() => { + isGeneratingGraph.value = false + isGraphGenerated.value = true + if (workflowData.value.length > 0) { + workflowData.value[0].expanded = true + if (workflowData.value[0].children) { + workflowData.value[0].children.forEach(child => { + child.expanded = false + if (child.children) { + child.children.forEach(gc => gc.expanded = false) + } + }) + } + } + }, 2500) + } + + return { + // State + workflowData, + isGeneratingGraph, + isGraphGenerated, + + // Methods + toggleNode, + renderWorkflow, + generateGraph, + } +}