From 66b0a8fe603e06e1585e6fbc8976a7c5966b7121 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 01:19:12 +0200 Subject: [PATCH] split assistant frontend modules --- public/js/assistant/export.js | 154 ++++++++++++++++++++ public/js/assistant/images.js | 59 ++++++++ public/js/assistant/sources.js | 47 +++++++ public/js/clinicalAssistant.js | 250 +++------------------------------ 4 files changed, 282 insertions(+), 228 deletions(-) create mode 100644 public/js/assistant/export.js create mode 100644 public/js/assistant/images.js create mode 100644 public/js/assistant/sources.js diff --git a/public/js/assistant/export.js b/public/js/assistant/export.js new file mode 100644 index 0000000..d202f20 --- /dev/null +++ b/public/js/assistant/export.js @@ -0,0 +1,154 @@ +import { escapeAttr, escapeHtml } from './citations.js'; + +export function createAssistantExporter(options) { + options = options || {}; + var exportCacheKey = ''; + var exportCacheItems = null; + + function invalidate() { + exportCacheKey = ''; + exportCacheItems = null; + } + + function exportAnswerPdf(state) { + state = state || {}; + if (!state.lastAnswer) { + if (typeof options.showToast === 'function') options.showToast('No answer to export', 'error'); + return; + } + var exportItems = collectExportItems(state.messages || [], state.lastAnswer, state.lastSources || []); + var cacheKey = buildExportCacheKey(exportItems, state.lastGeneratedImageSrc || ''); + var doc = openExportWindow(); + if (!doc) { + if (typeof options.showToast === 'function') options.showToast('Allow popups to export PDF', 'error'); + return; + } + if (exportCacheKey === cacheKey && exportCacheItems) { + writePrintableChatExport(doc, exportCacheItems, state.lastGeneratedImageSrc || ''); + return; + } + exportCacheKey = cacheKey; + exportCacheItems = exportItems; + writePrintableChatExport(doc, exportItems, state.lastGeneratedImageSrc || ''); + } + + function openExportWindow() { + var doc = window.open('', '_blank', 'width=900,height=1100'); + if (!doc) return null; + doc.document.open(); + doc.document.write('Preparing Clinical Assistant Export

Preparing PDF export...

'); + doc.document.close(); + return doc; + } + + function writePrintableChatExport(doc, items, imageSrc) { + if (!doc || doc.closed) return; + items = Array.isArray(items) && items.length ? items : []; + var imageHtml = imageSrc ? '

Generated Image

Generated clinical visual
' : ''; + 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 citedNumbers = extractCitedSourceNumbers([summary, answer].filter(Boolean).join('\n\n')); + var refs = renderExportRefs(sources, idx + 1, citedNumbers); + return '
' + + '

' + escapeHtml(heading) + '

' + + '
Question: ' + escapeHtml(item.question || '') + '
' + + (summary ? '

Summary

' + renderMarkdown(summary, sources, { citationLabel: 'number' }) + '
' : '') + + '

Full Generated Answer

' + renderMarkdown(answer, sources, { citationLabel: 'number' }) + '
' + + (refs ? '

References

    ' + refs + '
' : '') + + '
'; + }).join(''); + var html = 'Clinical Assistant Export' + + '' + + '

Clinical Assistant Export

' + + '
Export generated ' + escapeHtml(new Date().toLocaleString()) + '
' + + imageHtml + + sections + ''; + doc.document.open(); + doc.document.write(html); + doc.document.close(); + try { + var printBtn = doc.document.getElementById('assistant-export-print'); + if (printBtn) printBtn.addEventListener('click', function () { doc.focus(); doc.print(); }); + } catch (e) {} + setTimeout(function () { try { doc.focus(); doc.print(); } catch (e) {} }, 500); + } + + 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 collectExportItems(messages, lastAnswer, lastSources) { + 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; + items.push({ + question: pendingQuestion || 'Clinical question', + heading: deriveExportHeading(pendingQuestion, items.length), + summary: '', + answer: m.content, + 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, citedNumbers) { + var cited = citedNumbers && citedNumbers.length ? new Set(citedNumbers.map(String)) : null; + return (sources || []).filter(function (s, idx) { + var n = s.number || idx + 1; + return !cited || cited.has(String(n)); + }).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) : '') + '.
  • '; + }).join(''); +} + +function extractCitedSourceNumbers(text) { + var found = new Set(); + String(text || '').replace(/\[(\d+(?:\s*,\s*\d+)*)\]/g, function (_, nums) { + nums.split(',').forEach(function (n) { + n = String(n || '').trim(); + if (n) found.add(n); + }); + return _; + }); + return Array.from(found).sort(function (a, b) { return Number(a) - Number(b); }); +} + +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, s: (item.sources || []).map(function (s) { return [s.number, s.title, s.page]; }) }; + }), image: imageSrc ? '1' : '' }); +} diff --git a/public/js/assistant/images.js b/public/js/assistant/images.js new file mode 100644 index 0000000..6351a46 --- /dev/null +++ b/public/js/assistant/images.js @@ -0,0 +1,59 @@ +import { escapeAttr } from './citations.js'; + +export function createAssistantImageStore() { + var generatedImages = {}; + var generatedImageSeq = 0; + + function renderGeneratedImage(src, alt) { + var id = 'img-' + (++generatedImageSeq); + generatedImages[id] = src; + return '
    ' + escapeAttr(alt || 'Generated image') + '' + + '
    ' + + '' + + ' Download' + + '
    '; + } + + function openImagePreview(id) { + var src = generatedImages[id]; + if (!src) return; + closeImagePreview(); + var modal = document.createElement('div'); + modal.className = 'assistant-image-modal'; + modal.innerHTML = '
    Generated clinical visual
    '; + document.body.appendChild(modal); + } + + function closeImagePreview() { + document.querySelectorAll('.assistant-image-modal').forEach(function (el) { el.remove(); }); + } + + function clear() { + generatedImages = {}; + closeImagePreview(); + } + + return { + renderGeneratedImage: renderGeneratedImage, + openImagePreview: openImagePreview, + closeImagePreview: closeImagePreview, + clear: clear + }; +} + +export function buildContextualImagePrompt(request, lastAnswer, lastSources) { + var prompt = String(request || '').trim(); + if (!lastAnswer) return prompt; + var sourceText = (lastSources || []).slice(0, 6).map(function (s, idx) { + return '[' + (s.number || idx + 1) + '] ' + (s.title || s.resource || 'Source') + (s.page ? ', page ' + s.page : ''); + }).join('\n'); + return prompt + '\n\nUse this clinical answer as the required context. Do not switch topics or introduce unrelated scenes such as gardening. Create a medical teaching visual faithful to the answer.\n\nAnswer:\n' + String(lastAnswer || '').slice(0, 3000) + '\n\nSources:\n' + sourceText; +} + +export function isImageRequest(text) { + text = String(text || '').trim(); + return /^(image|photo|picture|visual|illustration|diagram|figure|flowchart|infographic)$/i.test(text) || + /\b(show|see|display|view|image|photo|picture|visual|illustration|diagram|figure)\b/i.test(text) && text.split(/\s+/).length <= 6 || + /\b(generate|create|draw|make|render)\b[\s\S]{0,80}\b(image|picture|illustration|visual|infographic|diagram|flowchart)\b/i.test(text) || + /\b(generate|create|draw|make|render)\b[\s\S]{0,80}\b(an?|the)?\s*(algorithm|pathway|poster|teaching visual)\b/i.test(text); +} diff --git a/public/js/assistant/sources.js b/public/js/assistant/sources.js new file mode 100644 index 0000000..832f38a --- /dev/null +++ b/public/js/assistant/sources.js @@ -0,0 +1,47 @@ +import { escapeHtml } from './citations.js'; + +export function renderSourcesList(sources) { + if (!sources || sources.length === 0) return '

    No citations returned.

    '; + return sources.map(function (s, idx) { + var n = s.number || idx + 1; + var page = s.page || s.page_number || s.pageNumber; + var meta = []; + if (page) meta.push('page ' + page); + if (s.source_type === 'multimodal_page') meta.push('visual PDF page'); + if (s.visual_caption_source) meta.push('caption: ' + s.visual_caption_source); + if (s.visual_kind || s.source_priority) meta.push(s.visual_kind || s.source_priority); + if (s.category) meta.push(s.category); + if (s.doc_type || s.type) meta.push(s.doc_type || s.type); + if (s.score != null) meta.push('score ' + Number(s.score).toFixed(3)); + return '
    ' + + '[' + n + '] ' + escapeHtml(s.title || s.resource || 'Untitled source') + '' + + renderSourceBadges(s) + + '
    ' + escapeHtml(meta.join(' · ') || 'indexed source') + '
    ' + + (s.excerpt ? '

    ' + escapeHtml(cleanSourceExcerpt(s.excerpt).slice(0, 900)) + '

    ' : '') + + '
    '; + }).join(''); +} + +function renderSourceBadges(source) { + var badges = []; + if (source.source_priority) badges.push(source.source_priority); + if (source.visual_kind && badges.indexOf(source.visual_kind) === -1) badges.push(source.visual_kind); + if (source.visual_caption_source && badges.indexOf(source.visual_caption_source) === -1) badges.push(source.visual_caption_source); + if (source.category) badges.push(source.category); + if (source.source_boost && Number(source.source_boost) !== 1) badges.push(Number(source.source_boost).toFixed(2) + 'x'); + if (!badges.length) return ''; + return '
    ' + badges.slice(0, 4).map(function (badge) { + return '' + escapeHtml(badge) + ''; + }).join('') + '
    '; +} + +function cleanSourceExcerpt(text) { + return String(text || '') + .replace(/^\[Page-image match\]\s*/i, '') + .replace(//gi, ' ') + .replace(/\*\*/g, '') + .replace(/\|\s*-{2,}\s*/g, ' ') + .replace(/\|/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index be5225b..d13e771 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -5,6 +5,9 @@ // ============================================================ import { EMPTY_PROMPT_SETS } from './assistant/data.js'; import { escapeAttr, escapeHtml, renderAssistantMarkdown } from './assistant/citations.js'; +import { renderSourcesList } from './assistant/sources.js'; +import { createAssistantExporter } from './assistant/export.js'; +import { buildContextualImagePrompt, createAssistantImageStore, isImageRequest } from './assistant/images.js'; import { deleteSavedAssistantChat, fetchAssistantChat, @@ -26,13 +29,11 @@ import { var lastSources = []; var mermaidReady = false; var dynamicExamples = []; - var generatedImages = {}; - var generatedImageSeq = 0; var lastGeneratedImageSrc = ''; - var exportCacheKey = ''; - var exportCacheItems = null; var markdownRenderer = null; var assistantBusy = false; + var exporter = createAssistantExporter({ renderMarkdown: renderMarkdown, showToast: window.showToast }); + var imageStore = createAssistantImageStore(); document.addEventListener('tabChanged', function (e) { if (e.detail && e.detail.tab === 'assistant') initIfNeeded(); @@ -120,8 +121,7 @@ import { } appendMessage('user', text); - exportCacheKey = ''; - exportCacheItems = null; + exporter.invalidate(); if (input) input.value = ''; if (isImageRequest(text)) { @@ -384,41 +384,7 @@ import { function renderSources(sources) { var wrap = document.getElementById('assistant-sources'); if (!wrap) return; - if (!sources || sources.length === 0) { - wrap.innerHTML = '

    No citations returned.

    '; - return; - } - wrap.innerHTML = sources.map(function (s, idx) { - var n = s.number || idx + 1; - var page = s.page || s.page_number || s.pageNumber; - var meta = []; - if (page) meta.push('page ' + page); - if (s.source_type === 'multimodal_page') meta.push('visual PDF page'); - if (s.visual_caption_source) meta.push('caption: ' + s.visual_caption_source); - if (s.visual_kind || s.source_priority) meta.push(s.visual_kind || s.source_priority); - if (s.category) meta.push(s.category); - if (s.doc_type || s.type) meta.push(s.doc_type || s.type); - if (s.score != null) meta.push('score ' + Number(s.score).toFixed(3)); - return '
    ' + - '[' + n + '] ' + escapeHtml(s.title || s.resource || 'Untitled source') + '' + - renderSourceBadges(s) + - '
    ' + escapeHtml(meta.join(' · ') || 'indexed source') + '
    ' + - (s.excerpt ? '

    ' + escapeHtml(cleanSourceExcerpt(s.excerpt).slice(0, 900)) + '

    ' : '') + - '
    '; - }).join(''); - } - - function renderSourceBadges(source) { - var badges = []; - if (source.source_priority) badges.push(source.source_priority); - if (source.visual_kind && badges.indexOf(source.visual_kind) === -1) badges.push(source.visual_kind); - if (source.visual_caption_source && badges.indexOf(source.visual_caption_source) === -1) badges.push(source.visual_caption_source); - if (source.category) badges.push(source.category); - if (source.source_boost && Number(source.source_boost) !== 1) badges.push(Number(source.source_boost).toFixed(2) + 'x'); - if (!badges.length) return ''; - return '
    ' + badges.slice(0, 4).map(function (badge) { - return '' + escapeHtml(badge) + ''; - }).join('') + '
    '; + wrap.innerHTML = renderSourcesList(sources); } function renderEmbeddedBlocks(root) { @@ -475,12 +441,11 @@ import { 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'); + exporter.invalidate(); + if (out) out.innerHTML = imageStore.renderGeneratedImage(src, 'Generated clinical visual'); if (fromChat) { setBusy(false, 'Ready'); - var html = renderGeneratedImage(src, 'Generated clinical visual'); + var html = imageStore.renderGeneratedImage(src, 'Generated clinical visual'); replaceLoadingMessage(loading, html, [], [], true); } }) @@ -492,16 +457,6 @@ import { }); } - function renderGeneratedImage(src, alt) { - var id = 'img-' + (++generatedImageSeq); - generatedImages[id] = src; - return '
    ' + escapeAttr(alt || 'Generated image') + '' + - '
    ' + - '' + - ' Download' + - '
    '; - } - function onAssistantDocumentClick(e) { var loadBtn = e.target.closest('[data-assistant-load-chat]'); if (loadBtn) { @@ -518,50 +473,25 @@ import { var openBtn = e.target.closest('[data-assistant-open-image]'); if (openBtn) { e.preventDefault(); - openImagePreview(openBtn.getAttribute('data-assistant-open-image')); + imageStore.openImagePreview(openBtn.getAttribute('data-assistant-open-image')); return; } if (e.target.closest('.assistant-image-modal-close') || e.target.classList.contains('assistant-image-modal')) { - closeImagePreview(); + imageStore.closeImagePreview(); } } - function openImagePreview(id) { - var src = generatedImages[id]; - if (!src) return; - closeImagePreview(); - var modal = document.createElement('div'); - modal.className = 'assistant-image-modal'; - modal.innerHTML = '
    Generated clinical visual
    '; - document.body.appendChild(modal); - } - - function closeImagePreview() { - document.querySelectorAll('.assistant-image-modal').forEach(function (el) { el.remove(); }); - } - function clearGeneratedImage() { lastGeneratedImageSrc = ''; - generatedImages = {}; + imageStore.clear(); 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; - var sourceText = lastSources.slice(0, 6).map(function (s, idx) { - return '[' + (s.number || idx + 1) + '] ' + (s.title || s.resource || 'Source') + (s.page ? ', page ' + s.page : ''); - }).join('\n'); - return prompt + '\n\nUse this clinical answer as the required context. Do not switch topics or introduce unrelated scenes such as gardening. Create a medical teaching visual faithful to the answer.\n\nAnswer:\n' + lastAnswer.slice(0, 3000) + '\n\nSources:\n' + sourceText; + exporter.invalidate(); } function prepareSidebarImagePrompt(request) { var promptEl = document.getElementById('assistant-image-prompt'); - var prompt = buildContextualImagePrompt(request); + var prompt = buildContextualImagePrompt(request, lastAnswer, lastSources); if (promptEl) { promptEl.value = prompt; promptEl.focus(); @@ -575,8 +505,7 @@ import { lastAnswer = ''; lastSources = []; lastGeneratedImageSrc = ''; - exportCacheKey = ''; - exportCacheItems = null; + exporter.invalidate(); var wrap = document.getElementById('assistant-messages'); if (wrap) { wrap.innerHTML = renderEmptyState(); @@ -633,122 +562,12 @@ import { } function exportAnswerPdf() { - if (!lastAnswer) { if (typeof showToast === 'function') showToast('No answer to export', 'error'); return; } - var exportItems = collectExportItems(); - var cacheKey = buildExportCacheKey(exportItems, lastGeneratedImageSrc); - var doc = openExportWindow(); - if (!doc) { if (typeof showToast === 'function') showToast('Allow popups to export PDF', 'error'); return; } - if (exportCacheKey === cacheKey && exportCacheItems) { - writePrintableChatExport(doc, exportCacheItems, lastGeneratedImageSrc); - return; - } - exportCacheKey = cacheKey; - exportCacheItems = exportItems; - writePrintableChatExport(doc, exportItems, lastGeneratedImageSrc); - } - - function openExportWindow() { - var doc = window.open('', '_blank', 'width=900,height=1100'); - if (!doc) return null; - doc.document.open(); - doc.document.write('Preparing Clinical Assistant Export

    Preparing PDF export...

    '); - doc.document.close(); - return doc; - } - - function writePrintableChatExport(doc, items, imageSrc) { - if (!doc || doc.closed) return; - items = Array.isArray(items) && items.length ? items : collectExportItems(); - var imageHtml = imageSrc ? '

    Generated Image

    Generated clinical visual
    ' : ''; - 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 citedNumbers = extractCitedSourceNumbers([summary, answer].filter(Boolean).join('\n\n')); - var refs = renderExportRefs(sources, idx + 1, citedNumbers); - return '
    ' + - '

    ' + escapeHtml(heading) + '

    ' + - '
    Question: ' + escapeHtml(item.question || '') + '
    ' + - (summary ? '

    Summary

    ' + renderMarkdown(summary, sources, { citationLabel: 'number' }) + '
    ' : '') + - '

    Full Generated Answer

    ' + renderMarkdown(answer, sources, { citationLabel: 'number' }) + '
    ' + - (refs ? '

    References

      ' + refs + '
    ' : '') + - '
    '; - }).join(''); - var html = 'Clinical Assistant Export' + - '' + - '

    Clinical Assistant Export

    ' + - '
    Export generated ' + escapeHtml(new Date().toLocaleString()) + '
    ' + - imageHtml + - sections + ''; - doc.document.open(); - doc.document.write(html); - doc.document.close(); - try { - var printBtn = doc.document.getElementById('assistant-export-print'); - if (printBtn) printBtn.addEventListener('click', function () { doc.focus(); doc.print(); }); - } catch (e) {} - setTimeout(function () { try { doc.focus(); doc.print(); } catch (e) {} }, 500); - } - - function collectExportItems() { - 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; - items.push({ - question: pendingQuestion || 'Clinical question', - heading: deriveExportHeading(pendingQuestion, items.length), - summary: '', - answer: m.content, - sources: Array.isArray(m.sources) && m.sources.length ? m.sources : lastSources - }); - pendingQuestion = ''; + exporter.exportAnswerPdf({ + messages: messages, + lastAnswer: lastAnswer, + lastSources: lastSources, + lastGeneratedImageSrc: lastGeneratedImageSrc }); - if (!items.length && lastAnswer) { - items.push({ question: 'Clinical question', heading: 'Clinical Answer', summary: '', answer: lastAnswer, sources: lastSources }); - } - return items; - } - - function renderExportRefs(sources, sectionNumber, citedNumbers) { - var cited = citedNumbers && citedNumbers.length ? new Set(citedNumbers.map(String)) : null; - return (sources || []).filter(function (s, idx) { - var n = s.number || idx + 1; - return !cited || cited.has(String(n)); - }).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) : '') + '.
  • '; - }).join(''); - } - - function extractCitedSourceNumbers(text) { - var found = new Set(); - String(text || '').replace(/\[(\d+(?:\s*,\s*\d+)*)\]/g, function (_, nums) { - nums.split(',').forEach(function (n) { - n = String(n || '').trim(); - if (n) found.add(n); - }); - return _; - }); - return Array.from(found).sort(function (a, b) { return Number(a) - Number(b); }); - } - - 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 || '')); } function saveCurrentChat() { @@ -859,7 +678,7 @@ import { } renderSources(lastSources); var out = document.getElementById('assistant-visual-output'); - if (out) out.innerHTML = lastGeneratedImageSrc ? renderGeneratedImage(lastGeneratedImageSrc, 'Generated clinical visual') : ''; + if (out) out.innerHTML = lastGeneratedImageSrc ? imageStore.renderGeneratedImage(lastGeneratedImageSrc, 'Generated clinical visual') : ''; } function appendMessageNode(role, content, sources) { @@ -884,28 +703,11 @@ import { return String(first && first.content || 'Clinical assistant chat').replace(/\s+/g, ' ').trim().slice(0, 80); } - function cleanSourceExcerpt(text) { - return String(text || '') - .replace(/^\[Page-image match\]\s*/i, '') - .replace(//gi, ' ') - .replace(/\*\*/g, '') - .replace(/\|\s*-{2,}\s*/g, ' ') - .replace(/\|/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - } - function imageForSavedChatPayload(src) { src = String(src || ''); return /^https?:\/\//i.test(src) ? src : ''; } - function buildExportCacheKey(items, imageSrc) { - return JSON.stringify({ items: items.map(function (item) { - return { q: item.question, a: item.answer, s: (item.sources || []).map(function (s) { return [s.number, s.title, s.page]; }) }; - }), image: imageSrc ? '1' : '' }); - } - function lastAssistantMessage(items) { for (var i = items.length - 1; i >= 0; i--) if (items[i].role === 'assistant') return items[i].content; return ''; @@ -920,14 +722,6 @@ import { return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } - function isImageRequest(text) { - text = String(text || '').trim(); - return /^(image|photo|picture|visual|illustration|diagram|figure|flowchart|infographic)$/i.test(text) || - /\b(show|see|display|view|image|photo|picture|visual|illustration|diagram|figure)\b/i.test(text) && text.split(/\s+/).length <= 6 || - /\b(generate|create|draw|make|render)\b[\s\S]{0,80}\b(image|picture|illustration|visual|infographic|diagram|flowchart)\b/i.test(text) || - /\b(generate|create|draw|make|render)\b[\s\S]{0,80}\b(an?|the)?\s*(algorithm|pathway|poster|teaching visual)\b/i.test(text); - } - function setBusy(isBusy, text, isError) { assistantBusy = !!isBusy; var status = document.getElementById('assistant-status');