/**
* Interactive Reader Module - Rewritten
* FIXED: Images from manual upload now render correctly using base64 data
* Features:
* - Single Start button, becomes play/pause after first click
* - Auto-advance to next block audio
* - Click any word to play from that point
* - Word + Sentence highlighting with smart sync
* - Left sidebar chapter navigation
* - Images rendered from base64 data (both processed and uploaded)
* - Pause/Resume works correctly
*/
// ============================================
// Reader State
// ============================================
let readerInstances = [];
let currentReaderInstance = null;
let currentReaderIndex = -1;
let readerStarted = false;
let readerUICreated = false;
// ============================================
// Render Reader
// ============================================
function renderInteractiveReader() {
const container = document.getElementById('readerContainer');
const chapters = collectEditorContent();
let hasAudio = false;
const chaptersWithAudio = [];
for (const chapter of chapters) {
const chapterBlocks = [];
for (const block of chapter.blocks) {
// Match editor block by ID lookup
const blockData = findEditorBlockForContent(block);
const isImageBlock = block.block_type === 'image' ||
(block.content && block.content.trim().startsWith(');
chapterBlocks.push({
...block,
_editorData: blockData || null,
_isImage: isImageBlock
});
if (!isImageBlock && blockData && blockData.audio_data) {
hasAudio = true;
}
}
chaptersWithAudio.push({
...chapter,
blocks: chapterBlocks
});
}
if (!hasAudio) {
container.innerHTML = `
Generate audio to view the interactive reader
Go to the Editor tab and click "Generate" on blocks or chapters
`;
removeReaderUI();
return;
}
let html = '';
readerInstances = [];
let globalBlockIndex = 0;
for (const chapter of chaptersWithAudio) {
html += ``;
html += `
Chapter ${chapter.chapter_number}
`;
for (const block of chapter.blocks) {
const blockData = block._editorData;
const isImageBlock = block._isImage;
const hasBlockAudio = !isImageBlock && blockData && blockData.audio_data;
const blockId = blockData ? blockData.id : `reader_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
html += `
`;
if (isImageBlock) {
const imageHtml = buildImageHtml(block, blockData);
html += `
${imageHtml}
`;
} else {
// Render before-position images from the block's images array
const blockImages = getBlockImages(block, blockData);
for (const img of blockImages) {
if (img.position === 'before' && img.data) {
html += `
`;
}
}
html += `
`;
// Render after-position images
for (const img of blockImages) {
if (img.position === 'after' && img.data) {
html += `
`;
}
}
}
html += `
`;
readerInstances.push({
index: globalBlockIndex,
blockId: blockId,
blockData: blockData,
content: block.content,
hasAudio: !!hasBlockAudio,
isImage: isImageBlock,
chapterNumber: chapter.chapter_number,
wordSpans: [],
wordMap: [],
sentenceData: [],
audio: null,
transcription: (!isImageBlock && blockData) ? (blockData.transcription || []) : [],
animFrameId: null,
lastWordSpan: null,
lastSentenceSpans: []
});
globalBlockIndex++;
}
html += `
`;
}
container.innerHTML = html;
for (const inst of readerInstances) {
if (inst.isImage || !inst.content) continue;
const contentEl = document.getElementById(`reader-content-${inst.index}`);
if (!contentEl) continue;
renderWordsIntoContainer(contentEl, inst);
if (inst.hasAudio && inst.transcription.length > 0) {
runReaderSmartSync(inst);
}
}
addReaderStyles();
setupReaderUI(chaptersWithAudio);
}
// ============================================
// Image Resolution Helpers
// ============================================
/**
* Find the editorBlocks entry that corresponds to a collected block.
* Uses multiple strategies: ID match, then content match.
*/
function findEditorBlockForContent(block) {
// Strategy 1: Try matching via DOM element ID
for (const eb of editorBlocks) {
const el = document.getElementById(eb.id);
if (el) {
const textarea = el.querySelector('.md-block-textarea');
if (textarea && textarea.value === block.content) {
return eb;
}
}
}
// Strategy 2: Match by content string directly
for (const eb of editorBlocks) {
if (eb.content === block.content) {
return eb;
}
}
return null;
}
/**
* Get all images for a block from every available source.
*/
function getBlockImages(block, blockData) {
// Priority 1: block.images from collectEditorContent (most reliable)
if (block.images && block.images.length > 0) {
const valid = block.images.filter(img => img.data && img.data.length > 0);
if (valid.length > 0) return valid;
}
// Priority 2: editorBlocks data
if (blockData && blockData.images && blockData.images.length > 0) {
const valid = blockData.images.filter(img => img.data && img.data.length > 0);
if (valid.length > 0) return valid;
}
return [];
}
/**
* Build the HTML for an image block in the reader.
* Resolves base64 data from multiple sources.
*/
function buildImageHtml(block, blockData) {
// Source 1: block.images array (from collectEditorContent)
if (block.images && block.images.length > 0) {
let html = '';
for (const img of block.images) {
if (img.data && img.data.length > 0) {
html += `