You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

524 lines
22 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
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 = `
<div class="flex-1 min-w-0 mr-2" onclick="event.stopPropagation()">
<div class="font-medium text-gray-900 truncate">Empty</div>
<div class="text-sm text-gray-500">ID: ${chatId.substring(0, 8)}...</div>
</div>
<button class="delete-chat p-1 hover:bg-red-100 rounded-lg transition-colors" onclick="event.stopPropagation()">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</button>
`;
// 添加删除按钮事件
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 = `
<div class="flex-1 min-w-0 mr-2" onclick="event.stopPropagation()">
<div class="font-medium text-gray-900 truncate">${firstMessage}</div>
<div class="text-sm text-gray-500">ID: ${id.substring(0, 8)}...</div>
</div>
<button class="delete-chat p-1 hover:bg-red-100 rounded-lg transition-colors" onclick="event.stopPropagation()">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</button>
`;
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));
});