import { escapeAttr } from './citations.js'; export function createAssistantImageStore() { var generatedImages = {}; var generatedImageSeq = 0; var previewKeyHandler = null; function renderGeneratedImage(src, alt) { var id = 'img-' + (++generatedImageSeq); generatedImages[id] = src; return '
' + escapeAttr(alt || 'Generated image') + '' + '
' + '' + '' + '
'; } function openImagePreview(id) { var src = generatedImages[id]; if (!src) return; closeImagePreview(); var modal = document.createElement('div'); modal.className = 'assistant-image-modal'; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.innerHTML = '
Generated clinical visual
'; modal.addEventListener('click', function (event) { if (event.target === modal || event.target.closest('.assistant-image-modal-close') || event.target.closest('.assistant-image-modal-cancel')) closeImagePreview(); }); previewKeyHandler = function (event) { if (event.key === 'Escape') closeImagePreview(); }; document.addEventListener('keydown', previewKeyHandler); document.body.appendChild(modal); document.body.classList.add('assistant-image-preview-open'); } function closeImagePreview() { document.querySelectorAll('.assistant-image-modal').forEach(function (el) { el.remove(); }); document.body.classList.remove('assistant-image-preview-open'); if (previewKeyHandler) document.removeEventListener('keydown', previewKeyHandler); previewKeyHandler = null; } async function downloadImage(id) { var src = generatedImages[id]; if (!src) return; try { if (await saveWithNativeShare(src)) return; if (isNativeApp()) { if (typeof window.showToast === 'function') window.showToast('Image saving is not available in this app build yet. Update the app and try again.', 'error'); return; } await downloadWithBrowser(src); } catch (e) { if (typeof window.showToast === 'function') window.showToast(e.message || 'Image download failed', 'error'); } } function clear() { generatedImages = {}; closeImagePreview(); } return { renderGeneratedImage: renderGeneratedImage, openImagePreview: openImagePreview, downloadImage: downloadImage, closeImagePreview: closeImagePreview, clear: clear }; } async function saveWithNativeShare(src) { if (isNativeApp()) { if (await saveWithNativeImageBridge(src)) return true; return await saveWithCapacitorShare(src); } var webShare = await shareWithWebFile(src); if (webShare !== 'unavailable') return true; return await saveWithCapacitorShare(src); } async function saveWithNativeImageBridge(src) { if (!window.NativeFiles || typeof window.NativeFiles.saveImage !== 'function') return false; try { var base64 = await imageSourceToBase64(src); var name = 'clinical-visual-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.png'; var result = String(window.NativeFiles.saveImage(name, base64) || ''); if (result.indexOf('saved:') === 0) { if (typeof window.showToast === 'function') window.showToast('Image saved to Photos', 'success'); return true; } if (result.indexOf('error:') === 0 && typeof window.showToast === 'function') { window.showToast(result.slice(6) || 'Native image save failed', 'error'); } } catch (e) { if (typeof window.showToast === 'function') window.showToast(e.message || 'Native image save failed', 'error'); } return false; } async function saveWithCapacitorShare(src) { var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null; if (!plugins) return false; if (!plugins.Filesystem && plugins.Share && !/^data:image\//i.test(src)) { try { await plugins.Share.share({ title: 'Clinical visual', text: 'Clinical Assistant generated visual', url: src, dialogTitle: 'Save or share clinical visual' }); return true; } catch (e) { return isShareCancel(e); } } if (!plugins.Filesystem) return false; try { var base64 = await imageSourceToBase64(src); var name = 'clinical-visual-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.png'; var saved = await plugins.Filesystem.writeFile({ path: name, data: base64, directory: 'CACHE' }); if (plugins.Share && saved && saved.uri) { try { await plugins.Share.share({ title: 'Clinical visual', text: 'Clinical Assistant generated visual', url: saved.uri, dialogTitle: 'Save or share clinical visual' }); } catch (e) { if (!isShareCancel(e)) throw e; } } if (typeof window.showToast === 'function') window.showToast('Image prepared', 'success'); return true; } catch (e) { if (typeof window.showToast === 'function') window.showToast('Could not save with the app. Opening browser download instead.', 'error'); return false; } } async function shareWithWebFile(src) { if (!navigator.share || !navigator.canShare || typeof File === 'undefined') return 'unavailable'; try { var blob = await imageSourceToBlob(src); var file = new File([blob], 'clinical-visual.png', { type: blob.type || 'image/png' }); if (!navigator.canShare({ files: [file] })) return 'unavailable'; await navigator.share({ files: [file], title: 'Clinical visual', text: 'Clinical Assistant generated visual' }); return 'shared'; } catch (e) { return isShareCancel(e) ? 'cancelled' : 'unavailable'; } } async function downloadWithBrowser(src) { var blob = await imageSourceToBlob(src); var objectUrl = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = objectUrl; a.download = 'clinical-visual.png'; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function() { URL.revokeObjectURL(objectUrl); }, 60000); if (isMobileBrowser() && typeof window.showToast === 'function') { window.showToast('If the download did not start, use Preview and long-press the image to save it.', 'info'); } } function isMobileBrowser() { return !isNativeApp() && /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || ''); } function isNativeApp() { return !!(window.Capacitor && (!window.Capacitor.isNativePlatform || window.Capacitor.isNativePlatform())); } function isShareCancel(error) { var message = String(error && (error.message || error.name) || '').toLowerCase(); return /cancel|abort|dismiss|user denied|share canceled/.test(message); } async function imageSourceToBase64(src) { if (/^data:image\//i.test(src)) return src.split(',')[1] || ''; var blob = await imageSourceToBlob(src); return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function() { resolve(String(reader.result || '').split(',')[1] || ''); }; reader.onerror = function() { reject(reader.error || new Error('Could not read image')); }; reader.readAsDataURL(blob); }); } async function imageSourceToBlob(src) { if (/^data:image\//i.test(src)) { var parts = src.split(','); var meta = parts[0] || 'data:image/png;base64'; var binary = atob(parts[1] || ''); var bytes = new Uint8Array(binary.length); for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return new Blob([bytes], { type: (meta.match(/data:([^;]+)/) || [])[1] || 'image/png' }); } var response = await fetch(src, { credentials: 'omit' }); if (!response.ok) throw new Error('Image download failed'); return response.blob(); } 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(); var visualNoun = /\b(image|photo|picture|visual|illustration|diagram|figure|flowchart|infographic)\b/i; return /^(image|photo|picture|visual|illustration|diagram|figure|flowchart|infographic)$/i.test(text) || (/\b(show|see|display|view)\b/i.test(text) && visualNoun.test(text) && text.split(/\s+/).length <= 8) || /\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); }