feat: 新增 Skill 和 Team 页面
- 添加 Skill 技能管理页面 - 添加 Team 团队管理页面 - 更新侧边栏导航和路由配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ interface MenuItem {
|
|||||||
const mainMenu: MenuItem[] = [
|
const mainMenu: MenuItem[] = [
|
||||||
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
||||||
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' },
|
{ 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: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
||||||
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
||||||
{ name: 'Database', icon: 'fa-database', path: '/database' },
|
{ name: 'Database', icon: 'fa-database', path: '/database' },
|
||||||
@@ -28,7 +29,6 @@ const mainMenu: MenuItem[] = [
|
|||||||
|
|
||||||
const bottomMenu: MenuItem[] = [
|
const bottomMenu: MenuItem[] = [
|
||||||
{ name: 'Settings', icon: 'fa-gear', path: '/settings' },
|
{ name: 'Settings', icon: 'fa-gear', path: '/settings' },
|
||||||
{ name: 'Team', icon: 'fa-users', path: '/team' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const bottomMenu2: MenuItem[] = [
|
const bottomMenu2: MenuItem[] = [
|
||||||
@@ -117,7 +117,7 @@ const handleUserCommand = (command: string) => {
|
|||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<li class="my-4 border-t border-dark-500"></li>
|
<li class="my-4 border-t border-dark-500"></li>
|
||||||
|
|
||||||
<!-- Settings & Team -->
|
<!-- Settings -->
|
||||||
<li v-for="item in bottomMenu" :key="item.name">
|
<li v-for="item in bottomMenu" :key="item.name">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -134,18 +134,13 @@ const handleUserCommand = (command: string) => {
|
|||||||
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="item.name === 'Team'" class="flex gap-1">
|
|
||||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
|
||||||
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
|
|
||||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<li class="my-4 border-t border-dark-500"></li>
|
<li class="my-4 border-t border-dark-500"></li>
|
||||||
|
|
||||||
<!-- Information -->
|
<!-- Account -->
|
||||||
<li v-for="item in bottomMenu2" :key="item.name">
|
<li v-for="item in bottomMenu2" :key="item.name">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import Dashboard from '@/views/Dashboard.vue'
|
import Dashboard from '@/views/Dashboard.vue'
|
||||||
import Login from '@/views/Login.vue'
|
import Login from '@/views/Login.vue'
|
||||||
import Agents from '@/views/Agents.vue'
|
import Agents from '@/views/Agents.vue'
|
||||||
import MCP from '@/views/MCP.vue'
|
import Team from '@/views/Team.vue'
|
||||||
|
import Skill from '@/views/Skill.vue'
|
||||||
import ModelAPIs from '@/views/ModelAPIs.vue'
|
import ModelAPIs from '@/views/ModelAPIs.vue'
|
||||||
import Database from '@/views/Database.vue'
|
import Database from '@/views/Database.vue'
|
||||||
import Knowledge from '@/views/Knowledge.vue'
|
import Knowledge from '@/views/Knowledge.vue'
|
||||||
@@ -26,10 +27,15 @@ const router = createRouter({
|
|||||||
name: 'agents',
|
name: 'agents',
|
||||||
component: Agents
|
component: Agents
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/team',
|
||||||
|
name: 'team',
|
||||||
|
component: Team
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/mcp',
|
path: '/mcp',
|
||||||
name: 'mcp',
|
name: 'mcp',
|
||||||
component: MCP
|
component: Skill
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/model-apis',
|
path: '/model-apis',
|
||||||
|
|||||||
1257
web/src/views/Skill.vue
Normal file
1257
web/src/views/Skill.vue
Normal file
File diff suppressed because it is too large
Load Diff
9
web/src/views/Team.vue
Normal file
9
web/src/views/Team.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-white mb-4">Team</h1>
|
||||||
|
<p class="text-gray-400">Team management page - Coming soon</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
46
web/src/views/skill/types.ts
Normal file
46
web/src/views/skill/types.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
340
web/src/views/skill/useFileTree.ts
Normal file
340
web/src/views/skill/useFileTree.ts
Normal file
@@ -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<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: DEFAULT_SKILL_CONTENT, type: 'file' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 展开/收起文件夹
|
||||||
|
const toggleFolder = (folder: FolderItem) => {
|
||||||
|
folder.expanded = !folder.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件夹 - 内联输入模式
|
||||||
|
const inlineNewFolderName = ref('')
|
||||||
|
const inlineCreatingFolderId = ref<string | null>(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<string | null>(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<FileItem | null>(null)
|
||||||
|
const editingFileName = ref('')
|
||||||
|
const editingFileContent = ref('')
|
||||||
|
const editingFileType = ref('')
|
||||||
|
|
||||||
|
// 选中文件/文件夹状态
|
||||||
|
const hoveringItem = ref<string | null>(null)
|
||||||
|
const selectedFolder = ref<string | null>(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,
|
||||||
|
}
|
||||||
|
}
|
||||||
46
web/src/views/skill/useSkillChat.ts
Normal file
46
web/src/views/skill/useSkillChat.ts
Normal file
@@ -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<ChatMessage[]>([])
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
114
web/src/views/skill/useSkillServers.ts
Normal file
114
web/src/views/skill/useSkillServers.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import type { MCPServer } from './types'
|
||||||
|
|
||||||
|
export function useSkillServers() {
|
||||||
|
// MCP Server 列表
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态样式
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
136
web/src/views/skill/useWorkflow.ts
Normal file
136
web/src/views/skill/useWorkflow.ts
Normal file
@@ -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<WorkflowNode[]>([
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user