feat: web ui channel update

This commit is contained in:
Saboteur7
2025-05-18 16:56:50 +08:00
parent 8c8e996c87
commit 03fc8c1202
2 changed files with 435 additions and 222 deletions

View File

@@ -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;
}
</style>
</head>
<body>
@@ -530,12 +617,12 @@
</div>
<div class="example-card">
<div class="example-title">编程帮助</div>
<div class="example-text">如何用Python创建一个简单的网络爬虫</div>
<div class="example-text">如何用Python一个简单的网络爬虫</div>
</div>
<div class="example-card">
<!-- <div class="example-card">
<div class="example-title">生活建议</div>
<div class="example-text">推荐一些提高工作效率的方法</div>
</div>
</div> -->
</div>
</div>
<!-- 消息将在这里动态添加 -->
@@ -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 = `
<h1 id="welcome-title">AI 助手</h1>
<p id="welcome-subtitle">我可以回答问题、提供信息或者帮助您完成各种任务</p>
<div class="examples-container">
<div class="example-card">
<div class="example-title">解释复杂概念</div>
<div class="example-text">用简单的语言解释量子计算</div>
</div>
<div class="example-card">
<div class="example-title">创意写作</div>
<div class="example-text">写一个关于未来城市的短篇故事</div>
</div>
<div class="example-card">
<div class="example-title">编程帮助</div>
<div class="example-text">如何用Python写一个简单的网络爬虫</div>
</div>
</div>
`;
// 创建新会话
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 = `
<i class="far fa-comment"></i>
<span>${title}</span>
`;
// 点击加载对话
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 = `
<div class="avatar bot-avatar">
<i class="fas fa-robot"></i>
</div>
<div class="message-content">
<div class="message">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
`;
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, // 启用换行符转换为 <br>
gfm: true // 启用 GitHub 风格的 Markdown
breaks: true, // 启用换行符转换为 <br>
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, '<br>');
}
}
// 添加消息后应用代码高亮
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 = `<p>${content.replace(/\n/g, '<br>')}</p>`;
}
messageContainer.innerHTML = `
<div class="avatar user-avatar">
<i class="fas fa-user"></i>
</div>
<div class="message-content">
<div class="message">${formatMessage(content)}</div>
<div class="message">${formattedContent}</div>
<div class="timestamp">${formatTimestamp(timestamp)}</div>
</div>
`;
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 = `<p>${content.replace(/\n/g, '<br>')}</p>`;
}
messageContainer.innerHTML = `
<div class="avatar bot-avatar">
<i class="fas fa-robot"></i>
</div>
<div class="message-content">
<div class="message">${formatMessage(content)}</div>
<div class="message">${formattedContent}</div>
<div class="timestamp">${formatTimestamp(timestamp)}</div>
</div>
`;
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();
}

View File

@@ -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("<IMAGE>")
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: