fix: translation 400 — translate raw markdown as text and re-render; download buttons on previews and library items; Done state on image completion; mic mirrors the working dictation flow; send→stop icon; grouped bottom-left tools
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 46s
Forgejo Android APK / Build signed APK (push) Successful in 2m4s

This commit is contained in:
Daniel 2026-09-09 04:17:18 +02:00
parent d6476f99d9
commit 2f7d929464
3 changed files with 30 additions and 20 deletions

View file

@ -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-goback-rail { display:inline-flex; align-self:stretch; justify-content:flex-start; margin-bottom:6px; }
.assistant-topbar #btn-assistant-goback { display:none; } .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; }

View file

@ -912,7 +912,6 @@ import {
} }
function startAssistantMic() { function startAssistantMic() {
unlockAudioPlayback();
var input = document.getElementById('assistant-input'); var input = document.getElementById('assistant-input');
assistantMic.finalText = String(input && input.value || ''); assistantMic.finalText = String(input && input.value || '');
assistantMic.sessionFinals = ''; assistantMic.sessionFinals = '';
@ -962,18 +961,17 @@ import {
showToast('No speech detected — try again', 'error'); showToast('No speech detected — try again', 'error');
} }
}; };
if (typeof transcribeAudio === 'function' && blob && blob.size > 100) { // Same flow as the working dictation tab: send whatever was recorded,
if (blob.size < 4000 && !live) { // fall back to the live transcript, never drop what the browser heard.
if (typeof showToast === 'function') showToast('Recording was too short or silent — speak closer to the microphone', 'error'); if (!blob || blob.size === 0) { if (live) finish(live); return; }
finish(''); if (typeof transcribeAudio === 'function') {
return; return transcribeAudio(blob).then(function(data) {
} if (data && data.success && data.text) finish(data.text);
transcribeAudio(blob).then(function(data) { else if (data && data.noProvider) finish(live);
// Server transcription wins; the live transcript is the fallback, else finish(live);
// never lose what the browser already heard.
finish(data && data.success && data.text ? data.text : live);
}).catch(function() { finish(live); }); }).catch(function() { finish(live); });
} else finish(live); }
finish(live);
}).catch(function() {}); }).catch(function() {});
} }
@ -1017,7 +1015,6 @@ import {
} }
function conversationStartListening() { function conversationStartListening() {
unlockAudioPlayback();
var status = document.getElementById('assistant-voice-status'); var status = document.getElementById('assistant-voice-status');
if (status) status.textContent = 'Listening…'; if (status) status.textContent = 'Listening…';
conversationMode.listening = true; conversationMode.listening = true;
@ -1162,7 +1159,7 @@ import {
}, },
onDone: function (url) { onDone: function (url) {
var progress = document.getElementById('create-image-progress'); 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(); renderCreateImageHistory();
openImagePreview(url); openImagePreview(url);
}, },
@ -1193,7 +1190,11 @@ import {
return; return;
} }
wrap.innerHTML = done.map(function (job) { wrap.innerHTML = done.map(function (job) {
return '<button type="button" class="assistant-gallery-item" data-gallery-image="' + escapeAttr(job.imageUrl) + '" title="' + escapeAttr(new Date(job.createdAt || '').toLocaleString()) + '"><img src="' + escapeAttr(job.imageUrl) + '" alt="Generated image" loading="lazy"></button>'; var dl = job.imageUrl + (job.imageUrl.indexOf('?') === -1 ? '?download=1' : '&download=1');
return '<div class="assistant-gallery-item-wrap">' +
'<button type="button" class="assistant-gallery-item" data-gallery-image="' + escapeAttr(job.imageUrl) + '" title="' + escapeAttr(new Date(job.createdAt || '').toLocaleString()) + '"><img src="' + escapeAttr(job.imageUrl) + '" alt="Generated image" loading="lazy"></button>' +
'<a class="assistant-gallery-download" href="' + escapeAttr(dl) + '" download title="Download this image"><i class="fas fa-download"></i></a>' +
'</div>';
}).join('') + running.map(function (job) { }).join('') + running.map(function (job) {
return '<div class="assistant-gallery-item assistant-gallery-running" title="Generating…"><i class="fas fa-spinner fa-spin"></i></div>'; return '<div class="assistant-gallery-item assistant-gallery-running" title="Generating…"><i class="fas fa-spinner fa-spin"></i></div>';
}).join(''); }).join('');
@ -1282,10 +1283,12 @@ import {
function openImagePreview(src) { function openImagePreview(src) {
if (!src) return; if (!src) return;
var downloadUrl = src + (src.indexOf('?') === -1 ? '?download=1' : '&download=1');
var modal = document.createElement('div'); var modal = document.createElement('div');
modal.className = 'assistant-takehome-modal'; modal.className = 'assistant-takehome-modal';
modal.id = 'assistant-image-preview-modal'; modal.id = 'assistant-image-preview-modal';
modal.innerHTML = '<div class="modal-content"><div class="modal-header"><h2>Generated image</h2>' + modal.innerHTML = '<div class="modal-content"><div class="modal-header"><h2>Generated image</h2>' +
'<a class="btn-sm btn-ghost" href="' + escapeAttr(downloadUrl) + '" download title="Download this image"><i class="fas fa-download"></i> Download</a>' +
'<button type="button" class="modal-close" data-image-preview-close aria-label="Close"><i class="fas fa-xmark"></i></button></div>' + '<button type="button" class="modal-close" data-image-preview-close aria-label="Close"><i class="fas fa-xmark"></i></button></div>' +
'<div class="modal-body"><img src="' + escapeAttr(src) + '" alt="Generated image" style="width:100%;border-radius:12px;"></div></div>'; '<div class="modal-body"><img src="' + escapeAttr(src) + '" alt="Generated image" style="width:100%;border-radius:12px;"></div></div>';
document.body.appendChild(modal); document.body.appendChild(modal);
@ -1593,11 +1596,14 @@ import {
var bubble = row && row.querySelector('.assistant-bubble'); var bubble = row && row.querySelector('.assistant-bubble');
if (!bubble) return; if (!bubble) return;
if (!bubble.assistantOriginalHtml) bubble.assistantOriginalHtml = bubble.innerHTML; if (!bubble.assistantOriginalHtml) bubble.assistantOriginalHtml = bubble.innerHTML;
// Translate the RENDERED HTML so headings, bold, lists and tables keep their shape. // Translate the raw markdown as text (LibreTranslate rejects rendered HTML),
translateAssistantMessage(bubble.innerHTML, target, provider, '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) { .then(function(data) {
if (!data.success) throw new Error(data.error || 'Translation failed'); if (!data.success) throw new Error(data.error || 'Translation failed');
bubble.innerHTML = sanitize(String(data.translated || '')) + bubble.innerHTML = renderMarkdown(String(data.translated || ''), [], {}) +
'<button type="button" class="btn-sm btn-ghost" data-assistant-msg-show-original><i class="fas fa-undo"></i> Show original</button>'; '<button type="button" class="btn-sm btn-ghost" data-assistant-msg-show-original><i class="fas fa-undo"></i> Show original</button>';
renderEmbeddedBlocks(bubble); renderEmbeddedBlocks(bubble);
}) })

View file

@ -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); const sent = JSON.parse(translateCalls[0].options.body);
assert.equal(sent.target, 'es'); assert.equal(sent.target, 'es');
assert.equal(sent.provider, 'libretranslate'); assert.equal(sent.provider, 'libretranslate');
assert.equal(sent.format, 'html'); assert.equal(sent.format, 'text');
assert.match(sent.message, /<p>Chest pain in a four year old\.<\/p>/); assert.equal(sent.message, 'Chest pain in a four year old.');
const bubble = row.querySelector('.assistant-bubble'); const bubble = row.querySelector('.assistant-bubble');
assert.match(bubble.textContent, /Traducción de "Chest pain in a four year old/); 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'); assert.equal(c.messages[0].content, 'Chest pain in a four year old.', 'canonical transcript unchanged');