514 lines
18 KiB
JavaScript
514 lines
18 KiB
JavaScript
|
||
var socket;
|
||
let system_message_idx = 0
|
||
const activeApprovals = {};
|
||
let chatManager; // 全局聊天管理器实例
|
||
|
||
// 聊天消息管理器 - 更现代的数据结构
|
||
class ChatMessageManager {
|
||
constructor() {
|
||
this.chatbox = document.getElementById('chatbox');
|
||
this.messageTemplate = {
|
||
user: `<div class="chat-message user-message"><b>你:</b> {{content}}</div>`,
|
||
assistant: `<div><b>华实君:</b></div><div class="chat-message server-message"> {{content}}</div>`
|
||
};
|
||
}
|
||
|
||
// 渲染单个消息
|
||
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
|
||
}
|
||
});
|
||
|
||
// 初始化聊天消息管理器
|
||
const 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 = `<div id="hint-title"><b>提示</b></div><div id="hint-message" class="chat-message server-message"> ${data}</div>`;
|
||
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 = `<b>Function:</b> ${element.data.name}(${argsDescription})`;
|
||
}else {
|
||
description.innerHTML = `<b>Function:</b> ${element.data.name}()`;
|
||
}
|
||
|
||
const approveButton = document.createElement('button');
|
||
approveButton.innerHTML = '<i class="fas fa-check-circle-check"></i>';
|
||
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 = `<b>Chapter ${data.chapter_id} Score:</b> ${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 += `<b>系统提示: </b> ${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) {
|
||
// 直接获取页面中的 h3 元素(不再使用 iframe)
|
||
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';
|
||
|
||
// 获取详细信息容器
|
||
const detailBox = document.getElementById('progress-detail');
|
||
|
||
// 根据N值动态调整每个进度节点的宽度
|
||
const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N
|
||
|
||
// 根据N值生成子框并设置颜色
|
||
for (let i = 0; i < N; i++) {
|
||
const titleBox = document.createElement('div');
|
||
titleBox.className = 'progress-title';
|
||
|
||
// 设置进度条的颜色
|
||
if (i < idx) {
|
||
titleBox.classList.add('green'); // 已完成部分
|
||
} else {
|
||
titleBox.classList.add('white'); // 未完成部分
|
||
}
|
||
|
||
// 设置每个子框的标题
|
||
titleBox.innerHTML = titles[i] || `Step ${i + 1}`;
|
||
|
||
// 设置每个进度节点的宽度
|
||
titleBox.style.width = `${nodeWidth}%`;
|
||
|
||
// 监听鼠标移入事件来显示详情
|
||
titleBox.addEventListener('mouseenter', function () {
|
||
detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`;
|
||
detailBox.style.display = 'block';
|
||
// 根据进度条位置显示详情
|
||
const rect = titleBox.getBoundingClientRect();
|
||
detailBox.style.top = `${rect.top + window.scrollY - 100}px`; // 微调位置
|
||
detailBox.style.left = `${rect.left + window.scrollX + rect.width / 2 - detailBox.offsetWidth / 2}px`;
|
||
});
|
||
|
||
// 监听鼠标移出事件来隐藏详情
|
||
titleBox.addEventListener('mouseleave', function () {
|
||
detailBox.style.display = 'none';
|
||
});
|
||
|
||
progressBox.appendChild(titleBox);
|
||
}
|
||
|
||
// 将生成的进度条添加到容器中
|
||
container.appendChild(progressBox);
|
||
} |