2026-01-16 17:20:27 +08:00
|
|
|
|
// 全局变量
|
|
|
|
|
|
let currentConversationId = null;
|
|
|
|
|
|
let currentUser = null; // 当前登录用户对象
|
|
|
|
|
|
let isStreaming = false;
|
|
|
|
|
|
let currentStreamResponse = null;
|
|
|
|
|
|
let currentStreamReader = null;
|
2026-01-27 17:26:17 +08:00
|
|
|
|
let currentAssistantMessageElement = null; // 保存当前助手消息元素
|
2026-01-16 17:20:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 15:48:25 +08:00
|
|
|
|
// 简单的本地 Markdown 渲染函数
|
|
|
|
|
|
function renderMarkdown(content) {
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 如果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;
|
2026-01-26 15:48:25 +08:00
|
|
|
|
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 按顺序处理,确保代码块优先
|
|
|
|
|
|
// 1. 处理代码块 (保持原样)
|
2026-01-26 15:48:25 +08:00
|
|
|
|
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
|
|
|
|
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 2. 处理行内代码
|
2026-01-26 15:48:25 +08:00
|
|
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
|
|
|
|
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 3. 处理加粗(使用**text**语法)
|
|
|
|
|
|
html = html.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 处理斜体(使用*text*语法)
|
|
|
|
|
|
html = html.replace(/\*([^*]+?)\*/g, '<em>$1</em>');
|
2026-01-26 15:48:25 +08:00
|
|
|
|
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 5. 处理标题(# 标题)
|
|
|
|
|
|
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
|
|
|
|
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
|
|
|
|
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
2026-01-26 15:48:25 +08:00
|
|
|
|
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 6. 处理无序列表
|
|
|
|
|
|
html = html.replace(/^\s*-\s+(.+)$/gm, '<li>$1</li>');
|
2026-01-26 15:48:25 +08:00
|
|
|
|
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
|
|
|
|
|
|
|
2026-01-27 16:23:21 +08:00
|
|
|
|
// 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>');
|
2026-01-26 15:48:25 +08:00
|
|
|
|
|
|
|
|
|
|
return html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:20:27 +08:00
|
|
|
|
// 添加消息
|
|
|
|
|
|
function addMessage(role, content, time) {
|
2026-01-26 15:48:25 +08:00
|
|
|
|
// 使用本地的 Markdown 渲染函数
|
|
|
|
|
|
const formattedContent = renderMarkdown(content);
|
|
|
|
|
|
|
2026-01-16 17:20:27 +08:00
|
|
|
|
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">
|
2026-01-26 15:48:25 +08:00
|
|
|
|
<div class="message text">${formattedContent}</div>
|
2026-01-16 17:20:27 +08:00
|
|
|
|
<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);
|
2026-01-27 17:26:17 +08:00
|
|
|
|
currentAssistantMessageElement = messageElement; // 保存到全局变量
|
2026-01-16 17:20:27 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-27 17:26:17 +08:00
|
|
|
|
// 检查是否是用户主动取消的错误
|
|
|
|
|
|
if (error.name === 'AbortError' || error.message.includes('cancel')) {
|
|
|
|
|
|
// 用户主动取消,不显示错误消息
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他错误,显示错误消息
|
|
|
|
|
|
updateAssistantMessageContent(messageElement, `流式响应错误: ${error.message}`);
|
|
|
|
|
|
}
|
2026-01-16 17:20:27 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
reader.releaseLock();
|
2026-01-27 17:26:17 +08:00
|
|
|
|
currentAssistantMessageElement = null; // 重置全局变量
|
2026-01-16 17:20:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建助手消息容器
|
|
|
|
|
|
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">
|
2026-01-27 17:26:17 +08:00
|
|
|
|
<div class="typing-dots" id="assistantTypingDots" style="display: flex; align-items: center;">
|
2026-01-16 17:20:27 +08:00
|
|
|
|
<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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 15:48:25 +08:00
|
|
|
|
// 使用本地的 Markdown 渲染函数
|
|
|
|
|
|
const formattedContent = renderMarkdown(content);
|
|
|
|
|
|
textElement.innerHTML = formattedContent;
|
2026-01-16 17:20:27 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-01-27 17:26:17 +08:00
|
|
|
|
// 更新助手消息内容,替换打字指示器
|
|
|
|
|
|
let assistantMessageElement = currentAssistantMessageElement;
|
|
|
|
|
|
// 如果 currentAssistantMessageElement 为 null,尝试查找最新的助手消息元素
|
|
|
|
|
|
if (!assistantMessageElement) {
|
|
|
|
|
|
const messageElements = elements.messagesContainer.querySelectorAll('.message.assistant');
|
|
|
|
|
|
if (messageElements.length > 0) {
|
|
|
|
|
|
assistantMessageElement = messageElements[messageElements.length - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (assistantMessageElement) {
|
|
|
|
|
|
updateAssistantMessageContent(assistantMessageElement, '流式响应已中止');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置全局变量
|
|
|
|
|
|
currentAssistantMessageElement = null;
|
2026-01-16 17:20:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新按钮状态
|
|
|
|
|
|
updateSendButtonState(false);
|
|
|
|
|
|
|
2026-01-27 17:26:17 +08:00
|
|
|
|
// 滚动到底部
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
|
2026-01-16 17:20:27 +08:00
|
|
|
|
} 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);
|