2025-03-07 00:00:11 +08:00
|
|
|
|
// 导入Live2D控制器
|
|
|
|
|
|
import { Live2DController } from './live2d-controller.js';
|
2025-03-06 20:11:54 +08:00
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
class AITeacherApp {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
// PDF相关变量
|
|
|
|
|
|
this.pdfDoc = null;
|
|
|
|
|
|
this.pageNum = 1;
|
|
|
|
|
|
this.pageRendering = false;
|
|
|
|
|
|
this.pageNumPending = null;
|
|
|
|
|
|
this.scale = 1.0;
|
|
|
|
|
|
this.canvas = document.getElementById('pdf-canvas');
|
|
|
|
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
|
|
|
this.messageTimeout = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Live2D控制器
|
|
|
|
|
|
this.live2dController = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化PDF.js
|
|
|
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化应用
|
2025-03-07 12:11:16 +08:00
|
|
|
|
this.global_setting = null;
|
|
|
|
|
|
this.api_host = null;
|
|
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
// 音频相关
|
|
|
|
|
|
this.audioPlayer = null;
|
|
|
|
|
|
this.currentAudioBase64 = null;
|
|
|
|
|
|
this.selectedVoice = 'zf_xiaoxiao';
|
|
|
|
|
|
this.speechSpeed = 1.0;
|
|
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
this.init();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async init() {
|
2025-03-07 12:11:16 +08:00
|
|
|
|
this.global_setting = await fetch('setting.json').then(response => response.json());
|
|
|
|
|
|
this.api_host = window.location.host;
|
|
|
|
|
|
if(this.api_host.includes(':')) {
|
|
|
|
|
|
this.api_host = this.api_host.split(':')[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
this.api_host = `${this.api_host}:${this.global_setting.websocket_port}`;
|
2025-03-07 00:00:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
console.log('初始化AI教学助手...');
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化Live2D控制器
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('初始化Live2D控制器...');
|
|
|
|
|
|
this.live2dController = new Live2DController('live2d-container');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('初始化Live2D控制器时出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
// 初始化音频播放器
|
|
|
|
|
|
this.audioPlayer = document.getElementById('audio-player');
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化语音和语速控制
|
|
|
|
|
|
this.initVoiceControls();
|
|
|
|
|
|
|
2025-03-07 12:11:16 +08:00
|
|
|
|
await this.loadDefaultPDF();
|
2025-03-07 00:00:11 +08:00
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
|
|
|
|
|
|
|
console.log('AI教学助手初始化成功');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('初始化AI教学助手时出错:', error);
|
|
|
|
|
|
this.showMessage('初始化失败: ' + error.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadDefaultPDF() {
|
|
|
|
|
|
try {
|
2025-03-07 22:17:52 +08:00
|
|
|
|
const defaultPdfPath = './public/pdf/test.pdf';
|
2025-03-07 00:00:11 +08:00
|
|
|
|
const loadingTask = pdfjsLib.getDocument(defaultPdfPath);
|
|
|
|
|
|
this.pdfDoc = await loadingTask.promise;
|
|
|
|
|
|
document.getElementById('page-count').textContent = this.pdfDoc.numPages;
|
|
|
|
|
|
this.renderPage(this.pageNum);
|
|
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
// 通知服务器加载PDF
|
|
|
|
|
|
this.notifyServerPdfLoad(defaultPdfPath);
|
2025-03-07 00:00:11 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载PDF时出错:', error);
|
|
|
|
|
|
this.showMessage('PDF加载失败: ' + error.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
async notifyServerPdfLoad(pdfPath) {
|
2025-03-07 00:00:11 +08:00
|
|
|
|
try {
|
2025-03-07 12:11:16 +08:00
|
|
|
|
const response = await fetch(`http://${this.api_host}/api/load_pdf`, {
|
2025-03-07 00:00:11 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ path: pdfPath })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('服务器响应错误');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log('服务器PDF加载响应:', data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showMessage(data.message, false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showMessage(data.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-03-07 22:17:52 +08:00
|
|
|
|
console.error('通知服务器加载PDF时出错:', error);
|
|
|
|
|
|
this.showMessage('通知服务器加载PDF时出错: ' + error.message, true);
|
2025-03-07 00:00:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderPage(num) {
|
|
|
|
|
|
this.pageRendering = true;
|
|
|
|
|
|
this.pdfDoc.getPage(num).then((page) => {
|
|
|
|
|
|
const viewport = page.getViewport({ scale: this.scale });
|
|
|
|
|
|
this.canvas.height = viewport.height;
|
|
|
|
|
|
this.canvas.width = viewport.width;
|
|
|
|
|
|
|
|
|
|
|
|
const renderContext = {
|
|
|
|
|
|
canvasContext: this.ctx,
|
|
|
|
|
|
viewport: viewport
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderTask = page.render(renderContext);
|
|
|
|
|
|
|
|
|
|
|
|
renderTask.promise.then(() => {
|
|
|
|
|
|
this.pageRendering = false;
|
|
|
|
|
|
if (this.pageNumPending !== null) {
|
|
|
|
|
|
this.renderPage(this.pageNumPending);
|
|
|
|
|
|
this.pageNumPending = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
// 清空讲解区域和停止音频播放
|
|
|
|
|
|
document.getElementById('explanation-text').textContent = '点击"生成讲解"按钮获取AI讲解';
|
|
|
|
|
|
this.stopAudio();
|
|
|
|
|
|
document.getElementById('play-btn').disabled = true;
|
2025-03-07 00:00:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('page-num').value = num;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queueRenderPage(num) {
|
|
|
|
|
|
if (this.pageRendering) {
|
|
|
|
|
|
this.pageNumPending = num;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.renderPage(num);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onPrevPage() {
|
|
|
|
|
|
if (this.pageNum <= 1) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pageNum--;
|
|
|
|
|
|
this.queueRenderPage(this.pageNum);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onNextPage() {
|
|
|
|
|
|
if (this.pageNum >= this.pdfDoc.numPages) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pageNum++;
|
|
|
|
|
|
this.queueRenderPage(this.pageNum);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onPageNumChange(e) {
|
|
|
|
|
|
const newPageNum = parseInt(e.target.value);
|
|
|
|
|
|
if (newPageNum > 0 && newPageNum <= this.pdfDoc.numPages) {
|
|
|
|
|
|
this.pageNum = newPageNum;
|
|
|
|
|
|
this.queueRenderPage(this.pageNum);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onZoomIn() {
|
|
|
|
|
|
this.scale += 0.1;
|
|
|
|
|
|
this.queueRenderPage(this.pageNum);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onZoomOut() {
|
|
|
|
|
|
if (this.scale <= 0.2) return;
|
|
|
|
|
|
this.scale -= 0.1;
|
|
|
|
|
|
this.queueRenderPage(this.pageNum);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onZoomReset() {
|
|
|
|
|
|
this.scale = 1.0;
|
|
|
|
|
|
this.queueRenderPage(this.pageNum);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async onFileUpload(e) {
|
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
|
if (file && file.type === 'application/pdf') {
|
|
|
|
|
|
const fileReader = new FileReader();
|
|
|
|
|
|
|
|
|
|
|
|
fileReader.onload = async (event) => {
|
|
|
|
|
|
const typedarray = new Uint8Array(event.target.result);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const loadingTask = pdfjsLib.getDocument(typedarray);
|
|
|
|
|
|
this.pdfDoc = await loadingTask.promise;
|
|
|
|
|
|
this.pageNum = 1;
|
|
|
|
|
|
document.getElementById('page-count').textContent = this.pdfDoc.numPages;
|
|
|
|
|
|
this.renderPage(this.pageNum);
|
|
|
|
|
|
this.showMessage('PDF加载成功', false);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载PDF时出错:', error);
|
|
|
|
|
|
this.showMessage('PDF加载失败: ' + error.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fileReader.readAsArrayBuffer(file);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showMessage('请选择有效的PDF文件', true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async onExplain() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 显示加载中的消息
|
|
|
|
|
|
document.getElementById('explanation-text').textContent = '正在生成AI讲解...';
|
2025-03-07 22:17:52 +08:00
|
|
|
|
document.getElementById('play-btn').disabled = true;
|
|
|
|
|
|
this.stopAudio();
|
2025-03-07 00:00:11 +08:00
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
// 获取当前选择的语音和语速
|
|
|
|
|
|
const voice = this.selectedVoice;
|
|
|
|
|
|
const speed = this.speechSpeed;
|
|
|
|
|
|
|
|
|
|
|
|
// 发送到服务器获取AI讲解和音频
|
|
|
|
|
|
const response = await fetch(`http://${this.api_host}/api/explain_with_audio`, {
|
2025-03-07 00:00:11 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
2025-03-07 22:17:52 +08:00
|
|
|
|
page: this.pageNum,
|
|
|
|
|
|
voice: voice,
|
|
|
|
|
|
speed: speed
|
2025-03-07 00:00:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('服务器响应错误');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
2025-03-07 22:17:52 +08:00
|
|
|
|
if (data.success) {
|
|
|
|
|
|
document.getElementById('explanation-text').textContent = data.explanation;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有音频数据,启用播放按钮并自动播放
|
|
|
|
|
|
if (data.audio_base64) {
|
|
|
|
|
|
this.currentAudioBase64 = data.audio_base64;
|
|
|
|
|
|
document.getElementById('play-btn').disabled = false;
|
|
|
|
|
|
this.playAudio();
|
|
|
|
|
|
} else if (data.tts_error) {
|
|
|
|
|
|
console.error('TTS生成失败:', data.tts_error);
|
|
|
|
|
|
this.showMessage('语音生成失败,但文本讲解已生成', true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果Live2D控制器已初始化,播放说话动作
|
|
|
|
|
|
if (this.live2dController && this.live2dController.initialized) {
|
|
|
|
|
|
this.live2dController.playMotion('Talk', 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.getElementById('explanation-text').textContent = data.explanation || '生成讲解失败';
|
|
|
|
|
|
this.showMessage('生成讲解失败', true);
|
2025-03-07 00:00:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取AI讲解时出错:', error);
|
|
|
|
|
|
document.getElementById('explanation-text').textContent = '获取AI讲解时出错: ' + error.message;
|
|
|
|
|
|
this.showMessage('获取AI讲解时出错: ' + error.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-07 22:17:52 +08:00
|
|
|
|
|
|
|
|
|
|
playAudio() {
|
|
|
|
|
|
if (!this.currentAudioBase64) {
|
|
|
|
|
|
this.showMessage('没有可播放的音频', true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 将base64转换为Blob
|
|
|
|
|
|
const byteCharacters = atob(this.currentAudioBase64);
|
|
|
|
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
|
|
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
|
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
|
|
|
|
const blob = new Blob([byteArray], { type: 'audio/wav' });
|
|
|
|
|
|
|
|
|
|
|
|
// 创建URL并设置到音频播放器
|
|
|
|
|
|
const audioUrl = URL.createObjectURL(blob);
|
|
|
|
|
|
this.audioPlayer.src = audioUrl;
|
|
|
|
|
|
this.audioPlayer.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
// 播放音频
|
|
|
|
|
|
this.audioPlayer.play();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新播放按钮状态
|
|
|
|
|
|
const playBtn = document.getElementById('play-btn');
|
|
|
|
|
|
playBtn.innerHTML = '<i class="bi bi-pause-fill"></i> 暂停';
|
|
|
|
|
|
playBtn.classList.remove('btn-success');
|
|
|
|
|
|
playBtn.classList.add('btn-warning');
|
|
|
|
|
|
|
|
|
|
|
|
// 监听音频播放结束事件
|
|
|
|
|
|
this.audioPlayer.onended = () => {
|
|
|
|
|
|
playBtn.innerHTML = '<i class="bi bi-play-fill"></i> 播放';
|
|
|
|
|
|
playBtn.classList.remove('btn-warning');
|
|
|
|
|
|
playBtn.classList.add('btn-success');
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('播放音频时出错:', error);
|
|
|
|
|
|
this.showMessage('播放音频时出错: ' + error.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopAudio() {
|
|
|
|
|
|
if (this.audioPlayer) {
|
|
|
|
|
|
this.audioPlayer.pause();
|
|
|
|
|
|
this.audioPlayer.currentTime = 0;
|
|
|
|
|
|
this.audioPlayer.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
// 更新播放按钮状态
|
|
|
|
|
|
const playBtn = document.getElementById('play-btn');
|
|
|
|
|
|
playBtn.innerHTML = '<i class="bi bi-play-fill"></i> 播放';
|
|
|
|
|
|
playBtn.classList.remove('btn-warning');
|
|
|
|
|
|
playBtn.classList.add('btn-success');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toggleAudio() {
|
|
|
|
|
|
if (this.audioPlayer.paused) {
|
|
|
|
|
|
this.audioPlayer.play();
|
|
|
|
|
|
document.getElementById('play-btn').innerHTML = '<i class="bi bi-pause-fill"></i> 暂停';
|
|
|
|
|
|
document.getElementById('play-btn').classList.remove('btn-success');
|
|
|
|
|
|
document.getElementById('play-btn').classList.add('btn-warning');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.audioPlayer.pause();
|
|
|
|
|
|
document.getElementById('play-btn').innerHTML = '<i class="bi bi-play-fill"></i> 播放';
|
|
|
|
|
|
document.getElementById('play-btn').classList.remove('btn-warning');
|
|
|
|
|
|
document.getElementById('play-btn').classList.add('btn-success');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initVoiceControls() {
|
|
|
|
|
|
// 初始化语音选择器
|
|
|
|
|
|
const voiceSelect = document.getElementById('voice-select');
|
|
|
|
|
|
voiceSelect.addEventListener('change', () => {
|
|
|
|
|
|
this.selectedVoice = voiceSelect.value;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化语速控制
|
|
|
|
|
|
const speedRange = document.getElementById('speed-range');
|
|
|
|
|
|
const speedValue = document.getElementById('speed-value');
|
|
|
|
|
|
|
|
|
|
|
|
speedRange.addEventListener('input', () => {
|
|
|
|
|
|
this.speechSpeed = parseFloat(speedRange.value);
|
|
|
|
|
|
speedValue.textContent = this.speechSpeed.toFixed(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置初始值
|
|
|
|
|
|
this.selectedVoice = voiceSelect.value;
|
|
|
|
|
|
this.speechSpeed = parseFloat(speedRange.value);
|
|
|
|
|
|
speedValue.textContent = this.speechSpeed.toFixed(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadVoices() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`http://${this.api_host}/api/voices`);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('获取语音列表失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.voices && data.voices.length > 0) {
|
|
|
|
|
|
const voiceSelect = document.getElementById('voice-select');
|
|
|
|
|
|
|
|
|
|
|
|
// 清空现有选项
|
|
|
|
|
|
voiceSelect.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新选项
|
|
|
|
|
|
data.voices.forEach(voice => {
|
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
|
option.value = voice.id;
|
|
|
|
|
|
option.textContent = `${voice.name} (${voice.gender === 'female' ? '女' : '男'})`;
|
|
|
|
|
|
voiceSelect.appendChild(option);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新选中的语音
|
|
|
|
|
|
this.selectedVoice = voiceSelect.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载语音列表时出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-07 00:00:11 +08:00
|
|
|
|
|
|
|
|
|
|
showMessage(message, isError = false) {
|
|
|
|
|
|
const statusMessage = document.getElementById('status-message');
|
|
|
|
|
|
statusMessage.textContent = message;
|
|
|
|
|
|
statusMessage.className = isError ? 'error' : 'success';
|
|
|
|
|
|
statusMessage.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的超时
|
|
|
|
|
|
if (this.messageTimeout) {
|
|
|
|
|
|
clearTimeout(this.messageTimeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3秒后隐藏消息
|
|
|
|
|
|
this.messageTimeout = setTimeout(() => {
|
|
|
|
|
|
statusMessage.style.display = 'none';
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setupEventListeners() {
|
|
|
|
|
|
document.getElementById('prev-page').addEventListener('click', () => this.onPrevPage());
|
|
|
|
|
|
document.getElementById('next-page').addEventListener('click', () => this.onNextPage());
|
|
|
|
|
|
document.getElementById('page-num').addEventListener('change', (e) => this.onPageNumChange(e));
|
|
|
|
|
|
document.getElementById('zoom-in').addEventListener('click', () => this.onZoomIn());
|
|
|
|
|
|
document.getElementById('zoom-out').addEventListener('click', () => this.onZoomOut());
|
|
|
|
|
|
document.getElementById('zoom-reset').addEventListener('click', () => this.onZoomReset());
|
|
|
|
|
|
document.getElementById('pdf-upload').addEventListener('change', (e) => this.onFileUpload(e));
|
|
|
|
|
|
document.getElementById('explain-btn').addEventListener('click', () => this.onExplain());
|
2025-03-07 22:17:52 +08:00
|
|
|
|
document.getElementById('play-btn').addEventListener('click', () => this.toggleAudio());
|
2025-03-07 12:11:16 +08:00
|
|
|
|
document.getElementById('model-select').addEventListener('change', () => {
|
|
|
|
|
|
const modelName = document.getElementById('model-select').value;
|
|
|
|
|
|
this.live2dController.loadModel(modelName);
|
|
|
|
|
|
});
|
2025-03-07 22:17:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 尝试加载可用的语音列表
|
|
|
|
|
|
this.loadVoices();
|
2025-03-06 20:11:54 +08:00
|
|
|
|
}
|
2025-03-07 00:00:11 +08:00
|
|
|
|
}
|
2025-03-06 20:11:54 +08:00
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
// 当页面加载完成后初始化应用
|
|
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
window.aiTeacherApp = new AITeacherApp();
|
2025-03-06 20:11:54 +08:00
|
|
|
|
});
|