389 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.

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');
let currentEventSource = null;
let currentChatId = null;
// 获取知识库列表
document.addEventListener('DOMContentLoaded', function() {
// 获取知识库列表
const loadRagOptions = () => {
const ragSelect = document.getElementById('ragSelect');
fetch('http://localhost:8095/api/v1/rag/query_rag_tag_list')
.then(response => response.json())
.then(data => {
if (data.code === '0000' && data.data) {
// 清空现有选项(保留第一个默认选项)
while (ragSelect.options.length > 1) {
ragSelect.remove(1);
}
// 添加新选项
data.data.forEach(tag => {
const option = new Option(`Rag${tag}`, tag);
ragSelect.add(option);
});
}
})
.catch(error => {
console.error('获取知识库列表失败:', error);
});
};
// 初始化加载
loadRagOptions();
});
function createNewChat() {
const chatId = Date.now().toString();
currentChatId = chatId;
localStorage.setItem('currentChatId', chatId);
// 修改数据结构为包含name和messages的对象
localStorage.setItem(`chat_${chatId}`, JSON.stringify({
name: '新聊天',
messages: []
}));
updateChatList();
clearChatArea();
}
function deleteChat(chatId) {
if (confirm('确定要删除这个聊天记录吗?')) {
localStorage.removeItem(`chat_${chatId}`); // Remove the chat from localStorage
if (currentChatId === chatId) { // If the current chat is being deleted
createNewChat(); // Create a new chat
}
updateChatList(); // Update the chat list to reflect changes
}
}
function updateChatList() {
chatList.innerHTML = '';
const chats = Object.keys(localStorage)
.filter(key => key.startsWith('chat_'));
const currentChatIndex = chats.findIndex(key => key.split('_')[1] === currentChatId);
if (currentChatIndex!== -1) {
const currentChat = chats[currentChatIndex];
chats.splice(currentChatIndex, 1);
chats.unshift(currentChat);
}
chats.forEach(chatKey => {
let chatData = JSON.parse(localStorage.getItem(chatKey));
const chatId = chatKey.split('_')[1];
// 数据迁移:将旧数组格式转换为新对象格式
if (Array.isArray(chatData)) {
chatData = {
name: `聊天 ${new Date(parseInt(chatId)).toLocaleDateString()}`,
messages: chatData
};
localStorage.setItem(chatKey, JSON.stringify(chatData));
}
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 ${chatId === currentChatId? 'bg-blue-50' : ''}`;
li.innerHTML = `
<div class="flex-1">
<div class="text-sm font-medium">${chatData.name}</div>
<div class="text-xs text-gray-400">${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}</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" onclick="renameChat('${chatId}')">重命名</button>
<button class="p-1 hover:bg-red-200 rounded text-red-500" onclick="deleteChat('${chatId}')">删除</button>
</div>
`;
li.addEventListener('click', (e) => {
if (!e.target.closest('.chat-actions')) {
loadChat(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);
});
}
let currentContextMenu = null;
// 优化后的上下文菜单
function showChatContextMenu(event, chatId) {
event.stopPropagation();
closeContextMenu();
const buttonRect = event.target.closest('button').getBoundingClientRect();
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.position = 'fixed';
menu.style.left = `${buttonRect.left}px`;
menu.style.top = `${buttonRect.bottom + 4}px`;
menu.innerHTML = `
<div class="context-menu-item" onclick="renameChat('${chatId}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
重命名
</div>
<div class="context-menu-item text-red-500" onclick="deleteChat('${chatId}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
删除
</div>
`;
document.body.appendChild(menu);
currentContextMenu = menu;
// 点击外部关闭菜单
setTimeout(() => {
document.addEventListener('click', closeContextMenu, { once: true });
});
}
function closeContextMenu() {
if (currentContextMenu) {
currentContextMenu.remove();
currentContextMenu = null;
}
}
function renameChat(chatId) {
const chatKey = `chat_${chatId}`;
const chatData = JSON.parse(localStorage.getItem(chatKey));
const currentName = chatData.name || `聊天 ${new Date(parseInt(chatId)).toLocaleString()}`;
const newName = prompt('请输入新的聊天名称', currentName);
if (newName) {
chatData.name = newName;
localStorage.setItem(chatKey, JSON.stringify(chatData));
updateChatList();
}
}
function loadChat(chatId) {
currentChatId = chatId;
localStorage.setItem('currentChatId', chatId);
clearChatArea();
const chatData = JSON.parse(localStorage.getItem(`chat_${chatId}`) || { messages: [] });
chatData.messages.forEach(msg => {
appendMessage(msg.content, msg.isAssistant, false);
});
updateChatList()
}
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) {
// 确保读取和保存完整的数据结构
const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}');
chatData.messages.push({ content, isAssistant });
localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData));
}
}
function startEventStream(message) {
if (currentEventSource) {
currentEventSource.close();
}
// 选项值,
// 组装01http://localhost:8095/api/v1/ollama/generate_stream?message=Hello&model=deepseek-r1:1.5b
// 组装02http://localhost:8095/api/v1/openai/generate_stream?message=Hello&model=gpt-4o
const ragTag = document.getElementById('ragSelect').value;
const aiModelSelect = document.getElementById('aiModel');
const aiModelValue = aiModelSelect.value; // 获取选中的 aiModel 的 value
const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model'); // 获取选中的 aiModel 的 model 属性
let url;
if (ragTag) {
url = `http://localhost:8095/api/v1/${aiModelValue}/generate_stream_rag?message=${encodeURIComponent(message)}&ragTag=${encodeURIComponent(ragTag)}&model=${encodeURIComponent(aiModelModel)}`;
} else {
url = `http://localhost:8095/api/v1/${aiModelValue}/generate_stream?message=${encodeURIComponent(message)}&model=${encodeURIComponent(aiModelModel)}`;
}
currentEventSource = new EventSource(url);
let accumulatedContent = '';
let tempMessageDiv = null;
currentEventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.result?.output?.content) {
const newContent = data.result.output.content;
accumulatedContent += newContent;
// 首次创建临时消息容器
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';
}
// 直接更新文本内容先不解析Markdown
tempMessageDiv.textContent = accumulatedContent;
chatArea.scrollTop = chatArea.scrollHeight;
}
if (data.result?.output?.properties?.finishReason === 'STOP') {
currentEventSource.close();
// 流式传输完成后进行最终渲染
const finalContent = accumulatedContent;
tempMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(finalContent));
// 添加复制按钮
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(finalContent).then(() => {
copyBtn.textContent = '已复制';
setTimeout(() => copyBtn.textContent = '复制', 2000);
});
};
tempMessageDiv.appendChild(copyBtn);
// 保存到本地存储
if (currentChatId) {
// 正确的数据结构应该是对象包含messages数组
const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}');
chatData.messages.push({ content: finalContent, isAssistant: true });
localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData));
}
}
} catch (e) {
console.error('Error parsing event data:', e);
}
};
currentEventSource.onerror = function(error) {
console.error('EventSource error:', error);
currentEventSource.close();
};
}
submitBtn.addEventListener('click', () => {
const message = messageInput.value.trim();
if (!message) return;
if (!currentChatId) {
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');
}
}
// Initialize
updateChatList();
const savedChatId = localStorage.getItem('currentChatId');
if (savedChatId) {
loadChat(savedChatId);
}
// Handle window resize for responsive design
window.addEventListener('resize', () => {
if (window.innerWidth > 768) {
sidebar.classList.remove('-translate-x-full');
} else {
sidebar.classList.add('-translate-x-full');
}
});
// Initial check for mobile devices
if (window.innerWidth <= 768) {
sidebar.classList.add('-translate-x-full');
}
updateSidebarIcon();
// 上传知识下拉菜单控制
const uploadMenuButton = document.getElementById('uploadMenuButton');
const uploadMenu = document.getElementById('uploadMenu');
// 切换菜单显示
uploadMenuButton.addEventListener('click', (e) => {
e.stopPropagation();
uploadMenu.classList.toggle('hidden');
});
// 点击外部区域关闭菜单
document.addEventListener('click', (e) => {
if (!uploadMenu.contains(e.target) && e.target !== uploadMenuButton) {
uploadMenu.classList.add('hidden');
}
});
// 菜单项点击后关闭菜单
document.querySelectorAll('#uploadMenu a').forEach(item => {
item.addEventListener('click', () => {
uploadMenu.classList.add('hidden');
});
});