app.js•14.9 kB
const API_URL = 'http://localhost:3000';
const messagesContainer = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const historyButton = document.getElementById('historyButton');
const newChatButton = document.getElementById('newChatButton');
const historySidebar = document.getElementById('historySidebar');
const closeHistoryButton = document.getElementById('closeHistoryButton');
const historyList = document.getElementById('historyList');
const overlay = document.getElementById('overlay');
let isProcessing = false;
let currentToolExecutionContainer = null;
let currentConversationId = null;
// 聊天历史管理
class ChatHistoryManager {
constructor() {
this.storageKey = 'mysql_mcp_chat_history';
this.currentChatKey = 'mysql_mcp_current_chat';
}
// 获取所有聊天历史
getAllChats() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
// 保存聊天历史
saveAllChats(chats) {
localStorage.setItem(this.storageKey, JSON.stringify(chats));
}
// 获取当前聊天 ID
getCurrentChatId() {
return localStorage.getItem(this.currentChatKey);
}
// 设置当前聊天 ID
setCurrentChatId(id) {
localStorage.setItem(this.currentChatKey, id);
}
// 创建新聊天
createNewChat() {
const chats = this.getAllChats();
const now = new Date();
const title = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
const newChat = {
id: Date.now().toString(),
title: title,
messages: [],
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
};
chats.unshift(newChat);
this.saveAllChats(chats);
this.setCurrentChatId(newChat.id);
return newChat;
}
// 获取聊天
getChat(id) {
const chats = this.getAllChats();
return chats.find(chat => chat.id === id);
}
// 更新聊天
updateChat(id, updates) {
const chats = this.getAllChats();
const index = chats.findIndex(chat => chat.id === id);
if (index !== -1) {
chats[index] = {
...chats[index],
...updates,
updatedAt: new Date().toISOString(),
};
this.saveAllChats(chats);
}
}
// 删除聊天
deleteChat(id) {
const chats = this.getAllChats();
const filtered = chats.filter(chat => chat.id !== id);
this.saveAllChats(filtered);
// 如果删除的是当前聊天,切换到第一个
if (this.getCurrentChatId() === id) {
if (filtered.length > 0) {
this.setCurrentChatId(filtered[0].id);
} else {
localStorage.removeItem(this.currentChatKey);
}
}
}
// 添加消息到当前聊天
addMessage(chatId, message) {
const chat = this.getChat(chatId);
if (chat) {
chat.messages.push(message);
// 自动生成标题(使用第一条用户消息)
if (message.type === 'user' && chat.messages.filter(m => m.type === 'user').length === 1) {
// 第一条用户消息,更新标题
chat.title = message.content.substring(0, 30) + (message.content.length > 30 ? '...' : '');
}
this.updateChat(chatId, { messages: chat.messages, title: chat.title });
}
}
}
const historyManager = new ChatHistoryManager();
// 添加消息到界面
function addMessage(role, content) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(contentDiv);
messagesContainer.appendChild(messageDiv);
scrollToBottom();
return messageDiv;
}
// 创建工具执行容器
function createToolExecutionContainer() {
const container = document.createElement('div');
container.className = 'tool-execution-container';
const header = document.createElement('div');
header.className = 'tool-execution-header';
header.innerHTML = `
<span class="title">🔧 工具执行过程</span>
<span class="toggle">点击展开 ▼</span>
`;
const content = document.createElement('div');
content.className = 'tool-execution-content';
header.addEventListener('click', () => {
const isExpanded = content.classList.contains('expanded');
if (isExpanded) {
content.classList.remove('expanded');
header.querySelector('.toggle').textContent = '点击展开 ▼';
} else {
content.classList.add('expanded');
header.querySelector('.toggle').textContent = '点击收起 ▲';
}
});
container.appendChild(header);
container.appendChild(content);
messagesContainer.appendChild(container);
scrollToBottom();
return content;
}
// 添加工具调用信息
function addToolCall(toolName, args) {
if (!currentToolExecutionContainer) {
currentToolExecutionContainer = createToolExecutionContainer();
}
const toolDiv = document.createElement('div');
toolDiv.className = 'tool-call';
toolDiv.innerHTML = `
<div class="tool-name">🔧 调用工具: ${toolName}</div>
<pre>${JSON.stringify(args, null, 2)}</pre>
`;
currentToolExecutionContainer.appendChild(toolDiv);
scrollToBottom();
}
// 添加工具结果
function addToolResult(toolName, result) {
if (!currentToolExecutionContainer) {
currentToolExecutionContainer = createToolExecutionContainer();
}
const resultDiv = document.createElement('div');
resultDiv.className = 'tool-result';
let resultText = result;
try {
const parsed = JSON.parse(result);
resultText = JSON.stringify(parsed, null, 2);
} catch (e) {
// 保持原样
}
resultDiv.innerHTML = `
<div class="tool-name">✅ ${toolName} 执行结果:</div>
<pre>${resultText}</pre>
`;
currentToolExecutionContainer.appendChild(resultDiv);
scrollToBottom();
}
// 添加工具错误
function addToolError(toolName, error) {
if (!currentToolExecutionContainer) {
currentToolExecutionContainer = createToolExecutionContainer();
}
const errorDiv = document.createElement('div');
errorDiv.className = 'tool-error';
errorDiv.innerHTML = `
<div class="tool-name">❌ ${toolName} 执行失败:</div>
<div>${error}</div>
`;
currentToolExecutionContainer.appendChild(errorDiv);
scrollToBottom();
}
// 添加加载状态
function addLoadingMessage() {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'message assistant';
loadingDiv.id = 'loading-message';
loadingDiv.innerHTML = `
<div class="message-content">
<span class="loading"></span>
<span class="loading" style="animation-delay: 0.2s;"></span>
<span class="loading" style="animation-delay: 0.4s;"></span>
</div>
`;
messagesContainer.appendChild(loadingDiv);
scrollToBottom();
return loadingDiv;
}
// 移除加载状态
function removeLoadingMessage() {
const loadingMsg = document.getElementById('loading-message');
if (loadingMsg) {
loadingMsg.remove();
}
}
// 滚动到底部
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// 初始化或加载聊天
function initializeChat() {
let chatId = historyManager.getCurrentChatId();
let chat = chatId ? historyManager.getChat(chatId) : null;
if (!chat) {
chat = historyManager.createNewChat();
chatId = chat.id;
}
currentConversationId = chatId;
// 加载聊天消息
messagesContainer.innerHTML = '';
if (chat.messages.length === 0) {
// 显示欢迎消息
addMessage('system', `欢迎使用 MySQL MCP 聊天助手!你可以用自然语言查询、插入、更新或删除数据库中的数据。
示例:
• "查询所有用户"
• "插入一个新用户,用户名是 test,邮箱是 test@example.com,年龄是 25"
• "更新 id 为 1 的用户年龄为 30"`);
} else {
// 恢复历史消息
chat.messages.forEach(msg => {
if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'system') {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${msg.type}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = msg.content;
messageDiv.appendChild(contentDiv);
messagesContainer.appendChild(messageDiv);
}
});
scrollToBottom();
}
updateHistoryUI();
}
// 发送消息
async function sendMessage() {
const message = messageInput.value.trim();
if (!message || isProcessing) return;
// 添加用户消息
addMessage('user', message);
// 保存到历史
historyManager.addMessage(currentConversationId, {
type: 'user',
content: message,
timestamp: new Date().toISOString(),
});
messageInput.value = '';
isProcessing = true;
sendButton.disabled = true;
sendButton.textContent = '处理中...';
currentToolExecutionContainer = null; // 重置工具执行容器
const loadingMsg = addLoadingMessage();
try {
const response = await fetch(`${API_URL}/chat/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
removeLoadingMessage();
continue;
}
try {
const event = JSON.parse(data);
switch (event.type) {
case 'thinking':
// 保持加载状态
break;
case 'tool_calls':
removeLoadingMessage();
addMessage('system', '需要调用以下工具:');
break;
case 'tool_executing':
addToolCall(event.tool, event.arguments);
break;
case 'tool_result':
addToolResult(event.tool, event.result);
break;
case 'tool_error':
addToolError(event.tool, event.error);
break;
case 'generating':
removeLoadingMessage();
addLoadingMessage();
break;
case 'message':
removeLoadingMessage();
addMessage('assistant', event.content);
// 保存到历史
historyManager.addMessage(currentConversationId, {
type: 'assistant',
content: event.content,
timestamp: new Date().toISOString(),
});
currentToolExecutionContainer = null; // 完成后重置
updateHistoryUI(); // 更新历史列表
break;
case 'error':
removeLoadingMessage();
addMessage('system', `错误: ${event.message || '未知错误'}`);
break;
}
} catch (e) {
console.error('解析事件失败:', e, data);
}
}
}
}
} catch (error) {
removeLoadingMessage();
addMessage('system', `连接失败: ${error.message}`);
} finally {
isProcessing = false;
sendButton.disabled = false;
sendButton.textContent = '发送';
messageInput.focus();
}
}
// 事件监听
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 更新历史记录 UI
function updateHistoryUI() {
const chats = historyManager.getAllChats();
const currentId = historyManager.getCurrentChatId();
if (chats.length === 0) {
historyList.innerHTML = '<div class="history-empty">暂无聊天记录</div>';
return;
}
historyList.innerHTML = '';
chats.forEach(chat => {
const item = document.createElement('div');
item.className = 'history-item' + (chat.id === currentId ? ' active' : '');
const preview = chat.messages.length > 0
? chat.messages[chat.messages.length - 1].content
: '空对话';
const time = new Date(chat.updatedAt).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
item.innerHTML = `
<div class="history-item-title">${chat.title}</div>
<div class="history-item-preview">${preview}</div>
<div class="history-item-time">${time}</div>
<button class="history-item-delete" onclick="deleteChat('${chat.id}', event)">删除</button>
`;
item.addEventListener('click', () => switchChat(chat.id));
historyList.appendChild(item);
});
}
// 切换聊天
function switchChat(chatId) {
if (chatId === currentConversationId) {
closeHistory();
return;
}
historyManager.setCurrentChatId(chatId);
currentConversationId = chatId;
// 重新加载聊天
initializeChat();
closeHistory();
}
// 删除聊天
function deleteChat(chatId, event) {
event.stopPropagation();
if (confirm('确定要删除这个对话吗?')) {
historyManager.deleteChat(chatId);
// 如果删除的是当前聊天,重新初始化
if (chatId === currentConversationId) {
initializeChat();
} else {
updateHistoryUI();
}
}
}
// 新建聊天
function createNewChat() {
const newChat = historyManager.createNewChat();
currentConversationId = newChat.id;
initializeChat();
closeHistory();
}
// 打开历史记录
function openHistory() {
updateHistoryUI();
historySidebar.classList.add('open');
overlay.classList.add('show');
}
// 关闭历史记录
function closeHistory() {
historySidebar.classList.remove('open');
overlay.classList.remove('show');
}
// 事件监听
historyButton.addEventListener('click', openHistory);
newChatButton.addEventListener('click', createNewChat);
closeHistoryButton.addEventListener('click', closeHistory);
overlay.addEventListener('click', closeHistory);
// 页面加载完成后初始化
window.addEventListener('load', () => {
initializeChat();
messageInput.focus();
});