136 lines
6.1 KiB
JavaScript
136 lines
6.1 KiB
JavaScript
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 '<div class="assistant-generated-image"><img src="' + escapeAttr(src) + '" alt="' + escapeAttr(alt || 'Generated image') + '">' +
|
|
'<div class="assistant-image-actions">' +
|
|
'<button type="button" class="btn-sm btn-ghost" data-assistant-open-image="' + escapeAttr(id) + '"><i class="fas fa-expand"></i> Preview</button>' +
|
|
'<button type="button" class="btn-sm btn-ghost" data-assistant-download-image="' + escapeAttr(id) + '"><i class="fas fa-download"></i> Download</button>' +
|
|
'</div></div>';
|
|
}
|
|
|
|
function openImagePreview(id) {
|
|
var src = generatedImages[id];
|
|
if (!src) return;
|
|
closeImagePreview();
|
|
var modal = document.createElement('div');
|
|
modal.className = 'assistant-image-modal';
|
|
modal.innerHTML = '<div class="assistant-image-modal-card"><button type="button" class="assistant-image-modal-close" aria-label="Close">×</button><img src="' + escapeAttr(src) + '" alt="Generated clinical visual"></div>';
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
function closeImagePreview() {
|
|
document.querySelectorAll('.assistant-image-modal').forEach(function (el) { el.remove(); });
|
|
}
|
|
|
|
async function downloadImage(id) {
|
|
var src = generatedImages[id];
|
|
if (!src) return;
|
|
if (await saveWithNativeShare(src)) return;
|
|
var a = document.createElement('a');
|
|
a.href = src;
|
|
a.download = 'clinical-visual.png';
|
|
a.rel = 'noopener';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|
|
|
|
function clear() {
|
|
generatedImages = {};
|
|
closeImagePreview();
|
|
}
|
|
|
|
return {
|
|
renderGeneratedImage: renderGeneratedImage,
|
|
openImagePreview: openImagePreview,
|
|
downloadImage: downloadImage,
|
|
closeImagePreview: closeImagePreview,
|
|
clear: clear
|
|
};
|
|
}
|
|
|
|
async function saveWithNativeShare(src) {
|
|
if (await shareWithWebFile(src)) return true;
|
|
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 false; }
|
|
}
|
|
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: 'DOCUMENTS' });
|
|
if (plugins.Share && saved && saved.uri) {
|
|
await plugins.Share.share({ title: 'Clinical visual', text: 'Clinical Assistant generated visual', url: saved.uri, dialogTitle: 'Save or share clinical visual' });
|
|
}
|
|
if (typeof window.showToast === 'function') window.showToast('Image saved to Documents', '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 false;
|
|
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 false;
|
|
await navigator.share({ files: [file], title: 'Clinical visual', text: 'Clinical Assistant generated visual' });
|
|
return true;
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
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);
|
|
}
|