389 lines
14 KiB
JavaScript
389 lines
14 KiB
JavaScript
|
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();
|
|||
|
}
|
|||
|
|
|||
|
// 选项值,
|
|||
|
// 组装01;http://localhost:8095/api/v1/ollama/generate_stream?message=Hello&model=deepseek-r1:1.5b
|
|||
|
// 组装02;http://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');
|
|||
|
});
|
|||
|
});
|