';
}
}
function appendMessage(role, content, sources, suggestions, rawHtml) {
var wrap = document.getElementById('assistant-messages');
if (!wrap) return;
var empty = wrap.querySelector('.assistant-empty');
if (empty) empty.remove();
messages.push({ role: role, content: content, sources: role === 'assistant' ? (sources || []) : [] });
var row = document.createElement('div');
row.className = 'assistant-msg ' + role;
var label = document.createElement('div');
label.className = 'assistant-msg-label';
label.textContent = role === 'user' ? 'You' : 'Assistant';
var bubble = document.createElement('div');
bubble.className = 'assistant-bubble';
bubble.innerHTML = rawHtml ? sanitize(String(content || '')) : (role === 'assistant' ? renderMarkdown(content, sources || []) : escapeHtml(content));
if (role === 'assistant' && suggestions && suggestions.length) {
bubble.appendChild(renderSuggestionButtons(suggestions));
}
row.appendChild(label);
row.appendChild(bubble);
wrap.appendChild(row);
renderEmbeddedBlocks(bubble);
wrap.scrollTop = wrap.scrollHeight;
}
function renderSuggestionButtons(suggestions) {
var wrap = document.createElement('div');
wrap.className = 'assistant-suggestion-buttons';
suggestions.forEach(function (s) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = s;
btn.addEventListener('click', function () {
var input = document.getElementById('assistant-input');
if (input) input.value = s;
onAsk(new Event('submit'));
});
wrap.appendChild(btn);
});
return wrap;
}
function renderMarkdown(md, sources, options) {
var opts = options || {};
return renderAssistantMarkdown(md, sources || [], {
marked: window.marked,
markdownIt: getMarkdownRenderer(),
katex: window.katex,
mathJax: window.MathJax,
sanitize: sanitize,
citationLabel: opts.citationLabel
});
}
function getMarkdownRenderer() {
if (markdownRenderer) return markdownRenderer;
if (typeof window.markdownit === 'function') {
markdownRenderer = window.markdownit({
html: false,
linkify: true,
typographer: true,
breaks: true
});
return markdownRenderer;
}
return null;
}
function renderSources(sources) {
var wrap = document.getElementById('assistant-sources');
if (!wrap) return;
wrap.innerHTML = renderSourcesList(sources);
}
function renderEmbeddedBlocks(root) {
root.querySelectorAll('[data-mermaid]').forEach(function (el) {
ensureMermaid().then(function () {
if (!window.mermaid) { el.textContent = el.getAttribute('data-mermaid'); return; }
var id = 'assistant-mermaid-' + Math.random().toString(16).slice(2);
window.mermaid.render(id, el.getAttribute('data-mermaid') || '')
.then(function (out) { el.innerHTML = out.svg || ''; })
.catch(function () { el.textContent = el.getAttribute('data-mermaid') || ''; });
});
});
root.querySelectorAll('canvas[data-chart]').forEach(function (canvas) {
if (!window.Chart) return;
try {
var cfg = JSON.parse(canvas.getAttribute('data-chart') || '{}');
new window.Chart(canvas.getContext('2d'), cfg);
} catch (e) {
canvas.replaceWith(document.createTextNode('Invalid chart JSON'));
}
});
}
function ensureMermaid() {
if (window.mermaid && mermaidReady) return Promise.resolve();
return new Promise(function (resolve) {
if (window.mermaid) { configureMermaid(); resolve(); return; }
var script = document.createElement('script');
script.src = '/vendor/mermaid.min.js';
script.onload = function () { configureMermaid(); resolve(); };
script.onerror = resolve;
document.head.appendChild(script);
});
}
function configureMermaid() {
if (!window.mermaid) return;
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default', flowchart: { useMaxWidth: true, htmlLabels: true } });
mermaidReady = true;
}
function generateImage(promptOverride, fromChat) {
var promptEl = document.getElementById('assistant-image-prompt');
var out = document.getElementById('assistant-visual-output');
var prompt = typeof promptOverride === 'string' ? promptOverride.trim() : (promptEl ? promptEl.value.trim() : '');
if (!prompt && lastAnswer) prompt = 'Create a concise pediatric clinical teaching visual from this answer:\n\n' + lastAnswer.slice(0, 3000);
if (!prompt) { if (typeof showToast === 'function') showToast('Enter an image prompt or ask a question first', 'error'); return; }
if (fromChat) setBusy(true, 'Generating image...');
var loading = fromChat ? appendLoadingMessage('Generating image', 'Creating the requested clinical visual...') : null;
if (out) out.innerHTML = '
Generating image...
';
requestAssistantImage(prompt)
.then(function (data) {
if (!data.success) throw new Error(data.error || 'Image generation failed');
var src = data.imageUrl || data.url || (data.base64 ? ('data:image/png;base64,' + data.base64) : '');
if (!src) throw new Error('No image returned');
lastGeneratedImageSrc = src;
exporter.invalidate();
if (out) out.innerHTML = imageStore.renderGeneratedImage(src, 'Generated clinical visual');
if (fromChat) {
setBusy(false, 'Ready');
var html = imageStore.renderGeneratedImage(src, 'Generated clinical visual');
replaceLoadingMessage(loading, html, [], [], true);
}
})
.catch(function (err) {
if (fromChat) setBusy(false, 'Error', true);
if (out) out.innerHTML = '
' + escapeHtml(err.message) + '
';
if (fromChat) replaceLoadingMessage(loading, 'Image generation failed: ' + err.message);
if (typeof showToast === 'function') showToast(err.message, 'error');
});
}
function onAssistantDocumentClick(e) {
var loadBtn = e.target.closest('[data-assistant-load-chat]');
if (loadBtn) {
e.preventDefault();
loadSavedChat(loadBtn.getAttribute('data-assistant-load-chat'));
return;
}
var deleteBtn = e.target.closest('[data-assistant-delete-chat]');
if (deleteBtn) {
e.preventDefault();
deleteSavedChat(deleteBtn.getAttribute('data-assistant-delete-chat'));
return;
}
var openBtn = e.target.closest('[data-assistant-open-image]');
if (openBtn) {
e.preventDefault();
imageStore.openImagePreview(openBtn.getAttribute('data-assistant-open-image'));
return;
}
if (e.target.closest('.assistant-image-modal-close') || e.target.classList.contains('assistant-image-modal')) {
imageStore.closeImagePreview();
}
}
function clearGeneratedImage() {
lastGeneratedImageSrc = '';
imageStore.clear();
var out = document.getElementById('assistant-visual-output');
if (out) out.innerHTML = '';
exporter.invalidate();
}
function prepareSidebarImagePrompt(request) {
var promptEl = document.getElementById('assistant-image-prompt');
var prompt = buildContextualImagePrompt(request, lastAnswer, lastSources);
if (promptEl) {
promptEl.value = prompt;
promptEl.focus();
}
appendMessage('assistant', 'I prepared the image prompt in the **Image / Graph** box on the right. Click **Generate image** there so the visual uses the current answer as context instead of treating this as a separate chat request.');
setBusy(false, 'Ready');
}
function clearConversation() {
messages = [];
lastAnswer = '';
lastSources = [];
lastGeneratedImageSrc = '';
exporter.invalidate();
var wrap = document.getElementById('assistant-messages');
if (wrap) {
wrap.innerHTML = renderEmptyState();
bindExampleButtons(wrap);
}
renderSources([]);
clearGeneratedImage();
loadSavedChats();
}
function renderEmptyState() {
var examples = pickExamples();
return '
' +
'
Evidence-first pediatric assistant
' +
'
Ask a real clinical question. Citations stay linked to the source cards on the right.