fix mobile assistant export and image actions
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m53s
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m53s
This commit is contained in:
parent
6b485a77d4
commit
25464a90bf
13 changed files with 148 additions and 39 deletions
|
|
@ -19,8 +19,6 @@ jobs:
|
|||
- name: Prepare compose env files
|
||||
run: |
|
||||
touch .env
|
||||
mkdir -p /root/litellm
|
||||
touch /root/litellm/.env
|
||||
|
||||
- name: Validate Compose config
|
||||
run: docker compose -f docker-compose.yml config >/tmp/ped-ai-compose.yml
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ services:
|
|||
- "127.0.0.1:3552:3000"
|
||||
env_file:
|
||||
- .env
|
||||
- /root/litellm/.env
|
||||
environment:
|
||||
CLINICAL_ASSISTANT_MCP_URL: http://mcp:8000/mcp
|
||||
REDIS_URL: redis://ped-ai-redis:6379
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ android {
|
|||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Version values below are overwritten by scripts/release.sh from
|
||||
// the root package.json. versionCode auto-increments per release.
|
||||
versionCode 714000
|
||||
versionName "7.14.0"
|
||||
versionCode 714001
|
||||
versionName "7.14.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
|||
14
mobile/package-lock.json
generated
14
mobile/package-lock.json
generated
|
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"name": "pedscribe-mobile",
|
||||
"version": "7.14.0",
|
||||
"version": "7.14.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pedscribe-mobile",
|
||||
"version": "7.14.0",
|
||||
"version": "7.14.1",
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-biometric-auth": "^8.0.0",
|
||||
"@capacitor/android": "^6.0.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/cli": "^6.0.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/filesystem": "^6.0.4",
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/ios": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
|
|
@ -98,6 +99,15 @@
|
|||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/filesystem": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-6.0.4.tgz",
|
||||
"integrity": "sha512-eFlg/ZrwYA4Y6ClLRRikudVu2XvuZxfX/XC0ky9MgfbC9dyqTnVkkEoWM6vr1xR89YNY4mB0EeVTet1m1Jcumw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/haptics": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pedscribe-mobile",
|
||||
"version": "7.14.0",
|
||||
"version": "7.14.1",
|
||||
"description": "PedScribe native mobile app — Capacitor wrapper for Pediatric AI Scribe",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
@ -11,19 +11,20 @@
|
|||
"build:ios": "npx cap sync ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-biometric-auth": "^8.0.0",
|
||||
"@capacitor/android": "^6.0.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/cli": "^6.0.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/ios": "^6.0.0",
|
||||
"@capacitor/filesystem": "^6.0.4",
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/ios": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/push-notifications": "^6.0.0",
|
||||
"@capacitor/screen-orientation": "^6.0.0",
|
||||
"@capacitor/share": "^6.0.0",
|
||||
"@capacitor/splash-screen": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@aparajita/capacitor-biometric-auth": "^8.0.0",
|
||||
"capacitor-secure-storage-plugin": "^0.10.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pediatric-ai-scribe",
|
||||
"version": "7.10.1",
|
||||
"version": "7.14.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pediatric-ai-scribe",
|
||||
"version": "7.10.1",
|
||||
"version": "7.14.1",
|
||||
"dependencies": {
|
||||
"@marp-team/marp-cli": "^4.3.1",
|
||||
"@marp-team/marp-core": "^4.3.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pediatric-ai-scribe",
|
||||
"version": "7.14.0",
|
||||
"version": "7.14.1",
|
||||
"description": "AI-powered pediatric clinical documentation platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -118,9 +118,11 @@
|
|||
.assistant-bubble ul, .assistant-bubble ol { padding-left:20px; margin:8px 0; }
|
||||
.assistant-bubble li { margin:4px 0; }
|
||||
.assistant-bubble blockquote { margin:10px 0; padding:8px 12px; border-left:3px solid var(--blue); background:var(--blue-light); color:var(--g700); border-radius:8px; }
|
||||
.assistant-bubble table { width:100%; max-width:100%; border-collapse:separate; border-spacing:0; margin:12px 0; overflow:hidden; border:1px solid var(--g200); border-radius:10px; font-size:12px; }
|
||||
.assistant-table-scroll { max-width:100%; overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch; margin:12px 0; border:1px solid var(--g200); border-radius:12px; background:white; box-shadow:inset 0 -1px 0 rgba(0,0,0,.03); }
|
||||
.assistant-table-scroll table { width:max-content; min-width:100%; max-width:none; border-collapse:separate; border-spacing:0; margin:0; border:0; border-radius:0; font-size:12px; }
|
||||
.assistant-table-scroll::after { content:'Swipe table'; display:none; position:sticky; left:0; bottom:0; padding:3px 9px; font-size:10px; font-weight:700; color:var(--g500); background:linear-gradient(90deg,rgba(255,255,255,.95),rgba(255,255,255,0)); pointer-events:none; }
|
||||
.assistant-bubble th, .assistant-bubble td { padding:8px 10px; border-bottom:1px solid var(--g200); vertical-align:top; text-align:left; }
|
||||
.assistant-bubble th, .assistant-bubble td { overflow-wrap:anywhere; }
|
||||
.assistant-bubble th, .assistant-bubble td { overflow-wrap:normal; word-break:normal; min-width:120px; }
|
||||
.assistant-bubble th { background:var(--g50); font-weight:700; color:var(--g800); }
|
||||
.assistant-bubble tr:last-child td { border-bottom:0; }
|
||||
.assistant-bubble code { background:var(--g100); border-radius:4px; padding:1px 4px; }
|
||||
|
|
@ -150,10 +152,12 @@
|
|||
.assistant-generated-image { display:grid; gap:8px; }
|
||||
.assistant-generated-image img { width:100%; border-radius:10px; border:1px solid var(--g200); background:white; }
|
||||
.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; }
|
||||
.assistant-image-modal-card { position:relative; max-width:min(96vw,1200px); max-height:92vh; }
|
||||
.assistant-image-modal-card { position:relative; display:grid; gap:10px; max-width:min(96vw,1200px); max-height:92vh; }
|
||||
.assistant-image-modal-card img { max-width:100%; max-height:92vh; border-radius:14px; background:white; box-shadow:0 24px 80px rgba(0,0,0,.35); }
|
||||
.assistant-image-modal-close { position:absolute; top:-12px; right:-12px; width:34px; height:34px; border:0; border-radius:999px; background:white; color:var(--g800); font-size:22px; line-height:1; cursor:pointer; box-shadow:var(--shadow); }
|
||||
.assistant-image-modal-close { position:absolute; top:8px; right:8px; z-index:1; width:38px; height:38px; border:0; border-radius:999px; background:white; color:var(--g800); font-size:24px; line-height:1; cursor:pointer; box-shadow:var(--shadow); }
|
||||
.assistant-image-modal-cancel { justify-self:center; border:0; border-radius:999px; background:white; color:var(--g800); font-weight:700; padding:9px 14px; box-shadow:var(--shadow); cursor:pointer; }
|
||||
.assistant-sources { padding:10px 12px; display:grid; gap:8px; max-height:520px; overflow-y:auto; }
|
||||
.assistant-saved-chats { padding:10px 12px; display:grid; gap:8px; max-height:220px; overflow-y:auto; }
|
||||
.assistant-saved-chat { border:1px solid var(--g200); border-radius:10px; padding:8px; background:white; display:grid; gap:5px; }
|
||||
|
|
@ -190,7 +194,7 @@
|
|||
.assistant-messages { padding:10px; }
|
||||
.assistant-msg, .assistant-msg.user { max-width:100%; }
|
||||
.assistant-bubble { font-size:13px; padding:11px 12px; }
|
||||
.assistant-bubble table { display:block; overflow-x:auto; -webkit-overflow-scrolling:touch; }
|
||||
.assistant-table-scroll::after { display:block; }
|
||||
.assistant-composer { position:sticky; bottom:0; z-index:3; }
|
||||
.assistant-composer-footer { flex-direction:column; align-items:stretch; }
|
||||
.assistant-composer-footer .btn-generate { width:100%; }
|
||||
|
|
|
|||
|
|
@ -26,10 +26,17 @@ export function renderAssistantMarkdown(md, sources, options) {
|
|||
if (block.lang === 'chart' || block.lang === 'chartjs') return '<canvas class="assistant-chart" data-chart="' + escapeAttr(block.code) + '"></canvas>';
|
||||
return '<pre><code>' + escapeHtml(block.code) + '</code></pre>';
|
||||
});
|
||||
html = wrapTables(html);
|
||||
|
||||
return typeof opts.sanitize === 'function' ? opts.sanitize(html) : html;
|
||||
}
|
||||
|
||||
function wrapTables(html) {
|
||||
return String(html || '')
|
||||
.replace(/<table(\s[^>]*)?>/g, '<div class="assistant-table-scroll"><table$1>')
|
||||
.replace(/<\/table>/g, '</table></div>');
|
||||
}
|
||||
|
||||
export function renderCitationLinks(html, sources, options) {
|
||||
var opts = options || {};
|
||||
return String(html || '').replace(/\[((?:\d+\s*,\s*)*\d+)\]/g, function (match, cluster) {
|
||||
|
|
|
|||
|
|
@ -68,12 +68,22 @@ export function createAssistantExporter(options) {
|
|||
closeInlineExport();
|
||||
var modal = document.createElement('div');
|
||||
modal.id = 'assistant-export-modal';
|
||||
modal.innerHTML = '<style>' + inlineExportCss() + '</style><div class="assistant-export-sheet">' + buildPrintableChatBody(items, imageSrc, true) + '</div>';
|
||||
modal.innerHTML = '<style>' + inlineExportCss() + exportTableScrollCss() + '</style><div class="assistant-export-sheet">' + buildPrintableChatBody(items, imageSrc, true) + '</div>';
|
||||
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', function () { window.print(); });
|
||||
if (printBtn) printBtn.addEventListener('click', async function () {
|
||||
printBtn.disabled = true;
|
||||
var originalText = printBtn.textContent;
|
||||
printBtn.textContent = 'Preparing export...';
|
||||
try {
|
||||
if (!(await saveInlineExport(buildPrintableChatHtml(items, imageSrc, false)))) window.print();
|
||||
} finally {
|
||||
printBtn.disabled = false;
|
||||
printBtn.textContent = originalText;
|
||||
}
|
||||
});
|
||||
if (closeBtn) closeBtn.addEventListener('click', closeInlineExport);
|
||||
modal.addEventListener('click', function (event) {
|
||||
if (event.target === modal) closeInlineExport();
|
||||
|
|
@ -90,7 +100,7 @@ export function createAssistantExporter(options) {
|
|||
|
||||
function buildPrintableChatHtml(items, imageSrc, inline) {
|
||||
return '<!doctype html><html><head><title>Clinical Assistant Export</title>' +
|
||||
'<style>' + exportWindowCss() + '</style>' +
|
||||
'<style>' + exportWindowCss() + exportTableScrollCss() + '</style>' +
|
||||
'</head><body>' + buildPrintableChatBody(items, imageSrc, inline) + '</body></html>';
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +145,40 @@ function shouldUseInlineExport() {
|
|||
return isCapacitor || isSmallTouch;
|
||||
}
|
||||
|
||||
async function saveInlineExport(html) {
|
||||
var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null;
|
||||
if (!plugins || !plugins.Filesystem) return false;
|
||||
try {
|
||||
var name = 'clinical-assistant-export-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.html';
|
||||
var saved = await plugins.Filesystem.writeFile({ path: name, data: utf8ToBase64(html), directory: 'DOCUMENTS' });
|
||||
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' });
|
||||
} catch (e) {
|
||||
if (!isShareCancel(e)) throw e;
|
||||
}
|
||||
}
|
||||
if (typeof window.showToast === 'function') window.showToast('Export saved to Documents', 'success');
|
||||
return true;
|
||||
} catch (e) {
|
||||
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-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}}';
|
||||
}
|
||||
|
|
@ -143,6 +187,10 @@ 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 '.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) {
|
||||
var items = [];
|
||||
var pendingQuestion = '';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { escapeAttr } from './citations.js';
|
|||
export function createAssistantImageStore() {
|
||||
var generatedImages = {};
|
||||
var generatedImageSeq = 0;
|
||||
var previewKeyHandler = null;
|
||||
|
||||
function renderGeneratedImage(src, alt) {
|
||||
var id = 'img-' + (++generatedImageSeq);
|
||||
|
|
@ -20,25 +21,44 @@ export function createAssistantImageStore() {
|
|||
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>';
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
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"><button type="button" class="assistant-image-modal-cancel">Close preview</button></div>';
|
||||
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;
|
||||
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();
|
||||
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;
|
||||
}
|
||||
var a = document.createElement('a');
|
||||
a.href = src;
|
||||
a.download = 'clinical-visual.png';
|
||||
a.rel = 'noopener';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
} catch (e) {
|
||||
if (typeof window.showToast === 'function') window.showToast(e.message || 'Image download failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
|
|
@ -56,14 +76,15 @@ export function createAssistantImageStore() {
|
|||
}
|
||||
|
||||
async function saveWithNativeShare(src) {
|
||||
if (await shareWithWebFile(src)) return true;
|
||||
var webShare = await shareWithWebFile(src);
|
||||
if (webShare !== 'unavailable') 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; }
|
||||
} catch (e) { return isShareCancel(e); }
|
||||
}
|
||||
if (!plugins.Filesystem) return false;
|
||||
try {
|
||||
|
|
@ -71,7 +92,11 @@ async function saveWithNativeShare(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' });
|
||||
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 saved to Documents', 'success');
|
||||
return true;
|
||||
|
|
@ -82,14 +107,23 @@ async function saveWithNativeShare(src) {
|
|||
}
|
||||
|
||||
async function shareWithWebFile(src) {
|
||||
if (!navigator.share || !navigator.canShare || typeof File === 'undefined') return false;
|
||||
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 false;
|
||||
if (!navigator.canShare({ files: [file] })) return 'unavailable';
|
||||
await navigator.share({ files: [file], title: 'Clinical visual', text: 'Clinical Assistant generated visual' });
|
||||
return true;
|
||||
} catch (e) { return false; }
|
||||
return 'shared';
|
||||
} catch (e) { return isShareCancel(e) ? 'cancelled' : 'unavailable'; }
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ import {
|
|||
})
|
||||
.then(function (data) {
|
||||
if (!data.success) throw new Error(data.error || 'Image generation failed');
|
||||
var src = data.imageUrl || data.url || (data.base64 ? ('data:image/png;base64,' + data.base64) : '');
|
||||
var src = data.base64 ? ('data:image/png;base64,' + data.base64) : (data.imageUrl || data.url || '');
|
||||
if (!src) throw new Error('No image returned');
|
||||
lastGeneratedImageSrc = src;
|
||||
exporter.invalidate();
|
||||
|
|
|
|||
|
|
@ -603,7 +603,15 @@ async function generateImage(prompt, model) {
|
|||
return generateImageRequest(model, renderedPrompt, '1024x1024', headers);
|
||||
});
|
||||
var item = resp.data && resp.data.data && resp.data.data[0] ? resp.data.data[0] : {};
|
||||
return { imageUrl: item.url || null, base64: item.b64_json || null, raw: (!item.url && !item.b64_json) ? resp.data : undefined };
|
||||
var base64 = item.b64_json || null;
|
||||
if (!base64 && item.url) base64 = await fetchImageUrlAsBase64(item.url).catch(function() { return null; });
|
||||
return { imageUrl: item.url || null, base64: base64, raw: (!item.url && !base64) ? resp.data : undefined };
|
||||
}
|
||||
|
||||
async function fetchImageUrlAsBase64(url) {
|
||||
if (!/^https?:\/\//i.test(String(url || ''))) return null;
|
||||
var resp = await axios.get(url, { responseType: 'arraybuffer', timeout: 60000 });
|
||||
return Buffer.from(resp.data).toString('base64');
|
||||
}
|
||||
|
||||
async function runImageJob(id, userId, prompt, model, req) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue