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
This commit is contained in:
parent
d6476f99d9
commit
2f7d929464
3 changed files with 30 additions and 20 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue