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();
}