// ============================================================ // LEARNING HUB — User content feed, viewer, quizzes + Admin CMS // ============================================================ import { clearQuestionBlocks, destroyOptionEditor, destroyQuestionBlockEditors, getTpHTML, makeTpEditor } from './learningHub/tiptapEditor.js'; import { esc, sanitizeHtml } from './learningHub/sanitize.js'; import { deleteJson, getJson, sendJson } from './learningHub/api.js'; import { renderCmsCategoryList, renderCmsContentEmpty, renderCmsContentRow, renderOptionRowShell, renderQuestionBlockShell } from './learningHub/cmsRenderer.js'; import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearchHeader } from './learningHub/feedRenderer.js'; import { renderQuizQuestions, renderQuizResultExplanations } from './learningHub/quizRenderer.js'; import { createSlideController } from './learningHub/slideController.js'; import { buildViewerMeta, renderPresentationCard, renderProgressList } from './learningHub/viewerRenderer.js'; var loaded = false; var cmsLoaded = false; var currentContent = null; var currentView = 'feed'; // 'feed' | 'category' | 'viewer' var _bodyEditor = null; // Tiptap Editor instance for body var slides = createSlideController({ getMarkdown: getMarpMarkdown, getTitle: getPresentationTitle, showBusy: function(message) { showBusy(message); }, hideBusy: function() { hideBusy(); }, showToast: function(message, type) { showToast(message, type); } }); // ── Load when tab activated ──────────────────────────────── document.addEventListener('tabChanged', function(e) { if (e.detail && e.detail.tab === 'learning') { if (!loaded) { loadFeed(); loadCategories(); loaded = true; wireSearch(); } } // Load CMS when Content Manager tab is opened if (e.detail && e.detail.tab === 'cms') { if (!cmsLoaded) { loadCms(); cmsLoaded = true; wireCmsFileInput(); wireCmsTypeSelect(); } } }); // ── Event delegation ─────────────────────────────────────── document.addEventListener('click', function(e) { // Slide preview modal controls (checked first — modal is outside CMS) if (e.target.closest('#slide-preview-close') || e.target.id === 'slide-preview-overlay') { slides.closeSlideModal(); return; } if (e.target.closest('#slide-nav-prev')) { slides.slideStep(-1); return; } if (e.target.closest('#slide-nav-next')) { slides.slideStep(1); return; } var dot = e.target.closest('.slide-dot'); if (dot) { slides.renderSlide(parseInt(dot.dataset.idx)); return; } // Feed item click var feedItem = e.target.closest('.lh-feed-item'); if (feedItem && feedItem.dataset.slug) { loadContent(feedItem.dataset.slug); return; } // Category pill click var catPill = e.target.closest('.lh-cat-pill'); if (catPill && catPill.dataset.slug) { if (catPill.dataset.slug === 'all') { loadFeed(); setActiveCatPill('all'); } else { loadCategoryContent(catPill.dataset.slug); setActiveCatPill(catPill.dataset.slug); } return; } // Back button if (e.target.closest('#lh-back')) { showFeed(); return; } // Submit quiz if (e.target.closest('#lh-submit-quiz')) { submitQuiz(); return; } // ── CMS events ───────────────────────────── if (e.target.closest('#btn-lh-refresh-content')) { loadCmsContent(); loadCmsStats(); return; } if (e.target.closest('#btn-lh-add-cat')) { addCategory(); return; } if (e.target.closest('#btn-lh-new-content')) { openEditor(null, 'article'); return; } if (e.target.closest('#btn-lh-new-quiz')) { openEditor(null, 'quiz'); return; } if (e.target.closest('#btn-lh-new-pearl')) { openEditor(null, 'pearl'); return; } if (e.target.closest('#btn-lh-new-presentation')) { openEditor(null, 'presentation'); return; } if (e.target.closest('#btn-lh-close-editor')) { closeEditor(); return; } if (e.target.closest('#btn-lh-save-content')) { saveContent(); return; } if (e.target.closest('#btn-lh-delete-content')) { showDeleteConfirm(); return; } if (e.target.closest('#btn-lh-delete-yes')) { confirmDelete(); return; } if (e.target.closest('#btn-lh-delete-cancel')) { hideDeleteConfirm(); return; } if (e.target.closest('#btn-lh-add-question')) { addQuestionBlock(); return; } // Presentation if (e.target.closest('#btn-lh-preview-slides')) { slides.previewCurrentSlides(); return; } if (e.target.closest('#btn-lh-download-pptx')) { slides.downloadPptx(); return; } // AI panel if (e.target.closest('#btn-lh-ai-open')) { openAiPanel(); return; } if (e.target.closest('#btn-lh-ai-close')) { closeAiPanel(); return; } if (e.target.closest('#btn-lh-ai-generate')) { runAiGenerate(); return; } if (e.target.closest('#btn-lh-ai-refine-body')) { toggleRefineBar(); return; } if (e.target.closest('#btn-lh-refine-submit')) { submitRefineBody(); return; } if (e.target.closest('#btn-lh-refine-cancel')) { toggleRefineBar(false); return; } if (e.target.closest('#btn-lh-webdav-refresh')) { browseWebdav(_aiWebdavCurrentPath); return; } if (e.target.closest('#btn-lh-webdav-deselect')) { deselectWebdavFile(); return; } // AI tabs var aiTab = e.target.closest('.lh-ai-tab'); if (aiTab && aiTab.dataset.aitab) { switchAiTab(aiTab.dataset.aitab); return; } // WebDAV list items var wdItem = e.target.closest('.lh-webdav-item'); if (wdItem) { if (wdItem.dataset.isdir === '1') { browseWebdav(wdItem.dataset.path); } else { selectWebdavFile(wdItem.dataset.path, wdItem.dataset.name); } return; } // Rich text toggle for question or option var rtBtn = e.target.closest('.lh-richtext-btn'); if (rtBtn) { toggleRichText(rtBtn); return; } // CMS content item edit var cmsItem = e.target.closest('.lh-cms-content-item'); if (cmsItem && cmsItem.dataset.id) { openEditor(cmsItem.dataset.id); return; } // CMS delete category var delCat = e.target.closest('.lh-cms-del-cat'); if (delCat) { deleteCategory(delCat.dataset.id); return; } // CMS sidebar category filter var cmsCat = e.target.closest('.lh-cms-cat-filter'); if (cmsCat && cmsCat.dataset.id) { filterCmsContentByCategory(cmsCat.dataset.id); return; } // Remove question block var rmQ = e.target.closest('.lh-rm-question'); if (rmQ) { var qb = rmQ.closest('.lh-question-block'); if (qb) { destroyQuestionBlockEditors(qb); qb.remove(); } return; } // Add option to question var addOpt = e.target.closest('.lh-add-option'); if (addOpt) { var qBlock = addOpt.closest('.lh-question-block'); if (qBlock) addOptionRow(qBlock.querySelector('.lh-options-list'), null); return; } // Remove option var rmOpt = e.target.closest('.lh-rm-option'); if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) { destroyOptionEditor(row); row.remove(); } return; } }); // wireSearch() called after component is in DOM var _searchTimeout = null; function wireSearch() { var searchEl = document.getElementById('lh-search'); if (!searchEl || searchEl._wired) return; searchEl._wired = true; searchEl.addEventListener('input', function() { clearTimeout(_searchTimeout); var q = searchEl.value.trim(); if (!q) { loadFeed(); return; } _searchTimeout = setTimeout(function() { searchContent(q); }, 350); }); searchEl.addEventListener('keydown', function(e) { if (e.key === 'Escape') { searchEl.value = ''; loadFeed(); } }); } // ============================================================ // USER-FACING: FEED, CATEGORIES, CONTENT, QUIZZES // ============================================================ function loadFeed() { var feedEl = document.getElementById('lh-feed'); if (!feedEl) return; feedEl.innerHTML = '

Loading...

'; showFeed(); getJson('/api/learning/feed') .then(function(data) { if (!data.success) { feedEl.innerHTML = '

Failed to load

'; return; } renderFeed(data.content, feedEl); }) .catch(function() { feedEl.innerHTML = '

Connection error

'; }); } function loadCategories() { getJson('/api/learning/categories') .then(function(data) { if (!data.success) return; var bar = document.getElementById('lh-categories'); if (!bar) return; bar.innerHTML = renderCategoryPills(data.categories); }); } function loadCategoryContent(slug) { var feedEl = document.getElementById('lh-feed'); if (!feedEl) return; feedEl.innerHTML = '

Loading...

'; showFeed(); getJson('/api/learning/category/' + encodeURIComponent(slug)) .then(function(data) { if (!data.success) { feedEl.innerHTML = '

Category not found

'; return; } renderFeed(data.content, feedEl); }); } function searchContent(q) { var feedEl = document.getElementById('lh-feed'); if (!feedEl) return; showFeed(); feedEl.innerHTML = '
' + ' Searching for "' + esc(q) + '"…' + '
'; getJson('/api/learning/search?q=' + encodeURIComponent(q)) .then(function(data) { if (!data.success) { feedEl.innerHTML = '

Search failed

'; return; } var header = renderSearchHeader(data.content.length, q); if (data.content.length === 0) { feedEl.innerHTML = header + renderEmptySearchMessage(); } else { var tempEl = document.createElement('div'); renderFeed(data.content, tempEl); feedEl.innerHTML = header + tempEl.innerHTML; } var clearBtn = document.getElementById('lh-search-clear'); if (clearBtn) { clearBtn.addEventListener('click', function() { var s = document.getElementById('lh-search'); if (s) s.value = ''; loadFeed(); }); } }) .catch(function() { feedEl.innerHTML = '

Search error

'; }); } function loadContent(slug) { getJson('/api/learning/content/' + encodeURIComponent(slug)) .then(function(data) { if (!data.success) { showToast(data.error || 'Content not found', 'error'); return; } currentContent = data.content; showViewer(data.content); }) .catch(function() { showToast('Failed to load content', 'error'); }); } function showFeed() { currentView = 'feed'; var feedEl = document.getElementById('lh-feed'); var viewerEl = document.getElementById('lh-viewer'); var catsEl = document.getElementById('lh-categories'); var searchEl = document.getElementById('lh-search'); // Use style.display — classList.add('hidden') is overridden by .lh-feed{display:flex} // and .lh-category-bar{display:flex} which are defined later in CSS if (feedEl) feedEl.style.display = 'flex'; if (viewerEl) viewerEl.style.display = 'none'; if (catsEl) catsEl.style.display = 'flex'; if (searchEl) { var sc = searchEl.closest('.card'); if (sc) sc.style.display = ''; } } function showViewer(item) { currentView = 'viewer'; var feedEl = document.getElementById('lh-feed'); var viewerEl = document.getElementById('lh-viewer'); var catsEl = document.getElementById('lh-categories'); var searchEl = document.getElementById('lh-search'); if (feedEl) feedEl.style.display = 'none'; if (viewerEl) viewerEl.style.display = 'block'; if (catsEl) catsEl.style.display = 'none'; if (searchEl) { var sc = searchEl.closest('.card'); if (sc) sc.style.display = 'none'; } var titleEl = document.getElementById('lh-viewer-title'); var metaEl = document.getElementById('lh-viewer-meta'); var bodyEl = document.getElementById('lh-viewer-body'); var quizSection = document.getElementById('lh-quiz-section'); var resultsSection = document.getElementById('lh-quiz-results'); var progressSection = document.getElementById('lh-progress-section'); if (titleEl) titleEl.textContent = item.title; if (metaEl) metaEl.textContent = buildViewerMeta(item); var presViewer = document.getElementById('lh-presentation-viewer'); var bodyCard = bodyEl ? bodyEl.closest('.card') : null; if (item.content_type === 'presentation') { // Hide body card, show presentation card with View Slides button if (bodyCard) bodyCard.style.display = 'none'; if (presViewer) { presViewer.className = ''; // remove hidden presViewer.innerHTML = renderPresentationCard(item); // Auto-load and open slides document.getElementById('btn-lh-view-slides').addEventListener('click', function() { slides.openSlidesFromSlug(item.slug); }); // Open automatically on load slides.openSlidesFromSlug(item.slug); } // Quiz for presentations (optional) if (quizSection) { if (item.questions && item.questions.length > 0) { quizSection.classList.remove('hidden'); renderQuiz(item.questions); } else { quizSection.classList.add('hidden'); } } } else { // Normal content — show body card, hide presentation card if (bodyCard) bodyCard.style.display = ''; if (presViewer) presViewer.className = 'hidden'; if (bodyEl) bodyEl.innerHTML = sanitizeHtml(item.body || ''); if (quizSection) { if (item.questions && item.questions.length > 0) { quizSection.classList.remove('hidden'); renderQuiz(item.questions); } else { quizSection.classList.add('hidden'); } } } if (resultsSection) resultsSection.classList.add('hidden'); // Progress if (progressSection && item.progress && item.progress.length > 0) { progressSection.classList.remove('hidden'); document.getElementById('lh-progress-list').innerHTML = renderProgressList(item.progress); } else if (progressSection) { progressSection.classList.add('hidden'); } } function renderQuiz(questions) { var container = document.getElementById('lh-quiz-questions'); var countEl = document.getElementById('lh-quiz-count'); if (!container) return; if (countEl) countEl.textContent = questions.length + ' question' + (questions.length !== 1 ? 's' : ''); container.innerHTML = renderQuizQuestions(questions); } function submitQuiz() { if (!currentContent || !currentContent.questions) return; var answers = []; currentContent.questions.forEach(function(q) { if (q.question_type === 'multi') { var checkedEls = document.querySelectorAll('input[name="lh-qm-' + q.id + '"]:checked'); var optionIds = []; checkedEls.forEach(function(el) { optionIds.push(parseInt(el.value)); }); answers.push({ questionId: q.id, optionIds: optionIds }); } else { var selected = document.querySelector('input[name="lh-q-' + q.id + '"]:checked'); answers.push({ questionId: q.id, optionId: selected ? parseInt(selected.value) : null }); } }); showLoading('Submitting quiz...'); sendJson('/api/learning/submit-quiz', 'POST', { contentId: currentContent.id, answers: answers }) .then(function(data) { hideLoading(); if (!data.success) { showToast(data.error || 'Submit failed', 'error'); return; } showQuizResults(data); }) .catch(function() { hideLoading(); showToast('Submit failed', 'error'); }); } function showQuizResults(data) { var resultsEl = document.getElementById('lh-quiz-results'); var scoreEl = document.getElementById('lh-quiz-score'); var explEl = document.getElementById('lh-quiz-explanations'); if (!resultsEl) return; resultsEl.classList.remove('hidden'); var pct = data.percentage; var color = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--amber)' : 'var(--red)'; if (scoreEl) { scoreEl.textContent = data.score + '/' + data.total + ' (' + pct + '%)'; scoreEl.style.background = color; scoreEl.style.color = 'white'; } if (explEl) { explEl.innerHTML = renderQuizResultExplanations(data.results); } // Highlight correct/incorrect in quiz data.results.forEach(function(r) { var qDiv = document.querySelector('.lh-quiz-q[data-qid="' + r.questionId + '"]'); if (!qDiv) return; var isMulti = r.questionType === 'multi'; qDiv.querySelectorAll('.lh-quiz-option').forEach(function(label) { var input = label.querySelector('input'); if (!input) return; var optId = parseInt(input.value); if (isMulti) { var correctIds = r.correctOptionIds || []; var selectedIds = r.selectedOptionIds || []; if (correctIds.indexOf(optId) !== -1) label.classList.add('lh-opt-correct'); if (selectedIds.indexOf(optId) !== -1 && correctIds.indexOf(optId) === -1) label.classList.add('lh-opt-wrong'); } else { if (optId === r.correctOptionId) label.classList.add('lh-opt-correct'); if (optId === r.selectedOptionId && !r.isCorrect) label.classList.add('lh-opt-wrong'); } input.disabled = true; }); }); // Disable submit button var submitBtn = document.getElementById('lh-submit-quiz'); if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ' Submitted'; } resultsEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function setActiveCatPill(slug) { document.querySelectorAll('.lh-cat-pill').forEach(function(p) { p.classList.toggle('active', p.dataset.slug === slug); }); } // ============================================================ // ADMIN CMS // ============================================================ function wireCmsTypeSelect() { var typeEl = document.getElementById('lh-cms-edit-type'); if (typeEl) { typeEl.addEventListener('change', function() { toggleEditorMode(typeEl.value); }); } } function wireCmsFileInput() { var fileInput = document.getElementById('lh-ai-file'); var fileLabel = document.getElementById('lh-ai-file-label'); var filesList = document.getElementById('lh-ai-files-list'); var dropzone = document.getElementById('lh-ai-dropzone'); function updateFilesList() { if (!fileInput || !fileLabel || !filesList) return; var files = fileInput.files; if (files.length === 0) { fileLabel.textContent = 'Drop files here or click to browse'; filesList.style.display = 'none'; filesList.innerHTML = ''; } else if (files.length === 1) { fileLabel.textContent = files[0].name; filesList.style.display = 'none'; filesList.innerHTML = ''; } else { fileLabel.textContent = files.length + ' files selected'; filesList.style.display = 'block'; filesList.innerHTML = ''; } } if (fileInput) { fileInput.addEventListener('change', updateFilesList); } if (dropzone) { dropzone.addEventListener('dragover', function(e) { e.preventDefault(); dropzone.style.borderColor = 'var(--blue)'; }); dropzone.addEventListener('dragleave', function() { dropzone.style.borderColor = ''; }); dropzone.addEventListener('drop', function(e) { e.preventDefault(); dropzone.style.borderColor = ''; var files = e.dataTransfer.files; if (files.length > 0 && fileInput) { // Transfer to file input via DataTransfer try { var dt = new DataTransfer(); for (var i = 0; i < files.length && i < 10; i++) { dt.items.add(files[i]); } fileInput.files = dt.files; updateFilesList(); } catch(ex) {} } }); } } function loadCms() { loadCmsCategories(); loadCmsContent(); loadCmsStats(); } // ============================================================ // AI GENERATE PANEL // ============================================================ var _aiWebdavCurrentPath = '/'; var _aiWebdavSelectedPath = ''; var _aiWebdavSelectedName = ''; function openAiPanel() { var panel = document.getElementById('lh-ai-panel'); if (!panel) return; panel.classList.remove('hidden'); populateAiModelSelect(); // Sync content type selector with the editor's current type var editorType = (document.getElementById('lh-cms-edit-type') || {}).value || 'article'; var ctype = document.getElementById('lh-ai-ctype'); if (ctype) ctype.value = editorType; updateAiOptions(editorType); // Hide WebDAV tab if Nextcloud not configured getJson('/api/auth/me') .then(function(d) { var tab = document.getElementById('lh-ai-tab-webdav'); if (tab) tab.style.display = (d.user && d.user.nextcloud_url) ? '' : 'none'; if (d.user && d.user.webdav_learning_path) _aiWebdavCurrentPath = d.user.webdav_learning_path || '/'; }); // Wire ctype change once (use a flag so it doesn't stack on re-opens) var ctypeEl = document.getElementById('lh-ai-ctype'); var toggleEl = document.getElementById('lh-ai-q-toggle'); if (ctypeEl && !ctypeEl._wired) { ctypeEl.addEventListener('change', function() { updateAiOptions(ctypeEl.value); }); ctypeEl._wired = true; } if (toggleEl && !toggleEl._wired) { toggleEl.addEventListener('change', function() { updateAiOptions((document.getElementById('lh-ai-ctype') || {}).value); }); toggleEl._wired = true; } panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function updateAiOptions(type) { var wordsField = document.getElementById('lh-ai-words-field'); var slidesField = document.getElementById('lh-ai-slides-field'); var quizRow = document.getElementById('lh-ai-quiz-row'); var cardTitle = document.getElementById('lh-ai-quiz-card-title'); var toggleLabel = document.getElementById('lh-ai-q-toggle-label'); var toggleCb = document.getElementById('lh-ai-q-toggle'); var countWrap = document.getElementById('lh-ai-q-count-wrap'); // Use style.display directly — classList.toggle('hidden') doesn't work because // .lh-ai-opt-field { display:flex } is defined later in CSS and overrides .hidden if (wordsField) wordsField.style.display = (type === 'quiz' || type === 'presentation') ? 'none' : 'flex'; if (slidesField) slidesField.style.display = (type === 'presentation') ? 'flex' : 'none'; if (type === 'quiz') { // Quiz: questions are the whole point — show the card prominently, no toggle needed if (quizRow) quizRow.style.display = 'block'; if (cardTitle) cardTitle.textContent = 'Quiz Questions'; if (toggleLabel) toggleLabel.style.display = 'none'; if (toggleCb) { toggleCb.checked = true; toggleCb.disabled = true; } if (countWrap) countWrap.style.display = 'flex'; } else if (type === 'presentation') { // Presentation: optional questions (same as article) if (quizRow) quizRow.style.display = 'block'; if (cardTitle) cardTitle.textContent = 'Quiz Questions (optional)'; if (toggleLabel) toggleLabel.style.display = 'flex'; if (toggleCb) toggleCb.disabled = false; var checkedP = toggleCb ? toggleCb.checked : false; if (countWrap) countWrap.style.display = checkedP ? 'flex' : 'none'; } else { // Article / pearl: optional questions via toggle if (quizRow) quizRow.style.display = 'block'; if (cardTitle) cardTitle.textContent = 'Quiz Questions (optional)'; if (toggleLabel) toggleLabel.style.display = 'flex'; if (toggleCb) toggleCb.disabled = false; var checked = toggleCb ? toggleCb.checked : false; if (countWrap) countWrap.style.display = checked ? 'flex' : 'none'; } } function closeAiPanel() { var panel = document.getElementById('lh-ai-panel'); if (panel) panel.classList.add('hidden'); } function populateAiModelSelect() { var sel = document.getElementById('lh-ai-model'); if (!sel || sel.options.length > 1) return; if (typeof window._buildModelOptions === 'function') { window._buildModelOptions(sel); } } function switchAiTab(tabName) { document.querySelectorAll('.lh-ai-tab').forEach(function(t) { t.classList.toggle('active', t.dataset.aitab === tabName); }); document.querySelectorAll('.lh-ai-tabpanel').forEach(function(p) { p.classList.add('hidden'); }); var tp = document.getElementById('lh-ai-tp-' + tabName); if (tp) tp.classList.remove('hidden'); if (tabName === 'webdav' && !document.querySelector('#lh-ai-webdav-list .lh-webdav-item')) { browseWebdav(_aiWebdavCurrentPath); } } // ── WebDAV browser ────────────────────────────────────────── function browseWebdav(path) { _aiWebdavCurrentPath = path || '/'; var list = document.getElementById('lh-ai-webdav-list'); var pathLabel = document.getElementById('lh-ai-webdav-path-label'); if (list) list.innerHTML = '
Loading...
'; if (pathLabel) pathLabel.textContent = _aiWebdavCurrentPath; getJson('/api/admin/learning/webdav-browse?path=' + encodeURIComponent(_aiWebdavCurrentPath)) .then(function(data) { if (!data.success) { if (list) list.innerHTML = '
' + esc(data.error) + '
'; return; } if (pathLabel) pathLabel.textContent = data.path; renderWebdavList(data); }) .catch(function(err) { if (list) list.innerHTML = '
' + esc(err.message) + '
'; }); } function renderWebdavList(data) { var list = document.getElementById('lh-ai-webdav-list'); if (!list) return; var html = ''; // Parent nav if (data.path !== '/' && data.path !== data.parentPath) { html += '
' + ' ..
'; } if (data.items.length === 0 && html === '') { html = '
Empty folder
'; } data.items.forEach(function(item) { var icon = item.isDir ? 'fa-folder' : getFileIcon(item.contentType, item.name); var iconColor = item.isDir ? 'var(--amber)' : 'var(--blue)'; var sizeStr = (!item.isDir && item.size) ? ' (' + formatBytes(item.size) + ')' : ''; html += '
' + '' + '' + esc(item.name) + '' + sizeStr + '
'; }); list.innerHTML = html; } function getFileIcon(mime, name) { if (!name) name = ''; var ext = name.split('.').pop().toLowerCase(); if (mime && mime.includes('pdf') || ext === 'pdf') return 'fa-file-pdf'; if (['doc','docx'].includes(ext)) return 'fa-file-word'; if (['txt','md'].includes(ext)) return 'fa-file-lines'; if (['html','htm'].includes(ext)) return 'fa-file-code'; return 'fa-file'; } function formatBytes(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024*1024) return (bytes/1024).toFixed(0) + ' KB'; return (bytes/1024/1024).toFixed(1) + ' MB'; } function selectWebdavFile(path, name) { _aiWebdavSelectedPath = path; _aiWebdavSelectedName = name; var browser = document.getElementById('lh-ai-webdav-browser'); var selDiv = document.getElementById('lh-ai-webdav-selected'); var selName = document.getElementById('lh-ai-webdav-selected-name'); // Hide browser, show selected indicator (use style.display — class="hidden" is overridden by inline styles) if (browser) browser.style.display = 'none'; if (selDiv) selDiv.style.display = 'flex'; if (selName) selName.textContent = name; } function deselectWebdavFile() { _aiWebdavSelectedPath = ''; _aiWebdavSelectedName = ''; var browser = document.getElementById('lh-ai-webdav-browser'); var selDiv = document.getElementById('lh-ai-webdav-selected'); if (selDiv) selDiv.style.display = 'none'; if (browser) browser.style.display = ''; } // ── Generate ──────────────────────────────────────────────── function runAiGenerate() { var activeTab = document.querySelector('.lh-ai-tab.active'); var tabName = activeTab ? activeTab.dataset.aitab : 'topic'; var model = document.getElementById('lh-ai-model') ? document.getElementById('lh-ai-model').value : getSelectedModel(); var contentType = document.getElementById('lh-ai-ctype') ? document.getElementById('lh-ai-ctype').value : 'article'; var refinement = document.getElementById('lh-ai-refinement') ? document.getElementById('lh-ai-refinement').value.trim() : ''; var toggleCb = document.getElementById('lh-ai-q-toggle'); var generateQs = contentType === 'quiz' || (toggleCb && toggleCb.checked); var questionCount = generateQs ? (document.getElementById('lh-ai-q-count') ? document.getElementById('lh-ai-q-count').value : 5) : 0; var wordCount = document.getElementById('lh-ai-word-count') ? document.getElementById('lh-ai-word-count').value : ''; var slideCount = document.getElementById('lh-ai-slide-count') ? document.getElementById('lh-ai-slide-count').value : ''; var formData = new FormData(); if (model) formData.append('model', model); formData.append('questionCount', questionCount); formData.append('contentType', contentType); if (refinement) formData.append('refinement', refinement); if (wordCount) formData.append('wordCount', wordCount); if (slideCount) formData.append('slideCount', slideCount); if (tabName === 'topic') { var topic = document.getElementById('lh-ai-topic') ? document.getElementById('lh-ai-topic').value.trim() : ''; if (!topic) { showToast('Describe what you want AI to create', 'error'); return; } formData.append('topic', topic); } else if (tabName === 'upload') { var fileInput = document.getElementById('lh-ai-file'); if (!fileInput || fileInput.files.length === 0) { showToast('Select at least one file to upload', 'error'); return; } for (var i = 0; i < fileInput.files.length; i++) { formData.append('files', fileInput.files[i]); } var uploadCtx = document.getElementById('lh-ai-upload-context'); if (uploadCtx && uploadCtx.value.trim()) formData.append('topic', uploadCtx.value.trim()); } else if (tabName === 'webdav') { if (!_aiWebdavSelectedPath) { showToast('Select a file from Nextcloud', 'error'); return; } formData.append('webdavPath', _aiWebdavSelectedPath); var webdavCtx = document.getElementById('lh-ai-webdav-context'); if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim()); } var btn = document.getElementById('btn-lh-ai-generate'); if (btn) { btn.disabled = true; btn.innerHTML = ' Generating...'; } showBusy('AI is generating content...'); // FormData: no Content-Type header (browser sets it with boundary) var token = window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || ''; fetch('/api/admin/learning/ai-generate', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }) .then(function(r) { return r.json(); }) .then(function(data) { hideBusy(); if (btn) { btn.disabled = false; btn.innerHTML = ' Generate Content'; } if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; } // Presentation returns marpMarkdown directly; others return content{} var payload = data.contentType === 'presentation' ? data : data.content; return applyAiContent(payload, contentType).then(function() { closeAiPanel(); showToast('Content generated! Review and save.', 'success'); }); }) .catch(function(err) { hideBusy(); if (btn) { btn.disabled = false; btn.innerHTML = ' Generate Content'; } showToast(err.message, 'error'); }); } function applyAiContent(content, contentType) { // Set content type first so toggleEditorMode works correctly var typeEl = document.getElementById('lh-cms-edit-type'); if (typeEl && contentType) { typeEl.value = contentType; toggleEditorMode(contentType); } // Presentation: fill Marp textarea if (contentType === 'presentation' && content.marpMarkdown) { var marpTa = document.getElementById('lh-marp-editor'); if (marpTa) marpTa.value = content.marpMarkdown; // Extract title from first # heading var titleMatch = content.marpMarkdown.match(/^#\s+(.+)$/m); var titleEl = document.getElementById('lh-cms-edit-title'); if (titleEl && titleMatch) titleEl.value = titleMatch[1]; // Fill questions if any were generated alongside slides var qContainerP = document.getElementById('lh-cms-questions'); if (qContainerP) { clearQuestionBlocks(qContainerP); if (content.questions && Array.isArray(content.questions)) { content.questions.forEach(function(q) { addQuestionBlock(q); }); } } return applyAiCategory(content.category_name || content.category); } // Fill title var titleEl2 = document.getElementById('lh-cms-edit-title'); if (titleEl2 && content.title) titleEl2.value = content.title; // Fill subject var subjectEl = document.getElementById('lh-cms-edit-subject'); if (subjectEl && content.subject) subjectEl.value = content.subject; // Fill body via Tiptap if (_bodyEditor && content.body) _bodyEditor.commands.setContent(content.body); // Replace questions var qContainer = document.getElementById('lh-cms-questions'); if (qContainer) { clearQuestionBlocks(qContainer); if (content.questions && Array.isArray(content.questions)) { content.questions.forEach(function(q) { addQuestionBlock(q); }); } } return applyAiCategory(content.category_name || content.category); } function applyAiCategory(categoryName) { categoryName = (categoryName || '').trim(); if (!categoryName) return Promise.resolve(); var sel = document.getElementById('lh-cms-edit-category'); if (!sel) return Promise.resolve(); var existing = findCategoryOption(sel, categoryName); if (existing) { sel.value = existing.value; return Promise.resolve(); } return sendJson('/api/admin/learning/categories', 'POST', { name: categoryName }) .then(function(data) { if (!data.success || !data.id) throw new Error(data.error || 'Category creation failed'); var opt = document.createElement('option'); opt.value = data.id; opt.textContent = categoryName; sel.appendChild(opt); sel.value = String(data.id); loadCmsCategories(String(data.id)); loadCategories(); }) .catch(function(err) { showToast('Generated category was not created: ' + err.message, 'error'); }); } function findCategoryOption(sel, categoryName) { var wanted = categoryName.toLowerCase(); for (var i = 0; i < sel.options.length; i++) { if ((sel.options[i].textContent || '').trim().toLowerCase() === wanted) return sel.options[i]; } return null; } function toggleRefineBar(forceShow) { var bar = document.getElementById('lh-ai-refine-bar'); var input = document.getElementById('lh-ai-refine-input'); if (!bar) return; var show = forceShow !== undefined ? forceShow : bar.style.display === 'none'; bar.style.display = show ? 'flex' : 'none'; if (show && input) { input.value = ''; input.focus(); } } function submitRefineBody() { var input = document.getElementById('lh-ai-refine-input'); var instructions = input ? input.value.trim() : ''; if (!instructions) { showToast('Enter instructions for refinement', 'error'); return; } var currentBody = _bodyEditor ? getTpHTML(_bodyEditor) : ''; if (!currentBody) { showToast('No body content to refine', 'error'); return; } var model = document.getElementById('lh-ai-model') ? document.getElementById('lh-ai-model').value : getSelectedModel(); toggleRefineBar(false); showBusy('Refining content...'); sendJson('/api/admin/learning/ai-refine', 'POST', { content: currentBody, instructions: instructions, model: model }) .then(function(data) { hideBusy(); if (!data.success) { showToast(data.error || 'Refine failed', 'error'); return; } if (_bodyEditor) _bodyEditor.commands.setContent(data.refined); showToast('Body refined!', 'success'); }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); } function loadCmsStats() { getJson('/api/admin/learning/stats') .then(function(data) { if (!data.success) return; var s = data.stats; var set = function(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; }; set('cms-stat-published', s.publishedContent); set('cms-stat-drafts', s.totalContent - s.publishedContent); set('cms-stat-categories', s.totalCategories); set('cms-stat-quizzes', s.totalQuizzes); set('cms-stat-attempts', s.totalAttempts); }); } function loadCmsCategories(editorCategoryId, filterCategoryId) { getJson('/api/admin/learning/categories') .then(function(data) { if (!data.success) return; var container = document.getElementById('lh-cms-categories'); if (!container) return; container.innerHTML = renderCmsCategoryList(data.categories); // Update category dropdowns (editor + filter) var sel = document.getElementById('lh-cms-edit-category'); if (sel) { var selectedEditorId = editorCategoryId !== undefined ? editorCategoryId : sel.value; sel.innerHTML = ''; data.categories.forEach(function(c) { sel.innerHTML += ''; }); if (selectedEditorId && hasSelectValue(sel, selectedEditorId)) sel.value = selectedEditorId; } var filterSel = document.getElementById('cms-filter-category'); if (filterSel) { var selectedFilterId = filterCategoryId !== undefined ? filterCategoryId : filterSel.value; filterSel.innerHTML = ''; data.categories.forEach(function(c) { filterSel.innerHTML += ''; }); if (selectedFilterId && hasSelectValue(filterSel, selectedFilterId)) filterSel.value = selectedFilterId; } setActiveCmsCat((document.getElementById('cms-filter-category') || {}).value || 'all'); }); } function hasSelectValue(sel, value) { value = String(value); for (var i = 0; i < sel.options.length; i++) { if (sel.options[i].value === value) return true; } return false; } function addCategory() { var input = document.getElementById('lh-cms-cat-name'); if (!input || !input.value.trim()) { showToast('Enter category name', 'error'); return; } sendJson('/api/admin/learning/categories', 'POST', { name: input.value.trim() }) .then(function(data) { if (data.success) { input.value = ''; loadCmsCategories(); loadCategories(); // refresh user-facing pills too showToast('Category created', 'success'); } else showToast(data.error || 'Failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } function deleteCategory(id) { if (!window.__confirmDeleteCat) { window.__confirmDeleteCat = id; showToast('Click delete again to confirm removing this category', 'info'); setTimeout(function() { window.__confirmDeleteCat = null; }, 4000); return; } window.__confirmDeleteCat = null; deleteJson('/api/admin/learning/categories/' + id) .then(function(data) { if (data.success) { loadCmsCategories(); loadCategories(); showToast('Category deleted', 'info'); } else showToast(data.error || 'Failed', 'error'); }); } function loadCmsContent() { var container = document.getElementById('lh-cms-content-list'); if (!container) return; container.innerHTML = '

Loading...

'; getJson('/api/admin/learning/content') .then(function(data) { if (!data.success) return; if (data.content.length === 0) { container.innerHTML = '

No content yet. Click "New Content" to create.

'; return; } window._cmsContentData = data.content; renderCmsContentList(data.content); }); } function renderCmsContentList(items) { var container = document.getElementById('lh-cms-content-list'); if (!container) return; if (!items || items.length === 0) { container.innerHTML = renderCmsContentEmpty(); return; } container.innerHTML = items.map(renderCmsContentRow).join(''); } // CMS search & filter document.addEventListener('input', function(e) { if (e.target.id === 'cms-search') filterCmsContent(); }); document.addEventListener('change', function(e) { if (e.target.id === 'cms-filter-status' || e.target.id === 'cms-filter-category') { if (e.target.id === 'cms-filter-category') setActiveCmsCat(e.target.value || 'all'); filterCmsContent(); } }); function filterCmsContentByCategory(categoryId) { var filterSel = document.getElementById('cms-filter-category'); if (filterSel) filterSel.value = String(categoryId); setActiveCmsCat(categoryId); filterCmsContent(); } function setActiveCmsCat(categoryId) { categoryId = String(categoryId || 'all'); document.querySelectorAll('.lh-cms-cat-filter').forEach(function(item) { item.classList.toggle('active', categoryId !== 'all' && item.dataset.id === categoryId); }); } function filterCmsContent() { var items = window._cmsContentData || []; var search = (document.getElementById('cms-search') || {}).value || ''; var statusFilter = (document.getElementById('cms-filter-status') || {}).value || 'all'; var catFilter = (document.getElementById('cms-filter-category') || {}).value || 'all'; search = search.toLowerCase().trim(); var filtered = items.filter(function(item) { if (search && item.title.toLowerCase().indexOf(search) === -1 && (item.subject || '').toLowerCase().indexOf(search) === -1) return false; if (statusFilter === 'published' && !item.published) return false; if (statusFilter === 'draft' && item.published) return false; if (catFilter !== 'all' && String(item.category_id) !== catFilter) return false; return true; }); renderCmsContentList(filtered); } // ── Body editor ──────────────────────────────────────────── function initBodyEditor() { var wrap = document.getElementById('lh-body-editor'); if (!wrap || _bodyEditor) return; _bodyEditor = makeTpEditor(wrap, '', false, false); } // ── Rich text toggle for questions / options ─────────────── function toggleRichText(btn) { var isActive = btn.classList.contains('active'); var target = btn.dataset.target; var block = btn.closest(target === 'question' ? '.lh-question-block' : '.lh-option-row'); if (!block) return; if (target === 'question') { var ta = block.querySelector('.lh-q-text'); var wrap = block.querySelector('.lh-q-richtext-wrap'); if (!isActive) { var val = ta ? ta.value : ''; btn.classList.add('active'); btn.textContent = 'Plain Text'; if (ta) ta.style.display = 'none'; if (!wrap) { wrap = document.createElement('div'); wrap.className = 'lh-q-richtext-wrap'; ta.parentNode.insertBefore(wrap, ta.nextSibling); } wrap.style.display = ''; if (!block._questionEditor) block._questionEditor = makeTpEditor(wrap, val, true, false); } else { btn.classList.remove('active'); btn.textContent = 'Rich Text'; if (block._questionEditor) { if (ta) { ta.value = getTpHTML(block._questionEditor); ta.style.display = ''; } } if (wrap) wrap.style.display = 'none'; } } else { var row = block; var optInput = row.querySelector('.lh-opt-text'); var optWrap = row.querySelector('.lh-opt-richtext-wrap'); if (!isActive) { var curVal = optInput ? optInput.value : ''; btn.classList.add('active'); btn.textContent = 'Plain'; if (optInput) optInput.style.display = 'none'; if (!optWrap) { optWrap = document.createElement('div'); optWrap.className = 'lh-opt-richtext-wrap'; optInput.parentNode.insertBefore(optWrap, optInput.nextSibling); } optWrap.style.display = ''; if (!row._optEditor) row._optEditor = makeTpEditor(optWrap, curVal, true, true); } else { btn.classList.remove('active'); btn.textContent = 'Rich'; if (row._optEditor) { if (optInput) { optInput.value = getTpHTML(row._optEditor); optInput.style.display = ''; } } if (optWrap) optWrap.style.display = 'none'; } } } function getQText(block) { var wrap = block.querySelector('.lh-q-richtext-wrap'); if (block._questionEditor && wrap && wrap.style.display !== 'none') return getTpHTML(block._questionEditor); var ta = block.querySelector('.lh-q-text'); return ta ? ta.value.trim() : ''; } function getOptText(row) { var wrap = row.querySelector('.lh-opt-richtext-wrap'); if (row._optEditor && wrap && wrap.style.display !== 'none') return getTpHTML(row._optEditor); var inp = row.querySelector('.lh-opt-text'); return inp ? inp.value.trim() : ''; } function openEditor(contentId, contentType) { var editor = document.getElementById('lh-cms-editor'); var listView = document.getElementById('cms-list-view'); if (!editor) return; editor.classList.remove('hidden'); if (listView) listView.classList.add('hidden'); // Reset document.getElementById('lh-cms-edit-id').value = ''; document.getElementById('lh-cms-edit-title').value = ''; document.getElementById('lh-cms-edit-subject').value = ''; document.getElementById('lh-cms-edit-category').value = ''; document.getElementById('lh-cms-edit-type').value = contentType || 'article'; document.getElementById('lh-cms-edit-published').value = 'false'; clearQuestionBlocks(document.getElementById('lh-cms-questions')); document.getElementById('btn-lh-delete-content').classList.add('hidden'); hideDeleteConfirm(); toggleEditorMode(contentType || 'article'); initBodyEditor(); if (_bodyEditor) _bodyEditor.commands.clearContent(true); var marpTa = document.getElementById('lh-marp-editor'); if (marpTa) marpTa.value = ''; // For quiz type, add a starter question if (contentType === 'quiz' && !contentId) { addQuestionBlock(); } if (!contentId) return; // Load existing content getJson('/api/admin/learning/content/' + contentId) .then(function(data) { if (!data.success) { showToast('Failed to load', 'error'); return; } var c = data.content; document.getElementById('lh-cms-edit-id').value = c.id; document.getElementById('lh-cms-edit-title').value = c.title; document.getElementById('lh-cms-edit-subject').value = c.subject || ''; document.getElementById('lh-cms-edit-category').value = c.category_id || ''; document.getElementById('lh-cms-edit-type').value = c.content_type || 'article'; document.getElementById('lh-cms-edit-published').value = c.published ? 'true' : 'false'; document.getElementById('btn-lh-delete-content').classList.remove('hidden'); toggleEditorMode(c.content_type || 'article'); if (c.content_type === 'presentation') { var marpEl = document.getElementById('lh-marp-editor'); if (marpEl) marpEl.value = c.body || ''; } else { if (_bodyEditor) _bodyEditor.commands.setContent(c.body || ''); } // Load questions var qContainer = document.getElementById('lh-cms-questions'); clearQuestionBlocks(qContainer); if (c.questions && c.questions.length > 0) { c.questions.forEach(function(q) { addQuestionBlock(q); }); } editor.scrollIntoView({ behavior: 'smooth' }); }); } function toggleEditorMode(contentType) { var bodySection = document.getElementById('lh-body-section'); var marpSection = document.getElementById('lh-marp-section'); var isPresentation = contentType === 'presentation'; if (bodySection) bodySection.classList.toggle('hidden', isPresentation); if (marpSection) marpSection.classList.toggle('hidden', !isPresentation); } function closeEditor() { var editor = document.getElementById('lh-cms-editor'); var listView = document.getElementById('cms-list-view'); if (editor) editor.classList.add('hidden'); if (listView) listView.classList.remove('hidden'); hideDeleteConfirm(); } function saveContent() { var id = document.getElementById('lh-cms-edit-id').value; var title = document.getElementById('lh-cms-edit-title').value.trim(); var subject = document.getElementById('lh-cms-edit-subject').value.trim(); var content_type = document.getElementById('lh-cms-edit-type').value; var body = content_type === 'presentation' ? (document.getElementById('lh-marp-editor') ? document.getElementById('lh-marp-editor').value : '') : (_bodyEditor ? getTpHTML(_bodyEditor) : ''); var category_id = document.getElementById('lh-cms-edit-category').value || null; var published = document.getElementById('lh-cms-edit-published').value === 'true'; if (!title) { showToast('Title required', 'error'); return; } // Gather questions with validation var questions = []; var validationError = false; var qBlocks = document.querySelectorAll('#lh-cms-questions .lh-question-block'); for (var qi = 0; qi < qBlocks.length; qi++) { var qBlock = qBlocks[qi]; var qText = getQText(qBlock); if (!qText) continue; var qType = qBlock.querySelector('.lh-q-type').value; var qExpl = qBlock.querySelector('.lh-q-explanation').value.trim(); var qId = qBlock.dataset.questionId || null; var options = []; qBlock.querySelectorAll('.lh-option-row').forEach(function(row) { var optText = getOptText(row); if (!optText) return; options.push({ option_text: optText, is_correct: row.querySelector('.lh-opt-correct').checked, explanation: row.querySelector('.lh-opt-expl').value.trim() }); }); // Validate: must have a correct answer marked var hasCorrect = options.some(function(o) { return o.is_correct; }); if (options.length > 0 && !hasCorrect) { showToast('Question "' + qText.substring(0, 50) + '..." needs a correct answer marked', 'error'); qBlock.style.outline = '2px solid var(--red)'; qBlock.scrollIntoView({ behavior: 'smooth', block: 'center' }); validationError = true; break; } qBlock.style.outline = ''; questions.push({ id: qId, question_text: qText, question_type: qType, explanation: qExpl, options: options }); } if (validationError) return; var payload = { title: title, body: body, category_id: category_id, subject: subject, content_type: content_type, published: published }; var method = id ? 'PUT' : 'POST'; var url = id ? '/api/admin/learning/content/' + id : '/api/admin/learning/content'; showLoading('Saving...'); sendJson(url, method, payload) .then(function(data) { if (!data.success) { hideLoading(); showToast(data.error || 'Save failed', 'error'); return; } var contentId = id || data.id; // Save questions sequentially saveQuestions(contentId, questions, 0, function() { hideLoading(); showToast('Content saved', 'success'); closeEditor(); loadCmsContent(); loadCmsCategories(); loadCmsStats(); loaded = false; // force refresh user feed }); }) .catch(function() { hideLoading(); showToast('Save failed', 'error'); }); } function saveQuestions(contentId, questions, index, callback) { if (index >= questions.length) { callback(); return; } var q = questions[index]; var qId = q.id; var method, url; if (qId) { method = 'PUT'; url = '/api/admin/learning/questions/' + qId; } else { method = 'POST'; url = '/api/admin/learning/content/' + contentId + '/questions'; } sendJson(url, method, { question_text: q.question_text, question_type: q.question_type, explanation: q.explanation, options: q.options }) .then(function() { saveQuestions(contentId, questions, index + 1, callback); }) .catch(function() { saveQuestions(contentId, questions, index + 1, callback); }); } function showDeleteConfirm() { var id = document.getElementById('lh-cms-edit-id').value; if (!id) return; var title = (document.getElementById('lh-cms-edit-title').value || 'this content').substring(0, 60); var ctype = (document.getElementById('lh-cms-edit-type') || {}).value || 'article'; var bar = document.getElementById('lh-delete-confirm-bar'); var msgEl = document.getElementById('lh-delete-confirm-msg'); if (!bar) return; // Build wording based on content type var typeLabel = { article: 'article', quiz: 'quiz', pearl: 'clinical pearl', presentation: 'presentation' }[ctype] || 'content'; var consequence = ''; if (ctype === 'quiz') consequence = ' All quiz questions will be permanently removed.'; else if (ctype === 'article' || ctype === 'pearl') consequence = ' Any quiz questions attached to it will also be removed.'; // Presentation has no questions → no extra wording if (msgEl) msgEl.innerHTML = 'Delete the ' + typeLabel + ' "' + esc(title) + '"?' + consequence + ' This cannot be undone.'; bar.classList.remove('hidden'); bar.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function hideDeleteConfirm() { var bar = document.getElementById('lh-delete-confirm-bar'); if (bar) bar.classList.add('hidden'); } function confirmDelete() { var id = document.getElementById('lh-cms-edit-id').value; if (!id) return; hideDeleteConfirm(); deleteJson('/api/admin/learning/content/' + id) .then(function(data) { if (data.success) { showToast('Content deleted', 'info'); closeEditor(); loadCmsContent(); loadCmsCategories(); loadCmsStats(); loaded = false; } else showToast(data.error || 'Failed', 'error'); }); } // ── Presentation / Marp ────────────────────────────────────── function getMarpMarkdown() { var ta = document.getElementById('lh-marp-editor'); return ta ? ta.value.trim() : ''; } function getPresentationTitle() { var titleEl = document.getElementById('lh-cms-edit-title'); return titleEl ? titleEl.value : 'presentation'; } function addQuestionBlock(existingQ) { var container = document.getElementById('lh-cms-questions'); if (!container) return; var block = document.createElement('div'); block.className = 'lh-question-block'; if (existingQ && existingQ.id) block.dataset.questionId = existingQ.id; var qNum = container.querySelectorAll('.lh-question-block').length + 1; block.innerHTML = renderQuestionBlockShell(existingQ, qNum); container.appendChild(block); // Set textarea values after insertion (can't use value="" attr on textarea) block.querySelector('.lh-q-text').value = existingQ ? existingQ.question_text || '' : ''; block.querySelector('.lh-q-explanation').value = existingQ ? existingQ.explanation || '' : ''; var optList = block.querySelector('.lh-options-list'); if (existingQ && existingQ.options && existingQ.options.length > 0) { existingQ.options.forEach(function(opt) { addOptionRow(optList, opt); }); } else if (existingQ && existingQ.question_type === 'true_false') { addOptionRow(optList, { option_text: 'True', is_correct: false, explanation: '' }); addOptionRow(optList, { option_text: 'False', is_correct: false, explanation: '' }); } else { // Add 4 blank MCQ options for (var i = 0; i < 4; i++) addOptionRow(optList, null); } } function addOptionRow(container, opt) { if (!container) return; var row = document.createElement('div'); row.className = 'lh-option-row'; row.innerHTML = renderOptionRowShell(opt); container.appendChild(row); // Set option text value after insertion (can't use value="" on nested input via innerHTML) row.querySelector('.lh-opt-text').value = opt ? opt.option_text || '' : ''; }