document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const chatMessages = document.getElementById('chat-messages');
const logMessages = document.getElementById('log-messages');
const chatHistory = document.getElementById('chat-history');
const rightPanel = document.getElementById('right-panel');
const togglePanel = document.getElementById('toggle-panel');
const newChatButton = document.getElementById('new-chat');
let chatId = uuidv4();
let currentConversation = null;
let abortController = null; // 用于取消请求
// 创建取消按钮
const cancelButton = document.createElement('button');
cancelButton.id = 'cancel-button';
cancelButton.className = 'w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 hidden absolute left-1/2 -translate-x-1/2 -top-10';
cancelButton.innerHTML = '';
messageInput.parentElement.style.position = 'relative';
messageInput.parentElement.appendChild(cancelButton);
// 取消按钮点击事件
cancelButton.addEventListener('click', () => {
if (abortController) {
abortController.abort();
abortController = null;
}
// 隐藏取消按钮,显示发送按钮
cancelButton.classList.add('hidden');
sendButton.classList.remove('hidden');
// 重新启用输入框
messageInput.disabled = false;
sendButton.disabled = false;
sendButton.classList.remove('opacity-50');
});
// 配置 marked
marked.setOptions({
highlight: function(code, language) {
if (Prism.languages[language]) {
return Prism.highlight(code, Prism.languages[language], language);
}
return code;
},
breaks: true,
gfm: true
});
// 处理消息内容的函数
function processMessageContent(content) {
return content; // 直接返回原始内容
}
// 添加复制按钮到代码块
function addCopyButtons() {
// 只选择包含 code 标签的 pre 元素
document.querySelectorAll('pre code').forEach(code => {
const pre = code.parentElement;
if (pre.classList.contains('copy-button-added')) {
return;
}
pre.classList.add('copy-button-added');
const button = document.createElement('button');
button.className = 'copy-button';
button.textContent = 'Copy';
button.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(code.textContent);
button.textContent = 'Copied!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = 'Copy';
button.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
button.textContent = 'Failed';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
}
});
pre.insertBefore(button, pre.firstChild);
});
}
// 面板控制
document.querySelectorAll('.panel-toggle').forEach(button => {
button.addEventListener('click', () => {
const target = document.getElementById(button.dataset.target);
const panel = button.closest('.panel');
const icon = button.querySelector('svg');
if (target.id === 'task-content') {
// Task panel 使用高度控制
if (panel.style.flex === '0 1 48px') {
panel.style.flex = '1 1 60%';
icon.style.transform = 'rotate(180deg)';
} else {
panel.style.flex = '0 1 48px';
icon.style.transform = 'rotate(0deg)';
}
} else {
// Log panel 高度收缩
if (panel.style.flex === '0 1 48px') {
panel.style.flex = '1 1 300px';
icon.style.transform = 'rotate(180deg)';
} else {
panel.style.flex = '0 1 48px';
icon.style.transform = 'rotate(0deg)';
}
}
});
});
// 右侧面板切换
togglePanel.addEventListener('click', () => {
rightPanel.classList.toggle('w-0');
rightPanel.classList.toggle('w-[500px]');
rightPanel.classList.toggle('opacity-0');
rightPanel.classList.toggle('opacity-100');
});
// 新建对话
newChatButton.addEventListener('click', () => {
chatId = uuidv4();
currentConversation = null;
chatMessages.innerHTML = '';
messageInput.value = '';
// 创建新的历史记录项
const historyItem = document.createElement('div');
historyItem.className = 'chat-item p-3 hover:bg-gray-100 cursor-pointer rounded-lg mb-2 transition-colors flex justify-between items-start';
historyItem.dataset.chatId = chatId;
historyItem.innerHTML = `
Empty
ID: ${chatId.substring(0, 8)}...
`;
// 添加删除按钮事件
const deleteButton = historyItem.querySelector('.delete-chat');
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
deleteConversation(chatId, historyItem);
});
// 添加点击事件
historyItem.querySelector('.flex-1').addEventListener('click', () => loadConversation(chatId));
// 将新对话添加到列表顶部
if (chatHistory.firstChild) {
chatHistory.insertBefore(historyItem, chatHistory.firstChild);
} else {
chatHistory.appendChild(historyItem);
}
highlightCurrentChat();
});
// 设置 SSE 日志监听
let isAutoScrollLog = true;
function connectLogStream() {
console.log('Connecting to log stream...');
const logSource = new EventSource('/agent/api/log');
logSource.onmessage = (event) => {
const logMessage = event.data;
const wasAtBottom = isAutoScrollLog;
// 创建新的日志行
const logLine = document.createElement('div');
logLine.className = 'log-line';
logLine.textContent = logMessage;
logMessages.appendChild(logLine);
// 保持最新的1000行日志
const maxLogLines = 1000;
while (logMessages.children.length > maxLogLines) {
logMessages.removeChild(logMessages.firstChild);
}
// 如果之前在底部,则自动滚动到新消息
if (wasAtBottom) {
logMessages.scrollTop = logMessages.scrollHeight;
}
};
logSource.onerror = (error) => {
console.error('Log SSE Error:', error);
logSource.close();
// 3秒后尝试重连
console.log('Reconnecting in 3 seconds...');
setTimeout(connectLogStream, 3000);
};
logSource.onopen = () => {
console.log('Log stream connected');
};
}
// 监听日志面板的滚动事件
logMessages.addEventListener('scroll', () => {
const scrollBottom = logMessages.scrollHeight - logMessages.clientHeight;
isAutoScrollLog = Math.abs(scrollBottom - logMessages.scrollTop) < 2;
});
// 初始连接
connectLogStream();
function highlightCurrentChat() {
document.querySelectorAll('.chat-item').forEach(item => {
item.classList.remove('bg-blue-50');
});
const currentItem = document.querySelector(`[data-chat-id="${chatId}"]`);
if (currentItem) {
currentItem.classList.add('bg-blue-50');
}
}
// 删除对话
async function deleteConversation(id, element) {
try {
const response = await fetch(`/agent/api/history?id=${id}`, {
method: 'DELETE'
});
if (response.ok) {
element.remove();
if (id === chatId) {
chatId = uuidv4();
currentConversation = null;
chatMessages.innerHTML = '';
}
} else {
console.error('Failed to delete conversation');
}
} catch (error) {
console.error('Error deleting conversation:', error);
}
}
// 加载对话
async function loadConversation(id) {
try {
const response = await fetch(`/agent/api/history?id=${id}`);
const data = await response.json();
if (data.conversation) {
chatId = id;
currentConversation = data.conversation;
chatMessages.innerHTML = '';
data.conversation.messages.forEach(msg => {
appendMessage(msg.content, msg.role === 'user', false);
});
highlightCurrentChat();
}
} catch (error) {
console.error('Error loading conversation:', error);
}
}
// 添加消息到聊天区域
function appendMessage(content, isUser, animate = true) {
const processedContent = processMessageContent(content);
const messageDiv = document.createElement('div');
messageDiv.className = 'flex items-start gap-3 mb-4';
// 添加头像
const avatar = document.createElement('div');
avatar.className = 'w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 flex-shrink-0';
avatar.textContent = isUser ? '🧑💻' : '🤖';
messageDiv.appendChild(avatar);
// 消息内容
const contentDiv = document.createElement('div');
contentDiv.className = `message markdown-body rounded-lg p-4 ${isUser ? 'bg-gray-100' : 'bg-gray-50'}`;
if (!animate || isUser) {
contentDiv.innerHTML = marked.parse(processedContent);
addCopyButtons();
} else {
const typingDiv = document.createElement('div');
contentDiv.appendChild(typingDiv);
new Typed(typingDiv, {
strings: [marked.parse(processedContent)],
typeSpeed: 20,
showCursor: false,
onComplete: () => addCopyButtons()
});
}
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// 如果是新对话的第一条消息,更新历史记录标题
const historyItem = document.querySelector(`[data-chat-id="${chatId}"]`);
if (historyItem && historyItem.querySelector('.font-medium').textContent === 'Empty') {
historyItem.querySelector('.font-medium').textContent = message;
}
appendMessage(message, true);
messageInput.value = '';
// 禁用输入框和发送按钮,显示取消按钮
messageInput.disabled = true;
sendButton.disabled = true;
sendButton.classList.add('opacity-50');
sendButton.classList.add('hidden');
cancelButton.classList.remove('hidden');
try {
console.log('Starting chat with ID:', chatId);
let hasReceivedMessage = false;
let currentMessageDiv = null;
let contentDiv = null;
let accumulatedContent = '';
let isFirstChunk = true;
let lastRenderTime = 0;
// 创建新的 AbortController
abortController = new AbortController();
// 使用 fetch 替代 EventSource,添加 signal
const response = await fetch(`/agent/api/chat?id=${chatId}&message=${encodeURIComponent(message)}`, {
signal: abortController.signal
});
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = ''; // 用于存储不完整的 SSE 消息
try {
while (true) {
const {value, done} = await reader.read();
if (done) break;
// 解码新的数据块并添加到缓冲区
buffer += decoder.decode(value, {stream: true});
// 按行分割并处理每一行
const lines = buffer.split(/\r\n|\r|\n/);
// 保留最后一个可能不完整的行
buffer = lines.pop() || '';
for (const line of lines) {
// 解析 SSE 格式的行
if (line.startsWith('data:')) {
// 保留 data: 后的所有内容,包括前导空格
const rawData = line.slice(5); // 直接截取 'data:' 后的内容
// console.log(`Raw SSE data: |${rawData}|`);
hasReceivedMessage = true;
// 如果是第一个 chunk,创建新的消息框
if (isFirstChunk) {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex items-start gap-3 mb-4';
// 添加头像
const avatar = document.createElement('div');
avatar.className = 'w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 flex-shrink-0';
avatar.textContent = '🤖';
messageDiv.appendChild(avatar);
// 消息内容
contentDiv = document.createElement('div');
contentDiv.className = 'message markdown-body rounded-lg p-4 bg-gray-50';
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
currentMessageDiv = contentDiv;
isFirstChunk = false;
accumulatedContent = rawData;
} else {
if (rawData === '') {
// 如果是空数据,添加换行符
accumulatedContent += '\n';
} else {
// 否则直接拼接数据
accumulatedContent += rawData;
}
}
// 限制渲染频率
const now = Date.now();
if (now - lastRenderTime >= 100) {
renderContent();
} else {
clearTimeout(window.renderTimeout);
window.renderTimeout = setTimeout(renderContent, 100 - (now - lastRenderTime));
}
}
}
}
function renderContent() {
currentMessageDiv.innerHTML = marked.parse(accumulatedContent);
addCopyButtons();
chatMessages.scrollTop = chatMessages.scrollHeight;
lastRenderTime = Date.now();
}
// 请求完成后,隐藏取消按钮,显示发送按钮
cancelButton.classList.add('hidden');
sendButton.classList.remove('hidden');
abortController = null;
} finally {
// 确保读取器被正确关闭
reader.cancel();
}
} catch (error) {
console.error('Error sending message:', error);
if (error.name === 'AbortError') {
} else {
appendMessage('Error: Failed to send message. Please try again.', false);
}
abortController = null;
} finally {
messageInput.disabled = false;
sendButton.disabled = false;
sendButton.classList.remove('opacity-50');
sendButton.classList.remove('hidden');
cancelButton.classList.add('hidden');
}
}
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 加载历史对话列表
fetch('/agent/api/history')
.then(response => response.json())
.then(data => {
if (data.ids && data.ids.length > 0) {
chatHistory.innerHTML = ''; // 清空现有历史
let firstConversationId = null;
const loadPromises = data.ids.map(id =>
fetch(`/agent/api/history?id=${id}`)
.then(response => response.json())
.then(convData => {
if (convData.conversation) {
const firstMessage = convData.conversation.messages.length > 0
? convData.conversation.messages[0].content
: 'Empty';
const historyItem = document.createElement('div');
historyItem.className = 'chat-item p-3 hover:bg-gray-100 cursor-pointer rounded-lg mb-2 transition-colors flex justify-between items-start';
historyItem.dataset.chatId = id;
historyItem.innerHTML = `
${firstMessage}
ID: ${id.substring(0, 8)}...
`;
const deleteButton = historyItem.querySelector('.delete-chat');
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
deleteConversation(id, historyItem);
});
historyItem.querySelector('.flex-1').addEventListener('click', () => loadConversation(id));
chatHistory.appendChild(historyItem);
// 记录第一个对话的ID
if (firstConversationId === null) {
firstConversationId = id;
}
}
})
);
// 等待所有历史记录加载完成后,加载第一个对话
Promise.all(loadPromises).then(() => {
if (firstConversationId) {
loadConversation(firstConversationId);
}
});
}
})
.catch(error => console.error('Error loading history:', error));
});