451 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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