var socket; let system_message_idx = 0 const activeApprovals = {}; let chatManager; // 全局聊天管理器实例 // 聊天消息管理器 - 更现代的数据结构 class ChatMessageManager { constructor() { this.chatbox = document.getElementById('chatbox'); this.messageTemplate = { user: `
你: {{content}}
`, assistant: `
华实君:
{{content}}
` }; } // 渲染单个消息 renderMessage(message) { let html; let content; // 处理不同的数据格式 if (typeof message === 'string') { // 旧格式:直接字符串 content = marked.parse(message); html = this.messageTemplate.assistant.replace('{{content}}', content); } else if (typeof message === 'object' && message !== null) { // 新格式:{role, content} 对象 content = marked.parse(message.content); if (message.role === 'user') { html = this.messageTemplate.user.replace('{{content}}', content); } else { html = this.messageTemplate.assistant.replace('{{content}}', content); } } if (html) { const messageDiv = document.createElement('div'); messageDiv.innerHTML = html; this.chatbox.appendChild(messageDiv); this.scrollToBottom(); } } // 渲染多条消息(用于恢复历史) renderMessages(messages) { messages.forEach(message => this.renderMessage(message)); } // 滚动到底部 scrollToBottom() { this.chatbox.scrollTop = this.chatbox.scrollHeight; } } // 清理过期的批准记录 function cleanupExpiredApprovals() { const now = Date.now(); for (const name in activeApprovals) { if (now - activeApprovals[name] > 30000) { // 30秒过期 delete activeApprovals[name]; } } } // 定时清理过期记录 setInterval(cleanupExpiredApprovals, 3000); // 每3秒检查一次 document.addEventListener('DOMContentLoaded', function () { data = window.appData; const host = window.location.host; console.log(`Connecting to WebSocket server at wss://${host}/agent`); socket = io(`wss://${host}/agent`, { query: { user_uuid: data.user_uuid, folder: data.folder } }); console.log('Connecting to WebSocket server at wss://${host}/agent'); console.log(data); // 初始化聊天消息管理器 chatManager = new ChatMessageManager(); socket.on('connect', function () { console.log('Connected to server'); socket.emit('login',JSON.stringify(data)); }); socket.on('open-iframe', function(data) { OpenIframe(); }); // 监听来自服务器的消息 socket.on('voiceMessage', function (data) { console.log(data); const input = document.getElementById('messageInput'); input.value = data; }); socket.on('message', function (data) { // 显示服务器的回复消息 - 单个消息 chatManager.renderMessage(data); }); socket.on('messages', function (data) { // 显示服务器发送的消息列表 - 用于恢复对话历史 chatManager.renderMessages(data); }); socket.on('message-hint', function(data){ console.log('get message-hint'); // 显示服务器的回复消息 const serverMessage = document.createElement('div'); serverMessage.innerHTML = `
提示
${data}
`; document.getElementById('chatbox').appendChild(serverMessage); // 滚动到最新消息 document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; }); socket.on('request_function', function(data){ try { console.log("request_function", data); if (typeof data === 'string') { data = JSON.parse(data); } if (typeof data === 'object') { data = [data]; } cleanupExpiredApprovals();// 先清理一次过期记录 for (let i = 0; i < data.length; i++) { const element = data[i]; const funcName = element.data.name; if (activeApprovals.hasOwnProperty(funcName)) { // 如果存在,找到并移除之前的元素 const existingElements = document.querySelectorAll(`.approve-message[data-function="${funcName}"]`); existingElements.forEach(el => el.remove()); } activeApprovals[funcName] = Date.now(); const requestMessage = document.createElement('div'); requestMessage.className = 'chat-message server-message approve-message'; requestMessage.setAttribute('data-function', funcName); const description = document.createElement('div'); const argsDescription = JSON.stringify(element.data.args); if (argsDescription) { description.innerHTML = `Function: ${element.data.name}(${argsDescription})`; }else { description.innerHTML = `Function: ${element.data.name}()`; } const approveButton = document.createElement('button'); approveButton.innerHTML = ''; approveButton.data = element; approveButton.className = 'approve-button'; approveButton.onclick = function (event) { console.log(element); socket.emit('message', JSON.stringify({ type: 'function', data: element })); event.currentTarget.disabled = true; event.currentTarget.classList.add('disabled'); }; requestMessage.appendChild(description); requestMessage.appendChild(approveButton); document.getElementById('chatbox').appendChild(requestMessage); document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; } } catch (error) { console.log(error) } }); socket.on('next_chapter', function (data) { next_chapter(data); }); socket.on('chapter_score', function (data) { if (typeof data === 'string') { data = JSON.parse(data); } console.log(data) const scoreMessage = document.createElement('div'); scoreMessage.className = 'chat-message server-message'; scoreMessage.innerHTML = `Chapter ${data.chapter_id} Score: ${JSON.stringify(data.data.content.speak)}`; document.getElementById('chatbox').appendChild(scoreMessage); document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; }) socket.on('system_message', function (data) { console.log(data) let systemMessage = document.createElement('div'); systemMessage.className = 'chat-message server-message'; let closeButton = document.createElement('button'); closeButton.className = 'close-button'; // 可以根据需要添加样式类 closeButton.innerHTML = '×'; // 关闭按钮的文本内容 closeButton.style.marginRight = '10px'; // 可选:设置关闭按钮的右边距 systemMessage.appendChild(closeButton); systemMessage.innerHTML += `系统提示: ${data}`; systemMessage.id = 'system_message_' + system_message_idx; system_message_idx += 1 document.getElementById('chatbox').appendChild(systemMessage); document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; $('.close-button').on('click', function (event) { $(this).css({ 'background-color': 'gray', 'color': 'white', // 可选:设置文字颜色为白色,以便更清晰地显示 'border': 'none' // 可选:去掉边框 }); $(this).parent().remove(); }); setTimeout(function () { $(systemMessage.id).remove(); }, 5000); // 5000 毫秒 = 5 秒 }) socket.on('process',function(data){ if (typeof data === 'string'){ data = JSON.parse(data); } console.log // 遍历每个节点,并更新相应的条目 for (const nodeName in data) { const statusData = data[nodeName]; updateEntry(nodeName, statusData); // 更新每个节点的条目 } }); }); // 控制文字大小的滑块 const fontSizeSlider = document.getElementById('fontSizeSlider'); const chatbox = document.getElementById('chatbox'); fontSizeSlider.addEventListener('input', function () { console.log(fontSizeSlider.value); chatbox.style.fontSize = fontSizeSlider.value + 'px'; }); // 语言切换按钮 const englishRadio = document.getElementById('english'); const chineseRadio = document.getElementById('chinese'); const englishLabel = document.getElementById('label_for_en'); const chineseLabel = document.getElementById('label_for_zh'); let language = 'zh'; // 默认语言 // 切换语言事件 englishRadio.addEventListener('change', function () { if (englishRadio.checked) { language = 'en'; englishLabel.classList.add('active'); chineseLabel.classList.remove('active'); } socket.emit('language', language); }); chineseRadio.addEventListener('change', function () { if (chineseRadio.checked) { language = 'zh'; chineseLabel.classList.add('active'); englishLabel.classList.remove('active'); } socket.emit('language', language); }); function sendInitiativeFunctionCall(function_name) { socket.emit('initiative', { 'name': function_name }); } // 发送消息 function sendMessage() { const input = document.getElementById('messageInput').value; if (input.trim() === '') return; // 发送消息到服务器 socket.emit('message', JSON.stringify({ data: input, type: 'text' })); // 使用聊天管理器显示用户消息 chatManager.renderMessage({ role: 'user', content: input }); // 清空输入框 document.getElementById('messageInput').value = ''; } //语音这一块// let mediaRecorderInstance = null; let audioContextInstance = null; let workletNodeInstance = null; let sourceInstance = null; let streamInstance = null; let isRecording = false; //发送语音 const startAudioRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true, noiseSuppression: true } }); // 保存实例到全局变量 streamInstance = stream; const mediaRecorder = new MediaRecorder(stream); mediaRecorderInstance = mediaRecorder; const audioContext = new AudioContext({ sampleRate: 16000 }); audioContextInstance = audioContext; // Load the AudioWorklet await audioContext.audioWorklet.addModule('/static/js/audio-processor.js'); const source = audioContext.createMediaStreamSource(stream); sourceInstance = source; const workletNode = new AudioWorkletNode(audioContext, 'audio-processor'); workletNodeInstance = workletNode; audioChunks = []; workletNode.port.onmessage = (e) => { if (!isRecording) return; audioChunks.push(new Float32Array(e.data)); if (audioChunks.length >= 16) { // 合并所有音频块 const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0); const mergedData = new Float32Array(totalLength); let offset = 0; for (const chunk of audioChunks) { mergedData.set(chunk, offset); offset += chunk.length; } const audioMessage = { id: window.data.user_uuid, type: 'audio', data: mergedData, sampleRate: 16000, timestamp: Date.now() }; socket.emit('voice', audioMessage); console.log("voice sended"); audioChunks = []; } }; source.connect(workletNode); workletNode.connect(audioContext.destination); isRecording = true; } catch (error) { console.error('Error starting audio recording:', error); // 发生错误时清理资源 stopRecording(); } }; //停止录音 function stopRecording() { if (!isRecording) return; // 停止所有音频处理 if (workletNodeInstance) { workletNodeInstance.disconnect(); workletNodeInstance = null; } if (sourceInstance) { sourceInstance.disconnect(); sourceInstance = null; } if (audioContextInstance && audioContextInstance.state !== 'closed') { audioContextInstance.close(); audioContextInstance = null; } // 停止媒体流 if (streamInstance) { streamInstance.getTracks().forEach(track => { track.stop(); }); streamInstance = null; } // 重置录音状态 isRecording = false; mediaRecorderInstance = null; audioChunks = []; // 恢复麦克风图标 const recordIcon = document.getElementById('recordIcon'); if (recordIcon) { recordIcon.className = 'bi bi-mic'; } // 移除点击事件监听器 document.removeEventListener('click', handleMouseClick); } //鼠标点击终止事件 function handleMouseClick(event) { event.stopPropagation(); stopRecording(); } function sendVoiceMessage() { if (isRecording) { stopRecording(); return; } startAudioRecording(); // 变换录音中图标 const recordIcon = document.getElementById('recordIcon'); if (recordIcon) { recordIcon.className = 'bi bi-record-circle'; } // 添加点击事件监听器 document.addEventListener('click', handleMouseClick); } //页面卸载时停止录音 window.addEventListener('beforeunload', () => { if (isRecording) { stopRecording(); } }); let currentChapterIndex = -1; function next_chapter(data) { var container = document.getElementById('markdown-content-container'); var titles = []; var h3Elements = container.querySelectorAll('h3'); console.log("h3Elements next"); if (currentChapterIndex >= h3Elements.length) { return; // 如果已经到了最后一个章节,则不进行任何操作 } // 隐藏当前章节及其后的内容 for (let i = 0; i < h3Elements.length; i++) { h3Elements[i].style.display = 'none'; titles.push(h3Elements[i].innerText); let nextSibling = h3Elements[i].nextElementSibling; while (nextSibling && nextSibling.tagName !== 'H3') { nextSibling.style.display = 'none'; nextSibling = nextSibling.nextElementSibling; } } // 显示下一个章节及其后的内容,直到再下一个h3元素或结束 if (currentChapterIndex + 1 < h3Elements.length) { h3Elements[currentChapterIndex + 1].style.display = 'block'; let nextSibling = h3Elements[currentChapterIndex + 1].nextElementSibling; while (nextSibling && nextSibling.tagName !== 'H3') { nextSibling.style.display = 'block'; nextSibling = nextSibling.nextElementSibling; } } currentChapterIndex++; generateProgressBar(h3Elements.length, currentChapterIndex, titles); } function generateProgressBar(N, idx, titles) { const container = document.getElementById('markdown-content-process'); container.innerHTML = ''; // 清空内容 // 创建进度条的外框 const progressBox = document.createElement('div'); progressBox.className = 'progress-box'; // 根据N值生成子框并设置颜色 for (let i = 0; i < N; i++) { const titleBox = document.createElement('div'); titleBox.className = 'progress-title'; // 设置进度条的颜色:已学习为蓝色,当前学习为绿色,未学习为白色 if (i < idx) { titleBox.classList.add('blue'); // 已完成部分 - 蓝色 } else if (i === idx) { titleBox.classList.add('green'); // 当前正在学习 - 绿色 } else { titleBox.classList.add('white'); // 未完成部分 - 白色 } // 移除进度条上的文字,因为现在进度条很细 titleBox.innerHTML = ''; // 使用attachImmediateTooltip函数,使提示框跟随鼠标移动 const tooltipText = `Step ${i + 1}: ${titles[i] || "No title"}`; attachImmediateTooltip(titleBox, tooltipText); progressBox.appendChild(titleBox); } // 将生成的进度条添加到容器中 container.appendChild(progressBox); }