1062 lines
32 KiB
JavaScript
1062 lines
32 KiB
JavaScript
// 全局变量
|
||
let currentConversationId = null;
|
||
let currentUser = null; // 当前登录用户对象
|
||
let isStreaming = false;
|
||
let currentStreamResponse = null;
|
||
let currentStreamReader = null;
|
||
|
||
// API 基础 URL
|
||
const API_BASE_URL = '/';
|
||
|
||
// 认证状态
|
||
let isAuthenticated = false;
|
||
|
||
// DOM 元素 - 延迟获取,确保DOM已完全加载
|
||
let elements = {};
|
||
|
||
// 初始化应用
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 等待DOM完全加载后获取元素
|
||
setTimeout(() => {
|
||
initializeApp();
|
||
}, 100);
|
||
});
|
||
|
||
function initializeApp() {
|
||
// 获取DOM元素
|
||
getDOMElements();
|
||
|
||
// 绑定事件监听器
|
||
bindEventListeners();
|
||
|
||
// 检查本地存储中的登录状态
|
||
checkAuthStatus();
|
||
|
||
// 更新用户信息显示
|
||
updateUserInfoDisplay();
|
||
|
||
// 如果未登录,显示登录模态框
|
||
if (!isAuthenticated) {
|
||
showLoginModal();
|
||
} else {
|
||
// 设置当前登录用户名
|
||
if (elements.newChatLogin) {
|
||
elements.newChatLogin.value = currentUser.username;
|
||
}
|
||
// 加载对话列表
|
||
loadConversations();
|
||
}
|
||
}
|
||
|
||
function getDOMElements() {
|
||
elements = {
|
||
// 侧边栏
|
||
conversationsList: document.getElementById('conversationsList'),
|
||
newChatBtn: document.getElementById('newChatBtn'),
|
||
currentUsername: document.getElementById('currentUsername'),
|
||
currentUserRole: document.getElementById('currentUserRole'),
|
||
logoutBtn: document.getElementById('logoutBtn'),
|
||
notLoggedInInfo: document.getElementById('notLoggedInInfo'),
|
||
loggedInInfo: document.getElementById('loggedInInfo'),
|
||
quickLoginBtn: document.getElementById('quickLoginBtn'),
|
||
|
||
// 主聊天区域
|
||
welcomeScreen: document.getElementById('welcomeScreen'),
|
||
chatScreen: document.getElementById('chatScreen'),
|
||
chatTitle: document.getElementById('chatTitle'),
|
||
messagesContainer: document.getElementById('messagesContainer'),
|
||
messageInput: document.getElementById('messageInput'),
|
||
sendBtn: document.getElementById('sendBtn'),
|
||
typingIndicator: document.getElementById('typingIndicator'),
|
||
startChatBtn: document.getElementById('startChatBtn'),
|
||
|
||
// 模态框
|
||
deleteChatModal: document.getElementById('deleteChatModal'),
|
||
confirmDeleteChat: document.getElementById('confirmDeleteChat'),
|
||
|
||
// 登录/注册模态框
|
||
loginModal: document.getElementById('loginModal'),
|
||
loginForm: document.getElementById('loginForm'),
|
||
loginUsername: document.getElementById('loginUsername'),
|
||
loginPassword: document.getElementById('loginPassword'),
|
||
loginCloseBtn: document.getElementById('loginCloseBtn'),
|
||
showRegisterBtn: document.getElementById('showRegisterBtn'),
|
||
|
||
registerModal: document.getElementById('registerModal'),
|
||
registerForm: document.getElementById('registerForm'),
|
||
registerUsername: document.getElementById('registerUsername'),
|
||
registerPassword: document.getElementById('registerPassword'),
|
||
registerConfirmPassword: document.getElementById('registerConfirmPassword'),
|
||
registerCloseBtn: document.getElementById('registerCloseBtn'),
|
||
showLoginBtn: document.getElementById('showLoginBtn')
|
||
};
|
||
}
|
||
|
||
function bindEventListeners() {
|
||
// 侧边栏事件
|
||
if (elements.newChatBtn) {
|
||
elements.newChatBtn.addEventListener('click', createNewConversation);
|
||
}
|
||
if (elements.startChatBtn) {
|
||
elements.startChatBtn.addEventListener('click', createNewConversation);
|
||
}
|
||
|
||
// 消息输入事件
|
||
if (elements.messageInput) {
|
||
elements.messageInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
if (isStreaming) {
|
||
stopStreamResponse();
|
||
} else {
|
||
sendMessage();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
if (elements.sendBtn) {
|
||
elements.sendBtn.addEventListener('click', () => {
|
||
if (isStreaming) {
|
||
stopStreamResponse();
|
||
} else {
|
||
sendMessage();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 模态框事件(仅保留删除对话模态框)
|
||
if (elements.deleteChatModal) {
|
||
const deleteChatClose = elements.deleteChatModal.querySelector('.close-btn');
|
||
const deleteChatCancel = elements.deleteChatModal.querySelector('.cancel-btn');
|
||
if (deleteChatClose) deleteChatClose.addEventListener('click', hideDeleteChatModal);
|
||
if (deleteChatCancel) deleteChatCancel.addEventListener('click', hideDeleteChatModal);
|
||
}
|
||
|
||
// 登录/注册模态框事件
|
||
if (elements.loginModal) {
|
||
// 登录模态框关闭按钮
|
||
if (elements.loginCloseBtn) {
|
||
elements.loginCloseBtn.addEventListener('click', hideLoginModal);
|
||
}
|
||
// 切换到注册模态框
|
||
if (elements.showRegisterBtn) {
|
||
elements.showRegisterBtn.addEventListener('click', () => {
|
||
hideLoginModal();
|
||
showRegisterModal();
|
||
});
|
||
}
|
||
// 登录表单提交
|
||
if (elements.loginForm) {
|
||
elements.loginForm.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
handleLogin();
|
||
});
|
||
}
|
||
// 点击模态框外部关闭
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === elements.loginModal) hideLoginModal();
|
||
});
|
||
}
|
||
|
||
if (elements.registerModal) {
|
||
// 注册模态框关闭按钮
|
||
if (elements.registerCloseBtn) {
|
||
elements.registerCloseBtn.addEventListener('click', hideRegisterModal);
|
||
}
|
||
// 切换到登录模态框
|
||
if (elements.showLoginBtn) {
|
||
elements.showLoginBtn.addEventListener('click', () => {
|
||
hideRegisterModal();
|
||
showLoginModal();
|
||
});
|
||
}
|
||
// 注册表单提交
|
||
if (elements.registerForm) {
|
||
elements.registerForm.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
handleRegister();
|
||
});
|
||
}
|
||
// 点击模态框外部关闭
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === elements.registerModal) hideRegisterModal();
|
||
});
|
||
}
|
||
|
||
// 确认创建新对话
|
||
if (elements.confirmNewChat) {
|
||
elements.confirmNewChat.addEventListener('click', createNewConversation);
|
||
}
|
||
|
||
// 确认删除对话
|
||
if (elements.confirmDeleteChat) {
|
||
elements.confirmDeleteChat.addEventListener('click', deleteCurrentConversation);
|
||
}
|
||
|
||
// 聊天界面事件 - 已移除待开发功能按钮
|
||
|
||
// 注销按钮事件
|
||
if (elements.logoutBtn) {
|
||
elements.logoutBtn.addEventListener('click', handleLogout);
|
||
}
|
||
|
||
// 快速登录按钮事件
|
||
if (elements.quickLoginBtn) {
|
||
elements.quickLoginBtn.addEventListener('click', showLoginModal);
|
||
}
|
||
}
|
||
|
||
// 认证相关函数
|
||
|
||
// 检查本地存储中的认证状态
|
||
function checkAuthStatus() {
|
||
const storedUser = localStorage.getItem('chatUser');
|
||
if (storedUser) {
|
||
try {
|
||
currentUser = JSON.parse(storedUser);
|
||
isAuthenticated = true;
|
||
} catch (e) {
|
||
console.error('Failed to parse stored user:', e);
|
||
logout();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新用户信息显示
|
||
function updateUserInfoDisplay() {
|
||
// 检查元素是否存在
|
||
if (!elements.notLoggedInInfo || !elements.loggedInInfo || !elements.logoutBtn) {
|
||
return;
|
||
}
|
||
|
||
if (currentUser && isAuthenticated) {
|
||
// 用户已登录
|
||
if (elements.currentUsername) {
|
||
elements.currentUsername.textContent = currentUser.username;
|
||
}
|
||
if (elements.currentUserRole) {
|
||
elements.currentUserRole.textContent = '用户'; // 目前所有用户都显示为普通用户
|
||
}
|
||
|
||
// 显示登录状态,隐藏未登录状态
|
||
elements.notLoggedInInfo.style.display = 'none';
|
||
elements.loggedInInfo.style.display = 'flex';
|
||
elements.logoutBtn.style.display = 'block';
|
||
} else {
|
||
// 用户未登录
|
||
// 显示未登录状态,隐藏登录状态
|
||
elements.notLoggedInInfo.style.display = 'flex';
|
||
elements.loggedInInfo.style.display = 'none';
|
||
elements.logoutBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 保存用户认证状态到本地存储
|
||
function saveAuthStatus(user) {
|
||
currentUser = user;
|
||
isAuthenticated = true;
|
||
localStorage.setItem('chatUser', JSON.stringify(user));
|
||
updateUserInfoDisplay();
|
||
}
|
||
|
||
// 处理注销操作
|
||
function handleLogout() {
|
||
if (confirm('确定要注销吗?')) {
|
||
logout();
|
||
}
|
||
}
|
||
|
||
// 清除用户认证状态
|
||
function logout() {
|
||
currentUser = null;
|
||
isAuthenticated = false;
|
||
localStorage.removeItem('chatUser');
|
||
|
||
// 重置当前对话
|
||
currentConversationId = null;
|
||
|
||
// 清空消息容器
|
||
if (elements.messagesContainer) {
|
||
elements.messagesContainer.innerHTML = '';
|
||
}
|
||
|
||
// 重置聊天界面
|
||
if (elements.chatScreen) {
|
||
elements.chatScreen.style.display = 'none';
|
||
}
|
||
if (elements.welcomeScreen) {
|
||
elements.welcomeScreen.style.display = 'flex';
|
||
}
|
||
|
||
// 更新用户信息显示
|
||
updateUserInfoDisplay();
|
||
|
||
// 清空对话列表
|
||
if (elements.conversationsList) {
|
||
elements.conversationsList.innerHTML = '';
|
||
}
|
||
|
||
// 显示登录模态框
|
||
showLoginModal();
|
||
}
|
||
|
||
// 显示登录模态框
|
||
function showLoginModal() {
|
||
if (elements.loginModal) {
|
||
elements.loginModal.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
// 隐藏登录模态框
|
||
function hideLoginModal() {
|
||
if (elements.loginModal) {
|
||
elements.loginModal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 显示注册模态框
|
||
function showRegisterModal() {
|
||
if (elements.registerModal) {
|
||
elements.registerModal.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
// 隐藏注册模态框
|
||
function hideRegisterModal() {
|
||
if (elements.registerModal) {
|
||
elements.registerModal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 处理登录表单提交
|
||
async function handleLogin() {
|
||
if (!elements.loginUsername || !elements.loginPassword) return;
|
||
|
||
const username = elements.loginUsername.value.trim();
|
||
const password = elements.loginPassword.value.trim();
|
||
|
||
if (!username || !password) {
|
||
alert('请输入用户名和密码');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}login`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ username, password })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.detail || '登录失败');
|
||
}
|
||
|
||
const userData = await response.json();
|
||
saveAuthStatus(userData);
|
||
|
||
// 更新界面
|
||
if (elements.newChatLogin) {
|
||
elements.newChatLogin.value = userData.username;
|
||
}
|
||
|
||
hideLoginModal();
|
||
loadConversations();
|
||
|
||
alert('登录成功!');
|
||
} catch (error) {
|
||
console.error('Login error:', error);
|
||
alert('登录失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 处理注册表单提交
|
||
async function handleRegister() {
|
||
if (!elements.registerUsername || !elements.registerPassword || !elements.registerConfirmPassword) return;
|
||
|
||
const username = elements.registerUsername.value.trim();
|
||
const password = elements.registerPassword.value.trim();
|
||
const confirmPassword = elements.registerConfirmPassword.value.trim();
|
||
|
||
if (!username || !password) {
|
||
alert('请输入用户名和密码');
|
||
return;
|
||
}
|
||
|
||
if (password !== confirmPassword) {
|
||
alert('两次输入的密码不一致');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}register`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ username, password })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.detail || '注册失败');
|
||
}
|
||
|
||
const userData = await response.json();
|
||
saveAuthStatus(userData);
|
||
|
||
// 更新界面
|
||
if (elements.newChatLogin) {
|
||
elements.newChatLogin.value = userData.username;
|
||
}
|
||
|
||
hideRegisterModal();
|
||
loadConversations();
|
||
|
||
alert('注册成功!');
|
||
} catch (error) {
|
||
console.error('Registration error:', error);
|
||
alert('注册失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 加载对话列表
|
||
async function loadConversations() {
|
||
try {
|
||
// 检查用户是否已登录
|
||
if (!isAuthenticated || !currentUser) {
|
||
// 如果未登录,清空对话列表并返回
|
||
console.log('User not logged in, not loading conversations');
|
||
if (elements.conversationsList) {
|
||
elements.conversationsList.innerHTML = '';
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 如果已登录,调用用户特定的对话API
|
||
const response = await fetch(`${API_BASE_URL}conversations/${currentUser.username}`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to load conversations');
|
||
}
|
||
|
||
const conversations = await response.json();
|
||
renderConversations(conversations);
|
||
|
||
// 如果有对话,默认选择第一个
|
||
if (conversations.length > 0 && !currentConversationId) {
|
||
selectConversation(conversations[0].id);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading conversations:', error);
|
||
showNotification('加载对话失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 渲染对话列表
|
||
function renderConversations(conversations) {
|
||
if (!elements.conversationsList) {
|
||
console.error('conversationsList element not found');
|
||
return;
|
||
}
|
||
|
||
elements.conversationsList.innerHTML = '';
|
||
|
||
conversations.forEach(conv => {
|
||
const convItem = document.createElement('div');
|
||
convItem.className = `conversation-item ${currentConversationId === conv.id ? 'active' : ''}`;
|
||
convItem.dataset.id = conv.id;
|
||
|
||
convItem.innerHTML = `
|
||
<div class="conversation-content">
|
||
<div class="conversation-title">${conv.title || '无标题对话'}</div>
|
||
<div class="conversation-time">${formatTime(conv.update_time)}</div>
|
||
</div>
|
||
<div class="conversation-actions">
|
||
<button class="delete-conv-btn" data-id="${conv.id}">×</button>
|
||
</div>
|
||
`;
|
||
|
||
// 添加点击事件
|
||
convItem.addEventListener('click', () => selectConversation(conv.id));
|
||
|
||
// 添加删除按钮事件
|
||
const deleteBtn = convItem.querySelector('.delete-conv-btn');
|
||
if (deleteBtn) {
|
||
deleteBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation(); // 阻止事件冒泡,避免触发对话选择
|
||
currentConversationId = conv.id;
|
||
showDeleteChatModal();
|
||
});
|
||
}
|
||
|
||
elements.conversationsList.appendChild(convItem);
|
||
});
|
||
}
|
||
|
||
// 选择对话
|
||
async function selectConversation(convId) {
|
||
if (convId === currentConversationId) return;
|
||
|
||
currentConversationId = convId;
|
||
|
||
// 更新对话列表选中状态
|
||
document.querySelectorAll('.conversation-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
document.querySelector(`[data-id="${convId}"]`).classList.add('active');
|
||
|
||
// 加载对话内容
|
||
await loadConversationMessages(convId);
|
||
|
||
// 显示聊天界面,隐藏欢迎界面
|
||
elements.welcomeScreen.style.display = 'none';
|
||
elements.chatScreen.style.display = 'flex';
|
||
}
|
||
|
||
// 加载对话消息
|
||
async function loadConversationMessages(convId) {
|
||
try {
|
||
// 检查用户是否已登录
|
||
if (!isAuthenticated || !currentUser) {
|
||
throw new Error('用户未登录');
|
||
}
|
||
|
||
const response = await fetch(`${API_BASE_URL}conversations/${currentUser.username}/${convId}`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to load conversation messages');
|
||
}
|
||
|
||
const conversation = await response.json();
|
||
elements.chatTitle.textContent = conversation.title || '无标题对话';
|
||
|
||
// 渲染消息
|
||
renderMessages(conversation.messages);
|
||
} catch (error) {
|
||
console.error('Error loading conversation messages:', error);
|
||
showNotification('加载对话消息失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 渲染消息列表
|
||
function renderMessages(messages) {
|
||
elements.messagesContainer.innerHTML = '';
|
||
|
||
messages.forEach(msg => {
|
||
addMessage(msg.role, msg.content, msg.time);
|
||
});
|
||
|
||
// 滚动到底部
|
||
scrollToBottom();
|
||
}
|
||
|
||
// 简单的本地 Markdown 渲染函数
|
||
function renderMarkdown(content) {
|
||
// 如果marked可用,则使用marked解析
|
||
if (typeof marked !== 'undefined') {
|
||
try {
|
||
// 配置marked选项(可选)
|
||
marked.setOptions({
|
||
breaks: true, // 自动转换换行为<br>
|
||
gfm: true // 启用GitHub风格的Markdown
|
||
});
|
||
return marked.parse(content);
|
||
} catch (error) {
|
||
console.error('Marked解析错误:', error);
|
||
return simpleMarkdownFallback(content);
|
||
}
|
||
} else {
|
||
// 如果marked未加载,使用降级方案
|
||
return simpleMarkdownFallback(content);
|
||
}
|
||
}
|
||
|
||
// 简单的Markdown降级处理函数
|
||
// 降级方案:当marked不可用时使用
|
||
function simpleMarkdownFallback(content) {
|
||
let html = content;
|
||
|
||
// 按顺序处理,确保代码块优先
|
||
// 1. 处理代码块 (保持原样)
|
||
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||
|
||
// 2. 处理行内代码
|
||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||
|
||
// 3. 处理加粗(使用**text**语法)
|
||
html = html.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
|
||
|
||
// 4. 处理斜体(使用*text*语法)
|
||
html = html.replace(/\*([^*]+?)\*/g, '<em>$1</em>');
|
||
|
||
// 5. 处理标题(# 标题)
|
||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||
|
||
// 6. 处理无序列表
|
||
html = html.replace(/^\s*-\s+(.+)$/gm, '<li>$1</li>');
|
||
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
|
||
|
||
// 7. 处理有序列表
|
||
html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
||
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, '<ol>$1</ol>');
|
||
|
||
// 8. 处理链接
|
||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||
|
||
// 9. 最后处理换行
|
||
html = html.replace(/\n/g, '<br>');
|
||
|
||
return html;
|
||
}
|
||
|
||
// 添加消息
|
||
function addMessage(role, content, time) {
|
||
// 使用本地的 Markdown 渲染函数
|
||
const formattedContent = renderMarkdown(content);
|
||
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `message ${role}`;
|
||
|
||
const avatarEmoji = role === 'user' ? '👤' : '🤖';
|
||
|
||
messageDiv.innerHTML = `
|
||
<div class="message-avatar">
|
||
${avatarEmoji}
|
||
</div>
|
||
<div class="message-content">
|
||
<div class="message text">${formattedContent}</div>
|
||
<div class="message-time">${formatTime(time)}</div>
|
||
</div>
|
||
`;
|
||
|
||
elements.messagesContainer.appendChild(messageDiv);
|
||
scrollToBottom();
|
||
}
|
||
|
||
// 发送消息
|
||
async function sendMessage() {
|
||
// 检查用户是否已登录
|
||
if (!isAuthenticated || !currentUser) {
|
||
alert('请先登录才能发送消息');
|
||
showLoginModal();
|
||
return;
|
||
}
|
||
|
||
const content = elements.messageInput.value.trim();
|
||
if (!content || !currentConversationId || isStreaming) return;
|
||
|
||
// 清空输入框
|
||
elements.messageInput.value = '';
|
||
|
||
// 添加用户消息
|
||
const now = new Date().toISOString();
|
||
addMessage('user', content, now);
|
||
|
||
// 显示打字指示器
|
||
showTypingIndicator();
|
||
|
||
try {
|
||
isStreaming = true;
|
||
|
||
// 更新发送按钮为停止按钮
|
||
updateSendButtonState(true);
|
||
|
||
// 创建流式响应
|
||
const response = await fetch(`${API_BASE_URL}conversations/${currentConversationId}/stream`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
content: content,
|
||
user_login: currentUser.username
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to send message');
|
||
}
|
||
|
||
// 处理流式响应
|
||
await handleStreamResponse(response);
|
||
|
||
// 检查当前对话的消息数量
|
||
const messages = elements.messagesContainer.querySelectorAll('.message');
|
||
if (messages.length === 1) { // 如果只有一条消息(即刚刚发送的这条),则更新对话标题
|
||
// 截取用户输入的前5个字符作为标题
|
||
const newTitle = content.substring(0, 5) + (content.length > 5 ? '...' : '');
|
||
|
||
// 更新对话标题
|
||
try {
|
||
await fetch(`${API_BASE_URL}conversations/${currentConversationId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
title: newTitle
|
||
})
|
||
});
|
||
|
||
// 更新界面上的对话标题
|
||
if (elements.chatTitle) {
|
||
elements.chatTitle.textContent = newTitle;
|
||
}
|
||
|
||
// 重新加载对话列表(更新标题和时间)
|
||
loadConversations();
|
||
} catch (error) {
|
||
console.error('Error updating conversation title:', error);
|
||
}
|
||
} else {
|
||
// 重新加载对话列表(更新时间)
|
||
loadConversations();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error sending message:', error);
|
||
addMessage('assistant', `发送消息失败: ${error.message}`, new Date().toISOString());
|
||
} finally {
|
||
isStreaming = false;
|
||
hideTypingIndicator();
|
||
|
||
// 重置发送按钮为发送状态
|
||
updateSendButtonState(false);
|
||
|
||
// 清理流式响应相关变量
|
||
currentStreamResponse = null;
|
||
currentStreamReader = null;
|
||
}
|
||
}
|
||
|
||
// 处理流式响应
|
||
async function handleStreamResponse(response) {
|
||
// 保存当前响应和阅读器对象,以便在需要时可以停止流式响应
|
||
currentStreamResponse = response;
|
||
const reader = response.body.getReader();
|
||
currentStreamReader = reader;
|
||
|
||
const decoder = new TextDecoder('utf-8');
|
||
let fullResponse = '';
|
||
let messageElement = null;
|
||
|
||
// 创建助手消息容器
|
||
const now = new Date().toISOString();
|
||
messageElement = createAssistantMessageContainer(now);
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
// 解码接收到的字节
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
fullResponse += chunk;
|
||
|
||
// 更新消息内容
|
||
updateAssistantMessageContent(messageElement, fullResponse);
|
||
}
|
||
|
||
// 保存完整消息
|
||
await saveAssistantMessage(fullResponse);
|
||
|
||
} catch (error) {
|
||
console.error('Error reading stream:', error);
|
||
updateAssistantMessageContent(messageElement, `流式响应错误: ${error.message}`);
|
||
} finally {
|
||
reader.releaseLock();
|
||
}
|
||
}
|
||
|
||
// 创建助手消息容器
|
||
function createAssistantMessageContainer(time) {
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = 'message assistant';
|
||
|
||
messageDiv.innerHTML = `
|
||
<div class="message-avatar">
|
||
🤖
|
||
</div>
|
||
<div class="message-content">
|
||
<div class="message text">
|
||
<div class="typing-dots" id="assistantTypingDots" style="display: flex;">
|
||
<div class="dot"></div>
|
||
<div class="dot"></div>
|
||
<div class="dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="message-time">${formatTime(time)}</div>
|
||
</div>
|
||
`;
|
||
|
||
elements.messagesContainer.appendChild(messageDiv);
|
||
scrollToBottom();
|
||
|
||
return messageDiv;
|
||
}
|
||
|
||
// 更新助手消息内容
|
||
function updateAssistantMessageContent(messageElement, content) {
|
||
const textElement = messageElement.querySelector('.message.text');
|
||
const typingDots = textElement.querySelector('.typing-dots');
|
||
|
||
// 替换打字指示器为实际内容
|
||
if (typingDots) {
|
||
typingDots.remove();
|
||
}
|
||
|
||
// 使用本地的 Markdown 渲染函数
|
||
const formattedContent = renderMarkdown(content);
|
||
textElement.innerHTML = formattedContent;
|
||
|
||
scrollToBottom();
|
||
}
|
||
|
||
// 停止当前的流式响应
|
||
async function stopStreamResponse() {
|
||
if (!isStreaming) return;
|
||
|
||
try {
|
||
// 调用API的中止端点
|
||
await fetch(`${API_BASE_URL}conversations/${currentConversationId}/abort`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
// 取消当前的流式响应
|
||
if (currentStreamReader) {
|
||
await currentStreamReader.cancel();
|
||
}
|
||
|
||
// 重置相关变量
|
||
isStreaming = false;
|
||
currentStreamResponse = null;
|
||
currentStreamReader = null;
|
||
|
||
// 隐藏打字指示器
|
||
hideTypingIndicator();
|
||
|
||
// 更新按钮状态
|
||
updateSendButtonState(false);
|
||
|
||
} catch (error) {
|
||
console.error('Error stopping stream:', error);
|
||
}
|
||
}
|
||
|
||
// 更新发送按钮的状态
|
||
function updateSendButtonState(isStreaming) {
|
||
if (!elements.sendBtn) return;
|
||
|
||
if (isStreaming) {
|
||
elements.sendBtn.textContent = '⏹️';
|
||
elements.sendBtn.title = '停止生成';
|
||
} else {
|
||
elements.sendBtn.textContent = '➤';
|
||
elements.sendBtn.title = '发送消息';
|
||
}
|
||
}
|
||
|
||
// 保存助手消息
|
||
async function saveAssistantMessage(content) {
|
||
// 这个函数主要是为了确保对话历史正确保存
|
||
// 在流式响应结束后,API 应该已经将完整消息保存到数据库
|
||
}
|
||
|
||
// 显示/隐藏打字指示器 - 现在通过创建助手消息容器来显示,这里仅保留空函数以保持兼容性
|
||
function showTypingIndicator() {
|
||
// 打字指示器现在显示在助手消息容器中
|
||
}
|
||
|
||
function hideTypingIndicator() {
|
||
// 打字指示器现在显示在助手消息容器中
|
||
}
|
||
|
||
|
||
|
||
// 创建新对话
|
||
async function createNewConversation() {
|
||
// 检查用户是否已登录
|
||
if (!isAuthenticated || !currentUser) {
|
||
alert('请先登录');
|
||
showLoginModal();
|
||
return;
|
||
}
|
||
|
||
const userLogin = currentUser.username;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}conversations`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
title: '新对话', // 默认标题,后续会根据第一条消息内容更新
|
||
user_login: userLogin
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to create conversation');
|
||
}
|
||
|
||
const newConv = await response.json();
|
||
|
||
// 隐藏模态框
|
||
|
||
|
||
// 加载对话列表并选择新对话
|
||
await loadConversations();
|
||
await selectConversation(newConv.id);
|
||
|
||
showNotification('新对话创建成功', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Error creating conversation:', error);
|
||
showNotification('创建对话失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 显示删除对话模态框
|
||
function showDeleteChatModal() {
|
||
if (!currentConversationId) return;
|
||
elements.deleteChatModal.style.display = 'block';
|
||
}
|
||
|
||
// 隐藏删除对话模态框
|
||
function hideDeleteChatModal() {
|
||
elements.deleteChatModal.style.display = 'none';
|
||
}
|
||
|
||
// 删除当前对话
|
||
async function deleteCurrentConversation() {
|
||
if (!currentConversationId) return;
|
||
|
||
try {
|
||
// 检查用户是否已登录
|
||
if (!isAuthenticated || !currentUser) {
|
||
throw new Error('用户未登录');
|
||
}
|
||
|
||
const response = await fetch(`${API_BASE_URL}conversations/${currentUser.username}/${currentConversationId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete conversation');
|
||
}
|
||
|
||
// 隐藏模态框
|
||
hideDeleteChatModal();
|
||
|
||
// 重置当前对话
|
||
currentConversationId = null;
|
||
|
||
// 重新加载对话列表
|
||
await loadConversations();
|
||
|
||
// 显示欢迎界面
|
||
elements.chatScreen.style.display = 'none';
|
||
elements.welcomeScreen.style.display = 'flex';
|
||
|
||
showNotification('对话删除成功', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting conversation:', error);
|
||
showNotification('删除对话失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 显示通知
|
||
function showNotification(message, type = 'info') {
|
||
// 创建简单的通知
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
notification.textContent = message;
|
||
|
||
// 样式
|
||
Object.assign(notification.style, {
|
||
position: 'fixed',
|
||
top: '20px',
|
||
right: '20px',
|
||
padding: '12px 20px',
|
||
borderRadius: '8px',
|
||
color: 'white',
|
||
fontWeight: '500',
|
||
zIndex: '10000',
|
||
animation: 'slideInRight 0.3s ease'
|
||
});
|
||
|
||
// 根据类型设置背景色
|
||
switch(type) {
|
||
case 'success':
|
||
notification.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
|
||
break;
|
||
case 'error':
|
||
notification.style.background = 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)';
|
||
break;
|
||
default:
|
||
notification.style.background = 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)';
|
||
}
|
||
|
||
// 添加到页面
|
||
document.body.appendChild(notification);
|
||
|
||
// 3秒后移除
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
// 工具函数
|
||
function formatTime(timeString) {
|
||
if (!timeString) return '';
|
||
|
||
const date = new Date(timeString);
|
||
const now = new Date();
|
||
|
||
// 今天的消息显示时间
|
||
if (date.toDateString() === now.toDateString()) {
|
||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
// 昨天的消息显示 "昨天 HH:MM"
|
||
const yesterday = new Date(now);
|
||
yesterday.setDate(yesterday.getDate() - 1);
|
||
if (date.toDateString() === yesterday.toDateString()) {
|
||
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
|
||
}
|
||
|
||
// 今年的消息显示 "MM-DD HH:MM"
|
||
if (date.getFullYear() === now.getFullYear()) {
|
||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
// 其他显示完整日期时间
|
||
return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
function scrollToBottom() {
|
||
setTimeout(() => {
|
||
elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
|
||
}, 100);
|
||
}
|
||
|
||
// 添加动画样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
`;
|
||
document.head.appendChild(style); |