fix: mobile menu — no icons, hard-truncated titles within the drawer width, ⋮ always visible without horizontal scroll; translation uses the reliable plain-text path
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 50s
Forgejo Android APK / Build signed APK (push) Successful in 2m11s

This commit is contained in:
Daniel 2026-09-09 12:29:37 +02:00
parent ec4f1905de
commit 094de9ebd1
3 changed files with 27 additions and 35 deletions

View file

@ -355,3 +355,12 @@ body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh
.assistant-history .card { border:none; box-shadow:none; background:none; padding:0; } .assistant-history .card { border:none; box-shadow:none; background:none; padding:0; }
.assistant-history .card-header { padding:2px 4px; } .assistant-history .card-header { padding:2px 4px; }
.assistant-history .card-header h3 { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; } .assistant-history .card-header h3 { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; }
/* Mobile drawer rows: no icons, hard-truncated titles, ⋮ always visible */
@media (max-width: 640px) {
.assistant-saved-chat-icon { display:none; }
.assistant-saved-chat { position:relative; overflow:hidden; max-width:100%; padding-right:36px; }
.assistant-saved-chat-title { flex:1 1 auto; min-width:0; max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.assistant-saved-chat-menu { position:absolute; right:6px; top:50%; transform:translateY(-50%); opacity:1; pointer-events:auto; }
.assistant-saved-chat-meta { display:none; }
.assistant-goback-rail i, .assistant-create-image i, .assistant-new-chat i { display:none; }
}

View file

@ -1617,20 +1617,15 @@ import {
document.querySelectorAll('[data-assistant-translate-pop]').forEach(function(pop) { pop.remove(); }); document.querySelectorAll('[data-assistant-translate-pop]').forEach(function(pop) { pop.remove(); });
} }
function simplifyHtmlForTranslation(html) { function plainTextForTranslation(markdown) {
if (typeof DOMParser === 'undefined') { return String(markdown || '')
// Older harnesses/browsers: send the raw markdown as text instead. .replace(/^#{1,6}\s+/gm, '')
return String(html || '').replace(/<[^>]*>/g, ''); .replace(/[*_~`]{1,3}/g, '')
} .replace(/^\s*[-*+]\s+/gm, '• ')
var doc = new DOMParser().parseFromString(String(html || ''), 'text/html'); .replace(/^\s*\d+\.\s+/gm, '')
doc.body.querySelectorAll('.katex, .assistant-cite, .assistant-code-copy, .assistant-table-actions, .assistant-msg-actions, mjx-container, script, style').forEach(function(el) { .replace(/^\s*[|>]\s*/gm, '')
el.replaceWith(doc.createTextNode(el.textContent || '')); .replace(/\|/g, ' ')
}); .trim();
doc.body.querySelectorAll('.assistant-table-scroll').forEach(function(el) {
var table = el.querySelector('table');
if (table) el.replaceWith(table);
});
return doc.body.innerHTML;
} }
function requestMessageTranslation(row, target, provider) { function requestMessageTranslation(row, target, provider) {
@ -1640,34 +1635,22 @@ import {
var imageCards = Array.prototype.slice.call(bubble.querySelectorAll('.assistant-image-card')); var imageCards = Array.prototype.slice.call(bubble.querySelectorAll('.assistant-image-card'));
imageCards.forEach(function(card) { card.remove(); }); imageCards.forEach(function(card) { card.remove(); });
if (!bubble.assistantOriginalHtml) bubble.assistantOriginalHtml = bubble.innerHTML; if (!bubble.assistantOriginalHtml) bubble.assistantOriginalHtml = bubble.innerHTML;
// Translate simplified rendered HTML so headings, bold, lists and tables // Reliable path: translate plain text (no markdown syntax to mangle) and
// survive; LibreTranslate keeps those tags in html mode. Citations render // show it as clean paragraphs; the original keeps its full formatting.
// as plain numbers inside their chips.
var raw = bubble.assistantRawContent !== undefined ? bubble.assistantRawContent : String(bubble.textContent || '').trim(); var raw = bubble.assistantRawContent !== undefined ? bubble.assistantRawContent : String(bubble.textContent || '').trim();
if (!String(raw).trim()) return; if (!String(raw).trim()) return;
var simpleHtml = simplifyHtmlForTranslation(renderMarkdown(raw, [], {})); var plain = plainTextForTranslation(raw);
translateAssistantMessage(simpleHtml, target, provider, 'html') if (!String(plain).trim()) return;
translateAssistantMessage(plain, 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 = '<p class="assistant-translated-text">' + escapeHtml(String(data.translated || '')) + '</p>' +
'<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>';
imageCards.forEach(function(card) { bubble.appendChild(card); }); // same node — its status polling continues imageCards.forEach(function(card) { bubble.appendChild(card); }); // same node — its status polling continues
renderEmbeddedBlocks(bubble); renderEmbeddedBlocks(bubble);
}) })
.catch(function(err) { .catch(function(err) {
// Html mode can be rejected by some LibreTranslate builds; fall back if (typeof showToast === 'function') showToast(err && err.message ? err.message : 'Translation failed', 'error');
// to plain text and re-render, so formatting is never fully lost.
translateAssistantMessage(raw, target, provider, 'text')
.then(function(data2) {
if (!data2.success) throw new Error(data2.error || 'Translation failed');
bubble.innerHTML = renderMarkdown(String(data2.translated || ''), [], {}) +
'<button type="button" class="btn-sm btn-ghost" data-assistant-msg-show-original><i class="fas fa-undo"></i> Show original</button>';
imageCards.forEach(function(card) { bubble.appendChild(card); });
renderEmbeddedBlocks(bubble);
})
.catch(function(err2) {
if (typeof showToast === 'function') showToast(err2 && err2.message ? err2.message : 'Translation failed', 'error');
});
}); });
} }

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, /Chest pain in a four year old\./, 'simplified rendered HTML carries the formatting'); 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');