Ai-Teacher/js/main.js

429 lines
16 KiB
JavaScript
Raw Normal View History

2025-03-07 00:00:11 +08:00
// 导入Live2D控制器
import { Live2DController } from './live2d-controller.js';
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;
// 讲解状态
this.explanationsGenerated = false;
this.explanationsGenerating = false;
// Live2D控制器
this.live2dController = null;
// 初始化PDF.js
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
// 初始化应用
this.init();
}
async init() {
try {
console.log('初始化AI教学助手...');
// 初始化Live2D控制器
try {
console.log('初始化Live2D控制器...');
this.live2dController = new Live2DController('live2d-container');
const success = await this.live2dController.init();
if (success) {
console.log('Live2D控制器初始化成功');
// 自动加载默认模型
this.onLoadModel();
} else {
console.error('Live2D控制器初始化失败');
this.showMessage('Live2D控制器初始化失败请检查控制台日志', true);
}
} catch (error) {
console.error('初始化Live2D控制器时出错:', error);
this.showMessage('初始化Live2D控制器时出错: ' + error.message, true);
}
// 加载默认PDF
await this.loadDefaultPDF();
// 设置事件监听器
this.setupEventListeners();
console.log('AI教学助手初始化成功');
} catch (error) {
console.error('初始化AI教学助手时出错:', error);
this.showMessage('初始化失败: ' + error.message, true);
}
}
async loadDefaultPDF() {
try {
const defaultPdfPath = './VLA4RM-仿生智能.pdf';
const loadingTask = pdfjsLib.getDocument(defaultPdfPath);
this.pdfDoc = await loadingTask.promise;
document.getElementById('page-count').textContent = this.pdfDoc.numPages;
this.renderPage(this.pageNum);
// 触发服务器端PDF加载和讲解生成
this.triggerServerPdfLoad(defaultPdfPath);
} catch (error) {
console.error('加载PDF时出错:', error);
this.showMessage('PDF加载失败: ' + error.message, true);
}
}
async triggerServerPdfLoad(pdfPath) {
try {
this.explanationsGenerating = true;
this.showMessage('正在生成所有页面的讲解,请稍候...', false);
const response = await fetch('/api/load_pdf', {
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);
// 开始轮询讲解生成状态
this.pollExplanationStatus();
} else {
this.showMessage(data.message, true);
}
} catch (error) {
console.error('触发服务器PDF加载时出错:', error);
this.showMessage('触发服务器PDF加载时出错: ' + error.message, true);
this.explanationsGenerating = false;
}
}
async pollExplanationStatus() {
// 如果已经生成完毕或不在生成状态,停止轮询
if (!this.explanationsGenerating) return;
try {
const response = await fetch('/api/explanation_status');
if (!response.ok) {
throw new Error('服务器响应错误');
}
const data = await response.json();
console.log('讲解状态:', data);
if (data.is_complete) {
this.explanationsGenerated = true;
this.explanationsGenerating = false;
this.showMessage(`所有 ${data.total_pages} 页的讲解已生成完毕`, false);
// 获取当前页面的讲解
this.fetchExplanationForCurrentPage();
} else {
// 更新生成进度
const progress = Math.round((data.explanations_generated / data.total_pages) * 100);
this.showMessage(`讲解生成中: ${progress}% (${data.explanations_generated}/${data.total_pages})`, false);
// 继续轮询
setTimeout(() => this.pollExplanationStatus(), 2000);
}
} catch (error) {
console.error('轮询讲解状态时出错:', error);
this.explanationsGenerating = false;
}
}
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;
}
// 页面渲染完成后,获取对应的讲解
this.fetchExplanationForCurrentPage();
});
});
document.getElementById('page-num').value = num;
}
async fetchExplanationForCurrentPage() {
// 如果讲解尚未生成完毕,使用传统方式获取讲解
if (!this.explanationsGenerated && !this.explanationsGenerating) {
this.onExplain();
return;
}
// 如果正在生成讲解,显示等待消息
if (this.explanationsGenerating) {
document.getElementById('explanation-text').textContent = '正在生成讲解,请稍候...';
return;
}
try {
// 显示加载中的消息
document.getElementById('explanation-text').textContent = '正在获取讲解...';
// 从服务器获取预生成的讲解
const response = await fetch(`/api/get_explanation/${this.pageNum}`);
if (!response.ok) {
throw new Error('服务器响应错误');
}
const data = await response.json();
if (data.success) {
document.getElementById('explanation-text').textContent = data.explanation;
// 如果Live2D控制器已初始化播放说话动作
if (this.live2dController && this.live2dController.initialized) {
this.live2dController.playMotion('Talk', 0);
}
} else {
// 如果预生成的讲解不存在,使用传统方式获取讲解
this.onExplain();
}
} catch (error) {
console.error('获取预生成讲解时出错:', error);
document.getElementById('explanation-text').textContent = '获取讲解时出错: ' + error.message;
// 尝试使用传统方式获取讲解
this.onExplain();
}
}
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);
// 重置讲解状态
this.explanationsGenerated = false;
this.explanationsGenerating = false;
// 对于上传的文件,我们暂时不触发服务器端讲解生成
// 因为服务器端需要访问文件,而上传的文件仅在客户端可用
} catch (error) {
console.error('加载PDF时出错:', error);
this.showMessage('PDF加载失败: ' + error.message, true);
}
};
fileReader.readAsArrayBuffer(file);
} else {
this.showMessage('请选择有效的PDF文件', true);
}
}
async onExplain() {
try {
// 获取当前页面的文本内容
const page = await this.pdfDoc.getPage(this.pageNum);
const textContent = await page.getTextContent();
const pageText = textContent.items.map(item => item.str).join(' ');
// 显示加载中的消息
document.getElementById('explanation-text').textContent = '正在生成AI讲解...';
// 发送到服务器获取AI讲解
const response = await fetch('/api/explain', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: pageText,
page: this.pageNum
})
});
if (!response.ok) {
throw new Error('服务器响应错误');
}
const data = await response.json();
document.getElementById('explanation-text').textContent = data.explanation;
// 如果Live2D控制器已初始化播放说话动作
if (this.live2dController && this.live2dController.initialized) {
this.live2dController.playMotion('Talk', 0);
}
} catch (error) {
console.error('获取AI讲解时出错:', error);
document.getElementById('explanation-text').textContent = '获取AI讲解时出错: ' + error.message;
this.showMessage('获取AI讲解时出错: ' + error.message, true);
}
}
async onLoadModel() {
if (!this.live2dController) {
this.showMessage('Live2D控制器未初始化', true);
return;
}
const modelSelect = document.getElementById('model-select');
const modelName = modelSelect.value;
// 根据模型名称构建模型路径
let modelPath;
if (modelName === 'Haru') {
modelPath = 'resources/Haru/Haru.model3.json';
} else {
modelPath = 'resources/Mao/Mao.model3.json';
}
console.log(`尝试加载模型: ${modelName},路径: ${modelPath}`);
try {
this.showMessage(`正在加载模型: ${modelName}...`, false);
// 检查模型文件是否存在
try {
const response = await fetch(modelPath, { method: 'HEAD' });
if (!response.ok) {
console.error(`模型文件不存在: ${modelPath}`);
this.showMessage(`模型文件不存在: ${modelPath}`, true);
return;
}
console.log(`模型文件存在: ${modelPath}`);
} catch (error) {
console.error(`检查模型文件时出错: ${error.message}`);
this.showMessage(`检查模型文件时出错: ${error.message}`, true);
return;
}
const model = await this.live2dController.loadModel(modelPath);
if (model) {
this.showMessage(`模型 ${modelName} 加载成功`, false);
// 播放初始动作
this.live2dController.playMotion('Idle', 0);
} else {
this.showMessage(`模型 ${modelName} 加载失败`, true);
}
} catch (error) {
console.error('加载模型时出错:', error);
this.showMessage('加载模型时出错: ' + error.message, true);
}
}
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());
document.getElementById('load-model-btn').addEventListener('click', () => this.onLoadModel());
}
2025-03-07 00:00:11 +08:00
}
2025-03-07 00:00:11 +08:00
// 当页面加载完成后初始化应用
window.addEventListener('DOMContentLoaded', () => {
window.aiTeacherApp = new AITeacherApp();
});