mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 06:07:13 +08:00
feat: web ui channel update
This commit is contained in:
@@ -190,12 +190,19 @@
|
|||||||
|
|
||||||
.bot-container {
|
.bot-container {
|
||||||
background-color: var(--bot-msg-bg);
|
background-color: var(--bot-msg-bg);
|
||||||
border-bottom: 1px solid var(--border-color);
|
/* border-bottom: 1px solid var(--border-color); */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 -20px;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -222,12 +229,30 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
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 {
|
.message pre {
|
||||||
@@ -319,8 +344,9 @@
|
|||||||
|
|
||||||
#send {
|
#send {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
bottom: 10px;
|
height: 100%;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
@@ -330,7 +356,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
@@ -484,6 +509,68 @@
|
|||||||
color: #d4d4d4 !important;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -530,12 +617,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="example-card">
|
<div class="example-card">
|
||||||
<div class="example-title">编程帮助</div>
|
<div class="example-title">编程帮助</div>
|
||||||
<div class="example-text">如何用Python创建一个简单的网络爬虫?</div>
|
<div class="example-text">如何用Python写一个简单的网络爬虫</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="example-card">
|
<!-- <div class="example-card">
|
||||||
<div class="example-title">生活建议</div>
|
<div class="example-title">生活建议</div>
|
||||||
<div class="example-text">推荐一些提高工作效率的方法</div>
|
<div class="example-text">推荐一些提高工作效率的方法</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 消息将在这里动态添加 -->
|
<!-- 消息将在这里动态添加 -->
|
||||||
@@ -564,41 +651,10 @@
|
|||||||
const newChatButton = document.getElementById('new-chat');
|
const newChatButton = document.getElementById('new-chat');
|
||||||
const chatHistory = document.getElementById('chat-history');
|
const chatHistory = document.getElementById('chat-history');
|
||||||
|
|
||||||
// 在页面顶部添加这些变量和函数
|
// 简化变量,只保留用户ID
|
||||||
let userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
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() {
|
input.addEventListener('input', function() {
|
||||||
this.style.height = 'auto';
|
this.style.height = 'auto';
|
||||||
@@ -623,60 +679,127 @@
|
|||||||
sidebar.classList.toggle('active');
|
sidebar.classList.toggle('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理新对话按钮
|
// 处理新对话按钮 - 创建新的用户ID和清空当前对话
|
||||||
newChatButton.addEventListener('click', function() {
|
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) {
|
messagesDiv.innerHTML = '';
|
||||||
if (messagesDiv.firstChild.id === 'welcome-screen') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
messagesDiv.removeChild(messagesDiv.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示欢迎屏幕
|
// 创建欢迎屏幕
|
||||||
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) {
|
if (window.innerWidth <= 768) {
|
||||||
sidebar.classList.remove('active');
|
sidebar.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 添加到历史记录
|
|
||||||
addToHistory('新对话', currentSessionId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加到历史记录
|
// 从localStorage加载消息 - 使用用户ID作为键
|
||||||
function addToHistory(title, sessionId) {
|
function loadMessagesFromLocalStorage() {
|
||||||
const historyItem = document.createElement('div');
|
try {
|
||||||
historyItem.className = 'history-item';
|
return JSON.parse(localStorage.getItem(`chatMessages_${userId}`) || '[]');
|
||||||
historyItem.dataset.sessionId = sessionId;
|
} catch (error) {
|
||||||
historyItem.innerHTML = `
|
console.error('Error loading messages from localStorage:', error);
|
||||||
<i class="far fa-comment"></i>
|
return [];
|
||||||
<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 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() {
|
sendButton.onclick = function() {
|
||||||
sendMessage();
|
sendMessage();
|
||||||
@@ -707,14 +830,20 @@
|
|||||||
const userMessage = input.value.trim();
|
const userMessage = input.value.trim();
|
||||||
if (userMessage) {
|
if (userMessage) {
|
||||||
// 隐藏欢迎屏幕
|
// 隐藏欢迎屏幕
|
||||||
welcomeScreen.style.display = 'none';
|
const welcomeScreenElement = document.getElementById('welcome-screen');
|
||||||
|
if (welcomeScreenElement) {
|
||||||
|
welcomeScreenElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = new Date();
|
const timestamp = new Date();
|
||||||
|
|
||||||
// 添加用户消息到界面
|
// 添加用户消息到界面
|
||||||
addUserMessage(userMessage, timestamp);
|
addUserMessage(userMessage, timestamp);
|
||||||
|
|
||||||
// 发送到服务器
|
// 添加一个等待中的机器人消息
|
||||||
|
const loadingContainer = addLoadingMessage();
|
||||||
|
|
||||||
|
// 发送到服务器并等待响应
|
||||||
fetch('/message', {
|
fetch('/message', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -726,80 +855,194 @@
|
|||||||
timestamp: timestamp.toISOString(),
|
timestamp: timestamp.toISOString(),
|
||||||
session_id: currentSessionId
|
session_id: currentSessionId
|
||||||
})
|
})
|
||||||
}).then(response => {
|
})
|
||||||
|
.then(response => {
|
||||||
if (!response.ok) {
|
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);
|
console.error('Error sending message:', error);
|
||||||
|
// 移除加载消息
|
||||||
|
if (loadingContainer.parentNode) {
|
||||||
|
messagesDiv.removeChild(loadingContainer);
|
||||||
|
}
|
||||||
|
// 显示错误消息
|
||||||
|
addBotMessage("抱歉,发生了错误,请稍后再试。", new Date());
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空输入框并重置高度
|
// 清空输入框并重置高度
|
||||||
input.value = '';
|
input.value = '';
|
||||||
input.style.height = '52px';
|
input.style.height = '52px';
|
||||||
sendButton.disabled = true;
|
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和代码高亮)
|
// 格式化消息内容(处理Markdown和代码高亮)
|
||||||
function formatMessage(content) {
|
function formatMessage(content) {
|
||||||
// 配置 marked 以使用 highlight.js
|
// 配置 marked 以使用 highlight.js
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
highlight: function(code, lang) {
|
highlight: function(code, language) {
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
if (language && hljs.getLanguage(language)) {
|
||||||
return hljs.highlight(code, { language: lang }).value;
|
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>
|
breaks: true, // 启用换行符转换为 <br>
|
||||||
gfm: true // 启用 GitHub 风格的 Markdown
|
gfm: true, // 启用 GitHub 风格的 Markdown
|
||||||
|
headerIds: true, // 为标题生成ID
|
||||||
|
mangle: false, // 不转义内联HTML
|
||||||
|
sanitize: false, // 不净化输出
|
||||||
|
smartLists: true, // 使用更智能的列表行为
|
||||||
|
smartypants: false, // 不使用更智能的标点符号
|
||||||
|
xhtml: false // 不使用自闭合标签
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用 marked 解析 Markdown
|
try {
|
||||||
return marked.parse(content);
|
// 使用 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() {
|
function applyHighlighting() {
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
try {
|
||||||
hljs.highlightBlock(block);
|
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()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新添加消息的函数
|
// 添加机器人消息的函数 (保存到localStorage)
|
||||||
function addUserMessage(content, timestamp) {
|
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');
|
const messageContainer = document.createElement('div');
|
||||||
messageContainer.className = 'message-container';
|
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 = `
|
messageContainer.innerHTML = `
|
||||||
<div class="avatar user-avatar">
|
<div class="avatar user-avatar">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<div class="message">${formatMessage(content)}</div>
|
<div class="message">${formattedContent}</div>
|
||||||
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
messagesDiv.appendChild(messageContainer);
|
userContainer.appendChild(messageContainer);
|
||||||
applyHighlighting(); // 应用代码高亮
|
messagesDiv.appendChild(userContainer);
|
||||||
|
|
||||||
|
// 应用代码高亮
|
||||||
|
setTimeout(() => {
|
||||||
|
applyHighlighting();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加机器人消息
|
// 只显示机器人消息而不保存到localStorage
|
||||||
function addBotMessage(content, timestamp) {
|
function displayBotMessage(content, timestamp) {
|
||||||
console.log('Adding bot message:', content, timestamp);
|
|
||||||
|
|
||||||
const botContainer = document.createElement('div');
|
const botContainer = document.createElement('div');
|
||||||
botContainer.className = 'bot-container';
|
botContainer.className = 'bot-container';
|
||||||
|
|
||||||
@@ -811,19 +1054,64 @@
|
|||||||
timestamp = new Date();
|
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 = `
|
messageContainer.innerHTML = `
|
||||||
<div class="avatar bot-avatar">
|
<div class="avatar bot-avatar">
|
||||||
<i class="fas fa-robot"></i>
|
<i class="fas fa-robot"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<div class="message">${formatMessage(content)}</div>
|
<div class="message">${formattedContent}</div>
|
||||||
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
botContainer.appendChild(messageContainer);
|
botContainer.appendChild(messageContainer);
|
||||||
messagesDiv.appendChild(botContainer);
|
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();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import web
|
import web
|
||||||
import json
|
import json
|
||||||
from queue import Queue
|
from queue import Queue, Empty
|
||||||
from bridge.context import *
|
from bridge.context import *
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
from channel.chat_channel import ChatChannel, check_prefix
|
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")
|
logger.warning(f"Web channel doesn't support {reply.type} yet")
|
||||||
return
|
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
|
# 获取用户ID
|
||||||
user_id = context.get("receiver", None)
|
user_id = context.get("receiver", None)
|
||||||
if not user_id:
|
if not user_id:
|
||||||
logger.error("No receiver found in context, cannot send message")
|
logger.error("No receiver found in context, cannot send message")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 确保用户有对应的消息队列
|
# 检查是否有响应队列
|
||||||
if user_id not in self.message_queues:
|
response_queue = context.get("response_queue", None)
|
||||||
self.message_queues[user_id] = Queue()
|
if response_queue:
|
||||||
logger.debug(f"Created message queue for user {user_id}")
|
# 直接将响应放入队列
|
||||||
|
response_data = {
|
||||||
# 将消息放入对应用户的队列
|
"type": str(reply.type),
|
||||||
message_data = {
|
"content": reply.content,
|
||||||
"type": str(reply.type),
|
"timestamp": time.time()
|
||||||
"content": reply.content,
|
}
|
||||||
"timestamp": time.time() # 使用 Unix 时间戳
|
response_queue.put(response_data)
|
||||||
}
|
logger.debug(f"Response sent to queue for user {user_id}")
|
||||||
self.message_queues[user_id].put(message_data)
|
else:
|
||||||
logger.debug(f"Message queued for user {user_id}: {reply.content[:30]}...")
|
logger.warning(f"No response queue found for user {user_id}, response dropped")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in send method: {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):
|
def post_message(self):
|
||||||
"""
|
"""
|
||||||
Handle incoming messages from users via POST request.
|
Handle incoming messages from users via POST request.
|
||||||
@@ -167,7 +105,7 @@ class WebChannel(ChatChannel):
|
|||||||
msg_id=msg_id,
|
msg_id=msg_id,
|
||||||
content=prompt,
|
content=prompt,
|
||||||
from_user_id=user_id,
|
from_user_id=user_id,
|
||||||
to_user_id="Chatgpt", # 明确指定接收者
|
to_user_id="Chatgpt",
|
||||||
other_user_id=user_id
|
other_user_id=user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,13 +113,24 @@ class WebChannel(ChatChannel):
|
|||||||
if not context:
|
if not context:
|
||||||
return json.dumps({"status": "error", "message": "Failed to process message"})
|
return json.dumps({"status": "error", "message": "Failed to process message"})
|
||||||
|
|
||||||
|
# 创建一个响应队列
|
||||||
|
response_queue = Queue()
|
||||||
|
|
||||||
# 确保上下文包含必要的信息
|
# 确保上下文包含必要的信息
|
||||||
context["isgroup"] = False
|
context["isgroup"] = False
|
||||||
context["receiver"] = user_id # 添加接收者信息,用于send方法中识别用户
|
context["receiver"] = user_id
|
||||||
context["session_id"] = session_id # 添加会话ID
|
context["session_id"] = user_id
|
||||||
|
context["response_queue"] = response_queue
|
||||||
|
|
||||||
|
# 发送消息到处理队列
|
||||||
self.produce(context)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error processing message: {e}")
|
logger.error(f"Error processing message: {e}")
|
||||||
@@ -203,31 +152,14 @@ class WebChannel(ChatChannel):
|
|||||||
logger.info(f"Created static directory: {static_dir}")
|
logger.info(f"Created static directory: {static_dir}")
|
||||||
|
|
||||||
urls = (
|
urls = (
|
||||||
'/sse/(.+)', 'SSEHandler',
|
|
||||||
'/poll/(.+)', 'PollHandler',
|
|
||||||
'/message', 'MessageHandler',
|
'/message', 'MessageHandler',
|
||||||
'/chat', 'ChatHandler',
|
'/chat', 'ChatHandler',
|
||||||
'/assets/(.*)', 'AssetsHandler', # 匹配 /static/任何路径
|
'/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径
|
||||||
)
|
)
|
||||||
port = conf().get("web_port", 9899)
|
port = conf().get("web_port", 9899)
|
||||||
app = web.application(urls, globals(), autoreload=False)
|
app = web.application(urls, globals(), autoreload=False)
|
||||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
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:
|
class MessageHandler:
|
||||||
def POST(self):
|
def POST(self):
|
||||||
@@ -242,13 +174,6 @@ class ChatHandler:
|
|||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
# 添加轮询处理器
|
|
||||||
class PollHandler:
|
|
||||||
def GET(self, user_id):
|
|
||||||
web.header('Content-Type', 'application/json')
|
|
||||||
return WebChannel().poll_messages(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
class AssetsHandler:
|
class AssetsHandler:
|
||||||
def GET(self, file_path): # 修改默认参数
|
def GET(self, file_path): # 修改默认参数
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user