1172 lines
41 KiB
JavaScript
1172 lines
41 KiB
JavaScript
|
|
/**
|
|||
|
|
* AI讲故事主要功能类
|
|||
|
|
* 提供故事库管理、阅读体验、AI语音交互等功能
|
|||
|
|
*/
|
|||
|
|
class AIStorytelling {
|
|||
|
|
constructor() {
|
|||
|
|
this.stories = [];
|
|||
|
|
this.currentStory = null;
|
|||
|
|
this.currentSection = 0;
|
|||
|
|
this.isSpeaking = false;
|
|||
|
|
this.isVoiceEnabled = true;
|
|||
|
|
this.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async init() {
|
|||
|
|
await this.loadStories();
|
|||
|
|
this.initEventListeners();
|
|||
|
|
this.renderStoryGrid();
|
|||
|
|
this.initAdvancedFeatures();
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
await window.virtualTeacher.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('🎭 AI讲故事系统初始化完成');
|
|||
|
|
this.showWelcomeMessage();
|
|||
|
|
this.updateStatsDisplay();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initAdvancedFeatures() {
|
|||
|
|
// 初始化搜索功能
|
|||
|
|
this.searchDelay = null;
|
|||
|
|
|
|||
|
|
// 初始化排序功能
|
|||
|
|
this.currentSort = 'default';
|
|||
|
|
|
|||
|
|
// 初始化收藏功能
|
|||
|
|
this.favorites = JSON.parse(localStorage.getItem('storyFavorites') || '[]');
|
|||
|
|
|
|||
|
|
// 初始化语速控制
|
|||
|
|
this.desiredRate = 0.8;
|
|||
|
|
|
|||
|
|
// 初始化自动播放
|
|||
|
|
this.autoPlay = false;
|
|||
|
|
|
|||
|
|
// 初始化进度追踪
|
|||
|
|
this.readingProgress = {};
|
|||
|
|
|
|||
|
|
// 初始化剧情选择系统
|
|||
|
|
this.currentChoices = [];
|
|||
|
|
this.choicesHistory = [];
|
|||
|
|
this.branchingActive = false;
|
|||
|
|
this.waitingForChoice = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async loadStories() {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch('../data/storyBank.json');
|
|||
|
|
const data = await response.json();
|
|||
|
|
this.stories = data.stories;
|
|||
|
|
console.log('📚 故事数据加载成功:', this.stories.length, '个故事');
|
|||
|
|
|
|||
|
|
// 调试:检查第一个故事的选择项
|
|||
|
|
if (this.stories.length > 0) {
|
|||
|
|
const firstStory = this.stories[0];
|
|||
|
|
console.log('🔍 第一个故事:', firstStory.title);
|
|||
|
|
console.log('🔍 第一个故事的选择项:', firstStory.content?.beginning?.choices);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 加载故事数据失败:', error);
|
|||
|
|
this.stories = this.getDefaultStories();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getDefaultStories() {
|
|||
|
|
return [{
|
|||
|
|
id: "story_default",
|
|||
|
|
title: "欢迎来到故事世界",
|
|||
|
|
category: "欢迎故事",
|
|||
|
|
difficulty: 1,
|
|||
|
|
cover: "asset/logo.png",
|
|||
|
|
description: "这是一个欢迎故事,帮助您熟悉AI讲故事功能",
|
|||
|
|
timeEstimate: "2-3分钟",
|
|||
|
|
ageRange: "3-8岁",
|
|||
|
|
content: {
|
|||
|
|
beginning: {
|
|||
|
|
text: "欢迎来到AI讲故事的神奇世界!在这里,每一页都是一个新奇的冒险,每一个故事都充满智慧与乐趣。",
|
|||
|
|
image: "asset/logo.png"
|
|||
|
|
},
|
|||
|
|
middle: {
|
|||
|
|
text: "AI老师会用温柔的声音为您朗读故事,就像真正的老师在身边一样。让我们一起来享受这个美好的故事时光吧!",
|
|||
|
|
image: "asset/maoz.png"
|
|||
|
|
},
|
|||
|
|
ending: {
|
|||
|
|
text: "希望您喜欢这个AI讲故事世界!现在可以选择您感兴趣的故事,开始精彩的阅读之旅吧!",
|
|||
|
|
image: "asset/sping.png"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initEventListeners() {
|
|||
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|||
|
|
btn.addEventListener('click', (e) => {
|
|||
|
|
this.filterStories(e.target.dataset.category);
|
|||
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|||
|
|
e.target.classList.add('active');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('closeReader').addEventListener('click', () => {
|
|||
|
|
this.closeReader();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('prevSection').addEventListener('click', () => {
|
|||
|
|
this.previousSection();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('nextSection').addEventListener('click', () => {
|
|||
|
|
this.nextSection();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('readCurrentSection').addEventListener('click', () => {
|
|||
|
|
this.readCurrentSection();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('playPauseBtn').addEventListener('click', () => {
|
|||
|
|
this.togglePlayPause();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('stopBtn').addEventListener('click', () => {
|
|||
|
|
this.stopSpeaking();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 语音控制面板切换按钮
|
|||
|
|
document.getElementById('voiceToggleBtn').addEventListener('click', () => {
|
|||
|
|
this.toggleVoicePanel();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 语音控制面板关闭按钮
|
|||
|
|
document.getElementById('voiceCloseBtn').addEventListener('click', () => {
|
|||
|
|
this.hideVoicePanel();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 语音开关功能(在语音控制面板内)
|
|||
|
|
document.getElementById('voiceOnOffBtn').addEventListener('click', () => {
|
|||
|
|
this.toggleVoice();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 新增事件监听器
|
|||
|
|
document.getElementById('speedControl')?.addEventListener('click', () => {
|
|||
|
|
this.openSpeedPanel();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('autoPlayBtn')?.addEventListener('click', () => {
|
|||
|
|
this.toggleAutoPlay();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('bookmarkBtn')?.addEventListener('click', () => {
|
|||
|
|
this.toggleFavorite(this.currentStory?.id);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('shareBtn')?.addEventListener('click', () => {
|
|||
|
|
this.shareStory();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('sortBy')?.addEventListener('change', (e) => {
|
|||
|
|
this.sortStories(e.target.value);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.addEventListener('keydown', (e) => {
|
|||
|
|
if (document.getElementById('storyReader').classList.contains('active')) {
|
|||
|
|
switch (e.key) {
|
|||
|
|
case 'Escape':
|
|||
|
|
this.closeReader();
|
|||
|
|
break;
|
|||
|
|
case 'ArrowLeft':
|
|||
|
|
this.previousSection();
|
|||
|
|
break;
|
|||
|
|
case 'ArrowRight':
|
|||
|
|
this.nextSection();
|
|||
|
|
break;
|
|||
|
|
case ' ':
|
|||
|
|
e.preventDefault();
|
|||
|
|
this.readCurrentSection();
|
|||
|
|
break;
|
|||
|
|
case 'a':
|
|||
|
|
case 'A':
|
|||
|
|
e.preventDefault();
|
|||
|
|
this.toggleAutoPlay();
|
|||
|
|
break;
|
|||
|
|
case 'b':
|
|||
|
|
case 'B':
|
|||
|
|
e.preventDefault();
|
|||
|
|
this.toggleFavorite(this.currentStory?.id);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('storyReader').addEventListener('click', (e) => {
|
|||
|
|
if (e.target.id === 'storyReader') {
|
|||
|
|
this.closeReader();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 语速面板事件监听
|
|||
|
|
const speedSlider = document.getElementById('speedSlider');
|
|||
|
|
const speedCloseBtn = document.getElementById('closeSpeedPanel');
|
|||
|
|
const speedPresets = document.querySelectorAll('.speed-preset');
|
|||
|
|
|
|||
|
|
speedSlider?.addEventListener('input', (e) => {
|
|||
|
|
this.desiredRate = parseFloat(e.target.value);
|
|||
|
|
document.getElementById('speedValue').textContent = `${this.desiredRate.toFixed(1)}x`;
|
|||
|
|
this.updateVoiceSettings();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
speedCloseBtn?.addEventListener('click', () => {
|
|||
|
|
this.closeSpeedPanel();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
speedPresets.forEach(preset => {
|
|||
|
|
preset.addEventListener('click', (e) => {
|
|||
|
|
const speed = e.target.dataset.speed;
|
|||
|
|
this.desiredRate = parseFloat(speed);
|
|||
|
|
speedSlider.value = speed;
|
|||
|
|
document.getElementById('speedValue').textContent = `${speed}x`;
|
|||
|
|
this.updateVoiceSettings();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 继续从分支剧情按钮
|
|||
|
|
document.getElementById('continueFromBranch')?.addEventListener('click', () => {
|
|||
|
|
this.continueFromBranch();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showWelcomeMessage() {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak("欢迎来到AI故事世界!这里有精彩的故事等着你,选择一个你喜欢的故事开始吧!", {
|
|||
|
|
rate: 0.8,
|
|||
|
|
pitch: 1.0
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
filterStories(category) {
|
|||
|
|
const filteredStories = category === 'all'
|
|||
|
|
? this.stories
|
|||
|
|
: this.stories.filter(story => story.category === category);
|
|||
|
|
|
|||
|
|
this.renderStoryGrid(filteredStories);
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
const categoryText = category === 'all' ? '所有故事' : category;
|
|||
|
|
window.virtualTeacher.speak(`现在显示${categoryText},共找到${filteredStories.length}个故事`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderStoryGrid(storiesToRender = this.stories) {
|
|||
|
|
const grid = document.getElementById('storyGrid');
|
|||
|
|
|
|||
|
|
if (storiesToRender.length === 0) {
|
|||
|
|
grid.innerHTML = `
|
|||
|
|
<div style="grid-column: 1 / -1; text-align: center; padding: 50px; color: rgba(255,255,255,0.7);">
|
|||
|
|
<i class="fa fa-book-open" style="font-size: 3rem; margin-bottom: 20px;"></i>
|
|||
|
|
<h3>暂无故事</h3>
|
|||
|
|
<p>该分类下暂时没有故事,请选择其他分类。</p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
grid.innerHTML = '<div class="loading-spinner"></div> 正在加载故事...';
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
grid.innerHTML = storiesToRender.map(story => `
|
|||
|
|
<div class="story-card animate__animated animate__fadeInUp" data-story-id="${story.id}">
|
|||
|
|
<div class="story-cover">
|
|||
|
|
<img src="../${story.cover}" alt="${story.title}" onerror="this.src='../asset/logo.png'">
|
|||
|
|
</div>
|
|||
|
|
<div class="story-info">
|
|||
|
|
<h3 class="story-title">${story.title}</h3>
|
|||
|
|
<p class="story-description">${story.description}</p>
|
|||
|
|
<div class="story-meta">
|
|||
|
|
<span class="story-category">${story.category}</span>
|
|||
|
|
<span class="story-difficulty">
|
|||
|
|
${this.generateDifficultyStars(story.difficulty)}
|
|||
|
|
<span style="margin-left: 5px;">${story.ageRange}</span>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="story-meta">
|
|||
|
|
<span><img src="../asset/icon-时间.png" alt="时间" class="meta-icon"> ${story.timeEstimate}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="story-actions">
|
|||
|
|
<button class="btn-story btn-primary start-reading" onclick="aiStorytelling.openReader('${story.id}')">
|
|||
|
|
<img src="../asset/icon-开始阅读 .png" alt="开始阅读" class="btn-icon"> 开始阅读
|
|||
|
|
</button>
|
|||
|
|
<button class="btn-story btn-secondary story-info" onclick="aiStorytelling.showStoryInfo('${story.id}')">
|
|||
|
|
<img src="../asset/icon-详情.png" alt="详情" class="btn-icon"> 详情
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}, 500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
generateDifficultyStars(difficulty) {
|
|||
|
|
return new Array(difficulty).fill('').map(() => '<span class="difficulty-star">⭐</span>').join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
openReader(storyId) {
|
|||
|
|
console.log('🔍 openReader被调用,storyId:', storyId);
|
|||
|
|
console.log('📚 当前故事数组:', this.stories?.length || '未定义');
|
|||
|
|
|
|||
|
|
this.currentStory = this.stories.find(story => story.id === storyId);
|
|||
|
|
if (!this.currentStory) {
|
|||
|
|
console.error('❌ 找不到指定故事:', storyId);
|
|||
|
|
console.log('💾 可用故事列表:', this.stories?.map(s => ({ id: s.id, title: s.title })) || 'stories为undefined');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('✅ 找到故事:', this.currentStory.title);
|
|||
|
|
this.currentSection = 0;
|
|||
|
|
document.getElementById('readerTitle').textContent = this.currentStory.title;
|
|||
|
|
// 初始化进度到第1段
|
|||
|
|
this.updateReadingProgress(this.currentStory.id, this.currentSection);
|
|||
|
|
this.renderStoryContent();
|
|||
|
|
|
|||
|
|
const reader = document.getElementById('storyReader');
|
|||
|
|
reader.classList.add('active');
|
|||
|
|
document.body.style.overflow = 'hidden';
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak(`欢迎来到《${this.currentStory.title}》的故事世界!`, {
|
|||
|
|
rate: 0.8,
|
|||
|
|
pitch: 1.1
|
|||
|
|
});
|
|||
|
|
}, 300);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('📖 打开故事:', this.currentStory.title);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
closeReader() {
|
|||
|
|
const reader = document.getElementById('storyReader');
|
|||
|
|
reader.classList.remove('active');
|
|||
|
|
document.body.style.overflow = '';
|
|||
|
|
|
|||
|
|
if (this.isSpeaking) {
|
|||
|
|
this.stopSpeaking();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak('故事时间结束了,期待下次再见!', {
|
|||
|
|
rate: 0.8,
|
|||
|
|
pitch: 1.0
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('📚 故事阅读器已关闭');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderStoryContent() {
|
|||
|
|
const content = document.getElementById('storyContent');
|
|||
|
|
const sections = ['beginning', 'middle', 'ending'];
|
|||
|
|
const currentSectionData = this.currentStory.content[sections[this.currentSection]];
|
|||
|
|
|
|||
|
|
console.log('🔍 当前段落数据:', currentSectionData);
|
|||
|
|
console.log('🔍 是否有选择项:', currentSectionData.choices);
|
|||
|
|
console.log('🔍 选择项数量:', currentSectionData.choices?.length || 0);
|
|||
|
|
|
|||
|
|
// 清除之前的定时器
|
|||
|
|
if (this.branchingTimeout) {
|
|||
|
|
clearTimeout(this.branchingTimeout);
|
|||
|
|
this.branchingTimeout = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清除之前的选择和分支内容
|
|||
|
|
const storyChoices = document.getElementById('storyChoices');
|
|||
|
|
const branchingContent = document.getElementById('branchingContent');
|
|||
|
|
const branchNarrative = document.getElementById('branchNarrative');
|
|||
|
|
const choicesContainer = document.getElementById('choicesContainer');
|
|||
|
|
const choiceFeedback = document.getElementById('choiceFeedback');
|
|||
|
|
|
|||
|
|
console.log('🔍 DOM元素检查:', {
|
|||
|
|
storyChoices: !!storyChoices,
|
|||
|
|
branchingContent: !!branchingContent,
|
|||
|
|
branchNarrative: !!branchNarrative,
|
|||
|
|
choicesContainer: !!choicesContainer,
|
|||
|
|
choiceFeedback: !!choiceFeedback
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (storyChoices) storyChoices.style.display = 'none';
|
|||
|
|
if (branchingContent) branchingContent.style.display = 'none';
|
|||
|
|
if (branchNarrative) branchNarrative.innerHTML = '';
|
|||
|
|
if (choicesContainer) choicesContainer.innerHTML = '';
|
|||
|
|
if (choiceFeedback) choiceFeedback.innerHTML = '';
|
|||
|
|
|
|||
|
|
// 只更新故事内容部分,不覆盖选择界面
|
|||
|
|
const storySection = document.createElement('div');
|
|||
|
|
storySection.className = 'story-section';
|
|||
|
|
storySection.innerHTML = `
|
|||
|
|
<h3 style="margin-bottom: 20px; color: #1e40af;">📖 故事 ${this.getCurrentSectionTitle()}</h3>
|
|||
|
|
${currentSectionData.image ? `
|
|||
|
|
<img src="../${currentSectionData.image}" alt="故事插图" class="section-image">
|
|||
|
|
` : ''}
|
|||
|
|
<p class="section-text">${currentSectionData.text}</p>
|
|||
|
|
<div style="text-align: center; margin-top: 20px; color: #64748b; font-size: 0.9rem;">
|
|||
|
|
${this.currentSection + 1} / ${sections.length}
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// 清除之前的故事内容,但保留选择界面
|
|||
|
|
const existingStorySection = content.querySelector('.story-section');
|
|||
|
|
if (existingStorySection) {
|
|||
|
|
content.removeChild(existingStorySection);
|
|||
|
|
}
|
|||
|
|
content.insertBefore(storySection, content.firstChild);
|
|||
|
|
|
|||
|
|
// 渲染内容后刷新进度显示
|
|||
|
|
this.updateReadingProgress(this.currentStory.id, this.currentSection);
|
|||
|
|
|
|||
|
|
// 检查是否有选择项 - 始终显示选择界面,不管是否朗读
|
|||
|
|
if (currentSectionData.choices && currentSectionData.choices.length > 0) {
|
|||
|
|
console.log('✅ 发现选择项,准备显示选择界面');
|
|||
|
|
// 立即显示选择,让用户可以随时选择
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.showStoryChoices(currentSectionData.choices);
|
|||
|
|
}, 500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 无论是否有选择项,都开始朗读(如果有语音功能)
|
|||
|
|
if (this.isVoiceEnabled) {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.readCurrentSection();
|
|||
|
|
}, 800);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getCurrentSectionTitle() {
|
|||
|
|
const titles = ['开始', '中间', '结尾'];
|
|||
|
|
return titles[this.currentSection] || '段落';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
previousSection() {
|
|||
|
|
if (this.currentSection > 0) {
|
|||
|
|
this.currentSection--;
|
|||
|
|
// 更新进度
|
|||
|
|
if (this.currentStory) {
|
|||
|
|
this.updateReadingProgress(this.currentStory.id, this.currentSection);
|
|||
|
|
}
|
|||
|
|
this.renderStoryContent();
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak(`现在来到了故事的${this.getCurrentSectionTitle()}部分`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nextSection() {
|
|||
|
|
const sections = ['beginning', 'middle', 'ending'];
|
|||
|
|
if (this.currentSection < sections.length - 1) {
|
|||
|
|
this.currentSection++;
|
|||
|
|
// 更新进度
|
|||
|
|
if (this.currentStory) {
|
|||
|
|
this.updateReadingProgress(this.currentStory.id, this.currentSection);
|
|||
|
|
}
|
|||
|
|
this.renderStoryContent();
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak(`现在来到了故事的${this.getCurrentSectionTitle()}部分`);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
this.showStoryEnding();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readCurrentSection() {
|
|||
|
|
if (!this.currentStory) return;
|
|||
|
|
|
|||
|
|
const sections = ['beginning', 'middle', 'ending'];
|
|||
|
|
const currentSectionData = this.currentStory.content[sections[this.currentSection]];
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
this.isSpeaking = true;
|
|||
|
|
const voiceBtn = document.getElementById('readCurrentSection');
|
|||
|
|
voiceBtn.classList.add('speaking');
|
|||
|
|
|
|||
|
|
window.virtualTeacher.speak(currentSectionData.text, {
|
|||
|
|
rate: 0.8,
|
|||
|
|
pitch: 1.1,
|
|||
|
|
onend: () => {
|
|||
|
|
this.isSpeaking = false;
|
|||
|
|
voiceBtn.classList.remove('speaking');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showStoryEnding() {
|
|||
|
|
const content = document.getElementById('storyContent');
|
|||
|
|
content.innerHTML = `
|
|||
|
|
<div class="story-section" style="text-align: center;">
|
|||
|
|
<h3 style="color: #667eea; margin-bottom: 20px;">🎉 故事结束了!</h3>
|
|||
|
|
<img src="../asset/maoz.png" alt="故事结束" style="width: 150px; height: 150px; border-radius: 50%; margin-bottom: 20px;">
|
|||
|
|
<p style="font-size: 1.2rem; margin-bottom: 20px;">
|
|||
|
|
恭喜你完成了《${this.currentStory.title}》的阅读!
|
|||
|
|
</p>
|
|||
|
|
${this.currentStory.description ? `
|
|||
|
|
<p style="color: #666;">
|
|||
|
|
${this.currentStory.description}
|
|||
|
|
</p>
|
|||
|
|
` : ''}
|
|||
|
|
<div style="margin-top: 30px;">
|
|||
|
|
<button class="btn-story btn-large btn-primary" onclick="aiStorytelling.closeReader()">
|
|||
|
|
结束阅读 <i class="fa fa-bookmark"></i>
|
|||
|
|
</button>
|
|||
|
|
<button class="btn-story btn-large btn-secondary" onclick="aiStorytelling.restartStory()">
|
|||
|
|
重新阅读 <i class="fa fa-refresh"></i>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// 结束时设置进度为 3/3
|
|||
|
|
const sections = ['beginning', 'middle', 'ending'];
|
|||
|
|
this.updateReadingProgress(this.currentStory?.id || '', sections.length - 1);
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak(`恭喜你完成了《${this.currentStory.title}》的阅读!希望你喜欢这个故事!`);
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
restartStory() {
|
|||
|
|
this.currentSection = 0;
|
|||
|
|
this.renderStoryContent();
|
|||
|
|
|
|||
|
|
// 重启时重置进度为 1/3
|
|||
|
|
if (this.currentStory) {
|
|||
|
|
this.updateReadingProgress(this.currentStory.id, this.currentSection);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak('让我们重新开始这个故事吧!');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showStoryInfo(storyId) {
|
|||
|
|
const story = this.stories.find(s => s.id === storyId);
|
|||
|
|
if (!story) return;
|
|||
|
|
|
|||
|
|
// 填充对话框内容
|
|||
|
|
const modal = document.getElementById('storyDetailModal');
|
|||
|
|
const detailTitle = document.getElementById('detailTitle');
|
|||
|
|
const detailImage = document.getElementById('detailImage');
|
|||
|
|
const detailCategory = document.getElementById('detailCategory');
|
|||
|
|
const detailCategoryIcon = document.getElementById('detailCategoryIcon');
|
|||
|
|
const detailDifficulty = document.getElementById('detailDifficulty');
|
|||
|
|
const detailDuration = document.getElementById('detailDuration');
|
|||
|
|
const detailAge = document.getElementById('detailAge');
|
|||
|
|
const detailDescription = document.getElementById('detailDescription');
|
|||
|
|
const detailStart = document.getElementById('detailStart');
|
|||
|
|
const detailFavorite = document.getElementById('detailFavorite');
|
|||
|
|
const detailShare = document.getElementById('detailShare');
|
|||
|
|
|
|||
|
|
if (!modal) return;
|
|||
|
|
|
|||
|
|
detailTitle.textContent = story.title;
|
|||
|
|
detailImage.src = `../${story.cover}`;
|
|||
|
|
detailCategory.textContent = story.category;
|
|||
|
|
|
|||
|
|
// 设置类别图标
|
|||
|
|
const categoryIconMap = {
|
|||
|
|
'生活故事': '../asset/icon-shenghuo.png',
|
|||
|
|
'经典故事': '../asset/icon-经典.png',
|
|||
|
|
'数学故事': '../asset/icon-xyz.png',
|
|||
|
|
'梦想故事': '../asset/icon-mengxiang1.png',
|
|||
|
|
'成长故事': '../asset/icon-chengzhang2.png'
|
|||
|
|
};
|
|||
|
|
detailCategoryIcon.src = categoryIconMap[story.category] || '../asset/icon-shenghuo.png';
|
|||
|
|
detailCategoryIcon.alt = story.category;
|
|||
|
|
|
|||
|
|
detailDifficulty.textContent = '⭐'.repeat(story.difficulty || 1);
|
|||
|
|
detailDuration.textContent = story.timeEstimate || '约3分钟';
|
|||
|
|
detailAge.textContent = story.ageRange || '3-8岁';
|
|||
|
|
detailDescription.textContent = story.description || '';
|
|||
|
|
|
|||
|
|
// 绑定按钮动作
|
|||
|
|
detailStart.onclick = () => {
|
|||
|
|
modal.style.display = 'none';
|
|||
|
|
this.openReader(story.id);
|
|||
|
|
};
|
|||
|
|
detailFavorite.onclick = () => this.toggleFavorite(story.id);
|
|||
|
|
detailShare.onclick = () => this.shareStory();
|
|||
|
|
|
|||
|
|
// 打开对话框
|
|||
|
|
modal.style.display = 'block';
|
|||
|
|
|
|||
|
|
// 关闭事件(遮罩与关闭按钮)
|
|||
|
|
const closeBtn = document.getElementById('closeDetail');
|
|||
|
|
const backdrop = document.getElementById('detailBackdrop');
|
|||
|
|
const close = () => { modal.style.display = 'none'; };
|
|||
|
|
closeBtn && (closeBtn.onclick = close);
|
|||
|
|
backdrop && (backdrop.onclick = close);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
togglePlayPause() {
|
|||
|
|
const btn = document.getElementById('playPauseBtn');
|
|||
|
|
const icon = btn.querySelector('i');
|
|||
|
|
|
|||
|
|
if (this.isSpeaking) {
|
|||
|
|
this.stopSpeaking();
|
|||
|
|
icon.className = 'fa fa-play';
|
|||
|
|
} else {
|
|||
|
|
this.readCurrentSection();
|
|||
|
|
icon.className = 'fa fa-pause';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stopSpeaking() {
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
window.virtualTeacher.stopAllSpeech();
|
|||
|
|
}
|
|||
|
|
this.isSpeaking = false;
|
|||
|
|
document.querySelectorAll('.speaking').forEach(el => el.classList.remove('speaking'));
|
|||
|
|
|
|||
|
|
const playBtn = document.getElementById('playPauseBtn');
|
|||
|
|
const icon = playBtn.querySelector('i');
|
|||
|
|
icon.className = 'fa fa-play';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toggleVoice() {
|
|||
|
|
this.isVoiceEnabled = !this.isVoiceEnabled;
|
|||
|
|
const btn = document.getElementById('voiceOnOffBtn');
|
|||
|
|
const icon = btn.querySelector('i');
|
|||
|
|
|
|||
|
|
if (this.isVoiceEnabled) {
|
|||
|
|
icon.className = 'fa fa-volume-up';
|
|||
|
|
btn.title = '关闭语音';
|
|||
|
|
voiceBroadcast('语音功能已开启');
|
|||
|
|
document.getElementById('voiceStatus').textContent = '语音已开启';
|
|||
|
|
document.getElementById('voiceStatus').style.color = '#3b82f6';
|
|||
|
|
} else {
|
|||
|
|
icon.className = 'fa fa-volume-off';
|
|||
|
|
btn.title = '开启语音';
|
|||
|
|
voiceBroadcast('语音功能已关闭');
|
|||
|
|
this.stopSpeaking();
|
|||
|
|
document.getElementById('voiceStatus').textContent = '语音已关闭';
|
|||
|
|
document.getElementById('voiceStatus').style.color = '#94a3b8';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 切换语音控制面板显示/隐藏
|
|||
|
|
toggleVoicePanel() {
|
|||
|
|
const voiceControls = document.getElementById('voiceControls');
|
|||
|
|
const toggleBtn = document.getElementById('voiceToggleBtn');
|
|||
|
|
|
|||
|
|
if (voiceControls.style.display === 'none' || voiceControls.style.display === '') {
|
|||
|
|
this.showVoicePanel();
|
|||
|
|
} else {
|
|||
|
|
this.hideVoicePanel();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示语音控制面板
|
|||
|
|
showVoicePanel() {
|
|||
|
|
const voiceControls = document.getElementById('voiceControls');
|
|||
|
|
const toggleBtn = document.getElementById('voiceToggleBtn');
|
|||
|
|
|
|||
|
|
voiceControls.style.display = 'block';
|
|||
|
|
toggleBtn.style.display = 'none';
|
|||
|
|
|
|||
|
|
// 添加显示动画
|
|||
|
|
setTimeout(() => {
|
|||
|
|
voiceControls.style.opacity = '1';
|
|||
|
|
voiceControls.style.transform = 'translateY(0)';
|
|||
|
|
}, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 隐藏语音控制面板
|
|||
|
|
hideVoicePanel() {
|
|||
|
|
const voiceControls = document.getElementById('voiceControls');
|
|||
|
|
const toggleBtn = document.getElementById('voiceToggleBtn');
|
|||
|
|
|
|||
|
|
voiceControls.style.opacity = '0';
|
|||
|
|
voiceControls.style.transform = 'translateY(20px)';
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
voiceControls.style.display = 'none';
|
|||
|
|
toggleBtn.style.display = 'block';
|
|||
|
|
}, 300);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 新增功能方法
|
|||
|
|
updateStatsDisplay() {
|
|||
|
|
const totalStories = this.stories.length;
|
|||
|
|
const totalDuration = this.stories.reduce((sum, story) => {
|
|||
|
|
const duration = parseInt(story.timeEstimate?.match(/\d+/)?.[0] || '3');
|
|||
|
|
return sum + duration;
|
|||
|
|
}, 0);
|
|||
|
|
|
|||
|
|
document.getElementById('totalStories').textContent = totalStories;
|
|||
|
|
document.getElementById('totalDuration').textContent = `${totalDuration}分钟`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
openSpeedPanel() {
|
|||
|
|
const panel = document.getElementById('speedPanel');
|
|||
|
|
if (panel) {
|
|||
|
|
panel.classList.add('active');
|
|||
|
|
document.getElementById('speedSlider').value = this.desiredRate;
|
|||
|
|
document.getElementById('speedValue').textContent = `${this.desiredRate.toFixed(1)}x`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
closeSpeedPanel() {
|
|||
|
|
const panel = document.getElementById('speedPanel');
|
|||
|
|
if (panel) {
|
|||
|
|
panel.classList.remove('active');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateVoiceSettings() {
|
|||
|
|
// 更新语音设置,将应用到后续的朗读中
|
|||
|
|
console.log('语速已更新为:', this.desiredRate);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toggleAutoPlay() {
|
|||
|
|
this.autoPlay = !this.autoPlay;
|
|||
|
|
const btn = document.getElementById('autoPlayBtn');
|
|||
|
|
const icon = btn.querySelector('i');
|
|||
|
|
|
|||
|
|
if (this.autoPlay) {
|
|||
|
|
icon.className = 'fa fa-pause-circle';
|
|||
|
|
btn.querySelector('.btn-text').textContent = '关闭自动';
|
|||
|
|
btn.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)';
|
|||
|
|
if (this.isVoiceEnabled && window.virtualTeacher) {
|
|||
|
|
window.virtualTeacher.speak('自动播放已开启,我将自动为您朗读每个段落');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
icon.className = 'fa fa-play-circle';
|
|||
|
|
btn.querySelector('.btn-text').textContent = '自动播放';
|
|||
|
|
btn.style.background = 'linear-gradient(135deg, #10b981, #059669)';
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
window.virtualTeacher.stopAllSpeech();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toggleFavorite(storyId) {
|
|||
|
|
if (!storyId) return;
|
|||
|
|
|
|||
|
|
const index = this.favorites.indexOf(storyId);
|
|||
|
|
const btn = document.getElementById('bookmarkBtn');
|
|||
|
|
const icon = btn.querySelector('i');
|
|||
|
|
|
|||
|
|
if (index > -1) {
|
|||
|
|
this.favorites.splice(index, 1);
|
|||
|
|
icon.className = 'fa fa-bookmark-o';
|
|||
|
|
btn.style.color = '#ffffff';
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak('已从收藏夹中移除');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
this.favorites.push(storyId);
|
|||
|
|
icon.className = 'fa fa-bookmark';
|
|||
|
|
btn.style.color = '#fbbf24';
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak('已添加到收藏夹');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
localStorage.setItem('storyFavorites', JSON.stringify(this.favorites));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shareStory() {
|
|||
|
|
if (!this.currentStory) return;
|
|||
|
|
|
|||
|
|
const storyInfo = {
|
|||
|
|
title: this.currentStory.title,
|
|||
|
|
description: this.currentStory.description,
|
|||
|
|
category: this.currentStory.category,
|
|||
|
|
url: window.location.href
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (navigator.share) {
|
|||
|
|
navigator.share({
|
|||
|
|
title: `${storyInfo.title} - AI讲故事`,
|
|||
|
|
text: storyInfo.description,
|
|||
|
|
url: storyInfo.url
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 复制到剪贴板
|
|||
|
|
const shareText = `🌟 推荐一个AI故事:《${storyInfo.title}》\n\n${storyInfo.description}\n\n🔗 ${storyInfo.url}`;
|
|||
|
|
navigator.clipboard.writeText(shareText).then(() => {
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak('故事链接已复制到剪贴板');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sortStories(sortType) {
|
|||
|
|
this.currentSort = sortType;
|
|||
|
|
let sortedStories = [...this.stories];
|
|||
|
|
|
|||
|
|
switch (sortType) {
|
|||
|
|
case 'difficulty':
|
|||
|
|
sortedStories.sort((a, b) => a.difficulty - b.difficulty);
|
|||
|
|
break;
|
|||
|
|
case 'duration':
|
|||
|
|
sortedStories.sort((a, b) => {
|
|||
|
|
const durationA = parseInt(a.timeEstimate?.match(/\d+/)?.[0] || '3');
|
|||
|
|
const durationB = parseInt(b.timeEstimate?.match(/\d+/)?.[0] || '3');
|
|||
|
|
return durationA - durationB;
|
|||
|
|
});
|
|||
|
|
break;
|
|||
|
|
case 'popularity':
|
|||
|
|
// 基于收藏数量或人气排序(这里简化为随机)
|
|||
|
|
sortedStories.sort(() => Math.random() - 0.5);
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
// 默认排序
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.renderStoryGrid(sortedStories);
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
const sortText = {
|
|||
|
|
'default': '默认顺序',
|
|||
|
|
'difficulty': '按难度排序',
|
|||
|
|
'duration': '按时长排序',
|
|||
|
|
'popularity': '按受欢迎度排序'
|
|||
|
|
};
|
|||
|
|
window.virtualTeacher.speak(`故事已按${sortText[sortType]}排列`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
searchStories(query) {
|
|||
|
|
if (!query.trim()) {
|
|||
|
|
this.renderStoryGrid();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const filteredStories = this.stories.filter(story => {
|
|||
|
|
const searchText = `${story.title} ${story.description} ${story.category}`.toLowerCase();
|
|||
|
|
return searchText.includes(query.toLowerCase());
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.renderStoryGrid(filteredStories);
|
|||
|
|
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak(`搜索到${filteredStories.length}个相关故事`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateReadingProgress(storyId, sectionIndex) {
|
|||
|
|
this.readingProgress[storyId] = sectionIndex;
|
|||
|
|
localStorage.setItem('readingProgress', JSON.stringify(this.readingProgress));
|
|||
|
|
|
|||
|
|
// 更新进度条
|
|||
|
|
const sections = ['beginning', 'middle', 'ending'];
|
|||
|
|
const progress = ((sectionIndex + 1) / sections.length) * 100;
|
|||
|
|
document.getElementById('progressFill').style.width = `${progress}%`;
|
|||
|
|
document.getElementById('progressText').textContent = `${sectionIndex + 1}/${sections.length}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 剧情选择系统方法
|
|||
|
|
showStoryChoices(choices) {
|
|||
|
|
console.log('🎭 showStoryChoices被调用,选择项:', choices);
|
|||
|
|
this.waitingForChoice = true;
|
|||
|
|
this.currentChoices = choices;
|
|||
|
|
|
|||
|
|
// 显示选择界面
|
|||
|
|
const choicesContainer = document.getElementById('choicesContainer');
|
|||
|
|
const choicesElement = document.getElementById('storyChoices');
|
|||
|
|
|
|||
|
|
console.log('🔍 选择界面元素检查:', {
|
|||
|
|
choicesContainer: !!choicesContainer,
|
|||
|
|
choicesElement: !!choicesElement
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (choicesContainer) {
|
|||
|
|
const choicesHTML = choices.map((choice, index) => `
|
|||
|
|
<button class="choice-btn" data-choice-index="${index}" onclick="aiStorytelling.makeChoice(${index})">
|
|||
|
|
${choice.choiceText}
|
|||
|
|
</button>
|
|||
|
|
`).join('');
|
|||
|
|
|
|||
|
|
console.log('🔍 生成的选择按钮HTML:', choicesHTML);
|
|||
|
|
choicesContainer.innerHTML = choicesHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (choicesElement) {
|
|||
|
|
choicesElement.style.display = 'block';
|
|||
|
|
console.log('✅ 选择界面已显示');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 不播报选择提示,让用户直接看到选择界面
|
|||
|
|
console.log('🎭 选择界面已显示,用户可以随时选择');
|
|||
|
|
|
|||
|
|
// 添加提示说明用户可以随时选择
|
|||
|
|
const choicesHeader = document.querySelector('.choices-header h3');
|
|||
|
|
if (choicesHeader) {
|
|||
|
|
choicesHeader.textContent = '🎭 在开始朗读前,请选择故事的走向!';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const choicesSubtitle = document.querySelector('.choices-header p');
|
|||
|
|
if (choicesSubtitle) {
|
|||
|
|
choicesSubtitle.textContent = '请选择一个选项,这将决定故事的发展方向。您可以在语音朗读过程中随时选择:';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
makeChoice(choiceIndex) {
|
|||
|
|
if (!this.waitingForChoice || choiceIndex < 0 || choiceIndex >= this.currentChoices.length) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const choice = this.currentChoices[choiceIndex];
|
|||
|
|
// 不设置waitingForChoice = false,让用户可以继续选择
|
|||
|
|
|
|||
|
|
// 记录选择历史(只记录最后一次选择)
|
|||
|
|
const currentSectionKey = ['beginning', 'middle', 'ending'][this.currentSection];
|
|||
|
|
|
|||
|
|
// 移除当前段落之前的选择记录
|
|||
|
|
this.choicesHistory = this.choicesHistory.filter(record =>
|
|||
|
|
!(record.storyId === this.currentStory.id && record.section === currentSectionKey)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 添加新的选择记录
|
|||
|
|
this.choicesHistory.push({
|
|||
|
|
storyId: this.currentStory.id,
|
|||
|
|
section: currentSectionKey,
|
|||
|
|
choice: choice.choiceText,
|
|||
|
|
consequence: choice.consequence,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 更新UI显示选择结果(只能选择一个选项,可以更换选择)
|
|||
|
|
const choiceButtons = document.querySelectorAll('.choice-btn');
|
|||
|
|
choiceButtons.forEach((btn, index) => {
|
|||
|
|
// 清除所有按钮的选择状态
|
|||
|
|
btn.classList.remove('clicked');
|
|||
|
|
btn.removeAttribute('data-selected');
|
|||
|
|
|
|||
|
|
// 只标记当前选择的按钮
|
|||
|
|
if (index === choiceIndex) {
|
|||
|
|
btn.classList.add('clicked');
|
|||
|
|
btn.setAttribute('data-selected', 'true');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 显示选择反馈
|
|||
|
|
const feedbackPosition = document.getElementById('choiceFeedback');
|
|||
|
|
if (feedbackPosition) {
|
|||
|
|
feedbackPosition.innerHTML = `
|
|||
|
|
<p class="choice-feedback-text">
|
|||
|
|
🎉 ${choice.effect}
|
|||
|
|
</p>
|
|||
|
|
`;
|
|||
|
|
console.log('📝 选择反馈已更新:', choice.effect);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 停止之前的语音播报
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
window.virtualTeacher.stopSpeaking();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 先显示分支剧情,然后按顺序播报语音
|
|||
|
|
console.log('🎭 立即显示分支剧情:', choice.consequence);
|
|||
|
|
this.showBranchingNarrative(choice.consequence);
|
|||
|
|
|
|||
|
|
// 语音播报选择的选项文字,读完后再读分支剧情
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
const cleanChoiceText = choice.choiceText.replace(/[🌟🐾🧺🌅🌞🌆🎣🎈👥🚪🌲🧱💨🤝🏡🌧️🎣🏃🏠🌾🤝🏃]/g, '');
|
|||
|
|
console.log('🔊 开始朗读选项文字:', cleanChoiceText);
|
|||
|
|
|
|||
|
|
// 朗读选项的文字内容,读完后再读分支剧情
|
|||
|
|
window.virtualTeacher.speak(cleanChoiceText, {
|
|||
|
|
rate: this.desiredRate,
|
|||
|
|
pitch: 1.0,
|
|||
|
|
onend: () => {
|
|||
|
|
console.log('✅ 选项文字朗读完成,停顿后读分支剧情');
|
|||
|
|
// 停顿2秒后读分支剧情
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const branchingNarrative = this.currentStory.branchingNarratives?.[choice.consequence];
|
|||
|
|
if (branchingNarrative) {
|
|||
|
|
console.log('🔊 开始朗读分支剧情:', branchingNarrative.text);
|
|||
|
|
window.virtualTeacher.speak(branchingNarrative.text, {
|
|||
|
|
rate: this.desiredRate,
|
|||
|
|
pitch: 1.0,
|
|||
|
|
onend: () => {
|
|||
|
|
console.log('✅ 分支剧情朗读完成');
|
|||
|
|
// 停顿2秒后提示继续
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak("点击继续故事按钮来继续阅读");
|
|||
|
|
}, 2000);
|
|||
|
|
},
|
|||
|
|
onerror: () => {
|
|||
|
|
console.log('⚠️ 分支剧情语音播报出错,直接提示继续');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak("点击继续故事按钮来继续阅读");
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, 2000);
|
|||
|
|
},
|
|||
|
|
onerror: () => {
|
|||
|
|
console.log('⚠️ 选项文字语音播报出错,直接读分支剧情');
|
|||
|
|
// 如果选项文字播报出错,直接读分支剧情
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const branchingNarrative = this.currentStory.branchingNarratives?.[choice.consequence];
|
|||
|
|
if (branchingNarrative) {
|
|||
|
|
console.log('🔊 开始朗读分支剧情:', branchingNarrative.text);
|
|||
|
|
window.virtualTeacher.speak(branchingNarrative.text, {
|
|||
|
|
rate: this.desiredRate,
|
|||
|
|
pitch: 1.0,
|
|||
|
|
onend: () => {
|
|||
|
|
console.log('✅ 分支剧情朗读完成');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak("点击继续故事按钮来继续阅读");
|
|||
|
|
}, 2000);
|
|||
|
|
},
|
|||
|
|
onerror: () => {
|
|||
|
|
console.log('⚠️ 分支剧情语音播报也出错,直接提示继续');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak("点击继续故事按钮来继续阅读");
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备用方案:如果3秒后还没有开始读分支剧情,强制开始
|
|||
|
|
this.branchingTimeout = setTimeout(() => {
|
|||
|
|
console.log('⚠️ 备用方案:强制开始读分支剧情');
|
|||
|
|
const branchingNarrative = this.currentStory.branchingNarratives?.[choice.consequence];
|
|||
|
|
if (branchingNarrative) {
|
|||
|
|
console.log('🔊 备用方案开始朗读分支剧情:', branchingNarrative.text);
|
|||
|
|
window.virtualTeacher.speak(branchingNarrative.text, {
|
|||
|
|
rate: this.desiredRate,
|
|||
|
|
pitch: 1.0,
|
|||
|
|
onend: () => {
|
|||
|
|
console.log('✅ 备用方案分支剧情朗读完成');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak("点击继续故事按钮来继续阅读");
|
|||
|
|
}, 2000);
|
|||
|
|
},
|
|||
|
|
onerror: () => {
|
|||
|
|
console.log('⚠️ 备用方案分支剧情语音播报也出错,直接提示继续');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.virtualTeacher.speak("点击继续故事按钮来继续阅读");
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showBranchingNarrative(consequenceKey) {
|
|||
|
|
console.log('🔍 查找分支剧情:', consequenceKey);
|
|||
|
|
const branchingNarrative = this.currentStory.branchingNarratives?.[consequenceKey];
|
|||
|
|
|
|||
|
|
if (!branchingNarrative) {
|
|||
|
|
console.error('❌ 找不到分支剧情:', consequenceKey);
|
|||
|
|
console.log('📚 当前故事的分支剧情:', this.currentStory.branchingNarratives);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('✅ 找到分支剧情:', branchingNarrative);
|
|||
|
|
|
|||
|
|
// 停止之前的语音播报
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
window.virtualTeacher.stopSpeaking();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const branchingContent = document.getElementById('branchingContent');
|
|||
|
|
const branchNarrative = document.getElementById('branchNarrative');
|
|||
|
|
|
|||
|
|
if (branchNarrative) {
|
|||
|
|
branchNarrative.innerHTML = `
|
|||
|
|
<div style="font-weight: 600; color: #1e40af; margin-bottom: 15px; font-size: 1.1rem;">🎭 你的选择带来了新的故事发展:</div>
|
|||
|
|
<div style="line-height: 1.7; color: #374151;">${branchingNarrative.text}</div>
|
|||
|
|
`;
|
|||
|
|
console.log('📖 分支剧情内容已更新:', branchingNarrative.text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (branchingContent) {
|
|||
|
|
branchingContent.style.display = 'block';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 语音播报现在在makeChoice方法中处理,这里不再播报
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
continueFromBranch() {
|
|||
|
|
// 清除之前的定时器
|
|||
|
|
if (this.branchingTimeout) {
|
|||
|
|
clearTimeout(this.branchingTimeout);
|
|||
|
|
this.branchingTimeout = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 隐藏并清除分支内容
|
|||
|
|
const branchingContent = document.getElementById('branchingContent');
|
|||
|
|
const branchNarrative = document.getElementById('branchNarrative');
|
|||
|
|
const storyChoices = document.getElementById('storyChoices');
|
|||
|
|
const choicesContainer = document.getElementById('choicesContainer');
|
|||
|
|
const choiceFeedback = document.getElementById('choiceFeedback');
|
|||
|
|
|
|||
|
|
if (branchingContent) branchingContent.style.display = 'none';
|
|||
|
|
if (storyChoices) storyChoices.style.display = 'none';
|
|||
|
|
|
|||
|
|
// 清除所有内容
|
|||
|
|
if (branchNarrative) branchNarrative.innerHTML = '';
|
|||
|
|
if (choicesContainer) choicesContainer.innerHTML = '';
|
|||
|
|
if (choiceFeedback) choiceFeedback.innerHTML = '';
|
|||
|
|
this.waitingForChoice = false;
|
|||
|
|
|
|||
|
|
// 点击"继续故事"后,总是进入下一段故事
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.nextSection();
|
|||
|
|
}, 1000);
|
|||
|
|
|
|||
|
|
// 语音反馈
|
|||
|
|
if (window.virtualTeacher && this.isVoiceEnabled) {
|
|||
|
|
window.virtualTeacher.speak("很好!故事继续发展...");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateControlButtons() {
|
|||
|
|
const prevBtn = document.getElementById('prevSection');
|
|||
|
|
const nextBtn = document.getElementById('nextSection');
|
|||
|
|
const readBtn = document.getElementById('readCurrentSection');
|
|||
|
|
|
|||
|
|
// 更新按钮状态
|
|||
|
|
if (prevBtn) prevBtn.disabled = this.currentSection === 0;
|
|||
|
|
|
|||
|
|
const sections = ['beginning', 'middle', 'ending'];
|
|||
|
|
if (nextBtn) nextBtn.disabled = this.currentSection >= sections.length - 1;
|
|||
|
|
|
|||
|
|
if (readBtn) readBtn.disabled = this.waitingForChoice;
|
|||
|
|
|
|||
|
|
// 更新按钮文字
|
|||
|
|
if (readBtn) {
|
|||
|
|
const readText = readBtn.querySelector('.btn-text');
|
|||
|
|
if (readText) {
|
|||
|
|
readText.textContent = this.waitingForChoice ? '请先做选择' : '朗读本段';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建全局实例
|
|||
|
|
window.aiStorytelling = new AIStorytelling();
|
|||
|
|
|
|||
|
|
// 防止页面刷新时状态丢失
|
|||
|
|
window.addEventListener('beforeunload', () => {
|
|||
|
|
if (window.virtualTeacher) {
|
|||
|
|
window.virtualTeacher.stopAllSpeech();
|
|||
|
|
}
|
|||
|
|
});
|