diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..6f0a0ed --- /dev/null +++ b/web/.env.example @@ -0,0 +1,11 @@ +# X-Agents Web 前端配置 +# 复制此文件为 .env.local 后修改 + +# 开发环境配置 +VITE_APP_TITLE=X-Agents + +# API 基础地址(Go 后端) +VITE_API_BASE=http://localhost:8082 + +# 开发服务器端口 +VITE_PORT=5173 diff --git a/web/package-lock.json b/web/package-lock.json index 9402911..03689f2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.13.6", "echarts": "^6.0.0", "element-plus": "^2.13.3", + "jszip": "^3.10.1", "lucide-vue-next": "^0.577.0", "marked": "^17.0.4", "monaco-editor": "^0.55.1", @@ -1563,6 +1564,12 @@ "node": ">= 6" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", @@ -2033,6 +2040,18 @@ "he": "bin/he" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2095,6 +2114,12 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", @@ -2105,6 +2130,27 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2356,6 +2402,12 @@ "node": ">= 6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmmirror.com/papaparse/-/papaparse-5.5.3.tgz", @@ -2599,6 +2651,12 @@ "dev": true, "license": "MIT" }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2636,6 +2694,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", @@ -2750,6 +2823,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2759,6 +2844,15 @@ "node": ">=0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", @@ -2986,7 +3080,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vite": { diff --git a/web/package.json b/web/package.json index d785a38..b667ee6 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "axios": "^1.13.6", "echarts": "^6.0.0", "element-plus": "^2.13.3", + "jszip": "^3.10.1", "lucide-vue-next": "^0.577.0", "marked": "^17.0.4", "monaco-editor": "^0.55.1", diff --git a/web/src/assets/styles/index.css b/web/src/assets/styles/index.css index 2876c8d..f7d2a2d 100644 --- a/web/src/assets/styles/index.css +++ b/web/src/assets/styles/index.css @@ -197,6 +197,42 @@ body { } /* Empty state */ +.empty-box { + min-height: 340px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.empty-icon { + width: 100px; + height: 100px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #1f2937, #111827); + border-radius: 24px; + margin-bottom: 20px; +} + +.empty-icon i { + font-size: 40px; + color: #6b7280; +} + +.empty-text { + color: #d1d5db; + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 8px; +} + +.empty-tip { + color: #6b7280; + font-size: 0.875rem; +} + .empty-state { padding: var(--spacing-2xl); text-align: center; diff --git a/web/src/composables/useApiBase.ts b/web/src/composables/useApiBase.ts new file mode 100644 index 0000000..8d1eb0b --- /dev/null +++ b/web/src/composables/useApiBase.ts @@ -0,0 +1,10 @@ +/** + * API 基础地址配置 + * 从环境变量读取,默认为 http://localhost:8082 + */ + +// 开发环境使用 vite 代理,生产环境需要配置实际地址 +const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8082' + +export const apiBase = API_BASE +export default API_BASE diff --git a/web/src/composables/useAuth.ts b/web/src/composables/useAuth.ts index 079092d..68ec667 100644 --- a/web/src/composables/useAuth.ts +++ b/web/src/composables/useAuth.ts @@ -1,4 +1,4 @@ -const API_BASE = 'http://localhost:8082' +import API_BASE from './useApiBase' export interface LoginRequest { username: string diff --git a/web/src/composables/useChat.ts b/web/src/composables/useChat.ts deleted file mode 100644 index ceeb303..0000000 --- a/web/src/composables/useChat.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { ref, nextTick, onMounted, onUnmounted } from 'vue' -import { marked } from 'marked' - -// 类型定义 -export interface ChatModel { - id: string - name: string - model_type: string - provider: string - model: string - status: string -} - -export interface ChatMessage { - id: number - role: 'user' | 'assistant' - content: string - timestamp: Date - isStreaming?: boolean -} - -export interface Agent { - id: number - name: string - avatar: string - description: string - accentColor: string - gradient: string - status: 'online' | 'offline' -} - -export interface ChatSession { - id: number - title: string - agentId: number - lastMessage: string - timestamp: Date -} - -export interface GroupChat { - id: number - name: string - members: string[] - lastMessage: string - timestamp: Date -} - -// 配置 marked -marked.setOptions({ - breaks: true, - gfm: true -}) - -// 预处理内容:修复一些常见的 Markdown 问题 -const preprocessContent = (content: string): string => { - if (!content) return '' - - // 1. 标题:# 标题 -> # 标题(确保 # 后有空格) - content = content.replace(/(^|\n)(#{1,6})([^\s#\n])/g, '$1$2 $3') - - // 2. 无序列表:-项目 -> - 项目 - content = content.replace(/(\n)(\s*)([-*+])(\S)/g, '$1$2$3 $4') - - // 3. 有序列表:1.项目 -> 1. 项目 - content = content.replace(/(\n)(\s*)(\d+\.)(\S)/g, '$1$2$3 $4') - - // 4. 引用:>引用 -> > 引用 - content = content.replace(/(\n)(>+)([^\s>\n])/g, '$1$2 $3') - - // 5. 修复 ##1. 这种情况(连续处理) - content = content.replace(/(#{1,6})(\d+\.)/g, '$1 $2') - - return content -} - -// 渲染 Markdown -export const renderMarkdown = (content: string): string => { - if (!content) return '' - try { - const processed = preprocessContent(content) - return marked.parse(processed) as string - } catch (e) { - console.error('Markdown parse error:', e) - return content - } -} - -// 根据 provider 获取图标 -export const getModelIcon = (provider: string) => { - const icons: Record = { - 'OpenAI': '🤖', - 'Claude': '🧠', - 'Google': '✨', - 'Gemini': '✨', - 'Ollama': '🦙', - 'DeepSeek': '🔮', - 'Moonshot': '🌙', - 'Kimi': '🌙', - 'Baidu': '🐉', - '文心一言': '🐉', - 'Aliyun': '☁️', - 'Ali': '☁️', - '通义千问': '☁️', - 'Azure': '⬛', - 'Anthropic': '🧠', - } - return icons[provider] || '💬' -} - -// 创建 composable -export function useChat() { - // 模型相关状态 - const chatModels = ref([]) - const selectedModel = ref(null) - const modelsLoading = ref(false) - const showModelDropdown = ref(false) - - // 助手相关状态 - const chatAgents = ref([ - { id: 1, name: 'Claude', avatar: '🧠', description: 'Anthropic AI', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'online' }, - { id: 2, name: 'Gemini', avatar: '✨', description: 'Google DeepMind', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'online' }, - { id: 3, name: 'ChatGPT', avatar: '💬', description: 'OpenAI', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'offline' }, - { id: 4, name: 'DeepSeek', avatar: '🔮', description: 'DeepSeek AI', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'online' }, - { id: 5, name: 'Kimi', avatar: '🌙', description: 'Moonshot AI', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'online' }, - { id: 6, name: '文心一言', avatar: '🐉', description: 'Baidu', accentColor: '#ef4444', gradient: 'from-red-500/20 to-orange-500/20', status: 'offline' }, - { id: 7, name: '通义千问', avatar: '☁️', description: 'Alibaba', accentColor: '#06b6d4', gradient: 'from-cyan-500/20 to-sky-500/20', status: 'online' }, - ]) - const selectedAgent = ref(chatAgents.value[0]) - - // 消息相关状态 - const messages = ref([ - { id: 1, role: 'assistant', content: '你好!我是 Claude,你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() }, - ]) - - // 历史对话 - const chatSessions = ref([ - { id: 1, title: '关于 Python 学习的讨论', agentId: 1, lastMessage: '谢谢你!', timestamp: new Date(Date.now() - 3600000) }, - { id: 2, title: '代码调试帮助', agentId: 1, lastMessage: '让我看看这个问题...', timestamp: new Date(Date.now() - 7200000) }, - { id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) }, - ]) - - // 群聊 - const groupChats = ref([ - { id: 1, name: 'AI 讨论组', members: ['Claude', 'GPT-4', 'Gemini'], lastMessage: '我们来讨论一下...', timestamp: new Date(Date.now() - 1800000) }, - { id: 2, name: '编程助手', members: ['Claude', 'DeepSeek'], lastMessage: '这段代码有问题吗?', timestamp: new Date(Date.now() - 3600000) }, - { id: 3, name: '创意头脑风暴', members: ['GPT-4', 'Claude', 'Kimi'], lastMessage: '有个新想法...', timestamp: new Date(Date.now() - 7200000) }, - ]) - - // 智能体选择弹窗 - const showAgentSelector = ref(false) - const selectMode = ref<'single' | 'group'>('single') - const selectedAgents = ref([]) - const groupChatName = ref('') - - // 输入相关 - const inputMessage = ref('') - const isLoading = ref(false) - - // 侧边栏 - const sidebarCollapsed = ref(false) - - // 获取模型列表 - const fetchModels = async () => { - modelsLoading.value = true - try { - const response = await fetch(`/model/list`) - const data = await response.json() - if (data.list) { - chatModels.value = data.list.filter((m: ChatModel) => m.model_type === 'chat' && m.status === 'active') - if (chatModels.value.length > 0 && !selectedModel.value) { - selectedModel.value = chatModels.value[0] - } - } - } catch (error) { - console.error('Failed to fetch models:', error) - } finally { - modelsLoading.value = false - } - } - - // 打开智能体选择器 - const openAgentSelector = (mode: 'single' | 'group') => { - selectMode.value = mode - selectedAgents.value = [] - groupChatName.value = '' - showAgentSelector.value = true - } - - // 切换智能体选择 - const toggleAgentSelection = (agent: Agent) => { - const index = selectedAgents.value.findIndex(a => a.id === agent.id) - if (index > -1) { - selectedAgents.value.splice(index, 1) - } else { - selectedAgents.value.push(agent) - } - } - - // 确认选择 - const confirmAgentSelection = () => { - if (selectMode.value === 'single') { - if (selectedAgents.value.length > 0) { - selectedAgent.value = selectedAgents.value[0] - messages.value = [ - { id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value.name},你的 AI 助手。有什么我可以帮助你的吗?`, timestamp: new Date() } - ] - } - } else { - const name = groupChatName.value.trim() || `群聊 (${selectedAgents.value.length}人)` - groupChats.value.unshift({ - id: Date.now(), - name: name, - members: selectedAgents.value.map(a => a.name), - lastMessage: 'New group created', - timestamp: new Date() - }) - } - showAgentSelector.value = false - } - - // 取消选择 - const cancelAgentSelection = () => { - showAgentSelector.value = false - } - - // 选择助手 - const selectAgent = (agent: Agent) => { - selectedAgent.value = agent - messages.value = [ - { id: 1, role: 'assistant', content: `你好!我是 ${agent.name}。有什么我可以帮助你的吗?`, timestamp: new Date() } - ] - } - - // 选择历史对话 - const selectSession = (session: ChatSession) => { - const agent = chatAgents.value.find(a => a.id === session.agentId) - if (agent) { - selectedAgent.value = agent - } - messages.value = [ - { id: 1, role: 'assistant', content: `已加载会话:${session.title}`, timestamp: new Date() } - ] - } - - // 新建聊天 - const newChat = () => { - messages.value = [ - { id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value?.name || 'Claude'}。有什么我可以帮助你的吗?`, timestamp: new Date() } - ] - } - - // 格式化时间 - const formatTime = (date: Date) => { - return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) - } - - // 格式化相对时间 - const formatRelativeTime = (date: Date) => { - const now = new Date() - const diff = now.getTime() - date.getTime() - const hours = Math.floor(diff / 3600000) - const days = Math.floor(diff / 86400000) - - if (hours < 1) return '刚刚' - if (hours < 24) return `${hours}小时前` - if (days < 7) return `${days}天前` - return date.toLocaleDateString('zh-CN') - } - - // 切换侧边栏 - const toggleSidebar = () => { - sidebarCollapsed.value = !sidebarCollapsed.value - } - - // 点击外部关闭下拉框 - const handleClickOutside = (e: MouseEvent) => { - const target = e.target as HTMLElement - if (!target.closest('.model-dropdown')) { - showModelDropdown.value = false - } - } - - // 初始化 - onMounted(() => { - fetchModels() - document.addEventListener('click', handleClickOutside) - }) - - onUnmounted(() => { - document.removeEventListener('click', handleClickOutside) - }) - - return { - // 模型 - chatModels, - selectedModel, - modelsLoading, - showModelDropdown, - fetchModels, - // 助手 - chatAgents, - selectedAgent, - selectAgent, - // 消息 - messages, - newChat, - // 历史对话 - chatSessions, - selectSession, - // 群聊 - groupChats, - // 智能体选择 - showAgentSelector, - selectMode, - selectedAgents, - groupChatName, - openAgentSelector, - toggleAgentSelection, - confirmAgentSelection, - cancelAgentSelection, - // 输入 - inputMessage, - isLoading, - // 侧边栏 - sidebarCollapsed, - toggleSidebar, - // 工具 - formatTime, - formatRelativeTime, - getModelIcon, - } -} diff --git a/web/src/views/Agents.vue b/web/src/views/Agents.vue index fd7bfd9..af13a41 100644 --- a/web/src/views/Agents.vue +++ b/web/src/views/Agents.vue @@ -30,6 +30,8 @@ const { stats, isAllSelected, isIndeterminate, + isAllSelectedEdit, + isIndeterminateEdit, fetchAgents, fetchSkills, fetchModels, @@ -46,6 +48,8 @@ const { handleSkillsModeClickEdit, toggleSelectAll, clearSkills, + toggleSelectAllEdit, + clearSkillsEdit, toggleSkillsMode, selectSkillsMode, toggleSubSkillsDropdown, @@ -62,9 +66,21 @@ const {
-
- - Agents +
+
+ + Agents +
+
+ Total: + {{ stats.total }} + + Active: + {{ stats.active }} + + Inactive: + {{ stats.inactive }} +
+