371 lines
24 KiB
JavaScript
371 lines
24 KiB
JavaScript
import { assetPath, imageDataUrl, imageJson } from '../generatedImages.js';
|
|
import { escapeAttr, escapeHtml } from './citations.js';
|
|
import { captureSharingOwner, assertSharingOwner, validSharingOwner, sharingFilename } from './sharing.js';
|
|
|
|
export function createAssistantExporter(options) {
|
|
options = options || {};
|
|
var exportCacheKey = '';
|
|
var exportCacheItems = null;
|
|
var inlineExportClose = null;
|
|
window.addEventListener('account-boundary', invalidate);
|
|
|
|
function invalidate() {
|
|
exportCacheKey = '';
|
|
exportCacheItems = null;
|
|
}
|
|
|
|
function exportAnswerPdf(state) {
|
|
var owner;
|
|
try { owner = captureSharingOwner(); } catch (e) { return; }
|
|
state = state || {};
|
|
if (!state.lastAnswer) {
|
|
if (typeof options.showToast === 'function') options.showToast('No answer to export', 'error');
|
|
return;
|
|
}
|
|
if ((state.generatedImageJobs || []).length || assetPath(state.lastGeneratedImageSrc) || (state.messages || []).some(m => (m.imageJobs || []).length)) {
|
|
var target = shouldUseInlineExport() ? null : openExportWindow(owner);
|
|
function closeTarget() { if (target) { try { target.close(); } catch (_) {} } }
|
|
owner.signal.addEventListener('abort', closeTarget, { once: true });
|
|
return preparePrivateExport(state, owner).then(function(prepared) {
|
|
assertSharingOwner(owner);
|
|
var items = collectExportItems(prepared.messages, prepared.lastAnswer, prepared.lastSources || [], options.presentMessage);
|
|
if (target) writePrintableChatExport(target, items, prepared.lastGeneratedImageSrc, owner);
|
|
else writeInlineChatExport(items, prepared.lastGeneratedImageSrc, owner);
|
|
}).catch(function(error) {
|
|
closeTarget();
|
|
if (!validSharingOwner(owner) || error.name === 'AbortError') return;
|
|
if (typeof options.showToast === 'function') options.showToast(error.message, 'error');
|
|
}).finally(function() { owner.signal.removeEventListener('abort', closeTarget); });
|
|
}
|
|
var exportItems = collectExportItems(state.messages || [], state.lastAnswer, state.lastSources || [], options.presentMessage);
|
|
var cacheKey = buildExportCacheKey(exportItems, state.lastGeneratedImageSrc || '');
|
|
if (exportCacheKey === cacheKey && exportCacheItems) {
|
|
showPrintableExport(exportCacheItems, state.lastGeneratedImageSrc || '', owner);
|
|
return;
|
|
}
|
|
exportCacheKey = cacheKey;
|
|
exportCacheItems = exportItems;
|
|
showPrintableExport(exportItems, state.lastGeneratedImageSrc || '', owner);
|
|
}
|
|
|
|
function showPrintableExport(items, imageSrc, owner) {
|
|
if (!validSharingOwner(owner)) return;
|
|
if (shouldUseInlineExport()) {
|
|
writeInlineChatExport(items, imageSrc, owner);
|
|
return;
|
|
}
|
|
var doc = openExportWindow(owner);
|
|
if (!doc) {
|
|
if (validSharingOwner(owner) && typeof options.showToast === 'function') options.showToast('Allow popups to export PDF', 'error');
|
|
return;
|
|
}
|
|
writePrintableChatExport(doc, items, imageSrc, owner);
|
|
}
|
|
|
|
function openExportWindow(owner) {
|
|
assertSharingOwner(owner);
|
|
var doc = window.open('', '_blank', 'width=900,height=1100');
|
|
if (!doc) return null;
|
|
if (!validSharingOwner(owner)) { doc.close(); return null; }
|
|
doc.document.open();
|
|
doc.document.write('<!doctype html><html><head><title>Preparing Clinical Assistant Export</title><style>body{font-family:Arial,sans-serif;color:#111827;margin:36px;line-height:1.5}.spinner{width:18px;height:18px;border:3px solid #e5e7eb;border-top-color:#7c3aed;border-radius:50%;display:inline-block;vertical-align:middle;margin-right:8px;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}</style></head><body><p><span class="spinner"></span>Preparing PDF export...</p></body></html>');
|
|
doc.document.close();
|
|
return doc;
|
|
}
|
|
|
|
function writePrintableChatExport(doc, items, imageSrc, owner) {
|
|
if (!doc || doc.closed) return;
|
|
var timer;
|
|
function close() {
|
|
clearTimeout(timer);
|
|
owner.signal.removeEventListener('abort', close);
|
|
try { doc.close(); } catch (e) { /* Only this export window is owned here. */ }
|
|
}
|
|
function print() {
|
|
if (!validSharingOwner(owner)) { close(); return; }
|
|
if (doc.closed) return;
|
|
try { doc.focus(); assertSharingOwner(owner); doc.print(); } catch (e) {}
|
|
}
|
|
owner.signal.addEventListener('abort', close, { once: true });
|
|
var html = buildPrintableChatHtml(items, imageSrc, false);
|
|
if (!validSharingOwner(owner)) { close(); return; }
|
|
doc.document.open();
|
|
doc.document.write(html);
|
|
doc.document.close();
|
|
var printBtn = doc.document.getElementById('assistant-export-print');
|
|
if (printBtn) printBtn.addEventListener('click', print);
|
|
var closeBtn = doc.document.getElementById('assistant-export-close');
|
|
if (closeBtn) closeBtn.addEventListener('click', close);
|
|
timer = setTimeout(print, 500);
|
|
}
|
|
|
|
function writeInlineChatExport(items, imageSrc, owner) {
|
|
assertSharingOwner(owner);
|
|
closeInlineExport();
|
|
var modal = document.createElement('div');
|
|
modal.id = 'assistant-export-modal';
|
|
modal.innerHTML = '<style>' + inlineExportCss() + exportTableScrollCss() + '</style><div class="assistant-export-sheet">' + buildPrintableChatBody(items, imageSrc, true) + '</div>';
|
|
if (!validSharingOwner(owner)) return;
|
|
function close() {
|
|
modal.remove();
|
|
owner.signal.removeEventListener('abort', close);
|
|
window.removeEventListener('popstate', close);
|
|
if (inlineExportClose === close) {
|
|
inlineExportClose = null;
|
|
document.body.classList.remove('assistant-export-open');
|
|
}
|
|
}
|
|
inlineExportClose = close;
|
|
owner.signal.addEventListener('abort', close, { once: true });
|
|
document.body.appendChild(modal);
|
|
document.body.classList.add('assistant-export-open');
|
|
var printBtn = modal.querySelector('#assistant-export-print');
|
|
var closeBtn = modal.querySelector('#assistant-export-close');
|
|
if (printBtn) printBtn.addEventListener('click', async function () {
|
|
if (!validSharingOwner(owner)) { close(); return; }
|
|
printBtn.disabled = true;
|
|
var originalText = printBtn.textContent;
|
|
printBtn.textContent = 'Preparing export...';
|
|
try {
|
|
var html = buildPrintableChatHtml(items, imageSrc, false);
|
|
var printed = await printWithNativeBridge(html, owner);
|
|
assertSharingOwner(owner);
|
|
if (printed) return;
|
|
var saved = await saveInlineExport(html, owner);
|
|
assertSharingOwner(owner);
|
|
if (!saved) window.print();
|
|
} catch (e) {
|
|
if (!validSharingOwner(owner) || e.name === 'AbortError') return;
|
|
if (typeof options.showToast === 'function') options.showToast(e.message || 'Export failed', 'error');
|
|
} finally {
|
|
if (validSharingOwner(owner)) {
|
|
printBtn.disabled = false;
|
|
printBtn.textContent = originalText;
|
|
} else close();
|
|
}
|
|
});
|
|
if (closeBtn) closeBtn.addEventListener('click', close);
|
|
modal.addEventListener('click', function (event) {
|
|
if (!event.defaultPrevented && !event.button && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {
|
|
var citation = event.target.closest('.assistant-cite');
|
|
var href = citation && citation.getAttribute('href');
|
|
var reference = /^#ref-\d+-\d+$/.test(href || '') &&
|
|
modal.querySelector(':scope > .assistant-export-sheet > .export-section > .refs > ' + href);
|
|
if (reference) {
|
|
event.preventDefault(); // Fragment navigation also fires the Back/popstate close handler.
|
|
reference.tabIndex = -1;
|
|
reference.focus({ preventScroll: true });
|
|
reference.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
}
|
|
if (event.target === modal) close();
|
|
});
|
|
try { window.history.pushState({ assistantExport: true }, '', window.location.href); } catch (e) {}
|
|
window.addEventListener('popstate', close, { once: true });
|
|
}
|
|
|
|
function closeInlineExport() {
|
|
if (inlineExportClose) inlineExportClose();
|
|
}
|
|
|
|
function buildPrintableChatHtml(items, imageSrc, inline) {
|
|
return '<!doctype html><html><head><title>Clinical Assistant Export</title>' +
|
|
'<style>' + exportWindowCss() + exportTableScrollCss() + '</style>' +
|
|
'</head><body>' + buildPrintableChatBody(items, imageSrc, inline) + '</body></html>';
|
|
}
|
|
|
|
function buildPrintableChatBody(items, imageSrc, inline) {
|
|
items = Array.isArray(items) && items.length ? items : [];
|
|
var imageHtml = imageSrc ? '<h2>Generated Image</h2><div class="export-image"><img src="' + escapeAttr(imageSrc) + '" alt="Generated clinical visual"></div>' : '';
|
|
var sections = items.map(function (item, idx) {
|
|
var sources = Array.isArray(item.sources) ? item.sources : [];
|
|
var heading = item.heading || deriveExportHeading(item.question, idx);
|
|
var summary = item.summary || '';
|
|
var answer = item.answer || '';
|
|
var renderOptions = { citationLabel: 'number', citationTargetPrefix: 'ref-' + (idx + 1) + '-', notice: item.notice };
|
|
var answerHtml = renderMarkdown(answer, sources, renderOptions);
|
|
// Read rendered citations so bare source-column numbers also get refs.
|
|
var cited = Array.from(answerHtml.matchAll(/data-source-number="(\d+)"/g), function(match) { return match[1]; });
|
|
var refs = renderExportRefs(sources, idx + 1, cited);
|
|
return '<section class="export-section">' +
|
|
'<h2>' + escapeHtml(heading) + '</h2>' +
|
|
'<div class="question"><strong>Question:</strong> ' + escapeHtml(item.question || '') + '</div>' +
|
|
(summary ? '<h3>Summary</h3><div class="answer">' + renderMarkdown(summary, sources, renderOptions) + '</div>' : '') +
|
|
'<h3>Full Generated Answer</h3><div class="answer full-answer">' + answerHtml + '</div>' +
|
|
(item.images || []).map(function(src) { return '<div class="export-image"><img alt="Generated teaching visual" src="' + escapeAttr(src) + '"></div>'; }).join('') +
|
|
(refs ? '<h3>References</h3><ol class="refs">' + refs + '</ol>' : '') +
|
|
'</section>';
|
|
}).join('');
|
|
return '<div class="assistant-export-actions"><button id="assistant-export-print" type="button">Print / Save PDF</button><button id="assistant-export-close" type="button">Close</button></div><h1>Clinical Assistant Export</h1>' +
|
|
'<div class="meta">Export generated ' + escapeHtml(new Date().toLocaleString()) + '</div>' +
|
|
imageHtml +
|
|
sections;
|
|
}
|
|
|
|
function renderMarkdown(md, sources, renderOptions) {
|
|
if (typeof options.renderMarkdown === 'function') return options.renderMarkdown(md, sources, renderOptions);
|
|
return escapeHtml(md);
|
|
}
|
|
|
|
return {
|
|
exportAnswerPdf: exportAnswerPdf,
|
|
invalidate: invalidate
|
|
};
|
|
}
|
|
|
|
function shouldUseInlineExport() {
|
|
var isCapacitor = !!(window.Capacitor && (!window.Capacitor.isNativePlatform || window.Capacitor.isNativePlatform()));
|
|
var isSmallTouch = window.matchMedia && window.matchMedia('(max-width: 700px), (pointer: coarse)').matches;
|
|
return isCapacitor || isSmallTouch;
|
|
}
|
|
|
|
async function saveInlineExport(html, owner) {
|
|
assertSharingOwner(owner);
|
|
var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null;
|
|
if (!plugins || !plugins.Filesystem) return false;
|
|
try {
|
|
var name = sharingFilename('clinical-assistant-export', 'html');
|
|
var saved = await plugins.Filesystem.writeFile({ path: name, data: utf8ToBase64(html), directory: 'CACHE' });
|
|
assertSharingOwner(owner);
|
|
if (plugins.Share && saved && saved.uri) {
|
|
try {
|
|
await plugins.Share.share({ title: 'Clinical Assistant Export', text: 'Open this export and use Print to save as PDF.', url: saved.uri, dialogTitle: 'Save or share export' });
|
|
assertSharingOwner(owner);
|
|
} catch (e) {
|
|
assertSharingOwner(owner);
|
|
if (e.name === 'AbortError' || !isShareCancel(e)) throw e;
|
|
}
|
|
}
|
|
if (typeof window.showToast === 'function') window.showToast('Export prepared', 'success');
|
|
return true;
|
|
} catch (e) {
|
|
assertSharingOwner(owner);
|
|
if (e.name === 'AbortError') throw e;
|
|
if (typeof window.showToast === 'function') window.showToast(e.message || 'Export failed', 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function printWithNativeBridge(html, owner) {
|
|
assertSharingOwner(owner);
|
|
if (!window.NativePrint || typeof window.NativePrint.printHtml !== 'function') return false;
|
|
try {
|
|
await window.NativePrint.printHtml('Clinical Assistant Export', utf8ToBase64(html));
|
|
assertSharingOwner(owner);
|
|
return true;
|
|
} catch (e) {
|
|
assertSharingOwner(owner);
|
|
if (e.name === 'AbortError') throw e;
|
|
if (typeof window.showToast === 'function') window.showToast(e.message || 'Native print failed', 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function utf8ToBase64(value) {
|
|
var bytes = new TextEncoder().encode(String(value || ''));
|
|
var binary = '';
|
|
for (var i = 0; i < bytes.length; i += 0x8000) {
|
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
function isShareCancel(error) {
|
|
var message = String(error && (error.message || error.name) || '').toLowerCase();
|
|
return /cancel|abort|dismiss|user denied|share canceled/.test(message);
|
|
}
|
|
|
|
function exportWindowCss() {
|
|
return 'body{font-family:Arial,sans-serif;color:#111827;line-height:1.55;margin:36px;max-width:820px}.assistant-table-actions,.assistant-msg-actions,.assistant-code-copy,.assistant-translate-pop,.assistant-takehome-actions,.assistant-takehome-modal,.assistant-voice-overlay{display:none!important}.assistant-export-actions{display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px}.assistant-export-actions button{padding:8px 12px}h1{font-size:22px;margin:0 0 4px}h2{font-size:17px;margin-top:26px;border-bottom:1px solid #e5e7eb;padding-bottom:4px;break-after:avoid}h3{font-size:14px;margin:18px 0 8px;break-after:avoid}.meta,.question{font-size:12px;color:#6b7280;margin-bottom:12px}.answer{font-size:13px}.export-section{margin-top:20px;break-before:auto}.full-answer{page-break-before:auto}.export-image img{max-width:100%;border:1px solid #e5e7eb;border-radius:10px}.refs{font-size:12px;padding-left:20px;margin-top:8px;break-inside:auto}.refs li{margin:6px 0;break-inside:avoid}.assistant-cite{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 5px;margin:0 1px;border-radius:999px;background:#f3e8ff;color:#7c3aed;border:1px solid rgba(124,58,237,.22);font-size:9px;font-weight:800;line-height:16px;text-decoration:none;text-transform:uppercase;letter-spacing:.03em;vertical-align:baseline;white-space:nowrap;-webkit-print-color-adjust:exact;print-color-adjust:exact}.answer table{width:100%;border-collapse:collapse;table-layout:auto;margin:12px 0 18px;border:1px solid #e5e7eb;page-break-inside:auto}.answer th,.answer td{padding:7px 8px;border:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:normal;overflow-wrap:break-word}.answer th{background:#f9fafb;font-weight:700;-webkit-print-color-adjust:exact;print-color-adjust:exact}.answer th:last-child,.answer td:last-child{width:1%;white-space:nowrap}.answer tr{break-inside:avoid;page-break-inside:avoid}.answer thead{display:table-header-group}.answer tbody{display:table-row-group}.answer p{margin:8px 0}.answer ul,.answer ol{padding-left:20px}.answer li{margin:4px 0}@media print{.assistant-export-actions{display:none}body{margin:24mm}.export-section{break-inside:auto}.refs{break-before:avoid}}';
|
|
}
|
|
|
|
function inlineExportCss() {
|
|
return '#assistant-export-modal{position:fixed;inset:0;z-index:10000;background:rgba(15,23,42,.72);overflow:auto;padding:16px}#assistant-export-modal .assistant-export-sheet{box-sizing:border-box;background:white;color:#111827;font-family:Arial,sans-serif;line-height:1.55;max-width:860px;margin:0 auto 24px;padding:24px;border-radius:14px;box-shadow:0 24px 80px rgba(0,0,0,.35)}#assistant-export-modal .assistant-export-actions{display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px}#assistant-export-modal .assistant-export-actions button{padding:8px 12px}#assistant-export-modal h1{font-size:22px;margin:0 0 4px}#assistant-export-modal h2{font-size:17px;margin-top:26px;border-bottom:1px solid #e5e7eb;padding-bottom:4px;break-after:avoid}#assistant-export-modal h3{font-size:14px;margin:18px 0 8px;break-after:avoid}#assistant-export-modal .meta,#assistant-export-modal .question{font-size:12px;color:#6b7280;margin-bottom:12px}#assistant-export-modal .answer{font-size:13px}#assistant-export-modal .export-section{margin-top:20px;break-before:auto}#assistant-export-modal .full-answer{page-break-before:auto}#assistant-export-modal .export-image img{max-width:100%;border:1px solid #e5e7eb;border-radius:10px}#assistant-export-modal .refs{font-size:12px;padding-left:20px;margin-top:8px;break-inside:auto}#assistant-export-modal .refs li{margin:6px 0;break-inside:avoid}#assistant-export-modal .assistant-cite{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 5px;margin:0 1px;border-radius:999px;background:#f3e8ff;color:#7c3aed;border:1px solid rgba(124,58,237,.22);font-size:9px;font-weight:800;line-height:16px;text-decoration:none;text-transform:uppercase;letter-spacing:.03em;vertical-align:baseline;white-space:nowrap;-webkit-print-color-adjust:exact;print-color-adjust:exact}#assistant-export-modal .answer table{width:100%;border-collapse:collapse;table-layout:auto;margin:12px 0 18px;border:1px solid #e5e7eb;page-break-inside:auto}#assistant-export-modal .answer th,#assistant-export-modal .answer td{padding:7px 8px;border:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:normal;overflow-wrap:break-word}#assistant-export-modal .answer th{background:#f9fafb;font-weight:700;-webkit-print-color-adjust:exact;print-color-adjust:exact}#assistant-export-modal .answer th:last-child,#assistant-export-modal .answer td:last-child{width:1%;white-space:nowrap}#assistant-export-modal .answer tr{break-inside:avoid;page-break-inside:avoid}#assistant-export-modal .answer thead{display:table-header-group}#assistant-export-modal .answer tbody{display:table-row-group}#assistant-export-modal .answer p{margin:8px 0}#assistant-export-modal .answer ul,#assistant-export-modal .answer ol{padding-left:20px}#assistant-export-modal .answer li{margin:4px 0}@media print{body.assistant-export-open>*:not(#assistant-export-modal){display:none!important}#assistant-export-modal{position:static!important;inset:auto!important;background:white!important;overflow:visible!important;padding:0!important}#assistant-export-modal .assistant-export-sheet{max-width:none!important;margin:0!important;padding:0!important;border-radius:0!important;box-shadow:none!important}#assistant-export-modal .assistant-export-actions{display:none!important}#assistant-export-modal .export-section{break-inside:auto}#assistant-export-modal .refs{break-before:avoid}}';
|
|
}
|
|
|
|
function exportTableScrollCss() {
|
|
return '.answer [align=right],#assistant-export-modal .answer [align=right]{text-align:right}.answer [align=center],#assistant-export-modal .answer [align=center]{text-align:center}.assistant-table-actions,.assistant-msg-actions,.assistant-code-copy,.assistant-translate-pop,.assistant-takehome-actions,.assistant-takehome-modal,.assistant-voice-overlay{display:none!important}.assistant-table-scroll{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;margin:12px 0 18px;border:1px solid #e5e7eb;border-radius:10px;background:white}.answer .assistant-table-scroll table,#assistant-export-modal .answer .assistant-table-scroll table{width:max-content;min-width:100%;max-width:none;margin:0;border:0;border-radius:0}.answer .assistant-table-scroll th,.answer .assistant-table-scroll td,#assistant-export-modal .answer .assistant-table-scroll th,#assistant-export-modal .answer .assistant-table-scroll td{min-width:120px;overflow-wrap:normal;word-break:normal}.assistant-table-scroll::after{content:"Swipe table";display:block;position:sticky;left:0;bottom:0;padding:3px 9px;font-size:10px;font-weight:700;color:#6b7280;background:linear-gradient(90deg,rgba(255,255,255,.95),rgba(255,255,255,0));pointer-events:none}@media print{.assistant-table-scroll{overflow:visible;border:0}.assistant-table-scroll::after{display:none}.answer .assistant-table-scroll table,#assistant-export-modal .answer .assistant-table-scroll table{width:100%;max-width:100%}}';
|
|
}
|
|
|
|
function collectExportItems(messages, lastAnswer, lastSources, presentMessage) {
|
|
var items = [];
|
|
var pendingQuestion = '';
|
|
(messages || []).forEach(function (m) {
|
|
if (m.role === 'user') {
|
|
pendingQuestion = m.content || pendingQuestion;
|
|
return;
|
|
}
|
|
if (m.role !== 'assistant' || !m.content) return;
|
|
if (isUtilityAssistantMessage(m.content)) return;
|
|
var display = typeof presentMessage === 'function' ? presentMessage(m) : { answer: m.content };
|
|
items.push({
|
|
question: pendingQuestion || 'Clinical question',
|
|
heading: deriveExportHeading(pendingQuestion, items.length),
|
|
summary: '',
|
|
answer: display.answer,
|
|
notice: display.notice,
|
|
images: m.images || [],
|
|
sources: Array.isArray(m.sources) && m.sources.length ? m.sources : lastSources
|
|
});
|
|
pendingQuestion = '';
|
|
});
|
|
if (!items.length && lastAnswer) {
|
|
items.push({ question: 'Clinical question', heading: 'Clinical Answer', summary: '', answer: lastAnswer, sources: lastSources });
|
|
}
|
|
return items;
|
|
}
|
|
|
|
function renderExportRefs(sources, sectionNumber, cited) {
|
|
return (sources || []).map(function (s, idx) {
|
|
var n = s.number || idx + 1;
|
|
if (cited.length && !cited.includes(String(n))) return '';
|
|
var title = s.title || s.resource || 'Untitled source';
|
|
var page = s.page || s.page_number || s.pageNumber;
|
|
return '<li id="ref-' + sectionNumber + '-' + escapeAttr(n) + '"><strong>[' + escapeHtml(n) + ']</strong> ' + escapeHtml(title) + (page ? ', page ' + escapeHtml(page) : '') + '.</li>';
|
|
}).join('');
|
|
}
|
|
|
|
function deriveExportHeading(question, idx) {
|
|
var text = String(question || '').replace(/\s+/g, ' ').trim();
|
|
if (!text || /^(what|which|when|why|how|and|also|what dose\??|dose\??)$/i.test(text)) return 'Clinical Question ' + (idx + 1);
|
|
return text.replace(/[?!.]+$/, '').slice(0, 90);
|
|
}
|
|
|
|
function isUtilityAssistantMessage(content) {
|
|
return /^I prepared the image prompt in the \*\*Image \/ Graph\*\* box/i.test(String(content || ''));
|
|
}
|
|
|
|
export function buildExportCacheKey(items, imageSrc) {
|
|
return JSON.stringify({ items: (items || []).map(function (item) {
|
|
return { q: item.question, a: item.answer, notice: item.notice, s: (item.sources || []).map(function (s) { return [s.number, s.title, s.page]; }) };
|
|
}), image: imageSrc ? '1' : '' });
|
|
}
|
|
|
|
async function preparePrivateExport(state, owner) {
|
|
assertSharingOwner(owner);
|
|
const messages = [];
|
|
for (const message of state.messages || []) {
|
|
const images = [];
|
|
for (const job of message.imageJobs || []) {
|
|
assertSharingOwner(owner);
|
|
const data = await imageJson('/api/clinical-assistant/image/jobs/' + job.jobId, {}, owner);
|
|
assertSharingOwner(owner);
|
|
if (data.status !== 'done') throw new Error('An image is not complete. Reopen image history before exporting; nothing was omitted.');
|
|
images.push(await imageDataUrl(data.imageUrl, owner));
|
|
assertSharingOwner(owner);
|
|
}
|
|
messages.push({ ...message, images });
|
|
}
|
|
let sidebarImage = state.lastGeneratedImageSrc || '';
|
|
if ((state.generatedImageJobs || []).length) {
|
|
assertSharingOwner(owner);
|
|
const data = await imageJson('/api/clinical-assistant/image/jobs/' + state.generatedImageJobs[0].jobId, {}, owner);
|
|
assertSharingOwner(owner);
|
|
if (data.status !== 'done') throw new Error('An image is not complete. Reopen image history before exporting; nothing was omitted.');
|
|
sidebarImage = data.imageUrl;
|
|
}
|
|
assertSharingOwner(owner);
|
|
const lastGeneratedImageSrc = await imageDataUrl(sidebarImage, owner);
|
|
assertSharingOwner(owner);
|
|
return { ...state, messages, lastGeneratedImageSrc };
|
|
}
|