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;
|
|
|
|
|
|
|
|
|
|
|
|
// 讲解状态
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化应用
|
2025-03-07 12:11:16 +08:00
|
|
|
|
this.global_setting = null;
|
|
|
|
|
|
this.api_host = null;
|
|
|
|
|
|
|
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 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 12:11:16 +08:00
|
|
|
|
const defaultPdfPath = 'pdf/VLA4RM-仿生智能.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);
|
|
|
|
|
|
|
|
|
|
|
|
// 触发服务器端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);
|
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);
|
|
|
|
|
|
// 开始轮询讲解生成状态
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-07 12:11:16 +08:00
|
|
|
|
|
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 12:11:16 +08:00
|
|
|
|
document.getElementById('model-select').addEventListener('change', () => {
|
|
|
|
|
|
const modelName = document.getElementById('model-select').value;
|
|
|
|
|
|
this.live2dController.loadModel(modelName);
|
|
|
|
|
|
});
|
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
|
|
|
|
});
|