Files
dsProject/dsLightRag/static/YunXiao/physics_quiz.html
2025-08-28 12:52:18 +08:00

741 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>物理知识测验 - 万有引力定律</title>
<style>
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 30px;
}
header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #eaeaea;
}
h1 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 15px;
}
.difficulty-indicator {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-bottom: 15px;
font-size: 16px;
}
.difficulty-medium {
background-color: #3498db;
color: white;
}
.difficulty-easy {
background-color: #2ecc71;
color: white;
}
.difficulty-hard {
background-color: #e74c3c;
color: white;
}
.quiz-info {
color: #7f8c8d;
font-size: 16px;
}
/* 题目样式 */
.question {
margin-bottom: 35px;
padding: 25px;
border-radius: 10px;
background-color: #f9f9f9;
border-left: 4px solid #3498db;
transition: all 0.3s ease;
}
.question:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.question-number {
font-weight: bold;
font-size: 18px;
color: #2c3e50;
}
.question-points {
background-color: #3498db;
color: white;
padding: 4px 10px;
border-radius: 15px;
font-size: 14px;
font-weight: bold;
}
.question-text {
font-size: 18px;
margin-bottom: 20px;
color: #34495e;
font-weight: 500;
}
.options {
margin-bottom: 20px;
}
.option {
margin-bottom: 12px;
padding: 12px 15px;
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.2s ease;
background-color: white;
display: flex;
align-items: center;
}
.option input[type="radio"] {
margin-right: 10px;
cursor: pointer;
flex-shrink: 0;
}
.option label {
cursor: pointer;
display: inline;
flex-grow: 1;
line-height: 1.4;
}
.option:hover {
border-color: #3498db;
background-color: #f0f8ff;
}
.option input[type="radio"] {
margin-right: 10px;
cursor: pointer;
}
.option label {
cursor: pointer;
display: block;
width: 100%;
}
.question-explanation {
display: none;
margin-top: 15px;
padding: 15px;
background-color: #e8f4fd;
border-radius: 8px;
border-left: 4px solid #3498db;
color: #2c3e50;
}
.hidden {
display: none;
}
/* 导航按钮样式 */
.navigation-section {
text-align: center;
margin-top: 40px;
padding: 25px;
background-color: #f8f9fa;
border-radius: 10px;
display: none;
}
#next-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 12px 30px;
border-radius: 30px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
#next-btn:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s ease;
}
#next-btn:hover:before {
left: 100%;
}
#next-btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
#next-btn:active {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.4);
}
.navigation-message {
font-size: 16px;
color: #333;
margin-bottom: 20px;
line-height: 1.6;
}
.submit-section {
text-align: center;
margin-top: 40px;
}
#submit-btn {
background-color: #2980b9;
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
}
#submit-btn:hover {
background-color: #1e6fa5;
transform: translateY(-2px);
}
#result {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
display: none;
}
.result-header {
font-size: 22px;
font-weight: bold;
margin-bottom: 15px;
color: #2c3e50;
}
.score {
font-size: 18px;
margin-bottom: 20px;
color: #333;
}
.correct-answers {
background-color: #e8f5e9;
color: #2e7d32;
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 10px;
}
.incorrect-answers {
background-color: #ffebee;
color: #c62828;
padding: 10px 15px;
border-radius: 6px;
}
/* 学伴模型选择器样式 */
.model-selector {
position: fixed; top: 40px; left: 20px; z-index: 1000;
padding: 10px; background-color: rgba(255, 255, 255, 0.8);
border-radius: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex; flex-direction: column; gap: 15px;
}
select { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background-color: white; font-size: 14px; }
/* 录音控制样式 */
.recording-controls {
position: static; display: flex; flex-direction: column; gap: 10px; margin: 0;
}
.record-button {
width: 70px; height: 70px; border-radius: 50%;
background-color: #dc3545; border: none; color: white;
font-size: 16px; cursor: pointer; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex; align-items: center; justify-content: center;
opacity: 1 !important; visibility: visible !important;
}
.record-button:hover { background-color: #c82333; }
.stop-button {
width: 70px; height: 70px; border-radius: 50%;
background-color: #6c757d; border: none; color: white;
font-size: 16px; cursor: pointer; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex; align-items: center; justify-content: center; display: none;
opacity: 1 !important; visibility: visible !important;
}
/* 录音指示器样式 */
.recording-indicator {
position: fixed; bottom: 20px; left: 20px; z-index: 1000;
padding: 10px 15px; background-color: rgba(220, 53, 69, 0.9);
color: white; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none; align-items: center;
}
.recording-dot {
width: 10px; height: 10px; background-color: white;
border-radius: 50%; margin-right: 8px; animation: pulse 1.5s infinite;
}
/* 思考中动画样式 */
.thinking-indicator {
position: fixed; bottom: 20px; right: 120px; z-index: 1000;
padding: 10px 15px; background-color: rgba(0, 123, 255, 0.9);
color: white; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none; align-items: center;
}
.thinking-dots { display: flex; gap: 4px; margin-right: 8px; }
.thinking-dot {
width: 8px; height: 8px; background-color: white; border-radius: 50%;
}
.thinking-dot:nth-child(1) { animation: pulse 1.5s infinite 0s; }
.thinking-dot:nth-child(2) { animation: pulse 1.5s infinite 0.3s; }
.thinking-dot:nth-child(3) { animation: pulse 1.5s infinite 0.6s; }
/* 结果显示容器样式 */
.result-container {
position: fixed; bottom: 80px; left: 20px; z-index: 1000;
padding: 15px; background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
max-width: 400px; width: 90%;
display: none; flex-direction: column; gap: 10px;
}
.result-header {
font-weight: bold; color: #495057; margin-bottom: 5px;
display: flex; align-items: center; gap: 5px;
}
.result-header.asr { color: #007bff; }
.result-header.feedback { color: #28a745; }
.result-text {
color: #333; line-height: 1.5; max-height: 200px; overflow-y: auto;
padding-right: 5px; word-break: break-all;
}
/* 音频播放器样式 */
.audio-player-container {
margin-top: 10px; display: flex; align-items: center; gap: 10px;
}
.play-button {
background-color: #28a745; color: white; border: none;
border-radius: 50%; width: 40px; height: 40px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.play-button:hover { background-color: #218838; }
.audio-progress {
flex-grow: 1; height: 6px; background-color: #e9ecef; border-radius: 3px;
overflow: hidden; cursor: pointer;
}
.progress-bar {
height: 100%; background-color: #28a745; width: 0%;
}
.audio-time { font-size: 12px; color: #6c757d; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
</style>
</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>
<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>
<!-- 引入试题数据和渲染逻辑 -->
<script src="physics_quiz.js"></script>
<!-- 学伴功能区域 -->
<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>
<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>
<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>
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
// 开始录音
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); }
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 rect = this.getBoundingClientRect();
const clickPosition = (e.clientX - rect.left) / rect.width;
audioElement.currentTime = clickPosition * 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" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/>
</svg>` : `
<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 updateAudioProgress() { if (audioElement) document.getElementById('progressBar').style.width = `${(audioElement.currentTime/audioElement.duration)*100}%`; }
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)}`;
}
// 模型配置映射
const modelConfig = {
shizuku: {
jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json"
},
koharu: {
jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json"
},
wanko: {
jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json"
}
};
let currentL2DWidget = null;
let modelElement = null;
// 获取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(modelName) {
// 如果没有传入modelName则从URL参数获取模型名称默认使用shizuku
if (!modelName) {
modelName = getUrlParam('model') || 'shizuku';
}
console.log("切换模型: ", modelName);
const config = modelConfig[modelName];
if (!config) {
console.error("模型配置不存在: ", modelName);
return;
}
console.log("模型JSON路径: ", config.jsonPath);
// 彻底清理现有实例
if (currentL2DWidget) {
try {
currentL2DWidget.destroy();
console.log("旧模型实例已销毁");
} catch (e) {
console.error("销毁实例失败: ", e);
}
// 修复强制移除所有canvas元素
const oldCanvases = document.querySelectorAll('canvas#l2dcanvas');
oldCanvases.forEach(canvas => canvas.remove());
currentL2DWidget = null;
modelElement = null;
}
// 创建新实例
try {
currentL2DWidget = L2Dwidget.init({
model: {
jsonPath: config.jsonPath,
scale: 1
},
display: {
position: "right",
width: 150,
height: 300,
hOffset: 0,
vOffset: -20
},
mobile: {
show: true,
scale: 0.5
},
react: {
opacityDefault: 0.7,
opacityOnHover: 0.8
}
});
// 保存模型DOM元素引用
// 修复使用setTimeout确保DOM更新完成后再获取元素
setTimeout(() => {
modelElement = document.querySelector('canvas#l2dcanvas');
if (modelElement) {
console.log("新模型DOM元素已获取");
} else {
console.warn("未找到新模型canvas元素");
}
console.log("新模型初始化成功");
}, 100);
} catch (e) {
console.error("模型初始化失败: ", e);
}
}
// 监听模型选择变化
const modelSelect = document.getElementById('model-select');
if (modelSelect) {
// 设置当前选中项
const currentModel = getUrlParam('model') || 'shizuku';
modelSelect.value = currentModel;
modelSelect.addEventListener('change', function() {
console.log("选择模型: ", this.value);
// 通过URL参数刷新页面实现模型切换
window.location.search = '?model=' + this.value;
});
console.log("模型选择监听器已绑定");
} else {
console.error("未找到模型选择器元素");
}
// 初始加载默认模型
document.addEventListener('DOMContentLoaded', function() {
initL2DWidget();
// 添加录音按钮事件绑定
document.getElementById('startRecordBtn').addEventListener('click', startRecording);
document.getElementById('stopRecordBtn').addEventListener('click', stopRecording);
});
</script>
</body>
</html>