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 '
 + ')
' +
'
' +
'' +
@@ -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();