/** * Markdown Editor Module * UPDATED: Lazy audio loading support — tracks db_id for each block */ // ============================================ // Editor State // ============================================ let editorBlocks = []; let activeBlockId = null; let isToolbarClick = false; let panelState = { startingBlockId: null, blockCount: 10, voice: 'af_heart', pickMode: false }; // ============================================ // Initialization // ============================================ function initMarkdownEditor() { const editor = document.getElementById('markdownEditor'); editor.addEventListener('click', function(e) { if (panelState.pickMode) { const blockEl = e.target.closest('.md-block'); if (blockEl && !blockEl.classList.contains('editing')) { setStartingBlock(blockEl.id); exitPickMode(); e.preventDefault(); e.stopPropagation(); return; } } if (e.target === editor || e.target.id === 'emptyEditorMessage') { if (editorBlocks.length === 0) { addBlock('paragraph', ''); repairAllNewBlockLines(); updatePanelUI(); } } }); document.addEventListener('keydown', handleGlobalKeydown); document.addEventListener('mousedown', function(e) { if (e.target.closest('.md-block-toolbar') || e.target.closest('.dropdown-menu') || e.target.closest('.toolbar-btn') || e.target.closest('.btn-merge-section')) { isToolbarClick = true; } else { isToolbarClick = false; } }); restorePanelSettings(); console.log('📝 Markdown editor initialized (lazy audio loading)'); } // ============================================ // Audiobook Maker Panel // ============================================ function initAudiobookMakerPanel() { updatePanelUI(); } function updatePanelUI() { const textBlocks = getTextBlocks(); const totalBlocks = textBlocks.length; const totalEl = document.getElementById('ampTotalBlocks'); if (totalEl) totalEl.textContent = totalBlocks; const genCount = textBlocks.filter(b => { const data = editorBlocks.find(eb => eb.id === b.id); return data && data.audio_data; }).length; const genEl = document.getElementById('ampGeneratedBlocks'); if (genEl) genEl.textContent = genCount; const remainEl = document.getElementById('ampRemainingBlocks'); if (remainEl) remainEl.textContent = (totalBlocks - genCount); if (!panelState.startingBlockId || !document.getElementById(panelState.startingBlockId)) { if (textBlocks.length > 0) { panelState.startingBlockId = textBlocks[0].id; } else { panelState.startingBlockId = null; } } updateStartingBlockDisplay(); updateBlockCountLimits(); updateBlockHighlights(); } function getTextBlocks() { const editor = document.getElementById('markdownEditor'); if (!editor) return []; const allBlocks = editor.querySelectorAll('.md-block'); const textBlocks = []; allBlocks.forEach(block => { const blockType = block.dataset.blockType || 'paragraph'; if (blockType !== 'image') { const textarea = block.querySelector('.md-block-textarea'); const content = textarea ? textarea.value.trim() : ''; const isImageContent = content.startsWith('![') && content.includes(']('); if (!isImageContent) { textBlocks.push(block); } } }); return textBlocks; } function getAllContentBlocks() { const editor = document.getElementById('markdownEditor'); if (!editor) return []; return Array.from(editor.querySelectorAll('.md-block')); } function getTextBlockIndex(blockId) { const textBlocks = getTextBlocks(); return textBlocks.findIndex(b => b.id === blockId); } function updateStartingBlockDisplay() { const numEl = document.getElementById('ampStartBlockNum'); if (!numEl) return; if (panelState.startingBlockId) { const idx = getTextBlockIndex(panelState.startingBlockId); numEl.textContent = idx >= 0 ? (idx + 1) : '-'; } else { numEl.textContent = '-'; } } function updateBlockCountLimits() { const input = document.getElementById('ampBlockCount'); if (!input) return; const textBlocks = getTextBlocks(); const startIdx = panelState.startingBlockId ? getTextBlockIndex(panelState.startingBlockId) : 0; let remaining = 0; if (startIdx >= 0) { remaining = textBlocks.length - startIdx; } const maxVal = Math.max(1, remaining); input.max = maxVal; if (panelState.blockCount > maxVal) panelState.blockCount = maxVal; if (panelState.blockCount < 1 && maxVal >= 1) panelState.blockCount = Math.min(10, maxVal); input.value = panelState.blockCount; } function updateBlockHighlights() { document.querySelectorAll('.md-block.starting-block').forEach(b => b.classList.remove('starting-block')); document.querySelectorAll('.md-block.in-gen-range').forEach(b => b.classList.remove('in-gen-range')); if (!panelState.startingBlockId) return; const startEl = document.getElementById(panelState.startingBlockId); if (!startEl) return; startEl.classList.add('starting-block'); const textBlocks = getTextBlocks(); const startIdx = getTextBlockIndex(panelState.startingBlockId); if (startIdx >= 0) { const endIdx = Math.min(startIdx + panelState.blockCount, textBlocks.length); for (let i = startIdx; i < endIdx; i++) { if (i !== startIdx) { textBlocks[i].classList.add('in-gen-range'); } } } } function setStartingBlock(blockId) { panelState.startingBlockId = blockId; updatePanelUI(); savePanelSettings(); } function enterPickMode() { panelState.pickMode = true; const editor = document.getElementById('markdownEditor'); if (editor) editor.classList.add('editor-pick-mode'); const indicator = document.getElementById('ampStartIndicator'); if (indicator) indicator.classList.add('pick-mode'); } function exitPickMode() { panelState.pickMode = false; const editor = document.getElementById('markdownEditor'); if (editor) editor.classList.remove('editor-pick-mode'); const indicator = document.getElementById('ampStartIndicator'); if (indicator) indicator.classList.remove('pick-mode'); } function togglePickMode() { if (panelState.pickMode) { exitPickMode(); } else { enterPickMode(); } } function handleBlockCountChange(value) { const num = parseInt(value); if (isNaN(num) || num < 1) return; const input = document.getElementById('ampBlockCount'); const maxVal = parseInt(input.max) || 999; panelState.blockCount = Math.min(num, maxVal); if (input) input.value = panelState.blockCount; updateBlockHighlights(); savePanelSettings(); } function adjustBlockCount(delta) { const input = document.getElementById('ampBlockCount'); const current = input ? (parseInt(input.value) || 1) : 1; const maxVal = input ? (parseInt(input.max) || 999) : 999; const newVal = Math.max(1, Math.min(current + delta, maxVal)); panelState.blockCount = newVal; if (input) input.value = newVal; updateBlockHighlights(); savePanelSettings(); } function handleVoiceChange(value) { panelState.voice = value; savePanelSettings(); } function savePanelSettings() { sessionStorage.setItem('ampSettings', JSON.stringify({ blockCount: panelState.blockCount, voice: panelState.voice })); } function restorePanelSettings() { const saved = sessionStorage.getItem('ampSettings'); if (saved) { try { const settings = JSON.parse(saved); if (settings.blockCount) panelState.blockCount = settings.blockCount; if (settings.voice) panelState.voice = settings.voice; } catch(e) { /* ignore */ } } } function advanceStartingBlockAfterGeneration(generatedCount) { const textBlocks = getTextBlocks(); const startIdx = getTextBlockIndex(panelState.startingBlockId); if (startIdx >= 0) { const nextIdx = startIdx + generatedCount; if (nextIdx < textBlocks.length) { panelState.startingBlockId = textBlocks[nextIdx].id; } else { if (textBlocks.length > 0) { panelState.startingBlockId = textBlocks[textBlocks.length - 1].id; } } } updatePanelUI(); savePanelSettings(); } // ============================================ // Section Markers // ============================================ function makeSectionStart(blockId, title = null) { const blockData = editorBlocks.find(b => b.id === blockId); if (!blockData) return; if (blockData.sectionStart) return; const sectionNum = editorBlocks.filter(b => b.sectionStart).length + 1; blockData.sectionStart = true; blockData.sectionName = title || `Section ${sectionNum}`; renderAllSectionDividers(); renderDocumentOutline(); } function removeSection(blockId) { const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.sectionStart = false; blockData.sectionName = ''; } renderAllSectionDividers(); renderDocumentOutline(); } function renderAllSectionDividers() { const editor = document.getElementById('markdownEditor'); if (!editor) return; editor.querySelectorAll('.section-divider').forEach(d => d.remove()); for (const blockData of editorBlocks) { if (!blockData.sectionStart) continue; const blockEl = document.getElementById(blockData.id); if (!blockEl) continue; const divider = document.createElement('div'); divider.className = 'section-divider'; divider.dataset.targetBlock = blockData.id; divider.innerHTML = `
${escapeHtml(blockData.sectionName || 'Section')}
`; blockEl.before(divider); } } function handleSectionTitleEdit(blockId, newTitle) { const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.sectionName = newTitle.trim() || 'Section'; } renderDocumentOutline(); } function renderDocumentOutline() { const outline = document.getElementById('documentOutlineList'); if (!outline) return; const sections = editorBlocks.filter(b => b.sectionStart); if (sections.length === 0) { outline.innerHTML = '
  • No sections found.
  • '; return; } outline.innerHTML = ''; sections.forEach((blockData) => { const title = blockData.sectionName || 'Section'; const li = document.createElement('li'); li.textContent = title; li.title = title; li.onclick = () => { const targetEl = document.getElementById(blockData.id); if (targetEl) { targetEl.scrollIntoView({behavior: 'smooth', block: 'center'}); } }; outline.appendChild(li); }); } // ============================================ // Block Merge & Split // ============================================ function mergeBlockUp(blockId) { const block = document.getElementById(blockId); if (!block) return; let prevBlock = block.previousElementSibling; while (prevBlock && !prevBlock.classList.contains('md-block')) { prevBlock = prevBlock.previousElementSibling; } if (!prevBlock) { showNotification('No previous text block found to merge with.', 'warning'); return; } const currentTextarea = block.querySelector('.md-block-textarea'); const prevTextarea = prevBlock.querySelector('.md-block-textarea'); if (!currentTextarea || !prevTextarea) return; prevTextarea.value = prevTextarea.value.trim() + " " + currentTextarea.value.trim(); const currentData = editorBlocks.find(b => b.id === blockId); const prevData = editorBlocks.find(b => b.id === prevBlock.id); if (currentData && currentData.sectionStart && prevData) { if (!prevData.sectionStart) { prevData.sectionStart = true; prevData.sectionName = currentData.sectionName; } } exitEditMode(prevBlock.id); deleteBlock(blockId); showNotification('Merged with previous block', 'info'); } function mergeBlockDown(blockId) { const block = document.getElementById(blockId); if (!block) return; let nextBlock = block.nextElementSibling; while (nextBlock && !nextBlock.classList.contains('md-block')) { nextBlock = nextBlock.nextElementSibling; } if (!nextBlock) { showNotification('No next text block found to merge with.', 'warning'); return; } const currentTextarea = block.querySelector('.md-block-textarea'); const nextTextarea = nextBlock.querySelector('.md-block-textarea'); if (!currentTextarea || !nextTextarea) return; currentTextarea.value = currentTextarea.value.trim() + " " + nextTextarea.value.trim(); exitEditMode(blockId); deleteBlock(nextBlock.id); showNotification('Merged with next block', 'info'); } function splitBlock(blockId) { const block = document.getElementById(blockId); if (!block) return; const textarea = block.querySelector('.md-block-textarea'); if (!textarea) return; const pos = textarea.selectionStart; const text = textarea.value; const part1 = text.substring(0, pos).trim(); const part2 = text.substring(pos).trim(); if (!part2) { showNotification('Cursor is at the end. Nothing to split.', 'warning'); return; } textarea.value = part1; exitEditMode(blockId); const newBlockId = addBlock('paragraph', part2, block); repairAllNewBlockLines(); enterEditMode(newBlockId); showNotification('Block split successfully', 'success'); } // ============================================ // Block Management // ============================================ function addBlock(type = 'paragraph', content = '', afterElement = null, images = []) { const blockId = 'block_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); const block = document.createElement('div'); block.className = 'md-block'; block.id = blockId; block.dataset.blockId = blockId; block.dataset.blockType = type; block.dataset.ttsText = ''; const renderedContent = renderBlockContent(type, content, blockId, images); const isImageBlock = (type === 'image'); block.innerHTML = ` ${!isImageBlock ? `
    ` : ''}
    ${!isImageBlock ? ` ` : ''}
    ${!isImageBlock ? `
    ` : ''}
    ${renderedContent}
    `; const editor = document.getElementById('markdownEditor'); if (afterElement) { if (afterElement.classList.contains('new-block-line')) { afterElement.after(block); } else { const nextSibling = afterElement.nextElementSibling; if (nextSibling && nextSibling.classList.contains('new-block-line')) { nextSibling.after(block); } else { afterElement.after(block); } } } else { editor?.appendChild(block); } editorBlocks.push({ id: blockId, db_id: null, // Database ID — set when loaded from server has_audio: false, // Server-reported audio presence type: type, content: content, images: images, sectionStart: false, sectionName: '', audio_data: null, audio_format: null, transcription: null, tts_text: '' }); if (!content && type !== 'image') { setTimeout(() => enterEditMode(blockId), 100); } setTimeout(() => updatePanelUI(), 50); return blockId; } function deleteBlock(blockId) { const block = document.getElementById(blockId); if (!block) return; if (activeBlockId === blockId) { activeBlockId = null; } if (panelState.startingBlockId === blockId) { const textBlocks = getTextBlocks(); const idx = textBlocks.findIndex(b => b.id === blockId); if (idx >= 0 && idx + 1 < textBlocks.length) { panelState.startingBlockId = textBlocks[idx + 1].id; } else if (idx > 0) { panelState.startingBlockId = textBlocks[idx - 1].id; } else { panelState.startingBlockId = null; } } const blockData = editorBlocks.find(b => b.id === blockId); const hadSection = blockData && blockData.sectionStart; const nextLine = block.nextElementSibling; if (nextLine && nextLine.classList.contains('new-block-line')) { nextLine.remove(); } const divider = document.querySelector(`.section-divider[data-target-block="${blockId}"]`); if (divider) divider.remove(); block.remove(); editorBlocks = editorBlocks.filter(b => b.id !== blockId); repairAllNewBlockLines(); if (hadSection) { renderAllSectionDividers(); renderDocumentOutline(); } checkEmptyEditor(); updatePanelUI(); } function enterEditMode(blockId) { if (activeBlockId && activeBlockId !== blockId) { exitEditMode(activeBlockId); } const block = document.getElementById(blockId); if (!block) return; block.classList.add('editing'); activeBlockId = blockId; const textarea = block.querySelector('.md-block-textarea'); if (textarea) { textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); autoResizeTextarea(textarea); } } function exitEditMode(blockId) { const block = document.getElementById(blockId); if (!block) return; block.classList.remove('editing'); const textarea = block.querySelector('.md-block-textarea'); const contentDiv = block.querySelector('.md-block-content'); if (textarea && contentDiv) { const content = textarea.value.trim(); const blockType = block.dataset.blockType || 'paragraph'; const blockData = editorBlocks.find(b => b.id === blockId); const images = blockData ? (blockData.images || []) : []; if (content === '') { contentDiv.innerHTML = renderBlockContent(blockType, '', blockId, images); if (blockData) blockData.content = ''; } else { contentDiv.innerHTML = renderBlockContent(blockType, content, blockId, images); if (blockData) blockData.content = content; } } if (activeBlockId === blockId) { activeBlockId = null; } repairAllNewBlockLines(); updatePanelUI(); } function handleBlockBlur(event, blockId) { if (isToolbarClick) { isToolbarClick = false; setTimeout(() => { const block = document.getElementById(blockId); if (block && block.classList.contains('editing')) { const textarea = block.querySelector('.md-block-textarea'); if (textarea) textarea.focus(); } }, 10); return; } exitEditMode(blockId); } function autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.max(textarea.scrollHeight, 60) + 'px'; } function handleBlockInput(event, blockId) { autoResizeTextarea(event.target); } function handleBlockKeydown(event, blockId) { const textarea = event.target; if (event.key === 'Escape') { event.preventDefault(); textarea.blur(); return; } if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); exitEditMode(blockId); const block = document.getElementById(blockId); addBlock('paragraph', '', block); repairAllNewBlockLines(); return; } if (event.key === 'Backspace' && textarea.value === '') { event.preventDefault(); const block = document.getElementById(blockId); let prevBlock = block.previousElementSibling; while (prevBlock && !prevBlock.classList.contains('md-block')) { prevBlock = prevBlock.previousElementSibling; } deleteBlock(blockId); if (prevBlock) enterEditMode(prevBlock.id); return; } } function handleGlobalKeydown(event) { if ((event.ctrlKey || event.metaKey) && event.key === 's') { event.preventDefault(); if (typeof saveProject === 'function') saveProject(); return; } if (event.key === 'Escape' && panelState.pickMode) { exitPickMode(); } } function applyFormat(format) { if (!activeBlockId) return; const block = document.getElementById(activeBlockId); const textarea = block.querySelector('.md-block-textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); if (start === end) return; let wrapper = ''; switch (format) { case 'bold': wrapper = '**'; break; case 'italic': wrapper = '*'; break; } const newText = textarea.value.substring(0, start) + wrapper + selectedText + wrapper + textarea.value.substring(end); textarea.value = newText; textarea.setSelectionRange(start + wrapper.length, end + wrapper.length); textarea.focus(); } function changeCase(caseType) { if (!activeBlockId) return; const block = document.getElementById(activeBlockId); const textarea = block.querySelector('.md-block-textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; let selectedText = textarea.value.substring(start, end); if (start === end) return; switch (caseType) { case 'lower': selectedText = selectedText.toLowerCase(); break; case 'upper': selectedText = selectedText.toUpperCase(); break; case 'sentence': selectedText = selectedText.toLowerCase().replace(/(^\w|\.\s+\w)/g, c => c.toUpperCase()); break; case 'title': selectedText = selectedText.toLowerCase().replace(/\b\w/g, c => c.toUpperCase()); break; } textarea.value = textarea.value.substring(0, start) + selectedText + textarea.value.substring(end); textarea.setSelectionRange(start, start + selectedText.length); textarea.focus(); } function escapeHtml(text) { if(!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function renderBlockContent(type, content, blockId, images) { if (!content) { return `Click to edit`; } if (type === 'image') { if (images && images.length > 0) { const img = images[0]; if (img.data) { return `
    ${img.alt_text || 'Image'}
    `; } } const dataUriMatch = content.match(/!\[([^\]]*)\]\((data:image\/[^;]+;base64,[^)]+)\)/); if (dataUriMatch) { return `
    ${dataUriMatch[1] || 'Image'}
    `; } return `

    Click to upload an image

    `; } let safeContent = content; if (content.length > 10000 && content.includes('base64,')) { safeContent = content.replace( /!\[([^\]]*)\]\(data:image\/[^;]+;base64,[^)]+\)/g, '![$1](embedded-image)' ); } try { return marked.parse(safeContent, { breaks: true, gfm: true }); } catch (e) { return `

    ${escapeHtml(content)}

    `; } } // ============================================ // New Block Line & DOM Cleanup // ============================================ function createNewBlockLine() { const line = document.createElement('div'); line.className = 'new-block-line'; const lineId = 'line_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); line.innerHTML = `
    `; return line; } function addBlockAtLine(button) { const line = button.closest('.new-block-line'); addBlock('paragraph', '', line); repairAllNewBlockLines(); } function handleImageAtLine(event, inputEl) { const file = event.target.files[0]; if (!file || !file.type.startsWith('image/')) return; const line = inputEl.closest('.new-block-line'); if (!line) return; const reader = new FileReader(); reader.onload = function(e) { const base64Data = e.target.result.split(',')[1]; const format = file.type.split('/')[1] === 'jpeg' ? 'jpeg' : file.type.split('/')[1]; const images = [{ data: base64Data, format: format, alt_text: file.name, position: 'before' }]; const content = `![${file.name}](embedded-image.${format})`; addBlock('image', content, line, images); repairAllNewBlockLines(); updatePanelUI(); }; reader.readAsDataURL(file); event.target.value = ''; } function addSectionAtLine(button) { const line = button.closest('.new-block-line'); const blockId = addBlock('heading2', 'New Section Heading', line); makeSectionStart(blockId, 'New Section Heading'); repairAllNewBlockLines(); enterEditMode(blockId); } function ensureNewBlockLineAfter(element) { if (!element) return; const next = element.nextElementSibling; if (next && next.classList.contains('new-block-line')) { return; } element.after(createNewBlockLine()); } function repairAllNewBlockLines() { const editor = document.getElementById('markdownEditor'); if (!editor) return; const children = Array.from(editor.children); for (let i = 0; i < children.length; i++) { const el = children[i]; if (el.classList.contains('new-block-line')) { const prev = el.previousElementSibling; if (!prev || prev.classList.contains('new-block-line')) { el.remove(); } } } const updatedChildren = Array.from(editor.children); for (let i = 0; i < updatedChildren.length; i++) { const el = updatedChildren[i]; if (el.classList.contains('md-block')) { ensureNewBlockLineAfter(el); } } } function checkEmptyEditor() { const editor = document.getElementById('markdownEditor'); if(!editor) return; const blocks = editor.querySelectorAll('.md-block'); const emptyMessage = document.getElementById('emptyEditorMessage'); const panel = document.getElementById('audiobookMakerPanel'); const sidebar = document.getElementById('documentOutlineSidebar'); if (blocks.length === 0) { editor.querySelectorAll('.new-block-line').forEach(line => line.remove()); editor.querySelectorAll('.section-divider').forEach(div => div.remove()); if (emptyMessage) emptyMessage.style.display = 'block'; if (panel) panel.style.display = 'none'; if (sidebar) sidebar.style.display = 'none'; } else { if (emptyMessage) emptyMessage.style.display = 'none'; if (panel) panel.style.display = 'flex'; if (sidebar) sidebar.style.display = 'block'; } } // ============================================ // Content Collection // ============================================ function collectEditorContent() { const editor = document.getElementById('markdownEditor'); if (!editor) return []; const chapters = []; let currentChapter = null; let blockOrder = 0; let sectionCounter = 0; const allBlockEls = editor.querySelectorAll('.md-block'); for (const el of allBlockEls) { const blockData = editorBlocks.find(b => b.id === el.id); if (!blockData) continue; if (blockData.sectionStart) { if (currentChapter && currentChapter.blocks.length > 0) { chapters.push(currentChapter); } sectionCounter++; currentChapter = { chapter_number: sectionCounter, title: blockData.sectionName || `Section ${sectionCounter}`, voice: panelState.voice || 'af_heart', blocks: [] }; blockOrder = 0; } if (!currentChapter) { sectionCounter++; const firstContent = el.querySelector('.md-block-textarea') ? el.querySelector('.md-block-textarea').value.trim() : ''; const plainText = stripMarkdown(firstContent); const snippet = plainText ? (plainText.substring(0, 40) + (plainText.length > 40 ? '...' : '')) : 'Section 1'; currentChapter = { chapter_number: sectionCounter, title: blockData.sectionName || snippet, voice: panelState.voice || 'af_heart', blocks: [] }; } blockOrder++; const textarea = el.querySelector('.md-block-textarea'); const content = textarea ? textarea.value : ''; const blockType = el.dataset.blockType || 'paragraph'; if (content.trim() || blockType === 'image') { currentChapter.blocks.push({ block_order: blockOrder, block_type: blockType, content: content, tts_text: el.dataset.ttsText || '', audio_data: blockData.audio_data || '', audio_format: blockData.audio_format || 'mp3', transcription: blockData.transcription || [], images: blockData.images || [] }); } } if (currentChapter && currentChapter.blocks.length > 0) { chapters.push(currentChapter); } return chapters; } function renderProjectInEditor(projectData) { const editor = document.getElementById('markdownEditor'); if(!editor) return; editor.innerHTML = ''; editorBlocks = []; const emptyMessage = document.getElementById('emptyEditorMessage'); if (emptyMessage) emptyMessage.style.display = 'none'; if (projectData.chapters && projectData.chapters.length > 0 && projectData.chapters[0].voice) { panelState.voice = projectData.chapters[0].voice; const voiceSelect = document.getElementById('ampVoiceSelect'); if (voiceSelect) voiceSelect.value = panelState.voice; } for (const chapter of projectData.chapters) { let isFirstInChapter = true; for (const block of chapter.blocks) { const lastChild = editor.lastElementChild; const blockId = addBlock( block.block_type, block.content, lastChild, block.images || [] ); if (isFirstInChapter) { const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.sectionStart = true; blockData.sectionName = chapter.title || 'Section'; } isFirstInChapter = false; } const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.audio_data = block.audio_data; blockData.audio_format = block.audio_format; blockData.transcription = block.transcription; blockData.tts_text = block.tts_text; blockData.db_id = block.id; // Track DB ID for lazy audio loading blockData.has_audio = !!block.has_audio; // Server-reported audio presence } const blockEl = document.getElementById(blockId); if (blockEl && block.tts_text) { blockEl.dataset.ttsText = block.tts_text; } // Show audio indicator based on has_audio flag (audio will be lazy-loaded) if (block.has_audio && block.block_type !== 'image') { if (blockEl) { const indicator = blockEl.querySelector('.audio-indicator'); if (indicator) { indicator.classList.remove('no-audio'); indicator.classList.add('has-audio'); indicator.title = 'Audio loading...'; } } } if (blockEl) { ensureNewBlockLineAfter(blockEl); } } } renderAllSectionDividers(); repairAllNewBlockLines(); const textBlocks = getTextBlocks(); let foundStart = false; for (const tb of textBlocks) { const data = editorBlocks.find(b => b.id === tb.id); if (!data || !data.has_audio) { panelState.startingBlockId = tb.id; foundStart = true; break; } } if (!foundStart && textBlocks.length > 0) { panelState.startingBlockId = textBlocks[textBlocks.length - 1].id; } if (textBlocks.length > 0) { panelState.blockCount = textBlocks.length; } updatePanelUI(); renderDocumentOutline(); checkEmptyEditor(); }