Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m20s
Forgejo Docker Build / Build Docker image (push) Successful in 19s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s
The route accepted useCorpus and reported grounding, but nothing in the admin screen sent the flag or showed the result — so the feature existed and was unreachable. Opt-out in the UI rather than opt-in. For clinical teaching the library is nearly always the right source, so someone who never notices the checkbox should get the grounded version. The help text explains when to turn it OFF, which is the non-obvious case: a topic the library does not cover is better written without grounding than padded with the nearest unrelated excerpts. Afterwards it says what happened — "Written from 12 library excerpts", or "Not grounded — nothing indexed matched. Written from the model alone." Ungrounded material presented as grounded is the failure worth preventing here, so the wording never implies the library was used when it was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
1022 lines
43 KiB
JavaScript
1022 lines
43 KiB
JavaScript
import { createLearningImages } from './learningHub/images.js';
|
|
// ============================================================
|
|
// 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 { createAiPanelController } from './learningHub/aiPanelController.js';
|
|
import {
|
|
renderOptionRowShell,
|
|
renderQuestionBlockShell
|
|
} from './learningHub/cmsRenderer.js';
|
|
import { createCmsController } from './learningHub/cmsController.js';
|
|
import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearchHeader } from './learningHub/feedRenderer.js';
|
|
import { createQuizController } from './learningHub/quizController.js';
|
|
import { createSlideController } from './learningHub/slideController.js';
|
|
import { buildViewerMeta, renderPresentationCard, renderProgressList } from './learningHub/viewerRenderer.js';
|
|
import { createWebdavController } from './learningHub/webdavController.js';
|
|
|
|
var loaded = false;
|
|
var cmsLoaded = false;
|
|
var currentContent = null;
|
|
var currentView = 'feed'; // 'feed' | 'category' | 'viewer'
|
|
var learningImages = createLearningImages(function() { return _bodyEditor; }, function() { return (document.getElementById('lh-cms-edit-type') || {}).value === 'presentation' ? document.getElementById('lh-marp-editor') : null; });
|
|
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); }
|
|
});
|
|
var cms = createCmsController({
|
|
refreshUserCategories: function() { loadCategories(); },
|
|
showToast: function(message, type) { showToast(message, type); }
|
|
});
|
|
var quiz = createQuizController({
|
|
getCurrentContent: function() { return currentContent; },
|
|
showLoading: function(message) { showLoading(message); },
|
|
hideLoading: function() { hideLoading(); },
|
|
showToast: function(message, type) { showToast(message, type); }
|
|
});
|
|
var webdav = createWebdavController({});
|
|
var aiPanel = createAiPanelController({
|
|
browseWebdav: function(path) { webdav.browse(path); },
|
|
getEditorType: function() { return (document.getElementById('lh-cms-edit-type') || {}).value; },
|
|
getWebdavPath: function() { return webdav.getCurrentPath(); },
|
|
setWebdavPath: function(path) { webdav.setCurrentPath(path); }
|
|
});
|
|
|
|
// ── 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')) {
|
|
quiz.submitQuiz();
|
|
return;
|
|
}
|
|
|
|
// ── CMS events ─────────────────────────────
|
|
if (e.target.closest('#btn-lh-refresh-content')) { cms.loadContent(); cms.loadStats(); 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')) { aiPanel.open(); return; }
|
|
if (e.target.closest('#btn-lh-ai-close')) { aiPanel.close(); 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')) { webdav.browse(webdav.getCurrentPath()); return; }
|
|
if (e.target.closest('#btn-lh-webdav-deselect')) { webdav.deselectFile(); return; }
|
|
|
|
// AI tabs
|
|
var aiTab = e.target.closest('.lh-ai-tab');
|
|
if (aiTab && aiTab.dataset.aitab) { aiPanel.switchTab(aiTab.dataset.aitab); return; }
|
|
|
|
// WebDAV list items
|
|
var wdItem = e.target.closest('.lh-webdav-item');
|
|
if (wdItem) {
|
|
webdav.openItem(wdItem);
|
|
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) { cms.deleteCategory(delCat.dataset.id); return; }
|
|
|
|
// CMS sidebar category filter
|
|
var cmsCat = e.target.closest('.lh-cms-cat-filter');
|
|
if (cmsCat && cmsCat.dataset.id) { cms.filterByCategory(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; }
|
|
});
|
|
|
|
// Native form submission handles both the visible Add button and Enter.
|
|
document.addEventListener('submit', function(e) {
|
|
if (e.target.id !== 'lh-cms-add-category') return;
|
|
e.preventDefault();
|
|
cms.addCategory();
|
|
});
|
|
|
|
document.addEventListener('input', function(e) { cms.handleFilterInput(e); });
|
|
document.addEventListener('change', function(e) { cms.handleFilterChange(e); });
|
|
|
|
// 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 = '<p style="text-align:center;color:var(--g400);padding:24px;">Loading...</p>';
|
|
showFeed();
|
|
|
|
getJson('/api/learning/feed')
|
|
.then(function(data) {
|
|
if (!data.success) { feedEl.innerHTML = '<p style="text-align:center;color:var(--red);padding:24px;">Failed to load</p>'; return; }
|
|
renderFeed(data.content, feedEl);
|
|
})
|
|
.catch(function() { feedEl.innerHTML = '<p style="text-align:center;color:var(--red);padding:24px;">Connection error</p>'; });
|
|
}
|
|
|
|
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 = '<p style="text-align:center;color:var(--g400);padding:24px;">Loading...</p>';
|
|
showFeed();
|
|
|
|
getJson('/api/learning/category/' + encodeURIComponent(slug))
|
|
.then(function(data) {
|
|
if (!data.success) { feedEl.innerHTML = '<p style="text-align:center;color:var(--red);padding:24px;">Category not found</p>'; return; }
|
|
renderFeed(data.content, feedEl);
|
|
});
|
|
}
|
|
|
|
function searchContent(q) {
|
|
var feedEl = document.getElementById('lh-feed');
|
|
if (!feedEl) return;
|
|
showFeed();
|
|
feedEl.innerHTML =
|
|
'<div style="padding:8px 0 12px;font-size:13px;color:var(--g500);">' +
|
|
'<i class="fas fa-search"></i> Searching for <strong>"' + esc(q) + '"</strong>…' +
|
|
'</div>';
|
|
|
|
getJson('/api/learning/search?q=' + encodeURIComponent(q))
|
|
.then(function(data) {
|
|
if (!data.success) { feedEl.innerHTML = '<p style="color:var(--red);padding:12px;">Search failed</p>'; 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 = '<p style="color:var(--red);padding:12px;">Search error</p>'; });
|
|
}
|
|
|
|
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');
|
|
quiz.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');
|
|
quiz.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 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 = '<ul style="list-style:none;padding:0;margin:0;font-size:0.9em;">' +
|
|
Array.from(files).map(function(f) {
|
|
return '<li style="padding:2px 0;"><i class="fas fa-file-pdf" style="color:var(--blue);margin-right:4px;"></i>' + f.name + '</li>';
|
|
}).join('') + '</ul>';
|
|
}
|
|
}
|
|
|
|
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() {
|
|
cms.refreshAll();
|
|
}
|
|
|
|
// ============================================================
|
|
// AI GENERATE PANEL
|
|
// ============================================================
|
|
|
|
// ── 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();
|
|
formData.append('idempotencyKey', crypto.randomUUID());
|
|
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') {
|
|
var webdavPath = webdav.getSelectedPath();
|
|
if (!webdavPath) { showToast('Select a file from Nextcloud', 'error'); return; }
|
|
formData.append('webdavPath', webdavPath);
|
|
var webdavCtx = document.getElementById('lh-ai-webdav-context');
|
|
if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim());
|
|
}
|
|
|
|
// Grounding is opt-out rather than opt-in in the UI: for clinical teaching
|
|
// the library is nearly always the right source, and someone who forgets
|
|
// the checkbox should get the grounded version.
|
|
var corpusBox = document.getElementById('lh-ai-use-corpus');
|
|
formData.append('useCorpus', corpusBox && corpusBox.checked === false ? 'false' : 'true');
|
|
var groundingLine = document.getElementById('lh-ai-grounding-result');
|
|
if (groundingLine) groundingLine.textContent = '';
|
|
|
|
var btn = document.getElementById('btn-lh-ai-generate');
|
|
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...'; }
|
|
showBusy('AI is generating content...');
|
|
|
|
// FormData: no Content-Type header (browser sets it with boundary)
|
|
var headers = getAuthHeaders();
|
|
delete headers['Content-Type'];
|
|
fetch('/api/admin/learning/ai-generate', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: formData
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideBusy();
|
|
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> Generate Content'; }
|
|
if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; }
|
|
// Say what it was written from. Ungrounded material presented as grounded
|
|
// is the failure worth preventing, so this never implies the library was
|
|
// used when it was not.
|
|
if (groundingLine) {
|
|
var g = data.grounding || {};
|
|
groundingLine.textContent = g.used
|
|
? 'Written from ' + g.count + ' library excerpt' + (g.count === 1 ? '' : 's') + '.'
|
|
: 'Not grounded' + (g.reason ? ' \u2014 ' + g.reason : '') + '. Written from the model alone.';
|
|
groundingLine.style.color = g.used ? 'var(--green)' : 'var(--g600)';
|
|
}
|
|
// Presentation returns marpMarkdown directly; others return content{}
|
|
var payload = data.contentType === 'presentation' ? data : data.content;
|
|
return applyAiContent(payload, contentType).then(function() {
|
|
learningImages.show(data.imageJobs || []);
|
|
aiPanel.close();
|
|
showToast('Content generated! Review and save.', 'success');
|
|
});
|
|
})
|
|
.catch(function(err) {
|
|
hideBusy();
|
|
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> 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);
|
|
cms.loadCategories(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 && !data.bodyPreserved) _bodyEditor.commands.setContent(data.refined);
|
|
learningImages.show(data.imageJobs || []);
|
|
showToast(data.bodyPreserved ? 'Body unchanged; image queued for insertion.' : 'Body refined!', 'success');
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
}
|
|
|
|
// ── Body editor ────────────────────────────────────────────
|
|
function initBodyEditor() {
|
|
var wrap = document.getElementById('lh-body-editor');
|
|
if (!wrap || _bodyEditor) return;
|
|
_bodyEditor = makeTpEditor(wrap, '', false, false);
|
|
learningImages.mount();
|
|
}
|
|
|
|
// ── 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();
|
|
cms.refreshAll();
|
|
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 + ' <strong>"' + esc(title) + '"</strong>?' + 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();
|
|
cms.refreshAll();
|
|
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 || '' : '';
|
|
}
|