Files
hsa/Html/apps/static/js/chatbox.js
2025-12-12 00:55:16 +08:00

513 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="bi bi-check-circle-fill"></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) {
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);
}