diff --git a/public/css/assistant.css b/public/css/assistant.css index 7f630b8a..b699cc5b 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -121,8 +121,11 @@ /* Sources: a full-page right column; the panel fills the height and scrolls */ .assistant-side { display:flex; flex-direction:column; gap:12px; height:100%; min-height:0; } .assistant-side .card { flex:1 1 auto; display:flex; flex-direction:column; min-height:0; } -.assistant-generated-image { display:grid; gap:8px; } -.assistant-generated-image img { width:100%; border-radius:10px; border:1px solid var(--g200); background:white; } +.assistant-generated-image { display:grid; gap:8px; justify-items:start; } +/* Thumbnail in the transcript; Preview (or clicking it) opens full resolution. */ +.assistant-generated-image img { width:auto; max-width:min(100%,320px); max-height:240px; object-fit:contain; border-radius:10px; border:1px solid var(--g200); background:white; cursor:zoom-in; } +.assistant-generated-image img:focus-visible { outline:2px solid var(--blue); outline-offset:2px; } +@media (max-width:960px) { .assistant-generated-image img { max-width:100%; max-height:200px; } } .assistant-image-actions { display:flex; gap:8px; flex-wrap:wrap; } .assistant-image-preview-open { overflow:hidden; } .assistant-image-modal { position:fixed; inset:0; z-index:9999; background:rgba(15,23,42,.82); display:flex; align-items:center; justify-content:center; padding:24px; } diff --git a/public/js/assistant/citations.js b/public/js/assistant/citations.js index c33db861..1d873979 100644 --- a/public/js/assistant/citations.js +++ b/public/js/assistant/citations.js @@ -82,7 +82,7 @@ export function renderAssistantMarkdown(md, sources, options) { return typeof opts.sanitize === 'function' ? opts.sanitize(html) : html; } -function wrapTables(html) { +export function wrapTables(html) { return String(html || '') .replace(/]*)?>/g, '
') .replace(/<\/table>/g, '
'); diff --git a/public/js/assistant/images.js b/public/js/assistant/images.js index 171675e3..36e25987 100644 --- a/public/js/assistant/images.js +++ b/public/js/assistant/images.js @@ -10,7 +10,10 @@ export function createAssistantImageStore() { function renderGeneratedImage(src, alt, downloadUrl) { var id = 'img-' + (++generatedImageSeq); generatedImages[id] = { src: src, downloadUrl: downloadUrl || (assetPath(src) ? src + '?download=1' : '') }; - return '
' + escapeAttr(alt || 'Generated image') + '' + + // The in-chat image is a thumbnail; full resolution is one click away, so a + // long answer is not pushed off the screen by the picture that illustrates it. + return '
' + escapeAttr(alt || 'Generated image') +
+      '' + '
' + '' + '' + diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 846c131d..3bf0cca1 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -4,7 +4,7 @@ // server can call native MCP directly without routing through mcpo. // ============================================================ import { EMPTY_PROMPT_SETS } from './assistant/data.js'; -import { escapeAttr, escapeHtml, renderAssistantMarkdown, renderCitationLinks, safeImageUrl } from './assistant/citations.js'; +import { escapeAttr, escapeHtml, renderAssistantMarkdown, renderCitationLinks, safeImageUrl, wrapTables } from './assistant/citations.js'; import { renderSourcesList } from './assistant/sources.js'; import { createAssistantExporter } from './assistant/export.js'; import { createAssistantImageStore } from './assistant/images.js'; @@ -1506,13 +1506,24 @@ import { // ── Patient take home: plain-language summary + copy/export/email ──── var takehomeBusy = false; - var takehomeText = ''; // canonical original, never overwritten - var takehomeTranslated = ''; // current translation, '' when showing the original + var takehomeText = ''; // canonical original markdown, never overwritten + var takehomeTranslatedHtml = ''; // translated HTML, '' when showing the original var takehomeLang = ''; - // Copy / Export / Email must hand over what the parent is actually reading. + // The take home is translated as HTML for the same reason chat messages are: + // LibreTranslate's text mode destroys tables and emphasis markers. + function takehomeVisibleHtml() { + return takehomeTranslatedHtml + ? wrapTables(sanitize(takehomeTranslatedHtml)) + : renderMarkdown(takehomeText, [], {}); + } + + // Copy / Export / Email must hand over what the caregiver is actually reading. function takehomeVisibleText() { - return takehomeTranslated || takehomeText; + if (!takehomeTranslatedHtml) return takehomeText; + var holder = document.createElement('div'); + holder.innerHTML = sanitize(takehomeTranslatedHtml); + return String(holder.textContent || '').replace(/\n{3,}/g, '\n\n').trim(); } function openPatientTakehome() { @@ -1524,7 +1535,7 @@ import { closePatientTakehomeModal(); takehomeBusy = true; takehomeText = ''; - takehomeTranslated = ''; + takehomeTranslatedHtml = ''; takehomeLang = ''; var modal = document.createElement('div'); modal.className = 'assistant-takehome-modal'; @@ -1560,7 +1571,7 @@ import { if (result) { // Same universal markdown renderer as chat bubbles; raw text stays // canonical for Copy/Export/email. - result.innerHTML = renderMarkdown(takehomeText, [], {}); + result.innerHTML = takehomeVisibleHtml(); result.classList.remove('hidden'); } if (actions) actions.classList.remove('hidden'); @@ -1584,7 +1595,7 @@ import { function renderTakehomeBody() { var result = document.querySelector('#assistant-takehome-modal .assistant-takehome-result'); - if (result) result.innerHTML = renderMarkdown(takehomeVisibleText(), [], {}); + if (result) result.innerHTML = takehomeVisibleHtml(); } function translateTakehome(target) { @@ -1592,16 +1603,16 @@ import { if (!takehomeText) return; if (!target) { // back to the original takehomeLang = ''; - takehomeTranslated = ''; + takehomeTranslatedHtml = ''; renderTakehomeBody(); return; } if (select) select.disabled = true; - translateAssistantMessage(takehomeText, target, translateProvider, 'text') + translateAssistantMessage(simplifyHtmlForTranslation(renderMarkdown(takehomeText, [], {})), target, translateProvider, 'html') .then(function(data) { if (!data.success) throw new Error(data.error || 'Translation failed'); takehomeLang = target; - takehomeTranslated = String(data.translated || ''); + takehomeTranslatedHtml = String(data.translated || ''); renderTakehomeBody(); }) .catch(function(err) { @@ -1621,7 +1632,7 @@ import { return; } assertSharingOwner(owner); - var html = renderMarkdown(takehomeVisibleText(), [], {}); + var html = takehomeVisibleHtml(); doc.document.write('Patient Take Home' + '