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:
parent
23d39dec65
commit
01f6a38ee2
4 changed files with 85 additions and 25 deletions
|
|
@ -21,8 +21,8 @@
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16.png">
|
<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="192x192" href="/icons/icon-192.png">
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512.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>
|
<!-- Auth screen hidden by default — auth.js shows it only when no valid session exists -->
|
||||||
<style>html.has-session #auth-screen { display: none !important; }</style>
|
<style>#auth-screen { display: none !important; }</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Make sure forms are visible/hidden correctly on load
|
// Make sure forms are visible/hidden correctly on load
|
||||||
showLoginForm();
|
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);
|
var savedToken = localStorage.getItem(TOKEN_KEY);
|
||||||
if (savedToken) {
|
if (savedToken) {
|
||||||
authScreen.classList.add('hidden');
|
// Token exists — verify it silently; auth screen stays hidden during check
|
||||||
fetch('/api/auth/me', {
|
fetch('/api/auth/me', {
|
||||||
headers: { 'Authorization': 'Bearer ' + savedToken }
|
headers: { 'Authorization': 'Bearer ' + savedToken }
|
||||||
})
|
})
|
||||||
|
|
@ -39,14 +43,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (data && data.user) {
|
if (data && data.user) {
|
||||||
enterApp(data.user, savedToken);
|
enterApp(data.user, savedToken);
|
||||||
} else {
|
} else {
|
||||||
authScreen.classList.remove('hidden');
|
showAuthScreen();
|
||||||
clearSession();
|
clearSession();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
authScreen.classList.remove('hidden');
|
showAuthScreen();
|
||||||
clearSession();
|
clearSession();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// No token — show login immediately
|
||||||
|
showAuthScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- HELPER FUNCTIONS ----
|
// ---- HELPER FUNCTIONS ----
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
var dot = e.target.closest('.slide-dot');
|
var dot = e.target.closest('.slide-dot');
|
||||||
if (dot) { renderSlide(parseInt(dot.dataset.idx)); return; }
|
if (dot) { renderSlide(parseInt(dot.dataset.idx)); return; }
|
||||||
|
|
||||||
|
|
||||||
// Feed item click
|
// Feed item click
|
||||||
var feedItem = e.target.closest('.lh-feed-item');
|
var feedItem = e.target.closest('.lh-feed-item');
|
||||||
if (feedItem && feedItem.dataset.slug) {
|
if (feedItem && feedItem.dataset.slug) {
|
||||||
|
|
@ -213,8 +214,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
feedEl.innerHTML = items.map(function(item) {
|
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 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)' : 'var(--blue)';
|
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 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 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() : '';
|
var date = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||||
|
|
@ -287,7 +288,7 @@
|
||||||
metaEl.textContent = parts.join(' | ');
|
metaEl.textContent = parts.join(' | ');
|
||||||
}
|
}
|
||||||
if (bodyEl) {
|
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 || '');
|
bodyEl.innerHTML = sanitizeHtml(item.body || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,9 +569,13 @@
|
||||||
if (toggleCb) { toggleCb.checked = true; toggleCb.disabled = true; }
|
if (toggleCb) { toggleCb.checked = true; toggleCb.disabled = true; }
|
||||||
if (countWrap) countWrap.style.display = 'flex';
|
if (countWrap) countWrap.style.display = 'flex';
|
||||||
} else if (type === 'presentation') {
|
} else if (type === 'presentation') {
|
||||||
// Presentation: no questions
|
// Presentation: optional questions (same as article)
|
||||||
if (quizRow) quizRow.style.display = 'none';
|
if (quizRow) quizRow.style.display = 'block';
|
||||||
if (toggleCb) { toggleCb.checked = false; toggleCb.disabled = true; }
|
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 {
|
} else {
|
||||||
// Article / pearl: optional questions via toggle
|
// Article / pearl: optional questions via toggle
|
||||||
if (quizRow) quizRow.style.display = 'block';
|
if (quizRow) quizRow.style.display = 'block';
|
||||||
|
|
@ -771,6 +776,14 @@
|
||||||
var titleMatch = content.marpMarkdown.match(/^#\s+(.+)$/m);
|
var titleMatch = content.marpMarkdown.match(/^#\s+(.+)$/m);
|
||||||
var titleEl = document.getElementById('lh-cms-edit-title');
|
var titleEl = document.getElementById('lh-cms-edit-title');
|
||||||
if (titleEl && titleMatch) titleEl.value = titleMatch[1];
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -951,7 +964,7 @@
|
||||||
var statusBadge = item.published
|
var statusBadge = item.published
|
||||||
? '<span class="cms-badge cms-badge-pub">Published</span>'
|
? '<span class="cms-badge cms-badge-pub">Published</span>'
|
||||||
: '<span class="cms-badge cms-badge-draft">Draft</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 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() : '';
|
var date = item.updated_at ? new Date(item.updated_at).toLocaleDateString() : '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,16 @@ function buildGeneratePrompt(opts) {
|
||||||
|
|
||||||
var refineInstr = refinement ? '\n\nAdditional instructions for tone/style/focus: ' + refinement : '';
|
var refineInstr = refinement ? '\n\nAdditional instructions for tone/style/focus: ' + refinement : '';
|
||||||
|
|
||||||
// ── Presentation: return Marp markdown, not JSON ──
|
// ── Presentation ──
|
||||||
if (contentType === 'presentation') {
|
if (contentType === 'presentation') {
|
||||||
var slideHint = slideCount ? slideCount + ' slides' : '8-12 slides';
|
var slideHint = slideCount ? slideCount + ' slides' : '8-12 slides';
|
||||||
return source + '\n' +
|
var hasQuestions = parseInt(questionCount) > 0;
|
||||||
'Create a professional Marp presentation (' + slideHint + ') suitable for medical education. ' +
|
|
||||||
'Each slide should be focused and readable.' + refineInstr + `
|
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:
|
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)
|
- Use bullet points (- ) for lists, keep them concise (max 5 bullets per slide)
|
||||||
- Include a summary/key takeaways slide at the end
|
- Include a summary/key takeaways slide at the end
|
||||||
- Do NOT include HTML tags or inline styles`;
|
- 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.' : '';
|
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();
|
var raw = result.content.trim();
|
||||||
|
|
||||||
// ── Presentation: return Marp markdown directly ──
|
// ── Presentation ──
|
||||||
if (contentType === '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*$/, '');
|
var marpMd = raw.replace(/^```(?:markdown|marp)?\s*/i, '').replace(/\s*```\s*$/, '');
|
||||||
return res.json({
|
return res.json({ success: true, contentType: 'presentation', marpMarkdown: marpMd, questions: [], model: result.model, docLength: docText.length });
|
||||||
success: true,
|
|
||||||
contentType: 'presentation',
|
|
||||||
marpMarkdown: marpMd,
|
|
||||||
model: result.model,
|
|
||||||
docLength: docText.length
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip code fences if model added them anyway
|
// Strip code fences if model added them anyway
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue