diff --git a/public/css/assistant.css b/public/css/assistant.css index c05d516..2888132 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -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; } +} diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 991e05b..ef4736b 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -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 = '

' + escapeHtml(String(data.translated || '')) + '

' + ''; 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 || ''), [], {}) + - ''; - 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'); }); } diff --git a/test/assistant-translate.test.js b/test/assistant-translate.test.js index 7999943..34c2f0b 100644 --- a/test/assistant-translate.test.js +++ b/test/assistant-translate.test.js @@ -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');