This commit is contained in:
2025-08-28 15:35:54 +08:00
parent f3ba32ca50
commit 968eee765d
6 changed files with 719 additions and 581 deletions

View File

@@ -179,287 +179,298 @@
有权限问题的话,请点击<a href="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/11%E3%80%81%E5%AD%A6%E4%BC%B4Chrome%E5%BD%95%E9%9F%B3%E9%85%8D%E7%BD%AE%E4%BF%AE%E6%94%B9.pdf">这里</a>
</div>
<script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script>
<script>
// 模型配置 - 使用与Sample.html相同的CDN链接
const models = {
shizuku: { jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json", name: "小智" },
koharu: { jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json", name: "小荷" },
wanko: { jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json", name: "汪喵" }
};
<!-- 移除内联看板娘代码引用独立JS文件 -->
<script src="YunXiao/live2d_widget.js"></script>
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
<!-- 保留模型选择器HTML -->
<div class="model-selector">
<label for="model-select">选择学伴:</label>
<select id="model-select">
<option value="shizuku">小智</option>
<option value="koharu">小荷</option>
<option value="wanko">汪喵</option>
</select>
</div>
// 获取URL参数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
// 模型配置 - 使用与Sample.html相同的CDN链接
const models = {
shizuku: { jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json", name: "小智" },
koharu: { jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json", name: "小荷" },
wanko: { jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json", name: "汪喵" }
};
// 开始录音
function startRecording() {
if (isRecording) return;
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
console.log("尝试开始录音");
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
// 获取URL参数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) audioChunks.push(event.data);
};
// 开始录音
function startRecording() {
if (isRecording) return;
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
console.log("录音完成,音频数据大小:", audioBlob.size);
const audioUrl = URL.createObjectURL(audioBlob);
console.log("录音URL:", audioUrl);
// 这里可以调用ASR服务
uploadAudioToServer(audioBlob);
};
console.log("尝试开始录音");
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.start();
isRecording = true;
document.getElementById('recordingIndicator').style.display = 'flex';
document.getElementById('startRecordBtn').style.display = 'none';
document.getElementById('stopRecordBtn').style.display = 'flex';
console.log("开始录音成功");
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) audioChunks.push(event.data);
};
// 设置最长录音时间为60秒
setTimeout(stopRecording, 60000);
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
console.log("录音完成,音频数据大小:", audioBlob.size);
const audioUrl = URL.createObjectURL(audioBlob);
console.log("录音URL:", audioUrl);
// 这里可以调用ASR服务
uploadAudioToServer(audioBlob);
};
// 停止录音
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.start();
isRecording = true;
document.getElementById('recordingIndicator').style.display = 'flex';
document.getElementById('startRecordBtn').style.display = 'none';
document.getElementById('stopRecordBtn').style.display = 'flex';
console.log("开始录音成功");
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingIndicator').style.display = 'none';
document.getElementById('startRecordBtn').style.display = 'flex';
document.getElementById('stopRecordBtn').style.display = 'none';
console.log("停止录音");
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
// 上传音频到服务器
function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器");
// 显示思考中动画
document.getElementById('thinkingIndicator').style.display = 'flex';
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('服务器响应错误');
}
return response.json();
})
.then(data => {
console.log("处理结果:", data);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
if (data.success) {
showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
}
// 设置最长录音时间为60秒
setTimeout(stopRecording, 60000);
})
.catch(error => {
console.error("上传音频失败:", error);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
alert('上传音频失败: ' + error.message);
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
// 停止录音
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingIndicator').style.display = 'none';
document.getElementById('startRecordBtn').style.display = 'flex';
document.getElementById('stopRecordBtn').style.display = 'none';
console.log("停止录音");
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
// 显示ASR识别结果和反馈
function showResults(data) {
// 更新结果显示容器
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'flex';
// 上传音频到服务器
function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器");
// 显示思考中动画
document.getElementById('thinkingIndicator').style.display = 'flex';
// 显示ASR结果
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
// 显示反馈文本
document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
// 准备音频播放
if (data.audio_url) {
if (audioElement) {
audioElement.pause();
audioElement = null;
}
audioElement = new Audio(data.audio_url);
audioElement.onloadedmetadata = function() {
updateAudioTimeDisplay();
// 音频加载完成后自动播放
try {
audioElement.play();
isPlaying = true;
updatePlayButton();
} catch (e) {
console.error("自动播放失败:", e);
}
// 无论自动播放是否成功,都显示播放按钮
document.getElementById('playAudioBtn').style.display = 'flex';
};
audioElement.ontimeupdate = function() {
updateAudioProgress();
updateAudioTimeDisplay();
};
audioElement.onended = function() {
isPlaying = false;
updatePlayButton();
};
// 绑定播放按钮事件
document.getElementById('playAudioBtn').onclick = togglePlayAudio;
// 绑定进度条点击事件
document.getElementById('audioProgress').onclick = function(e) {
if (!audioElement) return;
const progressBar = document.getElementById('audioProgress');
const rect = progressBar.getBoundingClientRect();
const clickPosition = (e.clientX - rect.left) / rect.width;
audioElement.currentTime = clickPosition * audioElement.duration;
};
fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('服务器响应错误');
}
}
return response.json();
})
.then(data => {
console.log("处理结果:", data);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
// 切换音频播放/暂停
function togglePlayAudio() {
if (!audioElement) return;
if (data.success) {
showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
}
})
.catch(error => {
console.error("上传音频失败:", error);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
if (isPlaying) {
alert('上传音频失败: ' + error.message);
});
}
// 显示ASR识别结果和反馈
function showResults(data) {
// 更新结果显示容器
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'flex';
// 显示ASR结果
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
// 显示反馈文本
document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
// 准备音频播放
if (data.audio_url) {
if (audioElement) {
audioElement.pause();
} else {
audioElement.play();
audioElement = null;
}
isPlaying = !isPlaying;
updatePlayButton();
audioElement = new Audio(data.audio_url);
audioElement.onloadedmetadata = function() {
updateAudioTimeDisplay();
// 音频加载完成后自动播放
try {
audioElement.play();
isPlaying = true;
updatePlayButton();
} catch (e) {
console.error("自动播放失败:", e);
}
// 无论自动播放是否成功,都显示播放按钮
document.getElementById('playAudioBtn').style.display = 'flex';
};
audioElement.ontimeupdate = function() {
updateAudioProgress();
updateAudioTimeDisplay();
};
audioElement.onended = function() {
isPlaying = false;
updatePlayButton();
};
// 绑定播放按钮事件
document.getElementById('playAudioBtn').onclick = togglePlayAudio;
// 绑定进度条点击事件
document.getElementById('audioProgress').onclick = function(e) {
if (!audioElement) return;
const progressBar = document.getElementById('audioProgress');
const rect = progressBar.getBoundingClientRect();
const clickPosition = (e.clientX - rect.left) / rect.width;
audioElement.currentTime = clickPosition * audioElement.duration;
};
}
}
// 更新播放按钮状态
function updatePlayButton() {
const playButton = document.getElementById('playAudioBtn');
if (isPlaying) {
playButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/>
</svg>
`;
} else {
playButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
`;
}
// 切换音频播放/暂停
function togglePlayAudio() {
if (!audioElement) return;
if (isPlaying) {
audioElement.pause();
} else {
audioElement.play();
}
isPlaying = !isPlaying;
updatePlayButton();
}
// 更新音频进度条
function updateAudioProgress() {
if (!audioElement || !audioElement.duration) return;
const progress = (audioElement.currentTime / audioElement.duration) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
// 更新播放按钮状态
function updatePlayButton() {
const playButton = document.getElementById('playAudioBtn');
if (isPlaying) {
playButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/>
</svg>
`;
} else {
playButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
`;
}
}
// 更新音频时间显示
function updateAudioTimeDisplay() {
if (!audioElement || !audioElement.duration) return;
// 更新音频进度条
function updateAudioProgress() {
if (!audioElement || !audioElement.duration) return;
const currentTime = formatTime(audioElement.currentTime);
const duration = formatTime(audioElement.duration);
document.getElementById('audioTime').textContent = `${currentTime} / ${duration}`;
}
const progress = (audioElement.currentTime / audioElement.duration) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
}
// 格式化时间为 MM:SS
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 更新音频时间显示
function updateAudioTimeDisplay() {
if (!audioElement || !audioElement.duration) return;
// 初始化看板娘 - 简化为Sample.html的工作版本
function initL2Dwidget() {
const modelId = getUrlParam('id') || 'koharu';
const model = models[modelId] || models.koharu;
const currentTime = formatTime(audioElement.currentTime);
const duration = formatTime(audioElement.duration);
document.getElementById('audioTime').textContent = `${currentTime} / ${duration}`;
}
document.getElementById('model-select').value = modelId;
console.log('加载模型:', model.name, model.jsonPath);
// 格式化时间为 MM:SS
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 初始化模型 - 与Sample.html相同的配置
L2Dwidget.init({
"model": { "jsonPath": model.jsonPath, "scale": 1 },
"display": {
"position": "right",
"width": 150,
"height": 300,
"hOffset": 0, // 重置水平偏移
"vOffset": -20 // 重置垂直偏移
},
"mobile": { "show": true, "scale": 0.5 },
"react": { "opacityDefault": 0.8, "opacityOnHover": 1 },
"dialog": { "enable": true, "script": {
'tap body': `你好啊,我是${model.name}`,
'tap face': '有什么问题或者烦心事都可以和我聊聊~'
}}
// 初始化看板娘 - 简化为Sample.html的工作版本
function initL2Dwidget() {
const modelId = getUrlParam('id') || 'koharu';
const model = models[modelId] || models.koharu;
document.getElementById('model-select').value = modelId;
console.log('加载模型:', model.name, model.jsonPath);
// 初始化模型 - 与Sample.html相同的配置
L2Dwidget.init({
"model": { "jsonPath": model.jsonPath, "scale": 1 },
"display": {
"position": "right",
"width": 150,
"height": 300,
"hOffset": 0, // 重置水平偏移
"vOffset": -20 // 重置垂直偏移
},
"mobile": { "show": true, "scale": 0.5 },
"react": { "opacityDefault": 0.8, "opacityOnHover": 1 },
"dialog": { "enable": true, "script": {
'tap body': `你好啊,我是${model.name}。`,
'tap face': '有什么问题或者烦心事都可以和我聊聊~'
}}
});
}
// 页面加载完成后初始化
window.onload = function() {
// 直接初始化看板娘,不添加额外延迟
initL2Dwidget();
// 监听下拉框变化使用独立JS暴露的接口
document.getElementById('model-select').addEventListener('change', function() {
window.switchL2DModel(this.value);
});
// 绑定录音按钮事件
document.getElementById('startRecordBtn').addEventListener('click', startRecording);
document.getElementById('stopRecordBtn').addEventListener('click', stopRecording);
// 页面加载时请求麦克风权限
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
console.log("麦克风权限已授予");
// 立即停止流,只获取权限
stream.getTracks().forEach(track => track.stop());
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
// 页面加载完成后初始化
window.onload = function() {
// 直接初始化看板娘,不添加额外延迟
initL2Dwidget();
// 监听下拉框变化
document.getElementById('model-select').addEventListener('change', function() {
window.location.search = '?id=' + this.value;
});
// 绑定录音按钮事件
document.getElementById('startRecordBtn').addEventListener('click', startRecording);
document.getElementById('stopRecordBtn').addEventListener('click', stopRecording);
// 页面加载时请求麦克风权限
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
console.log("麦克风权限已授予");
// 立即停止流,只获取权限
stream.getTracks().forEach(track => track.stop());
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
};
</script>
};
</body>
</html>

View File

@@ -1,51 +1,31 @@
// 看板娘初始化
L2Dwidget.init({
"model": {
"jsonPath": "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json",
"scale": 1
},
"display": {
"position": "right",
"width": 150,
"height": 300,
"hOffset": 0,
"vOffset": -20
},
"mobile": {
"show": true,
"scale": 0.5
},
"react": {
"opacityDefault": 0.8,
"opacityOnHover": 0.2
// 看板娘模型配置(基于原始代码提取)
const models = {
shizuku: { jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json", name: "小智" },
koharu: { jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json", name: "小荷" },
wanko: { jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json", name: "汪喵" }
};
// 获取URL参数工具函数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
// 看板娘初始化函数
function initL2Dwidget() {
const modelId = getUrlParam('id') || 'koharu';
const model = models[modelId] || models.koharu;
// 设置下拉框选中状态
if (document.getElementById('model-select')) {
document.getElementById('model-select').value = modelId;
}
});
console.log('加载模型:', model.name, model.jsonPath);
// 模型选择器事件监听
document.getElementById('model-select').addEventListener('change', function() {
const selectedModel = this.value;
let modelPath;
switch(selectedModel) {
case 'shizuku':
modelPath = "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json";
break;
case 'koharu':
modelPath = "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json";
break;
case 'wanko':
modelPath = "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json";
break;
default:
modelPath = "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json";
}
// 重新初始化看板娘
// 初始化L2Dwidget保留原始配置
L2Dwidget.init({
"model": {
"jsonPath": modelPath,
"scale": 1
},
"model": { "jsonPath": model.jsonPath, "scale": 1 },
"display": {
"position": "right",
"width": 150,
@@ -53,13 +33,19 @@ document.getElementById('model-select').addEventListener('change', function() {
"hOffset": 0,
"vOffset": -20
},
"mobile": {
"show": true,
"scale": 0.5
},
"react": {
"opacityDefault": 0.8,
"opacityOnHover": 0.2
}
"mobile": { "show": true, "scale": 0.5 },
"react": { "opacityDefault": 0.8, "opacityOnHover": 1 },
"dialog": { "enable": true, "script": {
'tap body': `你好啊,我是${model.name}`,
'tap face': '有什么问题或者烦心事都可以和我聊聊~'
}}
});
});
}
// 页面加载完成后初始化看板娘
window.addEventListener('load', initL2Dwidget);
// 暴露模型切换功能接口
window.switchL2DModel = function(modelId) {
window.location.search = '?id=' + modelId;
};

View File

@@ -113,8 +113,8 @@ h1 {
}
.option {
margin-bottom: 12px;
padding: 12px 15px;
margin-bottom: 15px; /* 从12px增加到15px */
padding: 15px 18px; /* 从12px 15px增加到15px 18px */
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
@@ -124,8 +124,9 @@ h1 {
align-items: center;
}
.option input[type="radio"] {
margin-right: 10px;
.option input[type="radio"],
.option input[type="checkbox"] {
margin-right: 20px; /* 增加间距到20px */
cursor: pointer;
flex-shrink: 0;
}

View File

@@ -7,108 +7,113 @@
<link rel="stylesheet" href="physics_quiz.css">
</head>
<body>
<div class="container">
<header>
<h1>物理知识测验 - 万有引力定律</h1>
<div class="difficulty-indicator difficulty-medium">当前难度:中等</div>
<div class="quiz-info">
共15道题分三个难度级别。完成当前难度后将根据您的表现推荐下一难度。
</div>
</header>
<div class="question-section">
<!-- 试题将通过JavaScript动态渲染 -->
<div class="container">
<header>
<h1>物理知识测验 - 万有引力定律</h1>
<div class="difficulty-indicator difficulty-medium">当前难度:中等</div>
<div class="quiz-info">
共15道题分三个难度级别。完成当前难度后将根据您的表现推荐下一难度。
</div>
</header>
<div class="navigation-section" id="navigation">
<h3 id="navigation-message"></h3>
<button type="button" id="next-btn">开始答题</button>
</div>
<div class="submit-section">
<button type="button" id="submit-btn">提交答案</button>
<div id="result">
<div class="result-header">测验结果</div>
<div class="score">您的得分:<span id="score-value">0</span>/100分</div>
<div class="correct-answers">正确题数:<span id="correct-count">0</span></div>
<div class="incorrect-answers">错误题数:<span id="incorrect-count">0</span></div>
</div>
</div>
<div class="question-section">
<!-- 试题将通过JavaScript动态渲染 -->
</div>
<!-- 学伴功能区域 -->
<div class="model-selector">
<label for="model-select">选择学伴:</label>
<select id="model-select">
<option value="shizuku">小智</option>
<option value="koharu">小荷</option>
<option value="wanko">汪喵</option>
</select>
<div class="navigation-section" id="navigation">
<h3 id="navigation-message"></h3>
<button type="button" id="next-btn">开始答题</button>
</div>
<div class="recording-indicator" id="recordingIndicator">
<div class="recording-dot"></div>
<span>正在录音...</span>
</div>
<div class="thinking-indicator" id="thinkingIndicator">
<div class="thinking-dots">
<div class="thinking-dot"></div>
<div class="thinking-dot"></div>
<div class="thinking-dot"></div>
</div>
<span>学伴正在思考中...</span>
</div>
<div class="recording-controls" style="position: fixed; right:37px; bottom: 230px; z-index: 998;">
<button class="record-button" id="startRecordBtn" style="font-size: 14px;">帮我讲题</button>
<button class="stop-button" id="stopRecordBtn" style="font-size: 14px;">停止讲话</button>
</div>
<div class="result-container" id="resultContainer">
<div>
<div class="result-header asr">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1H4C2.89 1 2 1.9 2 3V13C2 14.1 2.89 15 4 15H12C13.11 15 14 14.1 14 13V3C14 1.9 13.11 1 12 1ZM12 13H4V3H12V13Z" fill="#007bff"/>
<path d="M8 4C6.34 4 5 5.34 5 7C5 8.66 6.34 10 8 10C9.66 10 11 8.66 11 7C11 5.34 9.66 4 8 4ZM8 8.5C7.17 8.5 6.5 7.83 6.5 7C6.5 6.17 7.17 5.5 8 5.5C8.83 5.5 9.5 6.17 9.5 7C9.5 7.83 8.83 8.5 8 8.5Z" fill="#007bff"/>
<path d="M8 11C5.79 11 4 12.79 4 15V16H12V15C12 12.79 10.21 11 8 11ZM8 13C9.1 13 10 13.9 10 15H6C6 13.9 6.9 13 8 13Z" fill="#007bff"/>
</svg>
你讲的话
</div>
<div class="result-text" id="asrResultText"></div>
</div>
<div>
<div class="result-header feedback">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4C1 2.9 1.9 2 3 2H13C14.1 2 15 2.9 15 4V11C15 12.1 14.1 13 13 13H9L5 16V13H3C1.9 13 1 12.1 1 11V4Z" fill="#28a745"/>
<path d="M8 8H6V5H8V8ZM11 8H9V5H11V8ZM11 11H6V9H11V11Z" fill="#28a745"/>
</svg>
学伴回复
</div>
<div class="result-text" id="feedbackResultText"></div>
</div>
<div class="audio-player-container">
<button class="play-button" id="playAudioBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
</button>
<div class="audio-progress" id="audioProgress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="audio-time" id="audioTime">00:00 / 00:00</div>
<div class="submit-section">
<button type="button" id="submit-btn">提交答案</button>
<div id="result">
<div class="result-header">测验结果</div>
<div class="score">您的得分:<span id="score-value">0</span>/100分</div>
<div class="correct-answers">正确题数:<span id="correct-count">0</span></div>
<div class="incorrect-answers">错误题数:<span id="incorrect-count">0</span></div>
</div>
</div>
</div>
<!-- 学伴功能区域 -->
<div class="model-selector">
<label for="model-select">选择学伴:</label>
<select id="model-select">
<option value="shizuku">小智</option>
<option value="koharu">小荷</option>
<option value="wanko">汪喵</option>
</select>
</div>
<div class="recording-indicator" id="recordingIndicator">
<div class="recording-dot"></div>
<span>正在录音...</span>
</div>
<div class="thinking-indicator" id="thinkingIndicator">
<div class="thinking-dots">
<div class="thinking-dot"></div>
<div class="thinking-dot"></div>
<div class="thinking-dot"></div>
</div>
<span>学伴正在思考中...</span>
</div>
<div class="recording-controls" style="position: fixed; right:37px; bottom: 230px; z-index: 998;">
<button class="record-button" id="startRecordBtn" style="font-size: 14px;">帮我讲题</button>
<button class="stop-button" id="stopRecordBtn" style="font-size: 14px;">停止讲话</button>
</div>
<div class="result-container" id="resultContainer">
<div>
<a href="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/11%E3%80%81%E5%AD%A6%E4%BC%B4Chrome%E5%BD%95%E9%9F%B3%E9%85%8D%E7%BD%AE%E4%BF%AE%E6%94%B9.pdf">有权限问题?</a>
<div class="result-header asr">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1H4C2.89 1 2 1.9 2 3V13C2 14.1 2.89 15 4 15H12C13.11 15 14 14.1 14 13V3C14 1.9 13.11 1 12 1ZM12 13H4V3H12V13Z"
fill="#007bff"/>
<path d="M8 4C6.34 4 5 5.34 5 7C5 8.66 6.34 10 8 10C9.66 10 11 8.66 11 7C11 5.34 9.66 4 8 4ZM8 8.5C7.17 8.5 6.5 7.83 6.5 7C6.5 6.17 7.17 5.5 8 5.5C8.83 5.5 9.5 6.17 9.5 7C9.5 7.83 8.83 8.5 8 8.5Z"
fill="#007bff"/>
<path d="M8 11C5.79 11 4 12.79 4 15V16H12V15C12 12.79 10.21 11 8 11ZM8 13C9.1 13 10 13.9 10 15H6C6 13.9 6.9 13 8 13Z"
fill="#007bff"/>
</svg>
你讲的话
</div>
<div class="result-text" id="asrResultText"></div>
</div>
<div>
<div class="result-header feedback">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4C1 2.9 1.9 2 3 2H13C14.1 2 15 2.9 15 4V11C15 12.1 14.1 13 13 13H9L5 16V13H3C1.9 13 1 12.1 1 11V4Z"
fill="#28a745"/>
<path d="M8 8H6V5H8V8ZM11 8H9V5H11V8ZM11 11H6V9H11V11Z" fill="#28a745"/>
</svg>
学伴回复
</div>
<div class="result-text" id="feedbackResultText"></div>
</div>
<div class="audio-player-container">
<button class="play-button" id="playAudioBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
</button>
<div class="audio-progress" id="audioProgress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="audio-time" id="audioTime">00:00 / 00:00</div>
</div>
</div>
<!-- 引入看板娘相关脚本 -->
<script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script>
<script src="live2d_widget.js"></script>
<!-- 引入试题数据和渲染逻辑 -->
<script src="physics_quiz.js"></script>
<div>
<a href="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/11%E3%80%81%E5%AD%A6%E4%BC%B4Chrome%E5%BD%95%E9%9F%B3%E9%85%8D%E7%BD%AE%E4%BF%AE%E6%94%B9.pdf">有权限问题?</a>
</div>
<!-- 引入看板娘相关脚本 -->
<script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script>
<script src="live2d_widget.js"></script> <!-- 先加载基础配置 -->
<script src="xueban.js"></script> <!-- 后加载扩展功能 -->
<!-- 引入试题数据和渲染逻辑 -->
<script src="physics_quiz.js"></script>
</body>
</html>

View File

@@ -18,8 +18,8 @@ const correctAnswers = {
hq1: 'C',
hq2: 'A',
hq3: 'C',
hq4: 'A',
hq5: 'B'
hq4: ['C', 'D'], // 多选题正确答案是CD
hq5: ['A', 'C'] // 多选题正确答案是AC
}
};
@@ -47,10 +47,10 @@ const quizQuestions = [
points: 20,
text: '地球质量为M半径为R引力常量为G。一颗质量为m的人造地球卫星在距离地面高度为h的轨道上做匀速圆周运动  ',
options: [
{ id: 'mq2-a', label: 'A', text: '卫星的线速度大小为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0\/media/\image1.png" alt="公式">' },
{ id: 'mq2-b', label: 'B', text: '卫星的角速度大小为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0\/media/\image2.png" alt="公式">' },
{ id: 'mq2-c', label: 'C', text: '卫星的向心加速度大小为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0\/media/\image3.png" alt="公式">' },
{ id: 'mq2-d', label: 'D', text: '卫星的周期为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0\/media/\image4.png" alt="公式">' }
{ id: 'mq2-a', label: 'A', text: '卫星的线速度大小为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0/media/image1.png" alt="公式">' },
{ id: 'mq2-b', label: 'B', text: '卫星的角速度大小为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0/media/image2.png" alt="公式">' },
{ id: 'mq2-c', label: 'C', text: '卫星的向心加速度大小为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0/media/image3.png" alt="公式">' },
{ id: 'mq2-d', label: 'D', text: '卫星的周期为<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0/media/image4.png" alt="公式">' }
],
explanation: '正确答案C<br>解析A.卫星的线速度大小为v=√(GM/(R+h))故A错误B.卫星的角速度大小为ω=√(GM/(R+h)³)故B错误C.卫星的向心加速度大小为a=GM/(R+h)²故C正确D.卫星的周期为T=2π√((R+h)³/GM)故D错误。故答案为C。'
},
@@ -87,11 +87,11 @@ const quizQuestions = [
difficulty: 'medium',
number: 5,
points: 20,
text: '我国首颗超百Gbps容量高通量地球静止轨道通信卫星中星26号卫星于北京时间2023年2月23日在西昌卫星发射中心成功发射该卫星主要用于为固定端及车、船、机载终端提供高速宽带接入服务。如图某时刻中星26与椭圆轨道侦察卫星恰好位于C、D两点两星轨道相交于A、B两点C、D连线过地心D点为远地点两卫星运行周期都为T。下列说法正确的是  <br><br><img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/760c54b8e6491d7c3ac566d6299818a4\/media/\image1.png" alt="轨道图">',
text: '我国首颗超百Gbps容量高通量地球静止轨道通信卫星中星26号卫星于北京时间2023年2月23日在西昌卫星发射中心成功发射该卫星主要用于为固定端及车、船、机载终端提供高速宽带接入服务。如图某时刻中星26与椭圆轨道侦察卫星恰好位于C、D两点两星轨道相交于A、B两点C、D连线过地心D点为远地点两卫星运行周期都为T。下列说法正确的是  <br><br><img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/760c54b8e6491d7c3ac566d6299818a4/media/image1.png" alt="轨道图">',
options: [
{ id: 'mq5-a', label: 'A', text: '中星26与侦察卫星可能在A点或B点相遇' },
{ id: 'mq5-b', label: 'B', text: '侦查卫星从D点运动到A点过程中机械能增大' },
{ id: 'mq5-c', label: 'C', text: '中星26在C点线速度<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/760c54b8e6491d7c3ac566d6299818a4\/media/\image2.png" alt="公式">与侦察卫星在D点线速度<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/760c54b8e6491d7c3ac566d6299818a4\/media/\image3.png" alt="公式">相等' },
{ id: 'mq5-c', label: 'C', text: '中星26在C点线速度<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/760c54b8e6491d7c3ac566d6299818a4/media/image2.png" alt="公式">与侦察卫星在D点线速度<img src="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/760c54b8e6491d7c3ac566d6299818a4/media/image3.png" alt="公式">相等' },
{ id: 'mq5-d', label: 'D', text: '相等时间内中星26与地球的连线扫过的面积大于侦察卫星与地球的连线扫过的面积' }
],
explanation: '正确答案D<br>解析A.中星26与侦察卫星周期相同并且当中星26在下半周运动时卫星在上半周运动故不可能相遇故A错误B.侦查卫星在D到A点过程中只有引力做功故机械能不变故B错误C.开普勒第二定律可知在近地点速度大于远地点速度故中星26在C点线速度大于侦察卫星在D点线速度故C错误D.中星26与侦察卫星的周期相同由开普勒第三定律中星26轨道半径等于侦察卫星的半长轴运动一个周期中星26是一个圆而侦察卫星是一个椭圆由于圆的面积大于椭圆的面积故相等时间内中星26与地球的连线扫过的面积大于侦察卫星与地球的连线扫过的面积故D正确故答案为D。'
@@ -259,7 +259,7 @@ const incorrectCount = document.getElementById('incorrect-count');
const difficultyIndicator = document.querySelector('.difficulty-indicator');
const questionSection = document.querySelector('.question-section');
// 获取难度名称 - 移到全局作用域
// 获取难度名称
function getDifficultyName(difficulty) {
const names = {
easy: '简单',
@@ -273,199 +273,183 @@ function getDifficultyName(difficulty) {
document.addEventListener('DOMContentLoaded', function() {
// 渲染题目
renderQuestions();
// 修改录音按钮事件绑定代码
// 录音按钮事件绑定
const startRecordBtn = document.getElementById('startRecordBtn');
const stopRecordBtn = document.getElementById('stopRecordBtn');
const recordingIndicator = document.getElementById('recordingIndicator');
// 隐藏停止讲话按钮,因为我们不需要它
if (stopRecordBtn) {
stopRecordBtn.style.display = 'none';
}
if (startRecordBtn && stopRecordBtn && recordingIndicator) {
// 点击"帮我讲题"按钮
startRecordBtn.addEventListener('click', function() {
// 显示录音指示器
recordingIndicator.style.display = 'flex';
// 隐藏"帮我讲题"按钮,显示"停止讲话"按钮
startRecordBtn.style.display = 'none';
stopRecordBtn.style.display = 'flex';
// 这里添加实际录音功能逻辑
console.log('开始录音...');
// 示例调用录音API或相关函数
// startRecording();
});
startRecordBtn.addEventListener('click', function() {
recordingIndicator.style.display = 'flex';
startRecordBtn.style.display = 'none';
stopRecordBtn.style.display = 'flex';
console.log('开始录音...');
});
// 点击"停止讲话"按钮
stopRecordBtn.addEventListener('click', function() {
// 隐藏录音指示器
recordingIndicator.style.display = 'none';
// 隐藏"停止讲话"按钮,显示"帮我讲题"按钮
stopRecordBtn.style.display = 'none';
startRecordBtn.style.display = 'flex';
// 这里添加停止录音功能逻辑
console.log('停止录音...');
// 示例:停止录音并处理音频
// stopRecording();
});
stopRecordBtn.addEventListener('click', function() {
recordingIndicator.style.display = 'none';
stopRecordBtn.style.display = 'none';
startRecordBtn.style.display = 'flex';
console.log('停止录音...');
});
} else {
console.error('录音相关DOM元素未找到');
console.error('录音相关DOM元素未找到');
}
// 提交当前难度的答案
// 修改提交按钮的事件处理逻辑
// 提交按钮事件
submitBtn.addEventListener('click', function() {
const currentAnswers = correctAnswers[quizState.currentDifficulty];
const totalQuestions = Object.keys(currentAnswers).length;
let score = 0;
let correct = 0;
let incorrect = 0;
const currentAnswers = correctAnswers[quizState.currentDifficulty];
const totalQuestions = Object.keys(currentAnswers).length;
let score = 0;
let correct = 0;
let incorrect = 0;
// 检查所有问题的答案
for (const [questionName, correctOptions] of Object.entries(currentAnswers)) {
// 获取用户选择的所有选项
const selectedOptions = Array.from(document.querySelectorAll(`input[name="${questionName}"]:checked`))
.map(checkbox => checkbox.value);
// 检查所有问题的答案
for (const [questionName, correctOptions] of Object.entries(currentAnswers)) {
// 获取用户选择的所有选项
const selectedOptions = Array.from(document.querySelectorAll(`input[name="${questionName}"]:checked`))
.map(checkbox => checkbox.value);
const questionNumber = questionName.substring(2); // 从mq1提取1
const explanationElement = document.querySelector(`[data-difficulty="${quizState.currentDifficulty}"][data-question="${questionNumber}"] .question-explanation`);
const questionNumber = questionName.substring(2); // 从mq1提取1
const explanationElement = document.querySelector(`[data-difficulty="${quizState.currentDifficulty}"][data-question="${questionNumber}"] .question-explanation`);
// 显示解析
explanationElement.style.display = 'block';
// 显示解析
explanationElement.style.display = 'block';
if (selectedOptions.length > 0) {
// 标记用户选择的选项
let allCorrect = true;
if (selectedOptions.length > 0) {
// 标记用户选择的选项
let allCorrect = true;
// 标记用户选择的每个选项
document.querySelectorAll(`input[name="${questionName}"]:checked`).forEach(checkbox => {
const userAnswer = checkbox.value;
if (correctOptions.includes(userAnswer)) {
checkbox.closest('.option').style.backgroundColor = '#e8f5e9';
checkbox.closest('.option').style.borderColor = '#4caf50';
} else {
checkbox.closest('.option').style.backgroundColor = '#ffebee';
checkbox.closest('.option').style.borderColor = '#c62828';
allCorrect = false;
}
// 标记用户选择的每个选项
document.querySelectorAll(`input[name="${questionName}"]:checked`).forEach(checkbox => {
const userAnswer = checkbox.value;
if (correctOptions.includes(userAnswer)) {
checkbox.closest('.option').style.backgroundColor = '#e8f5e9';
checkbox.closest('.option').style.borderColor = '#4caf50';
} else {
checkbox.closest('.option').style.backgroundColor = '#ffebee';
checkbox.closest('.option').style.borderColor = '#c62828';
allCorrect = false;
}
});
// 标记用户未选择但正确的选项
document.querySelectorAll(`input[name="${questionName}"]`).forEach(checkbox => {
if (correctOptions.includes(checkbox.value) && !checkbox.checked) {
checkbox.closest('.option').style.backgroundColor = '#e3f2fd';
checkbox.closest('.option').style.borderColor = '#2196f3';
allCorrect = false;
}
});
// 判断是否全对
if (allCorrect && selectedOptions.length === correctOptions.length) {
score += 20; // 每题20分
correct++;
} else {
incorrect++;
}
} else {
// 未答题
incorrect++;
const questionElement = document.querySelector(`[data-difficulty="${quizState.currentDifficulty}"][data-question="${questionNumber}"]`);
questionElement.style.border = '2px solid #ff9800';
// 显示所有正确答案
document.querySelectorAll(`input[name="${questionName}"]`).forEach(checkbox => {
if (correctOptions.includes(checkbox.value)) {
checkbox.closest('.option').style.backgroundColor = '#e3f2fd';
checkbox.closest('.option').style.borderColor = '#2196f3';
}
});
}
}
// 保存当前难度得分
quizState.scores[quizState.currentDifficulty] = {
score: score,
correct: correct,
incorrect: incorrect
};
quizState.completedDifficulties.push(quizState.currentDifficulty);
// 显示当前难度结果
scoreValue.textContent = score;
correctCount.textContent = correct;
incorrectCount.textContent = incorrect;
result.style.display = 'block';
// 隐藏提交按钮
submitBtn.style.display = 'none';
// 根据当前难度和得分决定下一步
if (quizState.currentDifficulty === 'medium') {
if (correct < 3) {
// 中等难度答对少于3题推荐简单难度
showNavigation('您在中等难度题目中表现不佳,建议先完成简单难度题目来巩固基础。', 'easy');
} else {
// 中等难度答对3题及以上推荐高级难度
showNavigation('恭喜您完成中等难度题目!接下来挑战高级难度题目吧。', 'hard');
}
} else {
// 简单或高级难度完成后,显示最终结果
showFinalResult();
}
});
// 标记用户未选择但正确的选项
document.querySelectorAll(`input[name="${questionName}"]`).forEach(checkbox => {
if (correctOptions.includes(checkbox.value) && !checkbox.checked) {
checkbox.closest('.option').style.backgroundColor = '#e3f2fd';
checkbox.closest('.option').style.borderColor = '#2196f3';
allCorrect = false;
}
});
// 判断是否全对
if (allCorrect && selectedOptions.length === correctOptions.length) {
score += 20; // 每题20分
correct++;
} else {
incorrect++;
}
} else {
// 未答题
incorrect++;
const questionElement = document.querySelector(`[data-difficulty="${quizState.currentDifficulty}"][data-question="${questionNumber}"]`);
questionElement.style.border = '2px solid #ff9800';
// 显示所有正确答案
document.querySelectorAll(`input[name="${questionName}"]`).forEach(checkbox => {
if (correctOptions.includes(checkbox.value)) {
checkbox.closest('.option').style.backgroundColor = '#e3f2fd';
checkbox.closest('.option').style.borderColor = '#2196f3';
}
});
}
}
// 保存当前难度得分
quizState.scores[quizState.currentDifficulty] = {
score: score,
correct: correct,
incorrect: incorrect
};
quizState.completedDifficulties.push(quizState.currentDifficulty);
// 显示当前难度结果
scoreValue.textContent = score;
correctCount.textContent = correct;
incorrectCount.textContent = incorrect;
result.style.display = 'block';
// 隐藏提交按钮
submitBtn.style.display = 'none';
// 根据当前难度和得分决定下一步
if (quizState.currentDifficulty === 'medium') {
if (correct < 3) {
// 中等难度答对少于3题推荐简单难度
showNavigation('您在中等难度题目中表现不佳,建议先完成简单难度题目来巩固基础。', 'easy');
} else {
// 中等难度答对3题及以上推荐高级难度
showNavigation('恭喜您完成中等难度题目!接下来挑战高级难度题目吧。', 'hard');
}
} else {
// 简单或高级难度完成后,显示最终结果
showFinalResult();
}
});
// 显示导航信息
function showNavigation(message, nextDifficulty) {
navigationMessage.textContent = message;
navigationSection.dataset.nextDifficulty = nextDifficulty;
navigationSection.style.display = 'block';
}
// 开始下一组题目
nextBtn.addEventListener('click', function() {
const nextDifficulty = navigationSection.dataset.nextDifficulty;
if (!nextDifficulty) return;
const nextDifficulty = navigationSection.dataset.nextDifficulty;
if (!nextDifficulty) return;
// 更新当前难度
quizState.currentDifficulty = nextDifficulty;
// 更新当前难度
quizState.currentDifficulty = nextDifficulty;
// 隐藏导航和结果
navigationSection.style.display = 'none';
result.style.display = 'none';
// 隐藏导航和结果
navigationSection.style.display = 'none';
result.style.display = 'none';
// 显示提交按钮
submitBtn.style.display = 'inline-block';
// 显示提交按钮
submitBtn.style.display = 'inline-block';
// 更新难度指示器
difficultyIndicator.textContent = `当前难度:${getDifficultyName(nextDifficulty)}`;
difficultyIndicator.className = `difficulty-indicator difficulty-${nextDifficulty}`;
// 更新难度指示器
difficultyIndicator.textContent = `当前难度:${getDifficultyName(nextDifficulty)}`;
difficultyIndicator.className = `difficulty-indicator difficulty-${nextDifficulty}`;
// 隐藏所有题目
document.querySelectorAll('.question').forEach(question => {
question.classList.add('hidden');
// 隐藏所有题目
document.querySelectorAll('.question').forEach(question => {
question.classList.add('hidden');
});
// 显示下一组题目
document.querySelectorAll(`[data-difficulty="${nextDifficulty}"]`).forEach(question => {
question.classList.remove('hidden');
});
// 滚动到页面顶部
window.scrollTo(0, 0);
});
});
// 显示下一组题目
document.querySelectorAll(`[data-difficulty="${nextDifficulty}"]`).forEach(question => {
question.classList.remove('hidden');
});
// 显示导航信息
function showNavigation(message, nextDifficulty) {
navigationMessage.textContent = message;
navigationSection.dataset.nextDifficulty = nextDifficulty;
navigationSection.style.display = 'block';
}
// 滚动到页面顶部
window.scrollTo(0, 0);
});
// 显示最终结果
function showFinalResult() {
// 显示最终结果
function showFinalResult() {
// 计算总分
let totalScore = 0;
let totalCorrect = 0;
let totalIncorrect = 0;
for (const difficulty of quizState.completedDifficulties) {
totalScore += quizState.scores[difficulty].score;
totalCorrect += quizState.scores[difficulty].correct;
totalIncorrect += quizState.scores[difficulty].incorrect;
totalScore += quizState.scores[difficulty].score;
totalCorrect += quizState.scores[difficulty].correct;
totalIncorrect += quizState.scores[difficulty].incorrect;
}
// 更新结果显示
@@ -478,8 +462,7 @@ document.addEventListener('DOMContentLoaded', function() {
navigationMessage.textContent = '恭喜您完成所有推荐题目!';
navigationSection.style.display = 'block';
document.getElementById('next-btn').style.display = 'none';
}
});
}
// 渲染题目函数
function renderQuestions() {

View File

@@ -0,0 +1,152 @@
// 学伴录音功能核心逻辑
// 模型配置
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
// 获取URL参数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
// 开始录音
function startRecording() {
if (isRecording) return;
console.log("尝试开始录音");
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
console.log("录音完成,音频数据大小:", audioBlob.size);
uploadAudioToServer(audioBlob);
};
mediaRecorder.start();
isRecording = true;
document.getElementById('recordingIndicator').style.display = 'flex';
document.getElementById('startRecordBtn').style.display = 'none';
document.getElementById('stopRecordBtn').style.display = 'flex';
console.log("开始录音成功");
// 设置最长录音时间为60秒
setTimeout(stopRecording, 60000);
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
// 停止录音
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingIndicator').style.display = 'none';
document.getElementById('startRecordBtn').style.display = 'flex';
document.getElementById('stopRecordBtn').style.display = 'none';
console.log("停止录音");
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
// 上传音频到服务器
function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器");
document.getElementById('thinkingIndicator').style.display = 'flex';
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('服务器响应错误');
return response.json();
})
.then(data => {
console.log("处理结果:", data);
document.getElementById('thinkingIndicator').style.display = 'none';
if (data.success) {
showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
}
})
.catch(error => {
console.error("上传音频失败:", error);
document.getElementById('thinkingIndicator').style.display = 'none';
alert('上传音频失败: ' + error.message);
});
}
// 显示ASR识别结果和反馈
function showResults(data) {
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'flex';
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
if (data.audio_url) {
if (audioElement) audioElement.pause();
audioElement = new Audio(data.audio_url);
audioElement.onloadedmetadata = function() {
updateAudioTimeDisplay();
try { audioElement.play(); isPlaying = true; updatePlayButton(); }
catch (e) { console.error("自动播放失败:", e); }
};
audioElement.ontimeupdate = function() { updateAudioProgress(); updateAudioTimeDisplay(); };
audioElement.onended = function() { isPlaying = false; updatePlayButton(); };
document.getElementById('playAudioBtn').onclick = togglePlayAudio;
document.getElementById('audioProgress').onclick = function(e) {
const rect = this.getBoundingClientRect();
audioElement.currentTime = (e.clientX - rect.left) / rect.width * audioElement.duration;
};
}
}
// 音频播放控制函数
function togglePlayAudio() {
if (!audioElement) return;
isPlaying ? audioElement.pause() : audioElement.play();
isPlaying = !isPlaying;
updatePlayButton();
}
function updatePlayButton() {
const btn = document.getElementById('playAudioBtn');
btn.innerHTML = isPlaying ?
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/></svg>' :
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M8 5V19L19 12L8 5Z" fill="white"/></svg>';
}
function updateAudioProgress() {
if (!audioElement) return;
const progress = (audioElement.currentTime / audioElement.duration) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
}
function updateAudioTimeDisplay() {
if (!audioElement) return;
const format = s => `${Math.floor(s/60).toString().padStart(2,'0')}:${Math.floor(s%60).toString().padStart(2,'0')}`;
document.getElementById('audioTime').textContent = `${format(audioElement.currentTime)} / ${format(audioElement.duration)}`;
}