语音交互功能

This commit is contained in:
2025-09-29 12:26:48 +08:00
parent 271672c904
commit 504c5caed9
5 changed files with 276 additions and 123 deletions

View File

@@ -187,7 +187,6 @@ class ASEngineClient:
try: try:
# 编码语音数据为base64 # 编码语音数据为base64
# voice_data = 'data:image/jpeg;base64,' +base64.b64encode(voice_data).decode('utf-8') # voice_data = 'data:image/jpeg;base64,' +base64.b64encode(voice_data).decode('utf-8')
# 发送数据 # 发送数据
self.sio.emit( self.sio.emit(
route, route,
@@ -270,4 +269,3 @@ class HSAEngineClient(ASEngineClient):
self.send_text('markdown-in', fmd, self.id) self.send_text('markdown-in', fmd, self.id)
self.send_text('markdown-prompt-in', fmdp, self.id) self.send_text('markdown-prompt-in', fmdp, self.id)
self.send_text('score-prompt-in', fsp, self.id) self.send_text('score-prompt-in', fsp, self.id)

View File

@@ -63,6 +63,12 @@ class AgentNamespace(Namespace):
entry=self, entry=self,
init_data={'room': user_uuid} init_data={'room': user_uuid}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='voice_out',
callback=self.receive_ase_voice_out,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='judge', route='judge',
callback=self.receive_ase_judge, callback=self.receive_ase_judge,
@@ -119,11 +125,25 @@ class AgentNamespace(Namespace):
emit(d['route'], d, room=uuid, namespace='/agent') emit(d['route'], d, room=uuid, namespace='/agent')
user_uuid2ase_client[uuid].send_text(d['route'], data['data']) user_uuid2ase_client[uuid].send_text(d['route'], data['data'])
def on_VoiceMessage(self, voice_data):
print(f"Message from client: {voice_data}")
ex = current_app.extensions
chatmanager = ex["user_uuid2chatmanager"]
uuid = session.get('user_uuid')
user_uuid2ase_client = ex["user_uuid2ase_client"]
res = user_uuid2ase_client[uuid].send_voice('voice_in', voice_data)
print(f"Send voicce to route 'voice_in' success: {res}")
def receive_ase_dialog(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data): def receive_ase_dialog(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_dialog", data) print("receive_ase_dialog", data)
namespace_entry.emit('message', data['data'], room=init_data['room'], namespace='/agent') namespace_entry.emit('message', data['data'], room=init_data['room'], namespace='/agent')
#接受语音信息
def receive_ase_voice_out(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_voice_out", data)
namespace_entry.emit('VoiceMessage', data, room=init_data['room'], namespace='/agent')
def receive_ase_judge(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data): def receive_ase_judge(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_judge", data) print("receive_ase_judge", data)
namespace_entry.emit('request_function', data, room=init_data['room'], namespace='/agent') # data 是一个列表 /dict也支持 namespace_entry.emit('request_function', data, room=init_data['room'], namespace='/agent') # data 是一个列表 /dict也支持

View File

@@ -1,30 +1,52 @@
var socket; var socket;
let system_message_idx=0 let system_message_idx = 0
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
data = window.appData; data = window.appData;
console.log(data); console.log(data);
socket = io('wss://hsamooc.com/agent',{ socket = io('wss://hsamooc.com/agent', {
query:{ query: {
user_uuid:data.user_uuid, user_uuid: data.user_uuid,
folder:data.folder folder: data.folder
} }
}); });
socket.on('connect', function() { socket.on('connect', function () {
console.log('Connected to server'); console.log('Connected to server');
socket.emit('login',JSON.stringify(data)); socket.emit('login', JSON.stringify(data));
OpenIframe(); OpenIframe();
}); });
// 监听来自服务器的消息 // 监听来自服务器的消息
socket.on('message', function(data) { socket.on('message', function (data) {
// 显示服务器的回复消息 // 显示服务器的回复消息
const serverMessage = document.createElement('div'); const serverMessage = document.createElement('div');
serverMessage.innerHTML = `<div><b>华实君</b></div><div class="chat-message server-message"> ${marked.parse(data)}</div>`; serverMessage.innerHTML = `<div><b>华实君:</b></div><div class="chat-message server-message"> ${marked.parse(data)}</div>`;
document.getElementById('chatbox').appendChild(serverMessage); document.getElementById('chatbox').appendChild(serverMessage);
// 滚动到最新消息 // 滚动到最新消息
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
}); });
socket.on('request_function', function(data){
//监听来自服务器的消息
/*socket.on('VoiceMessage', function(data) {
console.log("Received voice answer message:", data);
// 显示服务器的回复消息
const VoiceMessage = document.createElement('div');
VoiceMessage.className = 'chat-message server-message';
VoiceMessage.innerHTML = `
<div><b>华实君:</b></div>
${marked.parse(data)}
`;
//插入与滚动
document.getElementById('chatbox').appendChild(VoiceMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
});*/
//监听来自服务器的语音识别消息
socket.on('VoiceMessage', function (data) {
console.log("Received voice message I sent from server:", data);
//将语音转文字显示在输入框中
document.getElementById('messageInput').value = data;
});
socket.on('request_function', function (data) {
try { try {
console.log("request_function", data); console.log("request_function", data);
if (typeof data === 'string') { if (typeof data === 'string') {
@@ -43,18 +65,18 @@ document.addEventListener('DOMContentLoaded', function() {
approveButton.innerHTML = '√'; // 使用勾选符号 approveButton.innerHTML = '√'; // 使用勾选符号
approveButton.data = element; approveButton.data = element;
approveButton.className = 'approve-button'; approveButton.className = 'approve-button';
approveButton.onclick = function(event) { approveButton.onclick = function (event) {
console.log(element); console.log(element);
socket.emit('message', JSON.stringify({ type: 'function', data: element })); socket.emit('message', JSON.stringify({ type: 'function', data: element }));
event.currentTarget.disabled = true; event.currentTarget.disabled = true;
event.currentTarget.classList.add('disabled'); event.currentTarget.classList.add('disabled');
}; };
requestMessage.appendChild(description); requestMessage.appendChild(description);
requestMessage.appendChild(approveButton); requestMessage.appendChild(approveButton);
document.getElementById('chatbox').appendChild(requestMessage); document.getElementById('chatbox').appendChild(requestMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
} }
// for (let i = 0; i < data.length; i++) { // for (let i = 0; i < data.length; i++) {
// const element = data[i] // const element = data[i]
// const requestMessage = document.createElement('div'); // const requestMessage = document.createElement('div');
@@ -82,11 +104,11 @@ document.addEventListener('DOMContentLoaded', function() {
console.log(error) console.log(error)
} }
}); });
socket.on('next_chapter', function(data){ socket.on('next_chapter', function (data) {
next_chapter(data); next_chapter(data);
}); });
socket.on('chapter_score',function(data){ socket.on('chapter_score', function (data) {
if (typeof data === 'string'){ if (typeof data === 'string') {
data = JSON.parse(data); data = JSON.parse(data);
} }
console.log(data) console.log(data)
@@ -96,7 +118,7 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('chatbox').appendChild(scoreMessage); document.getElementById('chatbox').appendChild(scoreMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
}) })
socket.on('system_message',function(data){ socket.on('system_message', function (data) {
console.log(data) console.log(data)
let systemMessage = document.createElement('div'); let systemMessage = document.createElement('div');
systemMessage.className = 'chat-message server-message'; systemMessage.className = 'chat-message server-message';
@@ -104,14 +126,14 @@ document.addEventListener('DOMContentLoaded', function() {
closeButton.className = 'close-button'; // 可以根据需要添加样式类 closeButton.className = 'close-button'; // 可以根据需要添加样式类
closeButton.innerHTML = '×'; // 关闭按钮的文本内容 closeButton.innerHTML = '×'; // 关闭按钮的文本内容
closeButton.style.marginRight = '10px'; // 可选:设置关闭按钮的右边距 closeButton.style.marginRight = '10px'; // 可选:设置关闭按钮的右边距
systemMessage.appendChild(closeButton); systemMessage.appendChild(closeButton);
systemMessage.innerHTML += `<b>系统提示: </b> ${data}`; systemMessage.innerHTML += `<b>系统提示: </b> ${data}`;
systemMessage.id = 'system_message_'+system_message_idx; systemMessage.id = 'system_message_' + system_message_idx;
system_message_idx+=1 system_message_idx += 1
document.getElementById('chatbox').appendChild(systemMessage); document.getElementById('chatbox').appendChild(systemMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
$('.close-button').on('click', function(event) { $('.close-button').on('click', function (event) {
$(this).css({ $(this).css({
'background-color': 'gray', 'background-color': 'gray',
'color': 'white', // 可选:设置文字颜色为白色,以便更清晰地显示 'color': 'white', // 可选:设置文字颜色为白色,以便更清晰地显示
@@ -119,7 +141,7 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
$(this).parent().remove(); $(this).parent().remove();
}); });
setTimeout(function() { setTimeout(function () {
$(systemMessage.id).remove(); $(systemMessage.id).remove();
}, 5000); // 5000 毫秒 = 5 秒 }, 5000); // 5000 毫秒 = 5 秒
}) })
@@ -130,7 +152,7 @@ document.addEventListener('DOMContentLoaded', function() {
const fontSizeSlider = document.getElementById('fontSizeSlider'); const fontSizeSlider = document.getElementById('fontSizeSlider');
const chatbox = document.getElementById('chatbox'); const chatbox = document.getElementById('chatbox');
fontSizeSlider.addEventListener('input', function() { fontSizeSlider.addEventListener('input', function () {
console.log(fontSizeSlider.value); console.log(fontSizeSlider.value);
chatbox.style.fontSize = fontSizeSlider.value + 'px'; chatbox.style.fontSize = fontSizeSlider.value + 'px';
}); });
@@ -144,27 +166,27 @@ const chineseLabel = document.getElementById('label_for_zh');
let language = 'zh'; // 默认语言 let language = 'zh'; // 默认语言
// 切换语言事件 // 切换语言事件
englishRadio.addEventListener('change', function() { englishRadio.addEventListener('change', function () {
if (englishRadio.checked) { if (englishRadio.checked) {
language = 'en'; language = 'en';
englishLabel.classList.add('active'); englishLabel.classList.add('active');
chineseLabel.classList.remove('active'); chineseLabel.classList.remove('active');
} }
socket.emit('language',language); socket.emit('language', language);
}); });
chineseRadio.addEventListener('change', function() { chineseRadio.addEventListener('change', function () {
if (chineseRadio.checked) { if (chineseRadio.checked) {
language = 'zh'; language = 'zh';
chineseLabel.classList.add('active'); chineseLabel.classList.add('active');
englishLabel.classList.remove('active'); englishLabel.classList.remove('active');
} }
socket.emit('language',language); socket.emit('language', language);
}); });
function sendInitiativeFunctionCall(function_name){ function sendInitiativeFunctionCall(function_name) {
socket.emit('initiative', {'name': function_name}); socket.emit('initiative', { 'name': function_name });
} }
// 发送消息 // 发送消息
function sendMessage() { function sendMessage() {
@@ -180,7 +202,7 @@ function sendMessage() {
document.getElementById('chatbox').appendChild(userMessage); document.getElementById('chatbox').appendChild(userMessage);
// 发送消息到服务器 // 发送消息到服务器
socket.emit('message', JSON.stringify({data: input, type:'text'})); socket.emit('message', JSON.stringify({ data: input, type: 'text' }));
// 清空输入框 // 清空输入框
document.getElementById('messageInput').value = ''; document.getElementById('messageInput').value = '';
@@ -189,6 +211,107 @@ function sendMessage() {
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
} }
let voice_stream = null;
let audio_chunks = [];
let isRecording = false;
let recordingInterval = null;
//鼠标点击终止事件
function handleMouseClick(event) {
event.stopPropagation();
stopRecording();
}
//发送语音
function sendVoiceMessage() {
p = pyaudio.PyAudio()
//变换录音中图标
document.getElementById('recordIcon').className = 'bi bi-record-circle';
//监听
document.addEventListener('click', handleMouseClick);
//打开麦克风流
voice_stream = p.open(format = FORMAT,
channels = CHANNELS,
rate = SAMPLE_RATE,
input = True,
frames_per_buffer = CHUNK_SIZE)
audio_chunks = []
recordingInterval = setInterval(() => {
if (!isRecording) {
clearInterval(recordingInterval);
return;
}
try {
// 从麦克风读取音频数据
let audio_data = voice_stream.read(CHUNK_SIZE);
if (!audio_data || audio_data.length === 0) {
stopRecording();
return;
}
// 保留原始 PCM 二进制数据
audio_chunks.push(audio_data);
// 当积累到一定数量的块时,发送
if (audio_chunks.length >= 16) {
const merged_data = new Uint8Array(
audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0)
);
let offset = 0;
for (const chunk of audio_chunks) {
merged_data.set(new Uint8Array(chunk), offset);
offset += chunk.length;
}
// 发送到服务器
socket.emit('VoiceMessage', merged_data, { binary: true });
// 清空音频块,准备接收下一批数据
audio_chunks = [];
}
} catch (error) {
console.error('Error reading audio data:', error);
stopRecording();
}
}, 10); // 每 10 毫秒检查一次录音状态
}
//停止录音
function stopRecording() {
if (!isRecording) return;
isRecording = false;
//取消监听
document.removeEventListener('click', handleMouseClick);
//取消定时器
if (recordingInterval) {
clearInterval(recordingInterval);
recordingInterval = null;
}
//变换图标
document.getElementById('recordIcon').className = 'bi bi-mic';
//发送剩下的数据
if (audio_chunks.length > 0) {
const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of audio_chunks) {
merged_data.set(new Uint8Array(chunk), offset);
offset += chunk.length;
}
socket.emit('VoiceMessage', JSON.stringify({ data: merged_data, type: 'audio' }));
audio_chunks = [];
}
overlay.style.display = 'none';
//关闭音频流
if (voice_stream) {
voice_stream.stop_stream();
voice_stream.close();
voice_stream = null;
}
if (p) {
p.terminate();
p = null;
}
}
let currentChapterIndex = -1; let currentChapterIndex = -1;
function next_chapter(data) { function next_chapter(data) {
// 获取所有的 h3 元素 // 获取所有的 h3 元素
@@ -231,22 +354,22 @@ function next_chapter(data) {
function generateProgressBar(N, idx, titles) { function generateProgressBar(N, idx, titles) {
const container = document.getElementById('markdown-content-process'); const container = document.getElementById('markdown-content-process');
container.innerHTML = ''; // 清空内容 container.innerHTML = ''; // 清空内容
// 创建进度条的外框 // 创建进度条的外框
const progressBox = document.createElement('div'); const progressBox = document.createElement('div');
progressBox.className = 'progress-box'; progressBox.className = 'progress-box';
// 获取详细信息容器 // 获取详细信息容器
const detailBox = document.getElementById('progress-detail'); const detailBox = document.getElementById('progress-detail');
// 根据N值动态调整每个进度节点的宽度 // 根据N值动态调整每个进度节点的宽度
const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N
// 根据N值生成子框并设置颜色 // 根据N值生成子框并设置颜色
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
const titleBox = document.createElement('div'); const titleBox = document.createElement('div');
titleBox.className = 'progress-title'; titleBox.className = 'progress-title';
// 设置进度条的颜色 // 设置进度条的颜色
if (i < idx) { if (i < idx) {
titleBox.classList.add('green'); // 已完成部分 titleBox.classList.add('green'); // 已完成部分
@@ -261,7 +384,7 @@ function generateProgressBar(N, idx, titles) {
titleBox.style.width = `${nodeWidth}%`; titleBox.style.width = `${nodeWidth}%`;
// 监听鼠标移入事件来显示详情 // 监听鼠标移入事件来显示详情
titleBox.addEventListener('mouseenter', function() { titleBox.addEventListener('mouseenter', function () {
detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`; detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`;
detailBox.style.display = 'block'; detailBox.style.display = 'block';
// 根据进度条位置显示详情 // 根据进度条位置显示详情
@@ -271,7 +394,7 @@ function generateProgressBar(N, idx, titles) {
}); });
// 监听鼠标移出事件来隐藏详情 // 监听鼠标移出事件来隐藏详情
titleBox.addEventListener('mouseleave', function() { titleBox.addEventListener('mouseleave', function () {
detailBox.style.display = 'none'; detailBox.style.display = 'none';
}); });

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -17,30 +18,32 @@
<script src="https://unpkg.com/split.js/dist/split.min.js"></script> <script src="https://unpkg.com/split.js/dist/split.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head> </head>
<body> <body>
<div class="maxcontainer" id="maxcontainer" style="height: 100%;"> <div class="maxcontainer" id="maxcontainer" style="height: 100%;">
<!-- 左侧内容 --> <!-- 左侧内容 -->
<div class="sidebar" id="leftSidebar" style="width: 100%;height: 100%;"> <div class="sidebar" id="leftSidebar" style="width: 100%;height: 100%;">
<div id="markdown-content"style="width: 100%;height: 93%;"> <div id="markdown-content" style="width: 100%;height: 93%;">
<!-- 动态加载的 HTML 会显示在这里 --> <!-- 动态加载的 HTML 会显示在这里 -->
</div> </div>
<div id="markdown-content-process"style="width: 100%;height: 5%;"> <div id="markdown-content-process" style="width: 100%;height: 5%;">
</div> </div>
<div id="progress-detail" style="position: absolute; top: 0; left: 0; display: none; padding: 10px; background-color: rgba(0, 0, 0, 0.7); color: white; border-radius: 5px;"> <div id="progress-detail"
style="position: absolute; top: 0; left: 0; display: none; padding: 10px; background-color: rgba(0, 0, 0, 0.7); color: white; border-radius: 5px;">
<!-- 这里将显示鼠标悬停的详情 --> <!-- 这里将显示鼠标悬停的详情 -->
</div> </div>
</div> </div>
<div id="dragbar" class="resizer"></div> <div id="dragbar" class="resizer"></div>
<!-- 中间嵌入的 Vscode-web 窗口,带有 tkn 参数 --> <!-- 中间嵌入的 Vscode-web 窗口,带有 tkn 参数 -->
<div class="vscode-web"id="vscodeWeb" > <div class="vscode-web" id="vscodeWeb">
</div> </div>
<div id="dragbar2" class="resizer"></div> <div id="dragbar2" class="resizer"></div>
<!-- 右侧内容 --> <!-- 右侧内容 -->
<div class="sidebar" id="rightSidebar"style="width: 100%;height: 100%;"> <div class="sidebar" id="rightSidebar" style="width: 100%;height: 100%;">
<div class="chatbox-header"> <div class="chatbox-header">
<div class="slider-container"> <div class="slider-container">
<label for="fontSizeSlider">Aa</label> <label for="fontSizeSlider">Aa</label>
@@ -48,7 +51,7 @@
</div> </div>
<div class="language-toggle btn-group btn-group-toggle" data-toggle="buttons"> <div class="language-toggle btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-outline-secondary " id="label_for_en"> <label class="btn btn-outline-secondary " id="label_for_en">
<input type="radio" name="language" id="english" autocomplete="off" > En <input type="radio" name="language" id="english" autocomplete="off"> En
</label> </label>
<label class="btn btn-outline-secondary active" id="label_for_zh"> <label class="btn btn-outline-secondary active" id="label_for_zh">
<input type="radio" name="language" id="chinese" autocomplete="off" checked> Zn <input type="radio" name="language" id="chinese" autocomplete="off" checked> Zn
@@ -56,27 +59,34 @@
</div> </div>
</div> </div>
<div class="chatbox" id="chatbox" > <div class="chatbox" id="chatbox">
<!-- 消息会在这里显示 --> <!-- 消息会在这里显示 -->
</div> </div>
<div class="input-area"> <div class="input-area">
<textarea id="messageInput" rows="2" class="form-control" placeholder="输入你的消息 (支持 Markdown 语法)"></textarea> <textarea id="messageInput" rows="2" class="form-control"
<button class="btn btn-primary" onclick="sendMessage()">发送</button> placeholder="输入你的消息 (支持 Markdown 语法)"></textarea>
<!--文本与语音的发送-->
<button class="btn btn-primary" onclick="sendMessage()">
<i class="bi bi-send"></i>
</button>
<button class="btn btn-secondary" onclick="sendAudioMessage()">
<i class="bi bi-mic" id = "recordIcon"></i>
</button>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
window.appData = { window.appData = {
user_uuid:"{{user_uuid}}", user_uuid: "{{user_uuid}}",
course_id:"{{course_id}}", course_id: "{{course_id}}",
lesson_name:"{{lesson_name}}", lesson_name: "{{lesson_name}}",
chapter_name:"{{chapter_name}}", chapter_name: "{{chapter_name}}",
folder:"{{workspace_path}}" folder: "{{workspace_path}}"
} }
</script> </script>
</div> </div>
</div> </div>
<!-- 侧边工具栏 --> <!-- 侧边工具栏 -->
<div class="sidebar-tools" id="sidebarTools"> <div class="sidebar-tools" id="sidebarTools">
<div class="tool-button" onclick="sendInitiativeFunctionCall('sample_judge')"> <div class="tool-button" onclick="sendInitiativeFunctionCall('sample_judge')">
@@ -101,37 +111,37 @@
<div class="toggle-button" id="toggleButton"> <div class="toggle-button" id="toggleButton">
<div class="icon" id="toggleButton_icon"></div> <div class="icon" id="toggleButton_icon"></div>
</div> </div>
<script src="/static/js/chatbox.js"></script> <script src="/static/js/chatbox.js"></script>
<script> <script>
// 实现左右边栏可拖动 // 实现左右边栏可拖动
const dragbar = document.getElementById('dragbar'); const dragbar = document.getElementById('dragbar');
const dragbar2 = document.getElementById('dragbar2'); const dragbar2 = document.getElementById('dragbar2');
const container = document.getElementById('maxcontainer'); const container = document.getElementById('maxcontainer');
let leftWidth = 500; let leftWidth = 500;
let rightWidth = 400; let rightWidth = 400;
let isDraggingLeft = false; let isDraggingLeft = false;
let isDraggingRight = false; let isDraggingRight = false;
// 当鼠标按下时,禁用 iframe 的鼠标事件 // 当鼠标按下时,禁用 iframe 的鼠标事件
dragbar.addEventListener('mousedown', function(e) { dragbar.addEventListener('mousedown', function (e) {
isDraggingLeft = true; isDraggingLeft = true;
document.querySelectorAll('iframe').forEach(function(iframe) { document.querySelectorAll('iframe').forEach(function (iframe) {
iframe.style.pointerEvents = 'none'; // 禁止 iframe 捕获鼠标事件 iframe.style.pointerEvents = 'none'; // 禁止 iframe 捕获鼠标事件
}); });
}); });
dragbar2.addEventListener('mousedown', function(e) { dragbar2.addEventListener('mousedown', function (e) {
isDraggingRight = true; isDraggingRight = true;
document.querySelectorAll('iframe').forEach(function(iframe) { document.querySelectorAll('iframe').forEach(function (iframe) {
iframe.style.pointerEvents = 'none'; // 禁止 iframe 捕获鼠标事件 iframe.style.pointerEvents = 'none'; // 禁止 iframe 捕获鼠标事件
}); });
}); });
document.addEventListener('mousemove', function(e) { document.addEventListener('mousemove', function (e) {
if (isDraggingLeft) { if (isDraggingLeft) {
leftWidth = e.clientX; // 更新左侧栏宽度 leftWidth = e.clientX; // 更新左侧栏宽度
container.style.gridTemplateColumns = `${leftWidth}px 5px 2fr 5px ${rightWidth}px`; container.style.gridTemplateColumns = `${leftWidth}px 5px 2fr 5px ${rightWidth}px`;
@@ -141,10 +151,10 @@
} }
}); });
document.addEventListener('mouseup', function() { document.addEventListener('mouseup', function () {
isDraggingLeft = false; isDraggingLeft = false;
isDraggingRight = false; isDraggingRight = false;
document.querySelectorAll('iframe').forEach(function(iframe) { document.querySelectorAll('iframe').forEach(function (iframe) {
iframe.style.pointerEvents = 'auto'; // 恢复 iframe 的鼠标事件 iframe.style.pointerEvents = 'auto'; // 恢复 iframe 的鼠标事件
}); });
}); });
@@ -158,26 +168,26 @@
method: 'GET', method: 'GET',
credentials: 'include' // 包含 Cookie确保 Flask 可以识别 session credentials: 'include' // 包含 Cookie确保 Flask 可以识别 session
}) })
.then(response => { .then(response => {
if (response.ok) return response.json(); if (response.ok) return response.json();
throw new Error('Failed to fetch session'); throw new Error('Failed to fetch session');
}) })
.then(data => { .then(data => {
console.log('Session data:', data); console.log('Session data:', data);
// 在成功获取 session 后创建并插入 iframe // 在成功获取 session 后创建并插入 iframe
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
const sessionInfo = data const sessionInfo = data
iframe.src = '{{vscode_web_url}}/?workspace={{workspace_path}}&folder={{workspace_path}}'; iframe.src = '{{vscode_web_url}}/?workspace={{workspace_path}}&folder={{workspace_path}}';
iframe.style.width = '100%'; iframe.style.width = '100%';
iframe.style.height = '99%'; iframe.style.height = '99%';
// 将 iframe 插入到指定的 div 中 // 将 iframe 插入到指定的 div 中
document.getElementById('vscodeWeb').appendChild(iframe); document.getElementById('vscodeWeb').appendChild(iframe);
}) })
.catch(error => { .catch(error => {
console.error('Error fetching session:', error); console.error('Error fetching session:', error);
}); });
@@ -186,46 +196,47 @@
// 动态加载 Markdown 文件 // 动态加载 Markdown 文件
function loadMarkdown(course_id, chapter_name, lesson_name) { function loadMarkdown(course_id, chapter_name, lesson_name) {
fetch(`/${course_id}-${chapter_name}-${lesson_name}-markdown`) fetch(`/${course_id}-${chapter_name}-${lesson_name}-markdown`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.html_url) { if (data.html_url) {
document.getElementById('markdown-content').innerHTML = `<iframe id="markdown-content-iframe" src="${data.html_url}"style="width: 100%;height: 100%;"></iframe>`; document.getElementById('markdown-content').innerHTML = `<iframe id="markdown-content-iframe" src="${data.html_url}"style="width: 100%;height: 100%;"></iframe>`;
document.getElementById('markdown-content-iframe').addEventListener('load', function() { document.getElementById('markdown-content-iframe').addEventListener('load', function () {
// 在这里执行你想要的操作 // 在这里执行你想要的操作
console.log('Iframe has finished loading'); console.log('Iframe has finished loading');
next_chapter(); next_chapter();
});
} else {
console.error('Markdown file not found');
}
})
.catch(error => {
console.error('Error loading markdown:', error);
}); });
} else { }
console.error('Markdown file not found'); document.addEventListener('DOMContentLoaded', function () {
} loadMarkdown('{{course_id}}', '{{chapter_name}}', '{{lesson_name}}');
})
.catch(error => {
console.error('Error loading markdown:', error);
}); });
}
document.addEventListener('DOMContentLoaded', function() {
loadMarkdown('{{course_id}}','{{chapter_name}}','{{lesson_name}}');
});
// 侧边工具栏切换 // 侧边工具栏切换
const sidebarTools = document.getElementById('sidebarTools'); const sidebarTools = document.getElementById('sidebarTools');
const toggleButton = document.getElementById('toggleButton'); const toggleButton = document.getElementById('toggleButton');
toggleButton.addEventListener('click', function() { toggleButton.addEventListener('click', function () {
sidebarTools.classList.toggle('open'); sidebarTools.classList.toggle('open');
toggleButton.classList.toggle('open'); toggleButton.classList.toggle('open');
if (document.getElementById('toggleButton_icon').innerText === ''){ if (document.getElementById('toggleButton_icon').innerText === '') {
document.getElementById('toggleButton_icon').innerText = ''; document.getElementById('toggleButton_icon').innerText = '';
}else{ } else {
document.getElementById('toggleButton_icon').innerText = ''; document.getElementById('toggleButton_icon').innerText = '';
} }
}); });
</script> </script>
</body> </body>
</html>
</html>

View File

@@ -0,0 +1 @@
{"user_uuid": "user_4e35d980-622b-49e0-ad29-3a4092274f9d", "course_id": "68bacdfadf5aeae0912f7f18", "chapter_name": "基础算法", "lesson_name": "二分", "path": "E:/agent/huashi/code-agent/study/cake/68bacdfadf5aeae0912f7f18/基础算法"}