451 lines
14 KiB
JavaScript
Raw Normal View History

// ===== index.js (Redis 存储版) =====
const chatArea = document.getElementById('chatArea');
const messageInput = document.getElementById('messageInput');
const submitBtn = document.getElementById('submitBtn');
const newChatBtn = document.getElementById('newChatBtn');
const chatList = document.getElementById('chatList');
const welcomeMessage = document.getElementById('welcomeMessage');
const toggleSidebarBtn = document.getElementById('toggleSidebar');
const sidebar = document.getElementById('sidebar');
// RAG 相关 DOM
const ragSelect = document.getElementById('ragSelect');
const deleteRagBtn = document.getElementById('deleteRagBtn');
const refreshRagBtn = document.getElementById('refreshRagBtn');
let currentEventSource = null;
let currentChatId = localStorage.getItem('currentChatId') || null;
// ==================== Redis 存储相关函数 ====================
/**
* 获取所有聊天会话及其标题
*/
async function getAllChats() {
try {
const response = await fetch('http://localhost:8095/api/v1/chat');
const chatIds = await response.json();
return Promise.all(chatIds.map(async chatId => {
const nameResponse = await fetch(`http://localhost:8095/api/v1/chat/${chatId}/name`);
const name = await nameResponse.text();
return { chatId, name };
}));
} catch (error) {
console.error('获取聊天列表失败:', error);
return [];
}
}
/**
* 创建新聊天会话
*/
async function createNewChat() {
const chatId = Date.now().toString();
try {
// 设置默认名称
await fetch(`http://localhost:8095/api/v1/chat/${chatId}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '新聊天' })
});
currentChatId = chatId;
localStorage.setItem('currentChatId', chatId);
updateChatList();
clearChatArea();
return chatId;
} catch (error) {
console.error('创建聊天失败:', error);
throw error;
}
}
/**
* 删除聊天会话
*/
async function deleteChat(chatId) {
if (!confirm('确定要删除这个聊天记录吗?')) return;
try {
const response = await fetch(`http://localhost:8095/api/v1/chat/${chatId}`, {
method: 'DELETE'
});
if (response.ok) {
if (currentChatId === chatId) {
await createNewChat();
}
updateChatList();
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('删除聊天失败:', error);
alert('删除失败: ' + error.message);
}
}
/**
* 重命名聊天会话
*/
async function renameChat(chatId) {
const currentName = await (await fetch(`http://localhost:8095/api/v1/chat/${chatId}/name`)).text();
const newName = prompt('请输入新的聊天名称', currentName);
if (newName) {
try {
await fetch(`http://localhost:8095/api/v1/chat/${chatId}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
updateChatList();
} catch (error) {
console.error('重命名失败:', error);
alert('重命名失败: ' + error.message);
}
}
}
/**
* 加载聊天历史
*/
async function loadChat(chatId) {
try {
const response = await fetch(`http://localhost:8095/chat/${chatId}`);
const messages = await response.json();
currentChatId = chatId;
localStorage.setItem('currentChatId', chatId);
clearChatArea();
messages.forEach(msg => {
appendMessage(msg.content, msg.role === 'assistant', false);
});
} catch (error) {
console.error('加载聊天失败:', error);
alert('加载聊天失败: ' + error.message);
}
}
/**
* 保存消息到 Redis
*/
async function saveMessage(chatId, content, isAssistant) {
const msg = {
role: isAssistant ? "assistant" : "user",
content: content,
ts: Date.now()
};
try {
await fetch(`http://localhost:8095/api/v1/chat/${chatId}/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(msg)
});
} catch (error) {
console.error('保存消息失败:', error);
}
}
/**
* 更新聊天列表 UI
*/
async function updateChatList() {
chatList.innerHTML = '';
const chats = await getAllChats();
// 当前聊天置顶
chats.sort((a, b) => {
if (a.chatId === currentChatId) return -1;
if (b.chatId === currentChatId) return 1;
return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序
});
chats.forEach(chat => {
const li = document.createElement('li');
li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${
chat.chatId === currentChatId ? 'bg-blue-50' : ''
}`;
const date = new Date(parseInt(chat.chatId)).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
li.innerHTML = `
<div class="flex-1">
<div class="text-sm font-medium">${chat.name}</div>
<div class="text-xs text-gray-400">${date}</div>
</div>
<div class="chat-actions flex items-center gap-1 opacity-0 transition-opacity duration-200">
<button class="p-1 hover:bg-gray-200 rounded text-gray-500">重命名</button>
<button class="p-1 hover:bg-red-200 rounded text-red-500">删除</button>
</div>
`;
// 事件绑定
li.querySelectorAll('button')[0].addEventListener('click', (e) => {
e.stopPropagation();
renameChat(chat.chatId);
});
li.querySelectorAll('button')[1].addEventListener('click', (e) => {
e.stopPropagation();
deleteChat(chat.chatId);
});
li.addEventListener('click', () => loadChat(chat.chatId));
li.addEventListener('mouseenter', () => {
li.querySelector('.chat-actions').classList.remove('opacity-0');
});
li.addEventListener('mouseleave', () => {
li.querySelector('.chat-actions').classList.add('opacity-0');
});
chatList.appendChild(li);
});
}
// ==================== 消息显示相关函数 ====================
function clearChatArea() {
chatArea.innerHTML = '';
welcomeMessage.style.display = 'flex';
}
function appendMessage(content, isAssistant = false, saveToStorage = true) {
welcomeMessage.style.display = 'none';
const messageDiv = document.createElement('div');
messageDiv.className = `max-w-4xl mx-auto mb-4 p-4 rounded-lg ${
isAssistant ? 'bg-gray-100' : 'bg-white border'
} markdown-body relative`;
const renderedContent = DOMPurify.sanitize(marked.parse(content));
messageDiv.innerHTML = renderedContent;
const copyBtn = document.createElement('button');
copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs';
copyBtn.textContent = '复制';
copyBtn.onclick = () => {
navigator.clipboard.writeText(content).then(() => {
copyBtn.textContent = '已复制';
setTimeout(() => (copyBtn.textContent = '复制'), 2000);
});
};
messageDiv.appendChild(copyBtn);
chatArea.appendChild(messageDiv);
chatArea.scrollTop = chatArea.scrollHeight;
if (saveToStorage && currentChatId) {
saveMessage(currentChatId, content, isAssistant);
}
}
// ==================== 事件流处理 ====================
function startEventStream(message) {
if (currentEventSource) currentEventSource.close();
const ragTag = ragSelect.value;
const aiModelSelect = document.getElementById('aiModel');
const aiModelValue = aiModelSelect.value;
const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model');
let url;
const base = `http://localhost:8095/api/v1/${aiModelValue}`;
const params = new URLSearchParams({ message, model: aiModelModel });
if (ragTag) {
params.append('ragTag', ragTag);
url = `${base}/generate_stream_rag?${params.toString()}`;
} else {
url = `${base}/generate_stream?${params.toString()}`;
}
currentEventSource = new EventSource(url);
let accumulatedContent = '';
let tempMessageDiv = null;
currentEventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
const content = data.result?.output?.content || data.content;
if (content) {
accumulatedContent += content;
if (!tempMessageDiv) {
tempMessageDiv = document.createElement('div');
tempMessageDiv.className = 'max-w-4xl mx-auto mb-4 p-4 rounded-lg bg-gray-100 markdown-body relative';
chatArea.appendChild(tempMessageDiv);
welcomeMessage.style.display = 'none';
}
tempMessageDiv.textContent = accumulatedContent;
chatArea.scrollTop = chatArea.scrollHeight;
}
if (data.result?.output?.properties?.finishReason === 'STOP') {
currentEventSource.close();
if (tempMessageDiv) {
tempMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(accumulatedContent));
const copyBtn = document.createElement('button');
copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs';
copyBtn.textContent = '复制';
copyBtn.onclick = () => {
navigator.clipboard.writeText(accumulatedContent).then(() => {
copyBtn.textContent = '已复制';
setTimeout(() => (copyBtn.textContent = '复制'), 2000);
});
};
tempMessageDiv.appendChild(copyBtn);
}
if (currentChatId) {
saveMessage(currentChatId, accumulatedContent, true);
}
}
} catch (e) {
console.error('解析事件数据失败:', e);
}
};
currentEventSource.onerror = function(error) {
console.error('EventSource 错误:', error);
currentEventSource.close();
};
}
// ==================== 事件绑定 ====================
submitBtn.addEventListener('click', async () => {
const message = messageInput.value.trim();
if (!message) return;
if (!currentChatId) {
await createNewChat();
}
appendMessage(message, false);
messageInput.value = '';
startEventStream(message);
});
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submitBtn.click();
}
});
newChatBtn.addEventListener('click', createNewChat);
toggleSidebarBtn.addEventListener('click', () => {
sidebar.classList.toggle('-translate-x-full');
updateSidebarIcon();
});
function updateSidebarIcon() {
const iconPath = document.getElementById('sidebarIconPath');
if (sidebar.classList.contains('-translate-x-full')) {
iconPath.setAttribute('d', 'M4 6h16M4 12h4m12 0h-4M4 18h16');
} else {
iconPath.setAttribute('d', 'M4 6h16M4 12h16M4 18h16');
}
}
// ==================== 初始化 ====================
// 初始化聊天列表
updateChatList();
// 加载当前聊天
if (currentChatId) {
loadChat(currentChatId);
}
// 响应式处理
window.addEventListener('resize', () => {
if (window.innerWidth > 768) {
sidebar.classList.remove('-translate-x-full');
} else {
sidebar.classList.add('-translate-x-full');
}
});
if (window.innerWidth <= 768) {
sidebar.classList.add('-translate-x-full');
}
updateSidebarIcon();
/** -------------------------
* 上传知识下拉菜单保持你现有 HTML 结构
* - 点击按钮展开/收起菜单
* - 点击外部区域/按下 Esc 关闭
* - 点击菜单项后自动关闭
* ------------------------- */
(function initUploadMenu() {
const uploadMenuButton = document.getElementById('uploadMenuButton');
const uploadMenu = document.getElementById('uploadMenu');
if (!uploadMenuButton || !uploadMenu) return;
// 切换菜单显示/隐藏
const toggleMenu = () => {
uploadMenu.classList.toggle('hidden');
};
// 显示菜单
const openMenu = () => {
uploadMenu.classList.remove('hidden');
};
// 隐藏菜单
const closeMenu = () => {
uploadMenu.classList.add('hidden');
};
// 点击按钮展开/收起
uploadMenuButton.addEventListener('click', (e) => {
e.stopPropagation();
toggleMenu();
});
// 键盘辅助Enter/Space 展开Esc 关闭
uploadMenuButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openMenu();
} else if (e.key === 'Escape') {
closeMenu();
}
});
// 点击菜单项后自动关闭
uploadMenu.querySelectorAll('a').forEach(a => {
a.addEventListener('click', () => closeMenu());
});
// 点击外部任意区域关闭
document.addEventListener('click', (e) => {
if (!uploadMenu.classList.contains('hidden')) {
if (!uploadMenu.contains(e.target) && e.target !== uploadMenuButton) {
closeMenu();
}
}
});
// Esc 关闭(全局)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
})();