From 2ca969e099ffac816eec4b0062f7b0a0d414730c Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 9 Jun 2026 15:20:06 +0200 Subject: [PATCH] Fix generated image mobile downloads --- mobile/android/app/build.gradle | 4 ++-- mobile/package-lock.json | 4 ++-- mobile/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- public/js/assistant/images.js | 25 +++++++++++++++++++++---- public/js/clinicalAssistant.js | 5 +++-- src/routes/clinicalAssistant.js | 17 +++++++++++++++++ 8 files changed, 49 insertions(+), 14 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 1fd6196..6d36be4 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -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 714012 - versionName "7.14.12" + versionCode 714013 + versionName "7.14.13" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 69e1614..e0bd616 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "pedscribe-mobile", - "version": "7.14.12", + "version": "7.14.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pedscribe-mobile", - "version": "7.14.12", + "version": "7.14.13", "dependencies": { "@aparajita/capacitor-biometric-auth": "^8.0.0", "@capacitor/android": "^6.0.0", diff --git a/mobile/package.json b/mobile/package.json index 841898b..e8bfe92 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "pedscribe-mobile", - "version": "7.14.12", + "version": "7.14.13", "description": "PedScribe native mobile app — Capacitor wrapper for Pediatric AI Scribe", "private": true, "scripts": { diff --git a/package-lock.json b/package-lock.json index ca07615..ab9464e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.12", + "version": "7.14.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pediatric-ai-scribe", - "version": "7.14.12", + "version": "7.14.13", "dependencies": { "@marp-team/marp-cli": "^4.3.1", "@marp-team/marp-core": "^4.3.0", diff --git a/package.json b/package.json index e015bf0..4ccacc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.12", + "version": "7.14.13", "description": "AI-powered pediatric clinical documentation platform", "main": "server.js", "scripts": { diff --git a/public/js/assistant/images.js b/public/js/assistant/images.js index 417b910..ab5d2ee 100644 --- a/public/js/assistant/images.js +++ b/public/js/assistant/images.js @@ -5,9 +5,9 @@ export function createAssistantImageStore() { var generatedImageSeq = 0; var previewKeyHandler = null; - function renderGeneratedImage(src, alt) { + function renderGeneratedImage(src, alt, downloadUrl) { var id = 'img-' + (++generatedImageSeq); - generatedImages[id] = src; + generatedImages[id] = { src: src, downloadUrl: downloadUrl || '' }; return '
' + escapeAttr(alt || 'Generated image') + '' + '
' + '' + @@ -16,7 +16,8 @@ export function createAssistantImageStore() { } function openImagePreview(id) { - var src = generatedImages[id]; + var item = generatedImages[id]; + var src = item && (item.src || item); if (!src) return; closeImagePreview(); var modal = document.createElement('div'); @@ -41,10 +42,16 @@ export function createAssistantImageStore() { } async function downloadImage(id) { - var src = generatedImages[id]; + var item = generatedImages[id]; + var src = item && (item.src || item); + var downloadUrl = item && item.downloadUrl; if (!src) return; try { if (await saveWithNativeShare(src)) return; + if (downloadUrl) { + downloadFromServer(downloadUrl); + 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; @@ -160,6 +167,16 @@ async function downloadWithBrowser(src) { setTimeout(function() { URL.revokeObjectURL(objectUrl); }, 60000); } +function downloadFromServer(url) { + var a = document.createElement('a'); + a.href = url; + a.download = 'clinical-visual.png'; + a.rel = 'noopener'; + document.body.appendChild(a); + a.click(); + a.remove(); +} + function isMobileBrowser() { return !isNativeApp() && /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || ''); } diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 1ea2072..1e62a98 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -492,10 +492,11 @@ import { if (!src) throw new Error('No image returned'); lastGeneratedImageSrc = src; exporter.invalidate(); - if (out) out.innerHTML = imageStore.renderGeneratedImage(src, 'Generated clinical visual'); + var downloadUrl = data.downloadUrl || ''; + if (out) out.innerHTML = imageStore.renderGeneratedImage(src, 'Generated clinical visual', downloadUrl); if (fromChat) { setBusy(false, 'Ready'); - var html = imageStore.renderGeneratedImage(src, 'Generated clinical visual'); + var html = imageStore.renderGeneratedImage(src, 'Generated clinical visual', downloadUrl); replaceLoadingMessage(loading, html, [], [], true); } }) diff --git a/src/routes/clinicalAssistant.js b/src/routes/clinicalAssistant.js index 5dc3ceb..f72530c 100644 --- a/src/routes/clinicalAssistant.js +++ b/src/routes/clinicalAssistant.js @@ -325,6 +325,7 @@ router.get('/clinical-assistant/image/jobs/:id', async function(req, res) { imageUrl: job.imageUrl || null, url: job.url || null, base64: job.base64 || null, + downloadUrl: job.status === 'done' && job.base64 ? '/api/clinical-assistant/image/jobs/' + encodeURIComponent(job.id) + '/download' : null, error: job.error || null }); } catch (e) { @@ -332,6 +333,22 @@ router.get('/clinical-assistant/image/jobs/:id', async function(req, res) { } }); +router.get('/clinical-assistant/image/jobs/:id/download', async function(req, res) { + try { + var job = await getImageJob(req.params.id); + if (!job || String(job.userId) !== String(req.user.id) || job.status !== 'done' || !job.base64) { + return res.status(404).json({ error: 'Image job not found' }); + } + var buffer = Buffer.from(String(job.base64), 'base64'); + res.setHeader('Content-Type', 'image/png'); + res.setHeader('Content-Disposition', 'attachment; filename="clinical-visual.png"'); + res.setHeader('Cache-Control', 'private, max-age=900'); + res.send(buffer); + } catch (e) { + res.status(500).json({ error: 'Image download failed' }); + } +}); + async function prepareAssistantChat(body) { body = body || {}; var message = String(body.message || '').trim();