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