';
}).join('');
}
function loadContent(slug) {
fetch('/api/learning/content/' + encodeURIComponent(slug), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.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) {
var parts = [];
if (item.category_name) parts.push(item.category_name);
if (item.subject) parts.push(item.subject);
if (item.author_name) parts.push('by ' + item.author_name);
if (item.created_at) parts.push(new Date(item.created_at).toLocaleDateString());
metaEl.textContent = parts.join(' | ');
}
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) {
var slideCount = (item.body || '').split(/\n---\n/).length;
presViewer.className = ''; // remove hidden
presViewer.innerHTML =
'
';
}).join('');
}
// Highlight correct/incorrect in quiz
data.results.forEach(function(r) {
var qDiv = document.querySelector('.lh-quiz-q[data-qid="' + r.questionId + '"]');
if (!qDiv) return;
var isMulti = r.questionType === 'multi';
qDiv.querySelectorAll('.lh-quiz-option').forEach(function(label) {
var input = label.querySelector('input');
if (!input) return;
var optId = parseInt(input.value);
if (isMulti) {
var correctIds = r.correctOptionIds || [];
var selectedIds = r.selectedOptionIds || [];
if (correctIds.indexOf(optId) !== -1) label.classList.add('lh-opt-correct');
if (selectedIds.indexOf(optId) !== -1 && correctIds.indexOf(optId) === -1) label.classList.add('lh-opt-wrong');
} else {
if (optId === r.correctOptionId) label.classList.add('lh-opt-correct');
if (optId === r.selectedOptionId && !r.isCorrect) label.classList.add('lh-opt-wrong');
}
input.disabled = true;
});
});
// Disable submit button
var submitBtn = document.getElementById('lh-submit-quiz');
if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ' Submitted'; }
resultsEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function setActiveCatPill(slug) {
document.querySelectorAll('.lh-cat-pill').forEach(function(p) {
p.classList.toggle('active', p.dataset.slug === slug);
});
}
// ============================================================
// ADMIN CMS
// ============================================================
function wireCmsTypeSelect() {
var typeEl = document.getElementById('lh-cms-edit-type');
if (typeEl) {
typeEl.addEventListener('change', function() { toggleEditorMode(typeEl.value); });
}
}
function wireCmsFileInput() {
var fileInput = document.getElementById('lh-ai-file');
var fileLabel = document.getElementById('lh-ai-file-label');
var dropzone = document.getElementById('lh-ai-dropzone');
if (fileInput) {
fileInput.addEventListener('change', function() {
if (fileInput.files[0] && fileLabel) fileLabel.textContent = fileInput.files[0].name;
});
}
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[0] && fileInput && fileLabel) {
// Transfer to file input via DataTransfer
try {
var dt = new DataTransfer();
dt.items.add(files[0]);
fileInput.files = dt.files;
} catch(ex) {}
fileLabel.textContent = files[0].name;
}
});
}
}
function loadCms() {
loadCmsCategories();
loadCmsContent();
loadCmsStats();
}
// ============================================================
// AI GENERATE PANEL
// ============================================================
var _aiWebdavCurrentPath = '/';
var _aiWebdavSelectedPath = '';
var _aiWebdavSelectedName = '';
function openAiPanel() {
var panel = document.getElementById('lh-ai-panel');
if (!panel) return;
panel.classList.remove('hidden');
populateAiModelSelect();
// Sync content type selector with the editor's current type
var editorType = (document.getElementById('lh-cms-edit-type') || {}).value || 'article';
var ctype = document.getElementById('lh-ai-ctype');
if (ctype) ctype.value = editorType;
updateAiOptions(editorType);
// Hide WebDAV tab if Nextcloud not configured
fetch('/api/auth/me', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(d) {
var tab = document.getElementById('lh-ai-tab-webdav');
if (tab) tab.style.display = (d.user && d.user.nextcloud_url) ? '' : 'none';
if (d.user && d.user.webdav_learning_path) _aiWebdavCurrentPath = d.user.webdav_learning_path || '/';
});
// Wire ctype change once (use a flag so it doesn't stack on re-opens)
var ctypeEl = document.getElementById('lh-ai-ctype');
var toggleEl = document.getElementById('lh-ai-q-toggle');
if (ctypeEl && !ctypeEl._wired) {
ctypeEl.addEventListener('change', function() { updateAiOptions(ctypeEl.value); });
ctypeEl._wired = true;
}
if (toggleEl && !toggleEl._wired) {
toggleEl.addEventListener('change', function() { updateAiOptions((document.getElementById('lh-ai-ctype') || {}).value); });
toggleEl._wired = true;
}
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function updateAiOptions(type) {
var wordsField = document.getElementById('lh-ai-words-field');
var slidesField = document.getElementById('lh-ai-slides-field');
var quizRow = document.getElementById('lh-ai-quiz-row');
var cardTitle = document.getElementById('lh-ai-quiz-card-title');
var toggleLabel = document.getElementById('lh-ai-q-toggle-label');
var toggleCb = document.getElementById('lh-ai-q-toggle');
var countWrap = document.getElementById('lh-ai-q-count-wrap');
// Use style.display directly — classList.toggle('hidden') doesn't work because
// .lh-ai-opt-field { display:flex } is defined later in CSS and overrides .hidden
if (wordsField) wordsField.style.display = (type === 'quiz' || type === 'presentation') ? 'none' : 'flex';
if (slidesField) slidesField.style.display = (type === 'presentation') ? 'flex' : 'none';
if (type === 'quiz') {
// Quiz: questions are the whole point — show the card prominently, no toggle needed
if (quizRow) quizRow.style.display = 'block';
if (cardTitle) cardTitle.textContent = 'Quiz Questions';
if (toggleLabel) toggleLabel.style.display = 'none';
if (toggleCb) { toggleCb.checked = true; toggleCb.disabled = true; }
if (countWrap) countWrap.style.display = 'flex';
} else if (type === 'presentation') {
// Presentation: optional questions (same as article)
if (quizRow) quizRow.style.display = 'block';
if (cardTitle) cardTitle.textContent = 'Quiz Questions (optional)';
if (toggleLabel) toggleLabel.style.display = 'flex';
if (toggleCb) toggleCb.disabled = false;
var checkedP = toggleCb ? toggleCb.checked : false;
if (countWrap) countWrap.style.display = checkedP ? 'flex' : 'none';
} else {
// Article / pearl: optional questions via toggle
if (quizRow) quizRow.style.display = 'block';
if (cardTitle) cardTitle.textContent = 'Quiz Questions (optional)';
if (toggleLabel) toggleLabel.style.display = 'flex';
if (toggleCb) toggleCb.disabled = false;
var checked = toggleCb ? toggleCb.checked : false;
if (countWrap) countWrap.style.display = checked ? 'flex' : 'none';
}
}
function closeAiPanel() {
var panel = document.getElementById('lh-ai-panel');
if (panel) panel.classList.add('hidden');
}
function populateAiModelSelect() {
var sel = document.getElementById('lh-ai-model');
if (!sel || sel.options.length > 1) return;
// Copy options from global model select
var globalSel = document.getElementById('global-model-select');
if (!globalSel) return;
sel.innerHTML = globalSel.innerHTML;
sel.value = globalSel.value;
}
function switchAiTab(tabName) {
document.querySelectorAll('.lh-ai-tab').forEach(function(t) { t.classList.toggle('active', t.dataset.aitab === tabName); });
document.querySelectorAll('.lh-ai-tabpanel').forEach(function(p) { p.classList.add('hidden'); });
var tp = document.getElementById('lh-ai-tp-' + tabName);
if (tp) tp.classList.remove('hidden');
if (tabName === 'webdav' && !document.querySelector('#lh-ai-webdav-list .lh-webdav-item')) {
browseWebdav(_aiWebdavCurrentPath);
}
}
// ── WebDAV browser ──────────────────────────────────────────
function browseWebdav(path) {
_aiWebdavCurrentPath = path || '/';
var list = document.getElementById('lh-ai-webdav-list');
var pathLabel = document.getElementById('lh-ai-webdav-path-label');
if (list) list.innerHTML = '
Loading...
';
if (pathLabel) pathLabel.textContent = _aiWebdavCurrentPath;
fetch('/api/admin/learning/webdav-browse?path=' + encodeURIComponent(_aiWebdavCurrentPath), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) { if (list) list.innerHTML = '
' + esc(data.error) + '
'; return; }
if (pathLabel) pathLabel.textContent = data.path;
renderWebdavList(data);
})
.catch(function(err) { if (list) list.innerHTML = '
' + esc(err.message) + '
'; });
}
function renderWebdavList(data) {
var list = document.getElementById('lh-ai-webdav-list');
if (!list) return;
var html = '';
// Parent nav
if (data.path !== '/' && data.path !== data.parentPath) {
html += '
' +
' ..
';
}
if (data.items.length === 0 && html === '') {
html = '
Empty folder
';
}
data.items.forEach(function(item) {
var icon = item.isDir ? 'fa-folder' : getFileIcon(item.contentType, item.name);
var iconColor = item.isDir ? 'var(--amber)' : 'var(--blue)';
var sizeStr = (!item.isDir && item.size) ? ' (' + formatBytes(item.size) + ')' : '';
html += '
' +
'' +
'' + esc(item.name) + '' + sizeStr +
'
';
});
list.innerHTML = html;
}
function getFileIcon(mime, name) {
if (!name) name = '';
var ext = name.split('.').pop().toLowerCase();
if (mime && mime.includes('pdf') || ext === 'pdf') return 'fa-file-pdf';
if (['doc','docx'].includes(ext)) return 'fa-file-word';
if (['txt','md'].includes(ext)) return 'fa-file-lines';
if (['html','htm'].includes(ext)) return 'fa-file-code';
return 'fa-file';
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024*1024) return (bytes/1024).toFixed(0) + ' KB';
return (bytes/1024/1024).toFixed(1) + ' MB';
}
function selectWebdavFile(path, name) {
_aiWebdavSelectedPath = path;
_aiWebdavSelectedName = name;
var browser = document.getElementById('lh-ai-webdav-browser');
var selDiv = document.getElementById('lh-ai-webdav-selected');
var selName = document.getElementById('lh-ai-webdav-selected-name');
// Hide browser, show selected indicator (use style.display — class="hidden" is overridden by inline styles)
if (browser) browser.style.display = 'none';
if (selDiv) selDiv.style.display = 'flex';
if (selName) selName.textContent = name;
}
function deselectWebdavFile() {
_aiWebdavSelectedPath = '';
_aiWebdavSelectedName = '';
var browser = document.getElementById('lh-ai-webdav-browser');
var selDiv = document.getElementById('lh-ai-webdav-selected');
if (selDiv) selDiv.style.display = 'none';
if (browser) browser.style.display = '';
}
// ── Generate ────────────────────────────────────────────────
function runAiGenerate() {
var activeTab = document.querySelector('.lh-ai-tab.active');
var tabName = activeTab ? activeTab.dataset.aitab : 'topic';
var model = document.getElementById('lh-ai-model') ? document.getElementById('lh-ai-model').value : getSelectedModel();
var contentType = document.getElementById('lh-ai-ctype') ? document.getElementById('lh-ai-ctype').value : 'article';
var refinement = document.getElementById('lh-ai-refinement') ? document.getElementById('lh-ai-refinement').value.trim() : '';
var toggleCb = document.getElementById('lh-ai-q-toggle');
var generateQs = contentType === 'quiz' || (toggleCb && toggleCb.checked);
var questionCount = generateQs ? (document.getElementById('lh-ai-q-count') ? document.getElementById('lh-ai-q-count').value : 5) : 0;
var wordCount = document.getElementById('lh-ai-word-count') ? document.getElementById('lh-ai-word-count').value : '';
var slideCount = document.getElementById('lh-ai-slide-count') ? document.getElementById('lh-ai-slide-count').value : '';
var formData = new FormData();
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[0]) { showToast('Select a file to upload', 'error'); return; }
formData.append('file', fileInput.files[0]);
var uploadCtx = document.getElementById('lh-ai-upload-context');
if (uploadCtx && uploadCtx.value.trim()) formData.append('topic', uploadCtx.value.trim());
} else if (tabName === 'webdav') {
if (!_aiWebdavSelectedPath) { showToast('Select a file from Nextcloud', 'error'); return; }
formData.append('webdavPath', _aiWebdavSelectedPath);
var webdavCtx = document.getElementById('lh-ai-webdav-context');
if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim());
}
var btn = document.getElementById('btn-lh-ai-generate');
if (btn) { btn.disabled = true; btn.innerHTML = ' Generating...'; }
showLoading('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) {
hideLoading();
if (btn) { btn.disabled = false; btn.innerHTML = ' Generate Content'; }
if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; }
// Presentation returns marpMarkdown directly; others return content{}
var payload = data.contentType === 'presentation' ? data : data.content;
applyAiContent(payload, contentType);
closeAiPanel();
showToast('Content generated! Review and save.', 'success');
})
.catch(function(err) {
hideLoading();
if (btn) { btn.disabled = false; btn.innerHTML = ' Generate Content'; }
showToast(err.message, 'error');
});
}
function applyAiContent(content, contentType) {
// Set content type first so toggleEditorMode works correctly
var typeEl = document.getElementById('lh-cms-edit-type');
if (typeEl && contentType) { typeEl.value = contentType; toggleEditorMode(contentType); }
// Presentation: fill Marp textarea
if (contentType === 'presentation' && content.marpMarkdown) {
var marpTa = document.getElementById('lh-marp-editor');
if (marpTa) marpTa.value = content.marpMarkdown;
// Extract title from first # heading
var titleMatch = content.marpMarkdown.match(/^#\s+(.+)$/m);
var titleEl = document.getElementById('lh-cms-edit-title');
if (titleEl && titleMatch) titleEl.value = titleMatch[1];
// Fill questions if any were generated alongside slides
var qContainerP = document.getElementById('lh-cms-questions');
if (qContainerP) {
qContainerP.innerHTML = '';
if (content.questions && Array.isArray(content.questions)) {
content.questions.forEach(function(q) { addQuestionBlock(q); });
}
}
return;
}
// 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) {
qContainer.innerHTML = '';
if (content.questions && Array.isArray(content.questions)) {
content.questions.forEach(function(q) { addQuestionBlock(q); });
}
}
}
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);
showLoading('Refining content…');
fetch('/api/admin/learning/ai-refine', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ content: currentBody, instructions: instructions, model: model })
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (!data.success) { showToast(data.error || 'Refine failed', 'error'); return; }
if (_bodyEditor) _bodyEditor.commands.setContent(data.refined);
showToast('Body refined!', 'success');
})
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });
}
function loadCmsStats() {
fetch('/api/admin/learning/stats', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var s = data.stats;
var set = function(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; };
set('cms-stat-published', s.publishedContent);
set('cms-stat-drafts', s.totalContent - s.publishedContent);
set('cms-stat-categories', s.totalCategories);
set('cms-stat-quizzes', s.totalQuizzes);
set('cms-stat-attempts', s.totalAttempts);
});
}
function loadCmsCategories() {
fetch('/api/admin/learning/categories', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var container = document.getElementById('lh-cms-categories');
if (!container) return;
if (data.categories.length === 0) {
container.innerHTML = '