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
This commit is contained in:
Daniel Onyejesi 2026-03-24 02:50:31 -04:00
parent 23d39dec65
commit 01f6a38ee2
4 changed files with 85 additions and 25 deletions

View file

@ -21,8 +21,8 @@
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16.png">
<link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512.png">
<script>if (localStorage.getItem('ped_scribe_token')) { document.documentElement.classList.add('has-session'); }</script>
<style>html.has-session #auth-screen { display: none !important; }</style>
<!-- Auth screen hidden by default — auth.js shows it only when no valid session exists -->
<style>#auth-screen { display: none !important; }</style>
</head>
<body>

View file

@ -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 ----

View file

@ -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 ? '<span class="lh-badge lh-badge-quiz"><i class="fas fa-clipboard-question"></i> ' + item.question_count + ' Q</span>' : '';
var catBadge = item.category_name ? '<span class="lh-badge">' + esc(item.category_name) + '</span>' : '<span class="lh-badge" style="opacity:0.5;">Uncategorized</span>';
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
? '<span class="cms-badge cms-badge-pub">Published</span>'
: '<span class="cms-badge cms-badge-draft">Draft</span>';
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 ? ' <span style="color:var(--amber);">' + item.question_count + 'Q</span>' : '';
var date = item.updated_at ? new Date(item.updated_at).toLocaleDateString() : '';

View file

@ -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