Merge branch 'feature/voice_send'

This commit is contained in:
Cai
2025-10-19 20:30:51 +08:00
15 changed files with 674 additions and 219 deletions

View File

@@ -8,13 +8,12 @@ from apps.extensions import socketio
app = create_app() app = create_app()
if __name__ == "__main__": if __name__ == "__main__":
# 仅本地开发用;生产不要用 werkzeug 自带的开发服务器
socketio.run( socketio.run(
app, app,
host="0.0.0.0", host="127.0.0.1",
port=5551, port=5551,
debug=True, # 开发期打开 debug=False, # 开发期打开
allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示 # allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示
# ssl_context=('server.crt', 'server.key') # ssl_context=('server.crt', 'server.key')
) )

View File

@@ -53,6 +53,7 @@ def create_app(config_object: type = Config) -> Flask:
# 2) 注册蓝图(把子文件暴露的蓝图统一挂到 app 上) # 2) 注册蓝图(把子文件暴露的蓝图统一挂到 app 上)
register_blueprints(app) register_blueprints(app)
app.config['TEMPLATES_AUTO_RELOAD'] = True
# 3) 其他钩子/命令/错误处理 # 3) 其他钩子/命令/错误处理
@app.route("/healthz") @app.route("/healthz")

View File

@@ -191,13 +191,15 @@ class ASEngineClient:
print(f"Failed to send text to route '{route}': {e}") print(f"Failed to send text to route '{route}': {e}")
return False return False
def send_voice(self, route: str, voice_data: bytes, id:str) -> bool: def send_voice(self, route: str, voice_data: bytes, id:str=None) -> bool:
""" """
发送语音数据 发送语音数据
:param route: 目标路由 :param route: 目标路由
:param voice_data: 语音数据字节流 :param voice_data: 语音数据字节流
:return: 是否发送成功 :return: 是否发送成功
""" """
if id is None:
id = self.id
if not self.connected: if not self.connected:
print("Not connected to server") print("Not connected to server")
return False return False
@@ -205,7 +207,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,
@@ -289,4 +290,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

@@ -91,8 +91,8 @@ class AgentNamespace(Namespace):
init_data={'room': user_uuid} init_data={'room': user_uuid}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog-hint', route='voice_out',
callback=self.receive_ase_dialog_hint, callback=self.receive_ase_voice_out,
entry=self, entry=self,
init_data={'room': user_uuid} init_data={'room': user_uuid}
) )
@@ -154,6 +154,14 @@ class AgentNamespace(Namespace):
chatmanager[uuid].chat_historys.append({'role': 'user', 'content': data['data']}) chatmanager[uuid].chat_historys.append({'role': 'user', 'content': data['data']})
user_uuid2ase_client[uuid].send_text(d['route'], data['data']) user_uuid2ase_client[uuid].send_text(d['route'], data['data'])
def on_voice(self, 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['data'])
print(f"Send voicce to route 'voice_in' success: {res}")
def on_connect_to_ase(client_entry: HSAEngineClient, namespace_entry, init_data): def on_connect_to_ase(client_entry: HSAEngineClient, namespace_entry, init_data):
''' '''
此时刚刚建立好aseclient连接并向前端发送打开code-server-like指令。 此时刚刚建立好aseclient连接并向前端发送打开code-server-like指令。
@@ -170,9 +178,10 @@ class AgentNamespace(Namespace):
client_entry.chatmanager.chat_historys.append({'role': 'assistant', 'content': data['data']}) client_entry.chatmanager.chat_historys.append({'role': 'assistant', 'content': data['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_dialog_hint(): #接受语音信息
pass 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)

View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>算法设计范式:分而治之与归并排序</h1>
<blockquote>
<p>Auto-generated at 2025-09-22 15:38</p>
</blockquote>
<h2>算法设计范式:分而治之与归并排序</h2>
<h3>第一关:理论探究:分而治之的力量</h3>
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
<p>在上周的工作中你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错但在应对大规模拥堵逆序数据其性能瓶颈非常明显。本周你的项目经理Agent将向你介绍一种更高效的算法设计思想——“分而治之”并要求你掌握其代表算法归并排序。</p>
<h4>任务:理解与分析</h4>
<h5>场景介绍</h5>
<p>你的经理告诉你归并排序Merge Sort是解决大规模排序问题的利器。在投入编码之前你必须先从理论上理解它为何如此高效。</p>
<h5>思考题</h5>
<p>请与右侧的Agent你的项目经理对话清晰地回答以下问题以证明你已理解核心思想</p>
<ol>
<li>
<p><strong>分治思想</strong>根据课程资料请解释什么是“分而治之”Divide-and-Conquer设计范式它包含哪三个基本步骤</p>
</li>
<li>
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
<ul>
<li><code>2</code> 代表什么?</li>
<li><code>T(n/2)</code> 代表什么?</li>
<li><code>Θ(n)</code> 代表什么?</li>
</ul>
</li>
<li>
<p><strong>效率对比</strong>你的同事认为插入排序经过极致优化后处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时即n趋于无穷大时哪个算法最终会胜出请从渐进增长的角度解释你的理由。</p>
</li>
<li>
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
<ul>
<li><eq>n^2</eq> (插入排序最坏情况)</li>
<li>nlogn (归并排序)</li>
<li>n (一种非常低效的蛮力算法)</li>
<li><eq>2^n</eq> (另一种指数级算法)</li>
<li>logn (类似二分查找的效率)</li>
</ul>
</li>
</ol>
<h3>第二关:编程实践:实现归并排序</h3>
<h4>任务:编码与对比</h4>
<h5>场景介绍</h5>
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
<h5>题目:实现归并排序并对比性能</h5>
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
<h5>代码框架</h5>
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code><code>merge(left, right)</code> 两个函数的实现。</p>
<pre><code class="language-python">import random
import time
#上周实现的插入排序(用于对比)
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j &gt;= 0 and arr[j] &gt; key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
#--- 本周任务:请在下方实现归并排序 ---
def merge_sort(arr):
&quot;&quot;&quot;
实现归并排序算法
参数arr: 待排序的交通数据数组
返回: 一个新的、排序好的数组
&quot;&quot;&quot;
# TODO: 实现归并排序的递归逻辑
# 提示当数组元素个数小于等于1时递归终止
def merge(left, right):
&quot;&quot;&quot;
合并两个已排序的子数组
参数left: 左侧已排序数组
参数right: 右侧已排序数组
返回: 合并后的一个有序数组
&quot;&quot;&quot;
#--- 测试与对比部分 ---
def measure_performance(sort_func, data):
start_time = time.perf_counter_ns()
sort_func(data.copy())
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6
network_sizes = [1000, 5000, 10000, 50000]
print(&quot;算法性能对比测试:&quot;)
for size in network_sizes:
# 生成逆序数据,这是插入排序的最坏情况
worst_case_data = list(range(size, 0, -1))
time_insertion = measure_performance(insertion_sort, worst_case_data)
time_merge = measure_performance(merge_sort, worst_case_data)
print(f&quot;--- 网络规模 n={size} (最坏情况) ---&quot;)
print(f&quot; - 插入排序: {time_insertion:.2f} ms&quot;)
print(f&quot; - 归并排序: {time_merge:.2f} ms&quot;)
</code></pre>
<h5>分析与讨论</h5>
<p>完成编程并得到输出后请与右侧Agent讨论以下问题</p>
<ol>
<li>
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
</li>
<li>
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
</li>
<li>
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
</li>
<li>
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong><strong>空间成本</strong>来陈述你的理由。</p>
</li>
</ol>
</body>
</html>

View File

@@ -50,6 +50,7 @@ body {
.dashboard { .dashboard {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 0 20px;
} }
.dashboard-title { .dashboard-title {
@@ -63,9 +64,13 @@ body {
/* 课程卡片容器 */ /* 课程卡片容器 */
.course-cards { .course-cards {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 20px; gap: 20px;
justify-content: center; justify-content: flex-start;
padding: 10px 0;
overflow: hidden;
position: relative;
} }
/* 课程卡片 */ /* 课程卡片 */

View File

@@ -0,0 +1,81 @@
/* 页眉导航栏 基础样式 */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #ffffff;
padding: 10px 20px;
border-bottom: 1px solid #eee;
margin: 0;
}
/* 导航栏 Logo 样式 */
.navbar .logo h1 {
font-size: 20px;
font-weight: bold;
color: #1f2937;
margin: 0;
}
/* 导航链接样式 */
.navbar-links a {
color: #4b5563;
text-decoration: none;
margin: 0 15px;
font-size: 18px;
transition: color 0.3s;
}
.navbar-links a:hover {
color: #2575fc;
}
/* 头像容器样式 */
.navbar .avatar {
margin-left: 0;
}
.navbar .avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
}
/* 下拉浮框 基础定位 */
.dropdown {
position: relative;
display: inline-block;
}
/* 下拉浮框 内容样式(默认隐藏) */
.dropdown-content {
display: none;
position: absolute;
right: 0;
background-color: white;
min-width: 160px;
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.1);
border-radius: 6px;
z-index: 1;
padding: 8px 0;
font-size: 14px;
}
/* 下拉浮框 链接样式 */
.dropdown-content a {
color: #4b5563;
text-decoration: none;
padding: 8px 12px;
display: block;
transition: background-color 0.3s;
}
.dropdown-content a:hover {
background-color: #f3f4f6;
}
/* 下拉浮框 hover 显示 */
.dropdown:hover .dropdown-content {
display: block;
}

View File

@@ -0,0 +1,11 @@
class AudioProcessor extends AudioWorkletProcessor {
process(inputs, outputs) {
const input = inputs[0];
if (input.length > 0) {
this.port.postMessage(input[0]);
}
return true;
}
}
registerProcessor('audio-processor', AudioProcessor);

View File

@@ -1,16 +1,16 @@
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));
}); });
@@ -18,10 +18,15 @@ document.addEventListener('DOMContentLoaded', function() {
OpenIframe(); OpenIframe();
}); });
// 监听来自服务器的消息 // 监听来自服务器的消息
socket.on('message', function(data) { socket.on('voiceMessage', function (data) {
console.log(data)
const input = document.createElement('messageInput');
input.data = data['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;
@@ -56,7 +61,7 @@ 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;
@@ -95,11 +100,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)
@@ -109,7 +114,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';
@@ -120,11 +125,11 @@ document.addEventListener('DOMContentLoaded', function() {
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', // 可选:设置文字颜色为白色,以便更清晰地显示
@@ -132,7 +137,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 秒
}) })
@@ -154,7 +159,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';
}); });
@@ -168,27 +173,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() {
@@ -204,7 +209,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 = '';
@@ -213,6 +218,267 @@ 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();
// }
// //发送语音
// const startAudioRecording = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// audio: {
// sampleRate: 16000,
// channelCount: 1,
// echoCancellation: true,
// noiseSuppression: true
// }
// });
// const mediaRecorder = new MediaRecorder(stream);
// const audioContext = new AudioContext({
// sampleRate: 16000
// });
// // Load the AudioWorklet
// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
// const source = audioContext.createMediaStreamSource(stream);
// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
// let audioChunks = [];
// workletNode.port.onmessage = (e) => {
// 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);
// mediaRecorder.value = {
// stop: () => {
// source.disconnect();
// workletNode.disconnect();
// stream.getTracks().forEach(track => track.stop());
// audioContext.close();
// }
// }
// isRecording.value = true;
// } catch (error) {
// console.error('Error starting audio recording:', error);
// }
// };
// function sendVoiceMessage() {
// startAudioRecording()
// //变换录音中图标
// document.getElementById('recordIcon').className = 'bi bi-record-circle';
// document.addEventListener('click', handleMouseClick);
// }
// //停止录音
// 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', merged_data, { binary: true });
// 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 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; let currentChapterIndex = -1;
function next_chapter(data) { function next_chapter(data) {
// 获取所有的 h3 元素 // 获取所有的 h3 元素
@@ -285,7 +551,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';
// 根据进度条位置显示详情 // 根据进度条位置显示详情
@@ -295,7 +561,7 @@ function generateProgressBar(N, idx, titles) {
}); });
// 监听鼠标移出事件来隐藏详情 // 监听鼠标移出事件来隐藏详情
titleBox.addEventListener('mouseleave', function() { titleBox.addEventListener('mouseleave', function () {
detailBox.style.display = 'none'; detailBox.style.display = 'none';
}); });

View File

@@ -16,6 +16,7 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
<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>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
</head> </head>
<body> <body>
<div class="maxcontainer" id="maxcontainer" style="height: 100%;"> <div class="maxcontainer" id="maxcontainer" style="height: 100%;">
@@ -61,7 +62,12 @@
</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" placeholder="输入你的消息 (支持 Markdown 语法)"></textarea>
<button class="btn btn-primary" onclick="sendMessage()">发送</button> <button class="btn btn-primary" onclick="sendMessage()">
<i class="bi bi-send"></i>
</button>
<button class="btn btn-secondary" onclick="sendVoiceMessage()">
<i class="bi bi-mic" id = "recordIcon"></i>
</button>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">

View File

@@ -9,20 +9,18 @@
<body> <body>
{% include 'navbar.html' %} <!-- 引入navbar.html --> {% include 'navbar.html' %} <!-- 引入navbar.html -->
<!-- 主页主体部分 -->
<div class="dashboard"> <div class="dashboard">
<h2 class="dashboard-title">—— 我的选课 ——</h2> <h2 class="dashboard-title">—— 我的选课 ——</h2>
<div class="course-cards" id="course-cards"> <div class="course-cards" id="course-cards">
<!-- 可以继续添加课程卡片 --> <!-- 循环生成课程卡片 -->
{% for course in user_selected_course %} {% for course in user_selected_course %}
<div class="course-card"onclick="openCourseProgress('{{course._id}}')"> <div class="course-card" onclick="openCourseProgress('{{course._id}}')">
<img src="{{course.image_url}}" alt="课程封面" class="course-image"> <img src="{{course.image_url}}" alt="课程封面" class="course-image">
<div class="course-info"> <div class="course-info">
<h3 class="course-name">{{course.name}}</h3> <h3 class="course-name">{{course.name}}</h3>
<p class="course-description">{{course.description}}</p> <p class="course-description">{{course.description}}</p>
</div> </div> <!-- 补充闭合标签 -->
</div> </div> <!-- 补充闭合标签 -->
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -45,7 +43,7 @@
</div> </div>
<script src="/static/js/dashboard.js"></script> <script src="/static/js/dashboard.js"></script>
<script> <script>
// 解析用户数据和课程数据
console.log(`{{user_data}}`.replaceAll("&#39;", "\"").replaceAll("True", "true").replaceAll("False", "false")) console.log(`{{user_data}}`.replaceAll("&#39;", "\"").replaceAll("True", "true").replaceAll("False", "false"))
console.log(`{{user_course_data}}`.replaceAll("&#34;", "\"").replaceAll("True", "true").replaceAll("False", "false")) console.log(`{{user_course_data}}`.replaceAll("&#34;", "\"").replaceAll("True", "true").replaceAll("False", "false"))
let user_data = JSON.parse(`{{user_data}}`.replaceAll("&#39;", "\"").replaceAll("True", "true").replaceAll("False", "false")); let user_data = JSON.parse(`{{user_data}}`.replaceAll("&#39;", "\"").replaceAll("True", "true").replaceAll("False", "false"));

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,21 +59,28 @@
</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="sendVoiceMessage()">
<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>
@@ -116,21 +126,21 @@
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`;
@@ -140,10 +150,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 的鼠标事件
}); });
}); });
@@ -199,7 +209,7 @@
.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();
@@ -212,21 +222,21 @@
console.error('Error loading markdown:', error); console.error('Error loading markdown:', error);
}); });
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
loadMarkdown('{{course_id}}','{{chapter_name}}','{{lesson_name}}'); 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 = '';
} }
}); });
@@ -235,4 +245,5 @@
<script src="/static/js/chatbox.js"></script> <script src="/static/js/chatbox.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,100 +1,22 @@
<!-- 页眉导航栏 --> <link rel="stylesheet" href="/static/css/navbar.css">
<header class="navbar"> <header class="navbar">
<style>
/* 页眉导航栏 */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2575fc;
padding: 10px 30px;
color: white;
border-radius: 10px;
margin: 20px;
margin-bottom: 30px;
}
.navbar .logo h1 {
font-size: 24px;
font-weight: bold;
}
.navbar-links a {
color: white;
text-decoration: none;
margin: 0 20px;
font-size: 16px;
transition: color 0.3s;
}
.navbar-links a:hover {
color: #f1c40f;
}
.navbar .avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid white;
cursor: pointer;
}
.avatar {
margin-left: 200px;
}
/* 控制浮框 */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
right: 0;
background-color: white;
color: black;
min-width: 200px;
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2);
border-radius: 10px;
z-index: 1;
padding: 10px;
font-size: 14px;
}
.dropdown-content a {
color: black;
text-decoration: none;
padding: 10px;
display: block;
transition: background-color 0.3s;
}
.dropdown-content a:hover {
background-color: #f1c40f;
}
/* 显示浮框 */
.dropdown:hover .dropdown-content {
display: block;
}
</style>
<div class="logo"> <div class="logo">
<h1>华实君:教学练评一体的虚拟助教</h1> <h1>华实君:教学练评一体的虚拟助教</h1>
</div> </div>
{% if role == 'teacher' %} {% if role == 'teacher' %}
<!-- 如果是教师角色 --> <!-- 如果是教师角色 -->
<div class="navbar-links">
<a href="/teacherindex">首页</a> <a href="/teacherindex">首页</a>
<a href="/teacherdashboard">课程</a> <a href="/teacherdashboard">课程</a>
<a href="/teachergrades">成绩</a> <a href="/teachergrades">成绩</a>
</div>
{% else %} {% else %}
<!-- 如果是学生角色 --> <!-- 如果是学生角色 -->
<div class="navbar-links">
<a href="/">首页</a> <a href="/">首页</a>
<a href="/dashboard">课程</a> <a href="/dashboard">课程</a>
<a href="/grades">成绩</a> <a href="/grades">成绩</a>
</div>
{% endif %} {% endif %}
<div class="avatar dropdown"> <div class="avatar dropdown">

View File

@@ -5,9 +5,9 @@ api_key = sk-aMpnWklN2IbsK44d1kNpy6YOP9bk1pdPJjFeEmbb0a5ytEFf
model=gpt-4.1-nano model=gpt-4.1-nano
[VSCODE_WEB] [VSCODE_WEB]
url = https://hsamooc.cn url = https://hsamooc.com
[CODE_LIKE] [CODE_LIKE]
url = https://hsamooc.cn/vsc-like url = https://hsamooc.com/vsc-like
#http://asengine.net:8282 #http://asengine.net:8282
[VSCODE_WEB_PATH] [VSCODE_WEB_PATH]
is_wsl = true is_wsl = true

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/基础算法"}