389 lines
14 KiB
JavaScript
Raw Normal View History

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');
});
});