991 lines
41 KiB
JavaScript
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 || '' : '';
|
|
}
|