From 01f6a38ee29c64acbcd602c984b5bbdfd4b5b238 Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Tue, 24 Mar 2026 02:50:31 -0400 Subject: [PATCH] Fix login flash, quiz in presentations, revert viewer body, feed labels Login flash: - Auth screen now hidden by CSS default (#auth-screen{display:none !important}) - auth.js shows it only when there is no valid token or token is expired - No more flash of login page on refresh for logged-in users Presentation quiz option (AI Generate panel): - Presentation type now shows optional quiz question toggle (same as article) - Backend: when questionCount>0, returns JSON {marpMarkdown, questions[]} instead of plain Marp markdown - applyAiContent fills both the Marp textarea and question blocks for presentations Viewer revert: - Removed the viewer presentation card change (user wants markdown as-is) - Removed dangling viewer button wiring Feed/CMS list: - Presentation type shows correct icon (fa-presentation-screen) and label "Slides" in the CMS content list --- public/index.html | 4 +-- public/js/auth.js | 15 +++++++--- public/js/learningHub.js | 27 ++++++++++++----- src/routes/learningAI.js | 64 ++++++++++++++++++++++++++++++++-------- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/public/index.html b/public/index.html index 430fa82..76bdd9a 100644 --- a/public/index.html +++ b/public/index.html @@ -21,8 +21,8 @@ - - + + diff --git a/public/js/auth.js b/public/js/auth.js index 4deb809..ad240cd 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -24,10 +24,14 @@ document.addEventListener('DOMContentLoaded', function() { // Make sure forms are visible/hidden correctly on load showLoginForm(); - // Check for saved session via localStorage token + // Auth screen is hidden by CSS default — only show it when there is no valid session + function showAuthScreen() { + if (authScreen) authScreen.style.display = 'flex'; + } + var savedToken = localStorage.getItem(TOKEN_KEY); if (savedToken) { - authScreen.classList.add('hidden'); + // Token exists — verify it silently; auth screen stays hidden during check fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + savedToken } }) @@ -39,14 +43,17 @@ document.addEventListener('DOMContentLoaded', function() { if (data && data.user) { enterApp(data.user, savedToken); } else { - authScreen.classList.remove('hidden'); + showAuthScreen(); clearSession(); } }) .catch(function() { - authScreen.classList.remove('hidden'); + showAuthScreen(); clearSession(); }); + } else { + // No token — show login immediately + showAuthScreen(); } // ---- HELPER FUNCTIONS ---- diff --git a/public/js/learningHub.js b/public/js/learningHub.js index b7350e0..b988205 100644 --- a/public/js/learningHub.js +++ b/public/js/learningHub.js @@ -30,6 +30,7 @@ var dot = e.target.closest('.slide-dot'); if (dot) { renderSlide(parseInt(dot.dataset.idx)); return; } + // Feed item click var feedItem = e.target.closest('.lh-feed-item'); if (feedItem && feedItem.dataset.slug) { @@ -213,8 +214,8 @@ } feedEl.innerHTML = items.map(function(item) { - var typeIcon = item.content_type === 'quiz' ? 'fa-clipboard-question' : item.content_type === 'pearl' ? 'fa-gem' : 'fa-file-alt'; - var typeColor = item.content_type === 'quiz' ? 'var(--amber)' : item.content_type === 'pearl' ? 'var(--purple, #8b5cf6)' : 'var(--blue)'; + var typeIcon = item.content_type === 'quiz' ? 'fa-clipboard-question' : item.content_type === 'pearl' ? 'fa-gem' : item.content_type === 'presentation' ? 'fa-presentation-screen' : 'fa-file-alt'; + var typeColor = item.content_type === 'quiz' ? 'var(--amber)' : item.content_type === 'pearl' ? 'var(--purple, #8b5cf6)' : item.content_type === 'presentation' ? '#065f46' : 'var(--blue)'; var quizBadge = item.question_count > 0 ? ' ' + item.question_count + ' Q' : ''; var catBadge = item.category_name ? '' + esc(item.category_name) + '' : 'Uncategorized'; var date = item.created_at ? new Date(item.created_at).toLocaleDateString() : ''; @@ -287,7 +288,7 @@ metaEl.textContent = parts.join(' | '); } if (bodyEl) { - // Render body as HTML (admin creates it) + // Render body as HTML (admin creates it); presentations store Marp markdown bodyEl.innerHTML = sanitizeHtml(item.body || ''); } @@ -568,9 +569,13 @@ if (toggleCb) { toggleCb.checked = true; toggleCb.disabled = true; } if (countWrap) countWrap.style.display = 'flex'; } else if (type === 'presentation') { - // Presentation: no questions - if (quizRow) quizRow.style.display = 'none'; - if (toggleCb) { toggleCb.checked = false; toggleCb.disabled = true; } + // 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'; @@ -771,6 +776,14 @@ 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; } @@ -951,7 +964,7 @@ var statusBadge = item.published ? 'Published' : 'Draft'; - var typeLabel = item.content_type === 'quiz' ? 'Quiz' : item.content_type === 'pearl' ? 'Pearl' : 'Article'; + var typeLabel = item.content_type === 'quiz' ? 'Quiz' : item.content_type === 'pearl' ? 'Pearl' : item.content_type === 'presentation' ? 'Slides' : 'Article'; var qBadge = item.question_count > 0 ? ' ' + item.question_count + 'Q' : ''; var date = item.updated_at ? new Date(item.updated_at).toLocaleDateString() : ''; diff --git a/src/routes/learningAI.js b/src/routes/learningAI.js index 573da1b..feb7b5f 100644 --- a/src/routes/learningAI.js +++ b/src/routes/learningAI.js @@ -60,12 +60,16 @@ function buildGeneratePrompt(opts) { var refineInstr = refinement ? '\n\nAdditional instructions for tone/style/focus: ' + refinement : ''; - // ── Presentation: return Marp markdown, not JSON ── + // ── Presentation ── if (contentType === 'presentation') { var slideHint = slideCount ? slideCount + ' slides' : '8-12 slides'; - return source + '\n' + - 'Create a professional Marp presentation (' + slideHint + ') suitable for medical education. ' + - 'Each slide should be focused and readable.' + refineInstr + ` + var hasQuestions = parseInt(questionCount) > 0; + + if (!hasQuestions) { + // No questions — return raw Marp markdown only + return source + '\n' + + 'Create a professional Marp presentation (' + slideHint + ') suitable for medical education. ' + + 'Each slide should be focused and readable.' + refineInstr + ` Return ONLY valid Marp markdown (no JSON, no code fences). Start with frontmatter: --- @@ -79,6 +83,34 @@ Then each slide separated by ---. Guidelines: - Use bullet points (- ) for lists, keep them concise (max 5 bullets per slide) - Include a summary/key takeaways slide at the end - Do NOT include HTML tags or inline styles`; + } + + // With questions — return JSON containing both Marp markdown and questions + return source + '\n' + + 'Create a professional Marp presentation (' + slideHint + ') suitable for medical education, ' + + 'AND ' + questionCount + ' quiz questions based on the content.' + refineInstr + ` + +Return ONLY a valid JSON object (no markdown, no code fences) with this exact structure: +{ + "title": "string", + "marpMarkdown": "string (the complete Marp markdown starting with frontmatter ---\\nmarp: true\\ntheme: default\\n---)", + "questions": [ + { + "question_text": "string", + "question_type": "mcq", + "explanation": "string", + "options": [ + { "option_text": "string", "is_correct": false, "explanation": "string" } + ] + } + ] +} + +Marp guidelines inside marpMarkdown: +- Start with: ---\\nmarp: true\\ntheme: default\\n--- +- Separate slides with \\n---\\n +- First slide: title + subtitle. Use # for titles, bullets for content. +- Each MCQ must have exactly 4 options, exactly 1 marked is_correct: true`; } var wordHint = wordCount ? ' Target approximately ' + wordCount + ' words for the body.' : ''; @@ -176,16 +208,24 @@ router.post('/ai-generate', upload.single('file'), async function(req, res) { var raw = result.content.trim(); - // ── Presentation: return Marp markdown directly ── + // ── Presentation ── if (contentType === 'presentation') { + if (questionCount > 0) { + // JSON response with marpMarkdown + questions + var cleanRaw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, ''); + var parsedPres; + try { parsedPres = JSON.parse(cleanRaw); } + catch(e) { + var m = cleanRaw.match(/\{[\s\S]*\}/); + try { parsedPres = m ? JSON.parse(m[0]) : null; } catch(e2) { parsedPres = null; } + } + if (parsedPres && parsedPres.marpMarkdown) { + return res.json({ success: true, contentType: 'presentation', marpMarkdown: parsedPres.marpMarkdown, questions: parsedPres.questions || [], model: result.model }); + } + } + // Plain Marp markdown (no questions requested, or parse failed) var marpMd = raw.replace(/^```(?:markdown|marp)?\s*/i, '').replace(/\s*```\s*$/, ''); - return res.json({ - success: true, - contentType: 'presentation', - marpMarkdown: marpMd, - model: result.model, - docLength: docText.length - }); + return res.json({ success: true, contentType: 'presentation', marpMarkdown: marpMd, questions: [], model: result.model, docLength: docText.length }); } // Strip code fences if model added them anyway