diff --git a/channel/web/chat.html b/channel/web/chat.html index 73881ca..5186a80 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -190,12 +190,19 @@ .bot-container { background-color: var(--bot-msg-bg); - border-bottom: 1px solid var(--border-color); + /* border-bottom: 1px solid var(--border-color); */ width: 100%; - margin: 0 -20px; + margin: 0; padding: 20px; } + .user-container { + width: 100%; + margin: 0; + padding: 20px; + border-bottom: 1px solid var(--border-color); + } + .avatar { width: 30px; height: 30px; @@ -222,12 +229,30 @@ flex: 1; line-height: 1.6; padding-top: 2px; + text-align: left; } .message { width: 100%; word-wrap: break-word; white-space: pre-wrap; + line-height: 1.3; + } + + .message p { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + .message p:empty { + margin: 0; + line-height: 0.7em; + } + + .message p:empty::before { + content: ""; + display: inline-block; + height: 0.7em; } .message pre { @@ -319,8 +344,9 @@ #send { position: absolute; + top: 0; right: 10px; - bottom: 10px; + height: 100%; background-color: transparent; border: none; color: var(--primary-color); @@ -330,7 +356,6 @@ align-items: center; justify-content: center; width: 32px; - height: 32px; border-radius: 4px; transition: background-color 0.2s; } @@ -484,6 +509,68 @@ color: #d4d4d4 !important; } } + + .typing-indicator { + display: inline-flex; + align-items: center; + margin-left: 0; + justify-content: flex-start; + width: auto; + position: relative; + top: -5px; + left: -10px; + } + + .typing-indicator span { + height: 8px; + width: 8px; + margin: 0 2px; + background-color: var(--text-light); + border-radius: 50%; + display: inline-block; + opacity: 0.4; + } + + .typing-indicator span:nth-child(1) { + animation: pulse 1s infinite; + } + + .typing-indicator span:nth-child(2) { + animation: pulse 1s infinite 0.2s; + } + + .typing-indicator span:nth-child(3) { + animation: pulse 1s infinite 0.4s; + } + + @keyframes pulse { + 0% { + opacity: 0.4; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.2); + } + 100% { + opacity: 0.4; + transform: scale(1); + } + } + + .history-divider { + padding: 10px; + color: var(--text-light); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 1px; + margin-top: 10px; + } + + .history-item.active { + background-color: rgba(255, 255, 255, 0.1); + font-weight: bold; + } @@ -530,12 +617,12 @@
编程帮助
-
如何用Python创建一个简单的网络爬虫?
+
如何用Python写一个简单的网络爬虫
-
+
@@ -564,41 +651,10 @@ const newChatButton = document.getElementById('new-chat'); const chatHistory = document.getElementById('chat-history'); - // 在页面顶部添加这些变量和函数 + // 简化变量,只保留用户ID let userId = 'user_' + Math.random().toString(36).substring(2, 10); - let currentSessionId = 'session_' + Date.now(); + let currentSessionId = 'default_session'; // 使用固定会话ID - // 轮询获取消息 - function pollMessages() { - fetch(`/poll/${userId}`) - .then(response => response.json()) - .then(messages => { - if (messages && messages.length > 0) { - console.log('Received messages via polling:', messages); - - // 隐藏欢迎屏幕 - document.getElementById('welcome-screen').style.display = 'none'; - - // 处理每条消息 - messages.forEach(message => { - addBotMessage(message.content, new Date(message.timestamp * 1000)); - }); - } - - // 继续轮询 - setTimeout(pollMessages, 1000); - }) - .catch(error => { - console.error('Polling error:', error); - setTimeout(pollMessages, 3000); - }); - } - - // 启动轮询 - document.addEventListener('DOMContentLoaded', function() { - pollMessages(); - }); - // 自动调整文本区域高度 input.addEventListener('input', function() { this.style.height = 'auto'; @@ -623,60 +679,127 @@ sidebar.classList.toggle('active'); }); - // 处理新对话按钮 + // 处理新对话按钮 - 创建新的用户ID和清空当前对话 newChatButton.addEventListener('click', function() { + // 生成新的用户ID + userId = 'user_' + Math.random().toString(36).substring(2, 10); + console.log('New conversation started with user ID:', userId); + + // 清空聊天记录 + clearChat(); + }); + + // 清空聊天记录并显示欢迎屏幕 + function clearChat() { // 清空消息区域 - while (messagesDiv.firstChild) { - if (messagesDiv.firstChild.id === 'welcome-screen') { - break; - } - messagesDiv.removeChild(messagesDiv.firstChild); - } + messagesDiv.innerHTML = ''; - // 显示欢迎屏幕 - welcomeScreen.style.display = 'flex'; + // 创建欢迎屏幕 + const newWelcomeScreen = document.createElement('div'); + newWelcomeScreen.id = 'welcome-screen'; + newWelcomeScreen.innerHTML = ` +

AI 助手

+

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

+ +
+
+
解释复杂概念
+
用简单的语言解释量子计算
+
+
+
创意写作
+
写一个关于未来城市的短篇故事
+
+
+
编程帮助
+
如何用Python写一个简单的网络爬虫
+
+
+ `; - // 创建新会话 - currentSessionId = 'session_' + Date.now(); + // 设置样式 + newWelcomeScreen.style.display = 'flex'; + newWelcomeScreen.style.flexDirection = 'column'; + newWelcomeScreen.style.alignItems = 'center'; + newWelcomeScreen.style.justifyContent = 'center'; + newWelcomeScreen.style.height = '100%'; + newWelcomeScreen.style.textAlign = 'center'; + newWelcomeScreen.style.padding = '20px'; + + // 添加到DOM + messagesDiv.appendChild(newWelcomeScreen); + + // 绑定示例卡片事件 + newWelcomeScreen.querySelectorAll('.example-card').forEach(card => { + card.addEventListener('click', function() { + const exampleText = this.querySelector('.example-text').textContent; + input.value = exampleText; + input.dispatchEvent(new Event('input')); + input.focus(); + }); + }); + + // 清空localStorage中的消息 - 使用用户ID作为键 + localStorage.setItem(`chatMessages_${userId}`, JSON.stringify([])); // 在移动设备上关闭侧边栏 if (window.innerWidth <= 768) { sidebar.classList.remove('active'); } - - // 添加到历史记录 - addToHistory('新对话', currentSessionId); - }); + } - // 添加到历史记录 - function addToHistory(title, sessionId) { - const historyItem = document.createElement('div'); - historyItem.className = 'history-item'; - historyItem.dataset.sessionId = sessionId; - historyItem.innerHTML = ` - - ${title} - `; - - // 点击加载对话 - historyItem.addEventListener('click', function() { - // 这里可以实现加载历史对话的功能 - // 在实际应用中,您需要存储和检索历史消息 - - // 在移动设备上关闭侧边栏 - if (window.innerWidth <= 768) { - sidebar.classList.remove('active'); - } - }); - - // 添加到历史记录顶部 - if (chatHistory.firstChild) { - chatHistory.insertBefore(historyItem, chatHistory.firstChild); - } else { - chatHistory.appendChild(historyItem); + // 从localStorage加载消息 - 使用用户ID作为键 + function loadMessagesFromLocalStorage() { + try { + return JSON.parse(localStorage.getItem(`chatMessages_${userId}`) || '[]'); + } catch (error) { + console.error('Error loading messages from localStorage:', error); + return []; } } + // 保存消息到localStorage - 使用用户ID作为键 + function saveMessageToLocalStorage(message) { + try { + const messages = loadMessagesFromLocalStorage(); + messages.push(message); + localStorage.setItem(`chatMessages_${userId}`, JSON.stringify(messages)); + } catch (error) { + console.error('Error saving message to localStorage:', error); + } + } + + // 初始化代码 + document.addEventListener('DOMContentLoaded', function() { + // 移除原始欢迎屏幕 + const originalWelcomeScreen = document.getElementById('welcome-screen'); + if (originalWelcomeScreen) { + originalWelcomeScreen.remove(); + } + + // 清空消息区域,确保不会重复显示消息 + messagesDiv.innerHTML = ''; + + // 加载消息 + const messages = loadMessagesFromLocalStorage(); + + if (messages.length === 0) { + // 如果没有消息,显示欢迎屏幕 + clearChat(); + } else { + // 显示现有消息 + messages.forEach(msg => { + if (msg.role === 'user') { + // 使用不保存到localStorage的版本显示消息 + displayUserMessage(msg.content, new Date(msg.timestamp)); + } else if (msg.role === 'assistant') { + // 使用不保存到localStorage的版本显示消息 + displayBotMessage(msg.content, new Date(msg.timestamp)); + } + }); + } + }); + // 发送按钮点击事件 sendButton.onclick = function() { sendMessage(); @@ -707,14 +830,20 @@ const userMessage = input.value.trim(); if (userMessage) { // 隐藏欢迎屏幕 - welcomeScreen.style.display = 'none'; + const welcomeScreenElement = document.getElementById('welcome-screen'); + if (welcomeScreenElement) { + welcomeScreenElement.remove(); + } const timestamp = new Date(); // 添加用户消息到界面 addUserMessage(userMessage, timestamp); - // 发送到服务器 + // 添加一个等待中的机器人消息 + const loadingContainer = addLoadingMessage(); + + // 发送到服务器并等待响应 fetch('/message', { method: 'POST', headers: { @@ -726,80 +855,194 @@ timestamp: timestamp.toISOString(), session_id: currentSessionId }) - }).then(response => { + }) + .then(response => { if (!response.ok) { - console.error('Failed to send message'); + throw new Error('Failed to send message'); } - }).catch(error => { + return response.json(); + }) + .then(data => { + // 移除加载消息 + if (loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + } + + // 添加AI回复 + if (data.reply) { + addBotMessage(data.reply, new Date()); + } + }) + .catch(error => { console.error('Error sending message:', error); + // 移除加载消息 + if (loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + } + // 显示错误消息 + addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); }); // 清空输入框并重置高度 input.value = ''; input.style.height = '52px'; sendButton.disabled = true; - - // 如果这是第一条消息,添加到历史记录 - const firstMessageInSession = !messagesDiv.querySelector('.message-container'); - if (firstMessageInSession) { - // 使用消息的前20个字符作为标题 - const title = userMessage.length > 20 ? - userMessage.substring(0, 20) + '...' : - userMessage; - addToHistory(title, currentSessionId); - } } } + // 添加加载中的消息 + function addLoadingMessage() { + const botContainer = document.createElement('div'); + botContainer.className = 'bot-container loading-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + messageContainer.innerHTML = ` +
+ +
+
+
+
+ + + +
+
+
+ `; + + botContainer.appendChild(messageContainer); + messagesDiv.appendChild(botContainer); + scrollToBottom(); + + return botContainer; + } + // 格式化消息内容(处理Markdown和代码高亮) function formatMessage(content) { // 配置 marked 以使用 highlight.js marked.setOptions({ - highlight: function(code, lang) { - if (lang && hljs.getLanguage(lang)) { - return hljs.highlight(code, { language: lang }).value; + highlight: function(code, language) { + if (language && hljs.getLanguage(language)) { + try { + return hljs.highlight(code, { language: language }).value; + } catch (e) { + console.error('Error highlighting code:', e); + return code; + } } - return hljs.highlightAuto(code).value; + return code; }, - breaks: true, // 启用换行符转换为
- gfm: true // 启用 GitHub 风格的 Markdown + breaks: true, // 启用换行符转换为
+ gfm: true, // 启用 GitHub 风格的 Markdown + headerIds: true, // 为标题生成ID + mangle: false, // 不转义内联HTML + sanitize: false, // 不净化输出 + smartLists: true, // 使用更智能的列表行为 + smartypants: false, // 不使用更智能的标点符号 + xhtml: false // 不使用自闭合标签 }); - // 使用 marked 解析 Markdown - return marked.parse(content); + try { + // 使用 marked 解析 Markdown + const parsed = marked.parse(content); + return parsed; + } catch (e) { + console.error('Error parsing markdown:', e); + // 如果解析失败,至少确保换行符正确显示 + return content.replace(/\n/g, '
'); + } } // 添加消息后应用代码高亮 function applyHighlighting() { - document.querySelectorAll('pre code').forEach((block) => { - hljs.highlightBlock(block); + try { + document.querySelectorAll('pre code').forEach((block) => { + // 手动应用高亮 + const language = block.className.replace('language-', ''); + if (language && hljs.getLanguage(language)) { + try { + hljs.highlightBlock(block); + } catch (e) { + console.error('Error highlighting block:', e); + } + } else { + hljs.highlightAuto(block); + } + }); + } catch (e) { + console.error('Error applying code highlighting:', e); + } + } + + // 添加用户消息的函数 (保存到localStorage) + function addUserMessage(content, timestamp) { + // 显示消息 + displayUserMessage(content, timestamp); + + // 保存到localStorage + saveMessageToLocalStorage({ + role: 'user', + content: content, + timestamp: timestamp.getTime() }); } - // 更新添加消息的函数 - function addUserMessage(content, timestamp) { + // 添加机器人消息的函数 (保存到localStorage) + function addBotMessage(content, timestamp) { + // 显示消息 + displayBotMessage(content, timestamp); + + // 保存到localStorage + saveMessageToLocalStorage({ + role: 'assistant', + content: content, + timestamp: timestamp.getTime() + }); + } + + // 只显示用户消息而不保存到localStorage + function displayUserMessage(content, timestamp) { + const userContainer = document.createElement('div'); + userContainer.className = 'user-container'; + const messageContainer = document.createElement('div'); messageContainer.className = 'message-container'; + // 安全地格式化消息 + let formattedContent; + try { + formattedContent = formatMessage(content); + } catch (e) { + console.error('Error formatting user message:', e); + formattedContent = `

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

`; + } + messageContainer.innerHTML = `
-
${formatMessage(content)}
+
${formattedContent}
${formatTimestamp(timestamp)}
`; - messagesDiv.appendChild(messageContainer); - applyHighlighting(); // 应用代码高亮 + userContainer.appendChild(messageContainer); + messagesDiv.appendChild(userContainer); + + // 应用代码高亮 + setTimeout(() => { + applyHighlighting(); + }, 0); + scrollToBottom(); } - // 添加机器人消息 - function addBotMessage(content, timestamp) { - console.log('Adding bot message:', content, timestamp); - + // 只显示机器人消息而不保存到localStorage + function displayBotMessage(content, timestamp) { const botContainer = document.createElement('div'); botContainer.className = 'bot-container'; @@ -811,19 +1054,64 @@ timestamp = new Date(); } + // 安全地格式化消息 + let formattedContent; + try { + formattedContent = formatMessage(content); + } catch (e) { + console.error('Error formatting bot message:', e); + formattedContent = `

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

`; + } + messageContainer.innerHTML = `
-
${formatMessage(content)}
+
${formattedContent}
${formatTimestamp(timestamp)}
`; botContainer.appendChild(messageContainer); messagesDiv.appendChild(botContainer); - applyHighlighting(); // 应用代码高亮 + + // 使用setTimeout确保DOM已更新,并延长等待时间 + setTimeout(() => { + try { + // 直接对新添加的消息应用高亮 + const codeBlocks = botContainer.querySelectorAll('pre code'); + codeBlocks.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 in delayed highlighting:', e); + } + }, 100); // 增加延迟以确保DOM完全更新 + scrollToBottom(); } diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index ee7b96d..c63571e 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -2,7 +2,7 @@ import sys import time import web import json -from queue import Queue +from queue import Queue, Empty from bridge.context import * from bridge.reply import Reply, ReplyType from channel.chat_channel import ChatChannel, check_prefix @@ -58,91 +58,29 @@ class WebChannel(ChatChannel): logger.warning(f"Web channel doesn't support {reply.type} yet") return - if reply.type == ReplyType.IMAGE: - from PIL import Image - - image_storage = reply.content - image_storage.seek(0) - img = Image.open(image_storage) - print("") - img.show() - elif reply.type == ReplyType.IMAGE_URL: - import io - - import requests - from PIL import Image - - img_url = reply.content - pic_res = requests.get(img_url, stream=True) - image_storage = io.BytesIO() - for block in pic_res.iter_content(1024): - image_storage.write(block) - image_storage.seek(0) - img = Image.open(image_storage) - print(img_url) - img.show() - else: - print(reply.content) - # 获取用户ID user_id = context.get("receiver", None) if not user_id: logger.error("No receiver found in context, cannot send message") return - # 确保用户有对应的消息队列 - if user_id not in self.message_queues: - self.message_queues[user_id] = Queue() - logger.debug(f"Created message queue for user {user_id}") - - # 将消息放入对应用户的队列 - message_data = { - "type": str(reply.type), - "content": reply.content, - "timestamp": time.time() # 使用 Unix 时间戳 - } - self.message_queues[user_id].put(message_data) - logger.debug(f"Message queued for user {user_id}: {reply.content[:30]}...") + # 检查是否有响应队列 + response_queue = context.get("response_queue", None) + if response_queue: + # 直接将响应放入队列 + response_data = { + "type": str(reply.type), + "content": reply.content, + "timestamp": time.time() + } + response_queue.put(response_data) + logger.debug(f"Response sent to queue for user {user_id}") + else: + logger.warning(f"No response queue found for user {user_id}, response dropped") except Exception as e: logger.error(f"Error in send method: {e}") - def sse_handler(self, user_id): - """ - Handle Server-Sent Events (SSE) for real-time communication. - """ - web.header('Content-Type', 'text/event-stream') - web.header('Cache-Control', 'no-cache') - web.header('Connection', 'keep-alive') - - logger.debug(f"SSE connection established for user {user_id}") - - # 确保用户有消息队列 - if user_id not in self.message_queues: - self.message_queues[user_id] = Queue() - logger.debug(f"Created new message queue for user {user_id}") - - try: - while True: - try: - # 发送心跳 - yield f": heartbeat\n\n" - - # 非阻塞方式获取消息 - if user_id in self.message_queues and not self.message_queues[user_id].empty(): - message = self.message_queues[user_id].get_nowait() - logger.debug(f"Sending message to user {user_id}: {message}") - data = json.dumps(message) - yield f"data: {data}\n\n" - logger.debug(f"Message sent to user {user_id}") - time.sleep(0.5) - except Exception as e: - logger.error(f"SSE Error for user {user_id}: {str(e)}") - break - finally: - # 清理资源 - logger.debug(f"SSE connection closed for user {user_id}") - def post_message(self): """ Handle incoming messages from users via POST request. @@ -167,7 +105,7 @@ class WebChannel(ChatChannel): msg_id=msg_id, content=prompt, from_user_id=user_id, - to_user_id="Chatgpt", # 明确指定接收者 + to_user_id="Chatgpt", other_user_id=user_id ) @@ -175,13 +113,24 @@ class WebChannel(ChatChannel): if not context: return json.dumps({"status": "error", "message": "Failed to process message"}) + # 创建一个响应队列 + response_queue = Queue() + # 确保上下文包含必要的信息 context["isgroup"] = False - context["receiver"] = user_id # 添加接收者信息,用于send方法中识别用户 - context["session_id"] = session_id # 添加会话ID + context["receiver"] = user_id + context["session_id"] = user_id + context["response_queue"] = response_queue + # 发送消息到处理队列 self.produce(context) - return json.dumps({"status": "success", "message": "Message received"}) + + # 等待响应,最多等待30秒 + try: + response = response_queue.get(timeout=30) + return json.dumps({"status": "success", "reply": response["content"]}) + except Empty: + return json.dumps({"status": "error", "message": "Response timeout"}) except Exception as e: logger.error(f"Error processing message: {e}") @@ -203,31 +152,14 @@ class WebChannel(ChatChannel): logger.info(f"Created static directory: {static_dir}") urls = ( - '/sse/(.+)', 'SSEHandler', - '/poll/(.+)', 'PollHandler', '/message', 'MessageHandler', '/chat', 'ChatHandler', - '/assets/(.*)', 'AssetsHandler', # 匹配 /static/任何路径 + '/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径 ) port = conf().get("web_port", 9899) app = web.application(urls, globals(), autoreload=False) web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) - def poll_messages(self, user_id): - """Poll for new messages.""" - messages = [] - - if user_id in self.message_queues: - while not self.message_queues[user_id].empty(): - messages.append(self.message_queues[user_id].get_nowait()) - - return json.dumps(messages) - - -class SSEHandler: - def GET(self, user_id): - return WebChannel().sse_handler(user_id) - class MessageHandler: def POST(self): @@ -242,13 +174,6 @@ class ChatHandler: return f.read() -# 添加轮询处理器 -class PollHandler: - def GET(self, user_id): - web.header('Content-Type', 'application/json') - return WebChannel().poll_messages(user_id) - - class AssetsHandler: def GET(self, file_path): # 修改默认参数 try: