diff --git a/public/css/assistant.css b/public/css/assistant.css index c2761c71..43694f87 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -324,3 +324,7 @@ body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh .assistant-goback-rail { display:inline-flex; align-self:stretch; justify-content:flex-start; margin-bottom:6px; } .assistant-topbar #btn-assistant-goback { display:none; } } +/* Library item hover download */ +.assistant-gallery-item-wrap { position:relative; } +.assistant-gallery-download { position:absolute; top:4px; right:4px; width:24px; height:24px; border-radius:6px; background:rgba(15,23,42,.6); color:#fff; display:flex; align-items:center; justify-content:center; font-size:11px; opacity:0; transition:opacity .12s ease; pointer-events:none; } +.assistant-gallery-item-wrap:hover .assistant-gallery-download, .assistant-gallery-item-wrap:focus-within .assistant-gallery-download { opacity:1; pointer-events:auto; } diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 450fb7d6..2423e289 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -912,7 +912,6 @@ import { } function startAssistantMic() { - unlockAudioPlayback(); var input = document.getElementById('assistant-input'); assistantMic.finalText = String(input && input.value || ''); assistantMic.sessionFinals = ''; @@ -962,18 +961,17 @@ import { showToast('No speech detected — try again', 'error'); } }; - if (typeof transcribeAudio === 'function' && blob && blob.size > 100) { - if (blob.size < 4000 && !live) { - if (typeof showToast === 'function') showToast('Recording was too short or silent — speak closer to the microphone', 'error'); - finish(''); - return; - } - transcribeAudio(blob).then(function(data) { - // Server transcription wins; the live transcript is the fallback, - // never lose what the browser already heard. - finish(data && data.success && data.text ? data.text : live); + // Same flow as the working dictation tab: send whatever was recorded, + // fall back to the live transcript, never drop what the browser heard. + if (!blob || blob.size === 0) { if (live) finish(live); return; } + if (typeof transcribeAudio === 'function') { + return transcribeAudio(blob).then(function(data) { + if (data && data.success && data.text) finish(data.text); + else if (data && data.noProvider) finish(live); + else finish(live); }).catch(function() { finish(live); }); - } else finish(live); + } + finish(live); }).catch(function() {}); } @@ -1017,7 +1015,6 @@ import { } function conversationStartListening() { - unlockAudioPlayback(); var status = document.getElementById('assistant-voice-status'); if (status) status.textContent = 'Listening…'; conversationMode.listening = true; @@ -1162,7 +1159,7 @@ import { }, onDone: function (url) { var progress = document.getElementById('create-image-progress'); - if (progress) { progress.textContent = ''; progress.hidden = true; } + if (progress) { progress.textContent = 'Done ✓'; progress.hidden = false; setTimeout(function() { progress.textContent = ''; progress.hidden = true; }, 1800); } renderCreateImageHistory(); openImagePreview(url); }, @@ -1193,7 +1190,11 @@ import { return; } wrap.innerHTML = done.map(function (job) { - return ''; + var dl = job.imageUrl + (job.imageUrl.indexOf('?') === -1 ? '?download=1' : '&download=1'); + return ''; }).join('') + running.map(function (job) { return ''; }).join(''); @@ -1282,10 +1283,12 @@ import { function openImagePreview(src) { if (!src) return; + var downloadUrl = src + (src.indexOf('?') === -1 ? '?download=1' : '&download=1'); var modal = document.createElement('div'); modal.className = 'assistant-takehome-modal'; modal.id = 'assistant-image-preview-modal'; modal.innerHTML = ''; document.body.appendChild(modal); @@ -1593,11 +1596,14 @@ import { var bubble = row && row.querySelector('.assistant-bubble'); if (!bubble) return; if (!bubble.assistantOriginalHtml) bubble.assistantOriginalHtml = bubble.innerHTML; - // Translate the RENDERED HTML so headings, bold, lists and tables keep their shape. - translateAssistantMessage(bubble.innerHTML, target, provider, 'html') + // Translate the raw markdown as text (LibreTranslate rejects rendered HTML), + // then re-render the translated text through the shared markdown pipeline. + var raw = bubble.assistantRawContent !== undefined ? bubble.assistantRawContent : String(bubble.textContent || '').trim(); + if (!String(raw).trim()) return; + translateAssistantMessage(raw, target, provider, 'text') .then(function(data) { if (!data.success) throw new Error(data.error || 'Translation failed'); - bubble.innerHTML = sanitize(String(data.translated || '')) + + bubble.innerHTML = renderMarkdown(String(data.translated || ''), [], {}) + ''; renderEmbeddedBlocks(bubble); }) diff --git a/test/assistant-translate.test.js b/test/assistant-translate.test.js index 9b5b3a47..34c2f0bd 100644 --- a/test/assistant-translate.test.js +++ b/test/assistant-translate.test.js @@ -177,8 +177,8 @@ test('per-message translate picker offers a provider choice and leaves the raw t const sent = JSON.parse(translateCalls[0].options.body); assert.equal(sent.target, 'es'); assert.equal(sent.provider, 'libretranslate'); - assert.equal(sent.format, 'html'); - assert.match(sent.message, /

Chest pain in a four year old\.<\/p>/); + assert.equal(sent.format, 'text'); + assert.equal(sent.message, 'Chest pain in a four year old.'); const bubble = row.querySelector('.assistant-bubble'); assert.match(bubble.textContent, /Traducción de "Chest pain in a four year old/); assert.equal(c.messages[0].content, 'Chest pain in a four year old.', 'canonical transcript unchanged');