Files
dsProject/dsLightRag/static/XueBan.html
2025-08-28 15:35:54 +08:00

476 lines
20 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>
.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;
}
.stop-button:hover { background-color: #5a6268; }
.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;
}
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
/* 添加思考中动画样式 */
.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;
display: flex; /* 始终显示播放按钮 */
}
.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;
}
</style>
</head>
<body>
<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>
<!-- 移除内联看板娘代码引用独立JS文件 -->
<script src="YunXiao/live2d_widget.js"></script>
<!-- 保留模型选择器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>
// 模型配置 - 使用与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: "汪喵" }
};
// 录音相关变量
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);
const audioUrl = URL.createObjectURL(audioBlob);
console.log("录音URL:", audioUrl);
// 这里可以调用ASR服务
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';
// 显示ASR结果
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
// 显示反馈文本
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;
};
}
}
// 切换音频播放/暂停
function togglePlayAudio() {
if (!audioElement) return;
if (isPlaying) {
audioElement.pause();
} else {
audioElement.play();
}
isPlaying = !isPlaying;
updatePlayButton();
}
// 更新播放按钮状态
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 updateAudioProgress() {
if (!audioElement || !audioElement.duration) return;
const progress = (audioElement.currentTime / audioElement.duration) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
}
// 更新音频时间显示
function updateAudioTimeDisplay() {
if (!audioElement || !audioElement.duration) return;
const currentTime = formatTime(audioElement.currentTime);
const duration = formatTime(audioElement.duration);
document.getElementById('audioTime').textContent = `${currentTime} / ${duration}`;
}
// 格式化时间为 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的工作版本
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("请授权麦克风权限以使用录音功能");
});
};
</body>
</html>