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-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() {
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 '<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) {
return '<div class="assistant-gallery-item assistant-gallery-running" title="Generating…"><i class="fas fa-spinner fa-spin"></i></div>';
}).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 = '<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>' +
'<div class="modal-body"><img src="' + escapeAttr(src) + '" alt="Generated image" style="width:100%;border-radius:12px;"></div></div>';
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 || ''), [], {}) +
'<button type="button" class="btn-sm btn-ghost" data-assistant-msg-show-original><i class="fas fa-undo"></i> Show original</button>';
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);
assert.equal(sent.target, 'es');
assert.equal(sent.provider, 'libretranslate');
assert.equal(sent.format, 'html');
assert.match(sent.message, /<p>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');