pediatric-ai-scribe-v3/public/js/learningHub.js
2026-05-08 16:54:48 +02:00

991 lines
41 KiB
JavaScript

// ============================================================
// 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 _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-add-cat')) { cms.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')) { 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; }
});
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();
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());
}
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 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 = '<i class="fas fa-wand-magic-sparkles"></i> 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() {
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) _bodyEditor.commands.setContent(data.refined);
showToast('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);
}
// ── 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 || '' : '';
}