diff --git a/channel/web/chat.html b/channel/web/chat.html index 2ad5474..0f54460 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -1,1545 +1,505 @@ - + - CowAgent - Personal AI Agent + CowAgent Console + + + + - - - - - - - - - - - -
- - -
-
- - -
AI 助手
- - GitHub - -
- -
- -
-

AI 助手

-

我可以回答问题、提供信息或者帮助您完成各种任务

- -
-
-
📁 系统管理
-
帮我查看工作空间里有哪些文件
-
-
-
⏰ 智能任务
-
提醒我5分钟后查看服务器情况
-
-
-
💻 编程助手
-
帮我编写一个Python爬虫脚本
-
- -
-
- -
- -
-
- - -
-
-
-
- + + + + + + + + + + + +
- // 添加一个变量来跟踪输入法状态 - let isComposing = false; - - // 监听输入法组合状态开始 - input.addEventListener('compositionstart', function() { - isComposing = true; - }); - - // 监听输入法组合状态结束 - input.addEventListener('compositionend', function() { - isComposing = false; - }); - - // 自动调整文本区域高度 - input.addEventListener('input', function() { - this.style.height = 'auto'; - this.style.height = (this.scrollHeight) + 'px'; - - // 启用/禁用发送按钮 - sendButton.disabled = !this.value.trim(); - }); - - // 处理示例卡片点击 - exampleCards.forEach(card => { - card.addEventListener('click', function() { - const exampleText = this.querySelector('.example-text').textContent; - input.value = exampleText; - input.dispatchEvent(new Event('input')); - input.focus(); - }); - }); - - // 处理菜单切换 - menuToggle.addEventListener('click', function(event) { - event.stopPropagation(); // 防止事件冒泡到 main-content - sidebar.classList.toggle('active'); - }); - - // 处理新对话按钮 - 创建新的会话ID和清空当前对话 - newChatButton.addEventListener('click', function() { - // 生成新的会话ID - sessionId = generateSessionId(); - // 将新的会话ID保存到全局变量,供轮询函数使用 - window.sessionId = sessionId; - console.log('New conversation started with new session ID:', sessionId); - - // 清空聊天记录 - clearChat(); - }); - - // 发送按钮点击事件 - sendButton.addEventListener('click', function() { - sendMessage(); - }); - - // 输入框按键事件 - input.addEventListener('keydown', function(event) { - // Ctrl+Enter 或 Shift+Enter 添加换行 - if ((event.ctrlKey || event.shiftKey) && event.key === 'Enter') { - const start = this.selectionStart; - const end = this.selectionEnd; - const value = this.value; - - this.value = value.substring(0, start) + '\n' + value.substring(end); - this.selectionStart = this.selectionEnd = start + 1; - - event.preventDefault(); - } - // Enter 键发送消息,但只在不是输入法组合状态时 - else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !isComposing) { - sendMessage(); - event.preventDefault(); - } - }); - - // 在发送消息函数前添加调试代码 - console.log('Axios loaded:', typeof axios !== 'undefined'); - - // 发送消息函数 - function sendMessage() { - console.log('Send message function called'); - const userMessage = input.value.trim(); - if (userMessage) { - // 隐藏欢迎屏幕 - const welcomeScreenElement = document.getElementById('welcome-screen'); - if (welcomeScreenElement) { - welcomeScreenElement.remove(); - } - - const timestamp = new Date(); - - // 添加用户消息到界面 - addUserMessage(userMessage, timestamp); - - // 添加一个等待中的机器人消息 - const loadingContainer = addLoadingMessage(); - - // 清空输入框并重置高度 - 移到这里,确保发送后立即清空 - input.value = ''; - input.style.height = '52px'; - sendButton.disabled = true; - - // 使用当前的全局会话ID - const currentSessionId = window.sessionId || sessionId; - - // 发送到服务器并获取请求ID - axios({ - method: 'post', - url: '/message', - data: { - session_id: currentSessionId, // 使用最新的会话ID - message: userMessage, - timestamp: timestamp.toISOString() - }, - timeout: 10000 // 10秒超时 - }) - .then(response => { - if (response.data.status === "success") { - // 保存当前请求ID,用于识别响应 - const currentRequestId = response.data.request_id; - - // 如果还没有开始轮询,则开始轮询 - if (!window.isPolling) { - startPolling(currentSessionId); - } - - // 将请求ID和加载容器关联起来 - window.loadingContainers = window.loadingContainers || {}; - window.loadingContainers[currentRequestId] = loadingContainer; - - // 初始化请求的响应容器映射 - window.requestContainers = window.requestContainers || {}; - } else { - // 处理错误 - if (loadingContainer.parentNode) { - messagesDiv.removeChild(loadingContainer); - } - addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); - } - }) - .catch(error => { - console.error('Error sending message:', error); - // 移除加载消息 - if (loadingContainer.parentNode) { - messagesDiv.removeChild(loadingContainer); - } - // 显示错误消息 - if (error.code === 'ECONNABORTED') { - addBotMessage("请求超时,请再试一次吧。", new Date()); - } else { - addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); - } - }); - } - } - - // 修改轮询函数,确保正确处理多条回复 - function startPolling(sessionId) { - if (window.isPolling) return; - - window.isPolling = true; - console.log('Starting polling with session ID:', sessionId); - - function poll() { - if (!window.isPolling) return; - - // 如果页面已关闭或导航离开,停止轮询 - if (document.hidden) { - setTimeout(poll, 5000); // 页面不可见时降低轮询频率 - return; - } - - // 使用当前的会话ID,而不是闭包中的sessionId - const currentSessionId = window.sessionId || sessionId; - - axios({ - method: 'post', - url: '/poll', - data: { - session_id: currentSessionId - }, - timeout: 5000 - }) - .then(response => { - if (response.data.status === "success") { - if (response.data.has_content) { - console.log('Received response:', response.data); - - // 获取请求ID和内容 - const requestId = response.data.request_id; - const content = response.data.content; - const timestamp = new Date(response.data.timestamp * 1000); - - // 检查是否有对应的加载容器 - if (window.loadingContainers && window.loadingContainers[requestId]) { - // 移除加载容器 - const loadingContainer = window.loadingContainers[requestId]; - if (loadingContainer && loadingContainer.parentNode) { - messagesDiv.removeChild(loadingContainer); - } - - // 删除已处理的加载容器引用 - delete window.loadingContainers[requestId]; - } - - // 始终创建新的消息,无论是否是同一个请求的后续回复 - addBotMessage(content, timestamp, requestId); - - // 滚动到底部 - scrollToBottom(); - } - - // 继续轮询,使用原来的2秒间隔 - setTimeout(poll, 2000); - } else { - // 处理错误但继续轮询 - console.error('Error in polling response:', response.data.message); - setTimeout(poll, 3000); - } - }) - .catch(error => { - console.error('Error polling for response:', error); - // 出错后继续轮询,但间隔更长 - setTimeout(poll, 3000); - }); - } - - // 开始轮询 - poll(); - } - - // 添加机器人消息的函数 (保存到localStorage),增加requestId参数 - function addBotMessage(content, timestamp, requestId) { - // 显示消息 - displayBotMessage(content, timestamp, requestId); - - // 保存到localStorage - saveMessageToLocalStorage({ - role: 'assistant', - content: content, - timestamp: timestamp.getTime(), - requestId: requestId - }); - } - - // 修改显示机器人消息的函数,增加requestId参数 - function displayBotMessage(content, timestamp, requestId) { - const botContainer = document.createElement('div'); - botContainer.className = 'bot-container'; - - // 如果有requestId,将其存储在数据属性中 - if (requestId) { - botContainer.dataset.requestId = requestId; - } - - const messageContainer = document.createElement('div'); - messageContainer.className = 'message-container'; - - // 安全地格式化消息 - let formattedContent; - try { - formattedContent = formatMessage(content); - } catch (e) { - console.error('Error formatting bot message:', e); - formattedContent = `

${content.replace(/\n/g, '
')}

`; - } - - messageContainer.innerHTML = ` -
- + + + + + + + + + + + +
+ +
+ + + + +
+ Chat + + Chat +
+ +
+ + + + + + + + + + + +
+ + +
+ + + + +
+ +
+ +
+ CowAgent +

CowAgent

+

I can help you answer questions, manage your computer, create and execute skills, and keep growing through long-term memory.

+ +
+
+
+
+ +
+ System +
+

Show me the files in the workspace

+
+
+
+
+ +
+ Smart Task +
+

Remind me to check the server in 5 minutes

+
+
+
+
+ +
+ Coding +
+

Write a Python web scraper script

+
+
+
+
+ + +
+
+ + +
- `; - - botContainer.appendChild(messageContainer); - messagesDiv.appendChild(botContainer); - scrollToBottom(); - - return botContainer; - } - // 自动将链接设置为在新标签页打开 - const externalLinksPlugin = (md) => { - // 保存原始的链接渲染器 - const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { - return self.renderToken(tokens, idx, options); - }; + + + +
+
+
+
+
+

Configuration

+

Manage model and agent settings

+
+
+
+ +
+
+
+ +
+

Model Configuration

+
+
+
+ Model + -- +
+
+ API Base + -- +
+
+
+ +
+
+
+ +
+

Agent Configuration

+
+
+
+ Agent Mode + -- +
+
+ Max Tokens + -- +
+
+ Max Turns + -- +
+
+ Max Steps + -- +
+
+
+ +
+
+
+ +
+

Channel Configuration

+
+
+
+ Channel Type + -- +
+
+
+
+ +
+ + Full editing capability coming soon. Currently displaying read-only configuration. +
+
+
+
- // 重写链接渲染器 - md.renderer.rules.link_open = function(tokens, idx, options, env, self) { - // 为所有链接添加 target="_blank" 和 rel="noopener noreferrer" - const token = tokens[idx]; - - // 添加 target="_blank" 属性 - token.attrPush(['target', '_blank']); - - // 添加 rel="noopener noreferrer" 以提高安全性 - token.attrPush(['rel', 'noopener noreferrer']); - - // 调用默认渲染器 - return defaultRender(tokens, idx, options, env, self); - }; - }; + + + +
+
+
+
+
+

Skills

+

View, enable, or disable agent skills

+
+
+
+
+ +
+

Loading skills...

+

Skills will be displayed here after loading

+
+
+
+
+
- // 替换 formatMessage 函数,使用 markdown-it 替代 marked - function formatMessage(content) { - try { - // 初始化 markdown-it 实例 - const md = window.markdownit({ - html: false, // 禁用 HTML 标签 - xhtmlOut: false, // 使用 '/' 关闭单标签 - breaks: true, // 将换行符转换为
- linkify: true, // 自动将 URL 转换为链接 - typographer: true, // 启用一些语言中性的替换和引号美化 - highlight: function(str, lang) { - if (lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(str, { language: lang }).value; - } catch (e) { - console.error('Error highlighting code:', e); - } - } - return hljs.highlightAuto(str).value; - } - }); + + + +
+
+
+
+
+

Memory

+

View agent memory files and contents

+
+
+
+
+ +
+

Loading memory files...

+

Memory files will be displayed here

+
+ +
+
+
- // 自动将图片URL转换为图片标签 - const autoImagePlugin = (md) => { - const defaultRender = md.renderer.rules.text || function(tokens, idx, options, env, self) { - return self.renderToken(tokens, idx, options); - }; + + + +
+
+
+
+
+

Channels

+

View and manage messaging channels

+
+
+
+
+ +
+

Coming Soon

+

Channel management will be available here

+
+
+
+
- md.renderer.rules.text = function(tokens, idx, options, env, self) { - const token = tokens[idx]; - const text = token.content.trim(); - - // 检测是否完全是一个图片链接 (以https://开头,以图片扩展名结尾) - const imageRegex = /^https?:\/\/\S+\.(jpg|jpeg|png|gif|webp)(\?\S*)?$/i; - if (imageRegex.test(text)) { - return `Image`; - } - - // 使用默认渲染 - return defaultRender(tokens, idx, options, env, self); - }; - }; + + + +
+
+
+
+
+

Scheduled Tasks

+

View and manage scheduled tasks

+
+
+
+
+ +
+

Coming Soon

+

Scheduled task management will be available here

+
+
+
+
- // 应用插件 - md.use(autoImagePlugin); - - // 应用外部链接插件 - md.use(externalLinksPlugin); + + + +
+
+
+
+
+

Logs

+

Real-time log output (run.log)

+
+
+ +
+
+
+ + + +
+ run.log +
+
+ + Live +
+
+
+

Log streaming will be available here. Connects to run.log for real-time output similar to tail -f.

+
+
+
+
+
- // 渲染 Markdown - return md.render(content); - } catch (e) { - console.error('Error parsing markdown:', e); - // 如果解析失败,至少确保换行符正确显示 - return content.replace(/\n/g, '
'); - } - } +
+
+
- // 更新 applyHighlighting 函数 - function applyHighlighting() { - try { - document.querySelectorAll('pre code').forEach((block) => { - // 确保代码块有正确的类 - if (!block.classList.contains('hljs')) { - block.classList.add('hljs'); - } - - // 尝试获取语言 - let language = ''; - block.classList.forEach(cls => { - if (cls.startsWith('language-')) { - language = cls.replace('language-', ''); - } - }); - - // 应用高亮 - if (language && hljs.getLanguage(language)) { - try { - hljs.highlightBlock(block); - } catch (e) { - console.error('Error highlighting specific language:', e); - hljs.highlightAuto(block); - } - } else { - hljs.highlightAuto(block); - } - }); - } catch (e) { - console.error('Error applying code highlighting:', e); - } - } - - // 在 #main-content 上添加点击事件,用于关闭侧边栏 - document.getElementById('main-content').addEventListener('click', function(event) { - // 只在移动视图下且侧边栏打开时处理 - if (window.innerWidth <= 768 && sidebar.classList.contains('active')) { - sidebar.classList.remove('active'); - } - }); - - // 阻止侧边栏内部点击事件冒泡到 main-content - document.getElementById('sidebar').addEventListener('click', function(event) { - event.stopPropagation(); - }); - - // 添加遮罩层点击事件,用于关闭侧边栏 - document.getElementById('sidebar-overlay').addEventListener('click', function() { - if (sidebar.classList.contains('active')) { - sidebar.classList.remove('active'); - } - }); - + - \ No newline at end of file + diff --git a/channel/web/static/css/console.css b/channel/web/static/css/console.css new file mode 100644 index 0000000..3775b5d --- /dev/null +++ b/channel/web/static/css/console.css @@ -0,0 +1,99 @@ +/* ===================================================================== + CowAgent Console Styles + ===================================================================== */ + +/* Animations */ +@keyframes pulseDot { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } +} + +/* Scrollbar */ +* { scrollbar-width: thin; scrollbar-color: #94a3b8 transparent; } +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #64748b; } +.dark ::-webkit-scrollbar-thumb { background: #475569; } +.dark ::-webkit-scrollbar-thumb:hover { background: #64748b; } + +/* Sidebar */ +.sidebar-item.active { + background: rgba(255, 255, 255, 0.08); + color: #FFFFFF; +} +.sidebar-item.active .item-icon { color: #4ABE6E; } + +/* Menu Groups */ +.menu-group-items { max-height: 0; overflow: hidden; transition: max-height 0.25s ease-out; } +.menu-group.open .menu-group-items { max-height: 500px; transition: max-height 0.35s ease-in; } +.menu-group .chevron { transition: transform 0.25s ease; } +.menu-group.open .chevron { transform: rotate(90deg); } + +/* View Switching */ +.view { display: none; height: 100%; } +.view.active { display: flex; flex-direction: column; } + +/* Markdown Content */ +.msg-content p { margin: 0.5em 0; line-height: 1.7; } +.msg-content p:first-child { margin-top: 0; } +.msg-content p:last-child { margin-bottom: 0; } +.msg-content h1, .msg-content h2, .msg-content h3, +.msg-content h4, .msg-content h5, .msg-content h6 { + margin-top: 1.2em; margin-bottom: 0.6em; font-weight: 600; line-height: 1.3; +} +.msg-content h1 { font-size: 1.4em; } +.msg-content h2 { font-size: 1.25em; } +.msg-content h3 { font-size: 1.1em; } +.msg-content ul, .msg-content ol { margin: 0.5em 0; padding-left: 1.8em; } +.msg-content li { margin: 0.25em 0; } +.msg-content pre { + border-radius: 8px; overflow-x: auto; margin: 0.8em 0; + background: #f1f5f9; padding: 1em; +} +.dark .msg-content pre { background: #111111; } +.msg-content code { + font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; + font-size: 0.875em; +} +.msg-content :not(pre) > code { + background: rgba(74, 190, 110, 0.1); color: #1C6B3B; + padding: 2px 6px; border-radius: 4px; +} +.dark .msg-content :not(pre) > code { + background: rgba(74, 190, 110, 0.15); color: #74E9A4; +} +.msg-content pre code { background: transparent; padding: 0; color: inherit; } +.msg-content blockquote { + border-left: 3px solid #4ABE6E; padding: 0.5em 1em; + margin: 0.8em 0; background: rgba(74, 190, 110, 0.05); border-radius: 0 6px 6px 0; +} +.dark .msg-content blockquote { background: rgba(74, 190, 110, 0.08); } +.msg-content table { border-collapse: collapse; width: 100%; margin: 0.8em 0; } +.msg-content th, .msg-content td { + border: 1px solid #e2e8f0; padding: 8px 12px; text-align: left; +} +.dark .msg-content th, .dark .msg-content td { border-color: rgba(255,255,255,0.1); } +.msg-content th { background: #f1f5f9; font-weight: 600; } +.dark .msg-content th { background: #111111; } +.msg-content img { max-width: 100%; height: auto; border-radius: 8px; margin: 0.5em 0; } +.msg-content a { color: #35A85B; text-decoration: underline; } +.msg-content a:hover { color: #228547; } +.msg-content hr { border: none; height: 1px; background: #e2e8f0; margin: 1.2em 0; } +.dark .msg-content hr { background: rgba(255,255,255,0.1); } + +/* Chat Input */ +#chat-input { + resize: none; height: 42px; max-height: 180px; + overflow-y: hidden; + transition: border-color 0.2s ease; +} + +/* Placeholder Cards */ +.placeholder-card { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} +.placeholder-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1); +} diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js new file mode 100644 index 0000000..372a0c9 --- /dev/null +++ b/channel/web/static/js/console.js @@ -0,0 +1,512 @@ +/* ===================================================================== + CowAgent Console - Main Application Script + ===================================================================== */ + +// ===================================================================== +// i18n +// ===================================================================== +const I18N = { + zh: { + console: '控制台', + nav_chat: '对话', nav_manage: '管理', nav_monitor: '监控', + menu_chat: '对话', menu_config: '配置', menu_skills: '技能', + menu_memory: '记忆', menu_channels: '通道', menu_tasks: '定时任务', + menu_logs: '日志', + welcome_subtitle: '我可以帮你解答问题、管理计算机、创造和执行技能,并通过长期记忆不断成长', + example_sys_title: '系统管理', example_sys_text: '帮我查看工作空间里有哪些文件', + example_task_title: '智能任务', example_task_text: '提醒我5分钟后查看服务器情况', + example_code_title: '编程助手', example_code_text: '帮我编写一个Python爬虫脚本', + input_placeholder: '输入消息...', + config_title: '配置管理', config_desc: '管理模型和 Agent 配置', + config_model: '模型配置', config_agent: 'Agent 配置', + config_channel: '通道配置', + config_agent_enabled: 'Agent 模式', config_max_tokens: '最大 Token', + config_max_turns: '最大轮次', config_max_steps: '最大步数', + config_channel_type: '通道类型', + config_coming_soon: '完整编辑功能即将推出,当前为只读展示。', + skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 技能', + skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处', + memory_title: '记忆管理', memory_desc: '查看 Agent 记忆文件和内容', + memory_loading: '加载记忆文件中...', memory_loading_desc: '记忆文件将显示在此处', + memory_col_name: '文件名', memory_col_type: '类型', memory_col_size: '大小', memory_col_updated: '更新时间', + channels_title: '通道管理', channels_desc: '查看和管理消息通道', + channels_coming: '即将推出', channels_coming_desc: '通道管理功能即将在此提供', + tasks_title: '定时任务', tasks_desc: '查看和管理定时任务', + tasks_coming: '即将推出', tasks_coming_desc: '定时任务管理功能即将在此提供', + logs_title: '日志', logs_desc: '实时日志输出 (run.log)', + logs_live: '实时', logs_coming_msg: '日志流即将在此提供。将连接 run.log 实现类似 tail -f 的实时输出。', + error_send: '发送失败,请稍后再试。', error_timeout: '请求超时,请再试一次。', + }, + en: { + console: 'Console', + nav_chat: 'Chat', nav_manage: 'Management', nav_monitor: 'Monitor', + menu_chat: 'Chat', menu_config: 'Config', menu_skills: 'Skills', + menu_memory: 'Memory', menu_channels: 'Channels', menu_tasks: 'Tasks', + menu_logs: 'Logs', + welcome_subtitle: 'I can help you answer questions, manage your computer, create and execute skills, and keep growing through long-term memory.', + example_sys_title: 'System', example_sys_text: 'Show me the files in the workspace', + example_task_title: 'Smart Task', example_task_text: 'Remind me to check the server in 5 minutes', + example_code_title: 'Coding', example_code_text: 'Write a Python web scraper script', + input_placeholder: 'Type a message...', + config_title: 'Configuration', config_desc: 'Manage model and agent settings', + config_model: 'Model Configuration', config_agent: 'Agent Configuration', + config_channel: 'Channel Configuration', + config_agent_enabled: 'Agent Mode', config_max_tokens: 'Max Tokens', + config_max_turns: 'Max Turns', config_max_steps: 'Max Steps', + config_channel_type: 'Channel Type', + config_coming_soon: 'Full editing capability coming soon. Currently displaying read-only configuration.', + skills_title: 'Skills', skills_desc: 'View, enable, or disable agent skills', + skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading', + memory_title: 'Memory', memory_desc: 'View agent memory files and contents', + memory_loading: 'Loading memory files...', memory_loading_desc: 'Memory files will be displayed here', + memory_col_name: 'Filename', memory_col_type: 'Type', memory_col_size: 'Size', memory_col_updated: 'Updated', + channels_title: 'Channels', channels_desc: 'View and manage messaging channels', + channels_coming: 'Coming Soon', channels_coming_desc: 'Channel management will be available here', + tasks_title: 'Scheduled Tasks', tasks_desc: 'View and manage scheduled tasks', + tasks_coming: 'Coming Soon', tasks_coming_desc: 'Scheduled task management will be available here', + logs_title: 'Logs', logs_desc: 'Real-time log output (run.log)', + logs_live: 'Live', logs_coming_msg: 'Log streaming will be available here. Connects to run.log for real-time output similar to tail -f.', + error_send: 'Failed to send. Please try again.', error_timeout: 'Request timeout. Please try again.', + } +}; + +let currentLang = localStorage.getItem('cow_lang') || 'zh'; + +function t(key) { + return (I18N[currentLang] && I18N[currentLang][key]) || (I18N.en[key]) || key; +} + +function applyI18n() { + document.querySelectorAll('[data-i18n]').forEach(el => { + el.textContent = t(el.dataset.i18n); + }); + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + el.placeholder = t(el.dataset['i18nPlaceholder']); + }); + document.getElementById('lang-label').textContent = currentLang === 'zh' ? 'EN' : '中文'; +} + +function toggleLanguage() { + currentLang = currentLang === 'zh' ? 'en' : 'zh'; + localStorage.setItem('cow_lang', currentLang); + applyI18n(); +} + +// ===================================================================== +// Theme +// ===================================================================== +let currentTheme = localStorage.getItem('cow_theme') || 'light'; + +function applyTheme() { + const root = document.documentElement; + if (currentTheme === 'dark') { + root.classList.add('dark'); + document.getElementById('theme-icon').className = 'fas fa-sun'; + document.getElementById('hljs-light').disabled = true; + document.getElementById('hljs-dark').disabled = false; + } else { + root.classList.remove('dark'); + document.getElementById('theme-icon').className = 'fas fa-moon'; + document.getElementById('hljs-light').disabled = false; + document.getElementById('hljs-dark').disabled = true; + } +} + +function toggleTheme() { + currentTheme = currentTheme === 'dark' ? 'light' : 'dark'; + localStorage.setItem('cow_theme', currentTheme); + applyTheme(); +} + +// ===================================================================== +// Sidebar & Navigation +// ===================================================================== +const VIEW_META = { + chat: { group: 'nav_chat', page: 'menu_chat' }, + config: { group: 'nav_manage', page: 'menu_config' }, + skills: { group: 'nav_manage', page: 'menu_skills' }, + memory: { group: 'nav_manage', page: 'menu_memory' }, + channels: { group: 'nav_manage', page: 'menu_channels' }, + tasks: { group: 'nav_manage', page: 'menu_tasks' }, + logs: { group: 'nav_monitor', page: 'menu_logs' }, +}; + +let currentView = 'chat'; + +function navigateTo(viewId) { + if (!VIEW_META[viewId]) return; + document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); + const target = document.getElementById('view-' + viewId); + if (target) target.classList.add('active'); + document.querySelectorAll('.sidebar-item').forEach(item => { + item.classList.toggle('active', item.dataset.view === viewId); + }); + const meta = VIEW_META[viewId]; + document.getElementById('breadcrumb-group').textContent = t(meta.group); + document.getElementById('breadcrumb-group').dataset.i18n = meta.group; + document.getElementById('breadcrumb-page').textContent = t(meta.page); + document.getElementById('breadcrumb-page').dataset.i18n = meta.page; + currentView = viewId; + if (window.innerWidth < 1024) closeSidebar(); +} + +function toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebar-overlay'); + const isOpen = !sidebar.classList.contains('-translate-x-full'); + if (isOpen) { + closeSidebar(); + } else { + sidebar.classList.remove('-translate-x-full'); + overlay.classList.remove('hidden'); + } +} + +function closeSidebar() { + document.getElementById('sidebar').classList.add('-translate-x-full'); + document.getElementById('sidebar-overlay').classList.add('hidden'); +} + +document.querySelectorAll('.menu-group > button').forEach(btn => { + btn.addEventListener('click', () => { + btn.parentElement.classList.toggle('open'); + }); +}); + +document.querySelectorAll('.sidebar-item').forEach(item => { + item.addEventListener('click', () => navigateTo(item.dataset.view)); +}); + +window.addEventListener('resize', () => { + if (window.innerWidth >= 1024) { + document.getElementById('sidebar').classList.remove('-translate-x-full'); + document.getElementById('sidebar-overlay').classList.add('hidden'); + } else { + if (!document.getElementById('sidebar').classList.contains('-translate-x-full')) { + closeSidebar(); + } + } +}); + +// ===================================================================== +// Markdown Renderer +// ===================================================================== +function createMd() { + const md = window.markdownit({ + html: false, breaks: true, linkify: true, typographer: true, + highlight: function(str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { return hljs.highlight(str, { language: lang }).value; } catch (_) {} + } + return hljs.highlightAuto(str).value; + } + }); + const defaultLinkOpen = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + md.renderer.rules.link_open = function(tokens, idx, options, env, self) { + tokens[idx].attrPush(['target', '_blank']); + tokens[idx].attrPush(['rel', 'noopener noreferrer']); + return defaultLinkOpen(tokens, idx, options, env, self); + }; + return md; +} + +const md = createMd(); + +function renderMarkdown(text) { + try { return md.render(text); } + catch (e) { return text.replace(/\n/g, '
'); } +} + +// ===================================================================== +// Chat Module +// ===================================================================== +let sessionId = generateSessionId(); +let isPolling = false; +let loadingContainers = {}; +let isComposing = false; +let appConfig = { use_agent: false, title: 'CowAgent', subtitle: '' }; + +function generateSessionId() { + return 'session_' + ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} + +fetch('/config').then(r => r.json()).then(data => { + if (data.status === 'success') { + appConfig = data; + const title = data.title || 'CowAgent'; + document.getElementById('welcome-title').textContent = title; + document.getElementById('cfg-model').textContent = data.model || '--'; + document.getElementById('cfg-api-base').textContent = data.open_ai_api_base || '--'; + document.getElementById('cfg-agent').textContent = data.use_agent ? 'Enabled' : 'Disabled'; + document.getElementById('cfg-max-tokens').textContent = data.agent_max_context_tokens || '--'; + document.getElementById('cfg-max-turns').textContent = data.agent_max_context_turns || '--'; + document.getElementById('cfg-max-steps').textContent = data.agent_max_steps || '--'; + document.getElementById('cfg-channel').textContent = data.channel_type || '--'; + } +}).catch(() => {}); + +const chatInput = document.getElementById('chat-input'); +const sendBtn = document.getElementById('send-btn'); +const messagesDiv = document.getElementById('chat-messages'); + +chatInput.addEventListener('compositionstart', () => { isComposing = true; }); +chatInput.addEventListener('compositionend', () => { isComposing = false; }); + +chatInput.addEventListener('input', function() { + this.style.height = '42px'; + const scrollH = this.scrollHeight; + const newH = Math.min(scrollH, 180); + this.style.height = newH + 'px'; + this.style.overflowY = scrollH > 180 ? 'auto' : 'hidden'; + sendBtn.disabled = !this.value.trim(); +}); + +chatInput.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.shiftKey) && e.key === 'Enter') { + const start = this.selectionStart; + const end = this.selectionEnd; + this.value = this.value.substring(0, start) + '\n' + this.value.substring(end); + this.selectionStart = this.selectionEnd = start + 1; + this.dispatchEvent(new Event('input')); + e.preventDefault(); + } else if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !isComposing) { + sendMessage(); + e.preventDefault(); + } +}); + +document.querySelectorAll('.example-card').forEach(card => { + card.addEventListener('click', () => { + const textEl = card.querySelector('[data-i18n*="text"]'); + if (textEl) { + chatInput.value = textEl.textContent; + chatInput.dispatchEvent(new Event('input')); + chatInput.focus(); + } + }); +}); + +function sendMessage() { + const text = chatInput.value.trim(); + if (!text) return; + + const ws = document.getElementById('welcome-screen'); + if (ws) ws.remove(); + + const timestamp = new Date(); + addUserMessage(text, timestamp); + + const loadingEl = addLoadingIndicator(); + + chatInput.value = ''; + chatInput.style.height = '42px'; + chatInput.style.overflowY = 'hidden'; + sendBtn.disabled = true; + + fetch('/message', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId, message: text, timestamp: timestamp.toISOString() }) + }) + .then(r => r.json()) + .then(data => { + if (data.status === 'success') { + loadingContainers[data.request_id] = loadingEl; + if (!isPolling) startPolling(); + } else { + loadingEl.remove(); + addBotMessage(t('error_send'), new Date()); + } + }) + .catch(err => { + loadingEl.remove(); + addBotMessage(err.name === 'AbortError' ? t('error_timeout') : t('error_send'), new Date()); + }); +} + +function startPolling() { + if (isPolling) return; + isPolling = true; + + function poll() { + if (!isPolling) return; + if (document.hidden) { setTimeout(poll, 5000); return; } + + fetch('/poll', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }) + }) + .then(r => r.json()) + .then(data => { + if (data.status === 'success' && data.has_content) { + const rid = data.request_id; + if (loadingContainers[rid]) { + loadingContainers[rid].remove(); + delete loadingContainers[rid]; + } + addBotMessage(data.content, new Date(data.timestamp * 1000), rid); + scrollChatToBottom(); + } + setTimeout(poll, 2000); + }) + .catch(() => { setTimeout(poll, 3000); }); + } + poll(); +} + +function addUserMessage(content, timestamp) { + const el = document.createElement('div'); + el.className = 'flex justify-end px-4 sm:px-6 py-3'; + el.innerHTML = ` +
+
+ ${renderMarkdown(content)} +
+
${formatTime(timestamp)}
+
+ `; + messagesDiv.appendChild(el); + scrollChatToBottom(); +} + +function addBotMessage(content, timestamp, requestId) { + const el = document.createElement('div'); + el.className = 'flex gap-3 px-4 sm:px-6 py-3'; + if (requestId) el.dataset.requestId = requestId; + el.innerHTML = ` + CowAgent +
+
+ ${renderMarkdown(content)} +
+
${formatTime(timestamp)}
+
+ `; + messagesDiv.appendChild(el); + applyHighlighting(el); + scrollChatToBottom(); +} + +function addLoadingIndicator() { + const el = document.createElement('div'); + el.className = 'flex gap-3 px-4 sm:px-6 py-3'; + el.innerHTML = ` + CowAgent +
+
+ + + +
+
+ `; + messagesDiv.appendChild(el); + scrollChatToBottom(); + return el; +} + +function newChat() { + sessionId = generateSessionId(); + isPolling = false; + loadingContainers = {}; + messagesDiv.innerHTML = ''; + const ws = document.createElement('div'); + ws.id = 'welcome-screen'; + ws.className = 'flex flex-col items-center justify-center h-full px-6 py-12'; + ws.innerHTML = ` + CowAgent +

${appConfig.title || 'CowAgent'}

+

${t('welcome_subtitle')}

+
+
+
+
+ +
+ ${t('example_sys_title')} +
+

${t('example_sys_text')}

+
+
+
+
+ +
+ ${t('example_task_title')} +
+

${t('example_task_text')}

+
+
+
+
+ +
+ ${t('example_code_title')} +
+

${t('example_code_text')}

+
+
+ `; + messagesDiv.appendChild(ws); + ws.querySelectorAll('.example-card').forEach(card => { + card.addEventListener('click', () => { + const textEl = card.querySelector('[data-i18n*="text"]'); + if (textEl) { + chatInput.value = textEl.textContent; + chatInput.dispatchEvent(new Event('input')); + chatInput.focus(); + } + }); + }); + if (currentView !== 'chat') navigateTo('chat'); +} + +// ===================================================================== +// Utilities +// ===================================================================== +function formatTime(date) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +function scrollChatToBottom() { + messagesDiv.scrollTop = messagesDiv.scrollHeight; +} + +function applyHighlighting(container) { + const root = container || document; + setTimeout(() => { + root.querySelectorAll('pre code').forEach(block => { + if (!block.classList.contains('hljs')) { + hljs.highlightElement(block); + } + }); + }, 0); +} + +// ===================================================================== +// Config View +// ===================================================================== +function loadConfigView() { + fetch('/config').then(r => r.json()).then(data => { + if (data.status !== 'success') return; + document.getElementById('cfg-model').textContent = data.model || '--'; + document.getElementById('cfg-api-base').textContent = data.open_ai_api_base || '--'; + document.getElementById('cfg-agent').textContent = data.use_agent ? 'Enabled' : 'Disabled'; + document.getElementById('cfg-max-tokens').textContent = data.agent_max_context_tokens || '--'; + document.getElementById('cfg-max-turns').textContent = data.agent_max_context_turns || '--'; + document.getElementById('cfg-max-steps').textContent = data.agent_max_steps || '--'; + document.getElementById('cfg-channel').textContent = data.channel_type || '--'; + }).catch(() => {}); +} + +// ===================================================================== +// Initialization +// ===================================================================== +applyTheme(); +applyI18n(); +chatInput.focus(); diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 43c245e..2ba25ee 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -282,22 +282,26 @@ class ChatHandler: class ConfigHandler: def GET(self): - """返回前端需要的配置信息""" + """Return configuration info for the web console.""" try: - use_agent = conf().get("agent", False) - + local_config = conf() + use_agent = local_config.get("agent", False) + if use_agent: title = "CowAgent" - subtitle = "我可以帮你解答问题、管理计算机、创造和执行技能,并通过长期记忆不断成长" else: - title = "AI 助手" - subtitle = "我可以回答问题、提供信息或者帮助您完成各种任务" - + title = "AI Assistant" + return json.dumps({ "status": "success", "use_agent": use_agent, "title": title, - "subtitle": subtitle + "model": local_config.get("model", ""), + "open_ai_api_base": local_config.get("open_ai_api_base", ""), + "channel_type": local_config.get("channel_type", ""), + "agent_max_context_tokens": local_config.get("agent_max_context_tokens", ""), + "agent_max_context_turns": local_config.get("agent_max_context_turns", ""), + "agent_max_steps": local_config.get("agent_max_steps", ""), }) except Exception as e: logger.error(f"Error getting config: {e}")