@@ -123,6 +142,7 @@
.assistant-side { display:grid; gap:12px; }
.assistant-side-body { padding:12px; display:grid; gap:10px; font-size:13px; }
.assistant-visual-output { display:grid; gap:8px; }
+.assistant-image-buttons { display:flex; gap:8px; flex-wrap:wrap; }
.assistant-visual-output img { width:100%; border-radius:10px; border:1px solid var(--g200); background:white; }
.assistant-generated-image { display:grid; gap:8px; }
.assistant-generated-image img { width:100%; border-radius:10px; border:1px solid var(--g200); background:white; }
@@ -132,9 +152,23 @@
.assistant-image-modal-card img { max-width:100%; max-height:92vh; border-radius:14px; background:white; box-shadow:0 24px 80px rgba(0,0,0,.35); }
.assistant-image-modal-close { position:absolute; top:-12px; right:-12px; width:34px; height:34px; border:0; border-radius:999px; background:white; color:var(--g800); font-size:22px; line-height:1; cursor:pointer; box-shadow:var(--shadow); }
.assistant-sources { padding:10px 12px; display:grid; gap:8px; max-height:520px; overflow-y:auto; }
+.assistant-saved-chats { padding:10px 12px; display:grid; gap:8px; max-height:220px; overflow-y:auto; }
+.assistant-saved-chat { border:1px solid var(--g200); border-radius:10px; padding:8px; background:white; display:grid; gap:5px; }
+.assistant-saved-chat-title { font-size:12px; font-weight:700; color:var(--g800); line-height:1.35; }
+.assistant-saved-chat-meta { font-size:11px; color:var(--g500); }
+.assistant-saved-chat-actions { display:flex; gap:6px; flex-wrap:wrap; }
+.assistant-save-panel { border-top:1px solid var(--g200); padding:10px 12px; display:grid; gap:7px; }
+.assistant-save-panel[hidden] { display:none; }
+.assistant-save-panel label { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; }
+.assistant-save-panel input { width:100%; border:1.5px solid var(--g300); border-radius:9px; padding:8px 10px; font-size:12px; outline:none; }
+.assistant-save-panel input:focus { border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-light); }
+.assistant-save-actions { display:flex; gap:6px; flex-wrap:wrap; }
.assistant-source { border:1px solid var(--g200); border-radius:10px; padding:9px; background:white; font-size:12px; line-height:1.5; }
.assistant-source strong { color:var(--g800); }
.assistant-source-meta { color:var(--g500); font-size:11px; margin-top:3px; }
+.assistant-source-preview { margin-top:8px; }
+.assistant-source-preview button { border:0; padding:0; background:transparent; cursor:pointer; width:100%; display:block; }
+.assistant-source-preview img { width:100%; max-height:220px; object-fit:contain; border:1px solid var(--g200); border-radius:10px; background:white; display:block; }
.assistant-source-excerpt { margin-top:7px; color:var(--g600); max-height:170px; overflow:auto; }
.assistant-source-excerpt p { margin:0 0 6px; }
.assistant-source-excerpt ul, .assistant-source-excerpt ol { padding-left:16px; margin:4px 0; }
diff --git a/public/js/assistant/citations.js b/public/js/assistant/citations.js
new file mode 100644
index 0000000..8f33d4e
--- /dev/null
+++ b/public/js/assistant/citations.js
@@ -0,0 +1,191 @@
+export function renderAssistantMarkdown(md, sources, options) {
+ var opts = options || {};
+ var text = normalizeMarkdownText(md);
+ var codeBlocks = [];
+ text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, function (_, lang, code) {
+ var idx = codeBlocks.length;
+ codeBlocks.push({ lang: (lang || '').toLowerCase(), code: code });
+ return '\n@@CODEBLOCK_' + idx + '@@\n';
+ });
+ text = stripSourcesSection(text);
+ text = renderLatexText(text, opts.katex);
+ text = normalizeAdjacentCitationClusters(text, sources || []);
+
+ var html;
+ if (opts.marked && typeof opts.marked.parse === 'function') {
+ html = opts.marked.parse(text, { breaks: true, gfm: true });
+ } else {
+ html = fallbackMarkdown(text);
+ }
+
+ html = renderCitationLinks(html, sources || []);
+ html = html.replace(/@@CODEBLOCK_(\d+)@@/g, function (_, idx) {
+ var block = codeBlocks[Number(idx)] || { lang: '', code: '' };
+ if (block.lang === 'mermaid') return '
Rendering graph...
';
+ if (block.lang === 'chart' || block.lang === 'chartjs') return '
';
+ return '
' + escapeHtml(block.code) + '
';
+ });
+
+ return typeof opts.sanitize === 'function' ? opts.sanitize(html) : html;
+}
+
+export function renderCitationLinks(html, sources) {
+ return String(html || '').replace(/\[((?:\d+\s*,\s*)*\d+)\]/g, function (match, cluster) {
+ var nums = cluster.split(',').map(function (n) { return Number(n.trim()); }).filter(function (n) { return Number.isInteger(n) && n > 0; });
+ if (!nums.length || nums.some(function (n) { return !sources[n - 1]; })) return match;
+ return '[' + nums.map(function (n) {
+ var source = sources[n - 1];
+ var title = source ? source.title || source.resource || 'Source' : 'Source';
+ return '
' + n + '';
+ }).join(', ') + ']';
+ });
+}
+
+export function normalizeAdjacentCitationClusters(text, sources) {
+ var available = Array.isArray(sources) ? sources : [];
+ var normalized = String(text || '').replace(/((?:\[(?:\d+\s*,\s*)*\d+\]\s*)+)\[((?:\d+\s*,\s*)*\d+)(?=$|[.,;:])/g, function(match, completeClusters, trailingCluster) {
+ var nums = citationNumbers(completeClusters).concat(parseCitationCluster(trailingCluster));
+ if (!allCitationsAvailable(nums, available)) return match;
+ return formatCitationCluster(nums);
+ });
+
+ return normalized.replace(/(?:\[(?:\d+\s*,\s*)*\d+\]\s*){2,}/g, function(match) {
+ var nums = citationNumbers(match);
+ if (!allCitationsAvailable(nums, available)) return match;
+ return formatCitationCluster(nums);
+ });
+}
+
+function citationNumbers(text) {
+ var clusters = String(text || '').match(/\[((?:\d+\s*,\s*)*\d+)\]/g) || [];
+ var nums = [];
+ clusters.forEach(function(cluster) {
+ nums = nums.concat(parseCitationCluster(cluster.slice(1, -1)));
+ });
+ return nums;
+}
+
+function parseCitationCluster(cluster) {
+ return String(cluster || '').split(',').map(function(n) { return Number(n.trim()); }).filter(function(n) { return Number.isInteger(n) && n > 0; });
+}
+
+function allCitationsAvailable(nums, sources) {
+ return nums.length > 0 && nums.every(function(n) { return sources[n - 1]; });
+}
+
+function formatCitationCluster(nums) {
+ nums = Array.from(new Set(nums)).sort(function(a, b) { return a - b; });
+ return '[' + nums.join(', ') + ']';
+}
+
+export function stripSourcesSection(text) {
+ return String(text || '')
+ .replace(/\s*(?:-{3,}\s*)?(?:#{1,6}\s*)?(?:Sources|References)\s*:?\s*[\s\S]*$/i, '')
+ .trim();
+}
+
+export function normalizeMarkdownText(text) {
+ return String(text || '')
+ .replace(/\r\n/g, '\n')
+ .replace(/(\[(?:\d+\s*,\s*)*\d+\])\s*-\s+/g, '$1\n- ')
+ .replace(/([.!?])\s*-\s+(\*\*)?/g, '$1\n- $2')
+ .replace(/([^\n])\s+(#{1,4}\s+)/g, '$1\n\n$2')
+ .replace(/(#{1,4}\s+[^\n]+?)\s+(-\s+)/g, '$1\n\n$2')
+ .replace(/(#{1,4}\s+[^\n]+)\n(-\s+)/g, '$1\n\n$2')
+ .replace(/([^\n])\s+(-\s+(?:Mainstay|Medications|Hospitalization|Other therapies|Prevention|Short-acting|Anticholinergics|Systemic|Adjuncts|Long-term|Infants|Differentiating|Persistent|Severe|Need for|Inadequate)\b)/g, '$1\n$2')
+ .replace(/([^\n])\s+(\|\s*Aspect\s*\|)/gi, '$1\n\n$2')
+ .replace(/([^\n])\s+(\|\s*[-:]+\s*\|)/g, '$1\n$2')
+ .replace(/\s+(\|\s*[^\n|]+\s*\|\s*[^\n|]+\s*\|)/g, '\n$1')
+ .replace(/([^\n])\s+(-\s+[^\n])/g, '$1\n$2')
+ .trim();
+}
+
+export function fallbackMarkdown(text) {
+ var html = escapeHtml(text)
+ .replace(/^### (.*)$/gm, '
$1
')
+ .replace(/^## (.*)$/gm, '
$1
')
+ .replace(/^# (.*)$/gm, '
$1
')
+ .replace(/\*\*(.*?)\*\*/g, '
$1')
+ .replace(/\*(.*?)\*/g, '
$1')
+ .replace(/`([^`]+)`/g, '
$1');
+ html = html.split(/\n{2,}/).map(function (block) {
+ if (/^\s*
' + lines.filter(Boolean).map(function (l) { return '' + l.replace(/^\s*[-*] /, '') + ''; }).join('') + '';
+ }
+ if (lines.every(function (l) { return /^\s*\d+\. /.test(l) || !l.trim(); })) {
+ return '' + lines.filter(Boolean).map(function (l) { return '- ' + l.replace(/^\s*\d+\. /, '') + '
'; }).join('') + '
';
+ }
+ return '' + block.replace(/\n/g, '
') + '
';
+ }).join('');
+ return html;
+}
+
+function renderMixedList(lines) {
+ var html = '';
+ var list = [];
+ var paragraph = [];
+ lines.forEach(function(line) {
+ if (/^\s*[-*] /.test(line)) {
+ if (paragraph.length) {
+ html += '' + paragraph.join('
') + '
';
+ paragraph = [];
+ }
+ list.push(line.replace(/^\s*[-*] /, ''));
+ return;
+ }
+ if (list.length) {
+ html += '' + list.map(function(item) { return '- ' + item + '
'; }).join('') + '
';
+ list = [];
+ }
+ if (line.trim()) paragraph.push(line);
+ });
+ if (paragraph.length) html += '' + paragraph.join('
') + '
';
+ if (list.length) html += '' + list.map(function(item) { return '- ' + item + '
'; }).join('') + '
';
+ return html;
+}
+
+function isMarkdownTable(lines) {
+ return lines.length >= 2 && /^\s*\|.*\|\s*$/.test(lines[0]) && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(lines[1]);
+}
+
+function renderFallbackTable(lines) {
+ var header = tableCells(lines[0]);
+ var rows = lines.slice(2).filter(function(line) { return /^\s*\|.*\|\s*$/.test(line); }).map(tableCells);
+ return '' + header.map(function(cell) { return '| ' + cell + ' | '; }).join('') + '
' +
+ rows.map(function(row) { return '' + row.map(function(cell) { return '| ' + cell + ' | '; }).join('') + '
'; }).join('') +
+ '
';
+}
+
+function tableCells(line) {
+ return String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function(cell) { return cell.trim(); });
+}
+
+export function renderLatexText(text, katex) {
+ if (!katex) return text;
+ return String(text || '')
+ .replace(/\$\$([\s\S]+?)\$\$/g, function(_, expr) { return safeKatex(katex, expr, true); })
+ .replace(/\\\[([\s\S]+?)\\\]/g, function(_, expr) { return safeKatex(katex, expr, true); })
+ .replace(/\$([^$\n]+?)\$/g, function(_, expr) { return safeKatex(katex, expr, false); })
+ .replace(/\\\((.+?)\\\)/g, function(_, expr) { return safeKatex(katex, expr, false); });
+}
+
+function safeKatex(katex, expr, displayMode) {
+ try { return katex.renderToString(expr, { displayMode: displayMode, throwOnError: false }); }
+ catch (e) { return escapeHtml(expr); }
+}
+
+export function escapeHtml(s) {
+ return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+}
+
+export function escapeAttr(s) {
+ return escapeHtml(s).replace(/'/g, ''');
+}
diff --git a/public/js/assistant/data.js b/public/js/assistant/data.js
index 43dbea1..fcb13eb 100644
--- a/public/js/assistant/data.js
+++ b/public/js/assistant/data.js
@@ -2,7 +2,7 @@ export var EMPTY_PROMPT_SETS = [
[
{ label: 'Status asthma escalation', prompt: 'In a 4-year-old with acute wheeze, when should magnesium sulfate be considered and what dose is recommended?' },
{ label: 'Bilious vomiting', prompt: 'What are the red flags for bilious vomiting in neonates, and what immediate workup is recommended?' },
- { label: 'Bronchiolitis vs asthma', prompt: 'Compare bronchiolitis and asthma management in infants, citing pediatric references.' }
+ { label: 'Bronchiolitis vs asthma', prompt: 'Compare bronchiolitis and asthma management in infants, citing clinical references.' }
],
[
{ label: 'Febrile neonate', prompt: 'What initial workup and empiric antibiotics are recommended for a well-appearing febrile neonate?' },
diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js
index aa5113c..697e594 100644
--- a/public/js/clinicalAssistant.js
+++ b/public/js/clinicalAssistant.js
@@ -4,6 +4,7 @@
// server can call native MCP directly without routing through mcpo.
// ============================================================
import { EMPTY_PROMPT_SETS } from './assistant/data.js';
+import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection } from './assistant/citations.js';
(function () {
'use strict';
@@ -16,6 +17,9 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
var dynamicExamples = [];
var generatedImages = {};
var generatedImageSeq = 0;
+ var lastGeneratedImageSrc = '';
+ var exportCacheKey = '';
+ var exportCacheItems = null;
document.addEventListener('tabChanged', function (e) {
if (e.detail && e.detail.tab === 'assistant') initIfNeeded();
@@ -35,21 +39,30 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
var form = document.getElementById('assistant-form');
var clearBtn = document.getElementById('btn-assistant-clear');
var copyBtn = document.getElementById('btn-assistant-copy');
+ var saveBtn = document.getElementById('btn-assistant-save');
+ var saveConfirmBtn = document.getElementById('btn-assistant-save-confirm');
+ var saveCancelBtn = document.getElementById('btn-assistant-save-cancel');
var exportBtn = document.getElementById('btn-assistant-export-pdf');
var imageBtn = document.getElementById('btn-assistant-image');
+ var imageClearBtn = document.getElementById('btn-assistant-image-clear');
var input = document.getElementById('assistant-input');
if (form) form.addEventListener('submit', onAsk);
if (clearBtn) clearBtn.addEventListener('click', clearConversation);
if (copyBtn) copyBtn.addEventListener('click', copyLastAnswer);
+ if (saveBtn) saveBtn.addEventListener('click', showSavePanel);
+ if (saveConfirmBtn) saveConfirmBtn.addEventListener('click', saveCurrentChat);
+ if (saveCancelBtn) saveCancelBtn.addEventListener('click', hideSavePanel);
if (exportBtn) exportBtn.addEventListener('click', exportAnswerPdf);
if (imageBtn) imageBtn.addEventListener('click', generateImage);
+ if (imageClearBtn) imageClearBtn.addEventListener('click', clearGeneratedImage);
document.addEventListener('click', onAssistantDocumentClick);
if (input) input.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') onAsk(e);
});
bindExampleButtons(document);
+ loadSavedChats();
}
function loadStatus() {
@@ -92,6 +105,8 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
}
appendMessage('user', text);
+ exportCacheKey = '';
+ exportCacheItems = null;
if (input) input.value = '';
if (isImageRequest(text)) {
@@ -100,7 +115,7 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
}
setBusy(true, 'Searching indexed resources...');
- var loading = appendLoadingMessage('Searching indexed resources', 'Retrieving and synthesizing cited pediatric references...');
+ var loading = appendLoadingMessage('Searching indexed resources', 'Retrieving and synthesizing cited references...');
fetch('/api/clinical-assistant/chat', {
method: 'POST',
@@ -168,7 +183,7 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
renderEmbeddedBlocks(bubble);
var wrap = document.getElementById('assistant-messages');
if (wrap) wrap.scrollTop = wrap.scrollHeight;
- messages.push({ role: 'assistant', content: content });
+ messages.push({ role: 'assistant', content: content, sources: sources || [] });
}
function appendMessage(role, content, sources, suggestions, rawHtml) {
@@ -177,7 +192,7 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
var empty = wrap.querySelector('.assistant-empty');
if (empty) empty.remove();
- messages.push({ role: role, content: content });
+ 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');
@@ -214,59 +229,11 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
}
function renderMarkdown(md, sources) {
- var text = String(md || '');
- var codeBlocks = [];
- text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, function (_, lang, code) {
- var idx = codeBlocks.length;
- codeBlocks.push({ lang: (lang || '').toLowerCase(), code: code });
- return '\n@@CODEBLOCK_' + idx + '@@\n';
+ return renderAssistantMarkdown(md, sources || [], {
+ marked: window.marked,
+ katex: window.katex,
+ sanitize: sanitize
});
- text = stripSourcesSection(text);
- text = renderLatexText(text);
- var html;
- if (window.marked && typeof window.marked.parse === 'function') {
- html = window.marked.parse(text, { breaks: true, gfm: true });
- } else {
- html = fallbackMarkdown(text);
- }
- html = html.replace(/\[((?:\d+\s*,\s*)*\d+)\]/g, function (match, cluster) {
- var nums = cluster.split(',').map(function (n) { return Number(n.trim()); }).filter(function (n) { return Number.isInteger(n) && n > 0; });
- if (!nums.length || nums.some(function (n) { return !sources[n - 1]; })) return match;
- return '[' + nums.map(function (n) {
- var source = sources[n - 1];
- var title = source ? source.title || source.resource || 'Source' : 'Source';
- return '' + n + '';
- }).join(', ') + ']';
- });
- html = html.replace(/@@CODEBLOCK_(\d+)@@/g, function (_, idx) {
- var block = codeBlocks[Number(idx)] || { lang: '', code: '' };
- if (block.lang === 'mermaid') return 'Rendering graph...
';
- if (block.lang === 'chart' || block.lang === 'chartjs') return '';
- return '' + escapeHtml(block.code) + '
';
- });
- return sanitize(html);
- }
-
- function fallbackMarkdown(text) {
- var html = escapeHtml(text)
- .replace(/^### (.*)$/gm, '$1
')
- .replace(/^## (.*)$/gm, '$1
')
- .replace(/^# (.*)$/gm, '$1
')
- .replace(/\*\*(.*?)\*\*/g, '$1')
- .replace(/\*(.*?)\*/g, '$1')
- .replace(/`([^`]+)`/g, '$1');
- html = html.split(/\n{2,}/).map(function (block) {
- if (/^\s*<(h\d|ul|ol|pre|div)/.test(block)) return block;
- var lines = block.split('\n');
- if (lines.every(function (l) { return /^\s*[-*] /.test(l) || !l.trim(); })) {
- return '' + lines.filter(Boolean).map(function (l) { return '- ' + l.replace(/^\s*[-*] /, '') + '
'; }).join('') + '
';
- }
- if (lines.every(function (l) { return /^\s*\d+\. /.test(l) || !l.trim(); })) {
- return '' + lines.filter(Boolean).map(function (l) { return '- ' + l.replace(/^\s*\d+\. /, '') + '
'; }).join('') + '
';
- }
- return '' + block.replace(/\n/g, '
') + '
';
- }).join('');
- return html;
}
function renderSources(sources) {
@@ -287,11 +254,24 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
return '' +
'
[' + n + '] ' + escapeHtml(s.title || s.resource || 'Untitled source') + '' +
'
' + escapeHtml(meta.join(' ยท ') || 'indexed source') + '
' +
- (s.excerpt ? '
' + escapeHtml(String(s.excerpt).slice(0, 900)) + '
' : '') +
+ renderSourceImage(s, n) +
+ (s.excerpt ? '
' + escapeHtml(cleanSourceExcerpt(s.excerpt).slice(0, 900)) + '
' : '') +
'
';
}).join('');
}
+ function renderSourceImage(source, number) {
+ var src = source && (source.image_url || source.imageUrl);
+ if (!src || source.source_type !== 'multimodal_page') return '';
+ var id = 'source-img-' + number + '-' + (++generatedImageSeq);
+ generatedImages[id] = src;
+ return '' +
+ '
' +
+ '
';
+ }
+
function renderEmbeddedBlocks(root) {
root.querySelectorAll('[data-mermaid]').forEach(function (el) {
ensureMermaid().then(function () {
@@ -349,6 +329,9 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
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;
+ exportCacheKey = '';
+ exportCacheItems = null;
if (out) out.innerHTML = renderGeneratedImage(src, 'Generated clinical visual');
if (fromChat) {
setBusy(false, 'Ready');
@@ -375,6 +358,18 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
}
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();
@@ -400,6 +395,16 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
document.querySelectorAll('.assistant-image-modal').forEach(function (el) { el.remove(); });
}
+ function clearGeneratedImage() {
+ lastGeneratedImageSrc = '';
+ generatedImages = {};
+ var out = document.getElementById('assistant-visual-output');
+ if (out) out.innerHTML = '';
+ closeImagePreview();
+ exportCacheKey = '';
+ exportCacheItems = null;
+ }
+
function buildContextualImagePrompt(request) {
var prompt = String(request || '').trim();
if (!lastAnswer) return prompt;
@@ -424,12 +429,17 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
messages = [];
lastAnswer = '';
lastSources = [];
+ lastGeneratedImageSrc = '';
+ exportCacheKey = '';
+ exportCacheItems = null;
var wrap = document.getElementById('assistant-messages');
if (wrap) {
wrap.innerHTML = renderEmptyState();
bindExampleButtons(wrap);
}
renderSources([]);
+ clearGeneratedImage();
+ loadSavedChats();
}
function renderEmptyState() {
@@ -479,66 +489,291 @@ import { EMPTY_PROMPT_SETS } from './assistant/data.js';
function exportAnswerPdf() {
if (!lastAnswer) { if (typeof showToast === 'function') showToast('No answer to export', 'error'); return; }
+ var exportItems = collectExportItems();
+ var cacheKey = buildExportCacheKey(exportItems, lastGeneratedImageSrc);
+ if (exportCacheKey === cacheKey && exportCacheItems) {
+ openPrintableChatExport(exportCacheItems, lastGeneratedImageSrc);
+ return;
+ }
setBusy(true, 'Preparing PDF...');
fetch('/api/clinical-assistant/export-summary', {
method: 'POST', headers: getAuthHeaders(), credentials: 'same-origin',
- body: JSON.stringify({ answer: lastAnswer, sources: lastSources })
+ body: JSON.stringify({ items: exportItems })
})
.then(function (r) { return r.json().then(function (data) { data._status = r.status; return data; }); })
.then(function (data) {
setBusy(false, 'Ready');
- openPrintableExport(data.success && data.summary ? data.summary : lastAnswer, lastSources);
+ var items = data.success && Array.isArray(data.items) && data.items.length ? data.items : exportItems;
+ exportCacheKey = cacheKey;
+ exportCacheItems = items;
+ openPrintableChatExport(items, lastGeneratedImageSrc);
})
.catch(function () {
setBusy(false, 'Ready');
- openPrintableExport(lastAnswer, lastSources);
+ openPrintableChatExport(exportItems, lastGeneratedImageSrc);
});
}
- function openPrintableExport(answer, sources) {
+ function openPrintableChatExport(items, imageSrc) {
var doc = window.open('', '_blank', 'width=900,height=1100');
if (!doc) { if (typeof showToast === 'function') showToast('Allow popups to export PDF', 'error'); return; }
- var refs = (sources || []).map(function (s, idx) {
- var n = s.number || idx + 1;
- var title = s.title || s.resource || 'Untitled source';
- var page = s.page || s.page_number || s.pageNumber;
- return '[' + n + '] ' + escapeHtml(title) + (page ? ', page ' + escapeHtml(page) : '') + '.';
+ items = Array.isArray(items) && items.length ? items : collectExportItems();
+ var imageHtml = imageSrc ? 'Generated Image
' : '';
+ var sections = items.map(function (item, idx) {
+ var sources = Array.isArray(item.sources) ? item.sources : [];
+ var refs = renderExportRefs(sources, idx + 1);
+ var heading = item.heading || deriveExportHeading(item.question, idx);
+ var summary = item.summary || '';
+ var answer = stripSourcesSection(item.answer || '');
+ return '' +
+ '' + escapeHtml(heading) + '
' +
+ 'Question: ' + escapeHtml(item.question || '') + '
' +
+ (summary ? 'Summary
' + renderMarkdown(summary, sources) + '
' : '') +
+ 'Full Generated Answer
' + renderMarkdown(answer, sources) + '
' +
+ (refs ? 'References
' + refs + '
' : '') +
+ '';
}).join('');
var html = 'Clinical Assistant Export' +
- '' +
+ '' +
'Clinical Assistant Export
' +
- 'Abridged export generated ' + escapeHtml(new Date().toLocaleString()) + '
' +
- 'Summary
' + renderMarkdown(answer, sources || []) + '
' +
- 'References
' + refs + '
';
+ 'Export generated ' + escapeHtml(new Date().toLocaleString()) + '
' +
+ imageHtml +
+ sections + '