feat: device TTS preferred in voice mode with model fallback; permanent src-garbage scrub; simplified-HTML translation with text fallback; New chat closes the drawer; real FA icons; audio unlock restored
This commit is contained in:
parent
3a55937177
commit
07af95b89a
2 changed files with 51 additions and 6 deletions
|
|
@ -167,7 +167,11 @@ import {
|
||||||
// Open WebUI behavior: the send button becomes Stop while working.
|
// Open WebUI behavior: the send button becomes Stop while working.
|
||||||
if (assistantBusy) { e.preventDefault(); cancelAssistantSearch(); }
|
if (assistantBusy) { e.preventDefault(); cancelAssistantSearch(); }
|
||||||
});
|
});
|
||||||
if (clearBtn) clearBtn.addEventListener('click', clearConversation);
|
if (clearBtn) clearBtn.addEventListener('click', function(ev) {
|
||||||
|
var layout = document.getElementById('assistant-layout');
|
||||||
|
if (layout) layout.classList.remove('mobile-chats-open'); // back to the chat
|
||||||
|
clearConversation(ev);
|
||||||
|
});
|
||||||
document.getElementById('btn-assistant-download-chat').addEventListener('click', downloadTranscript);
|
document.getElementById('btn-assistant-download-chat').addEventListener('click', downloadTranscript);
|
||||||
if (goBackBtn) goBackBtn.addEventListener('click', goBackToMainMenu);
|
if (goBackBtn) goBackBtn.addEventListener('click', goBackToMainMenu);
|
||||||
if (input) {
|
if (input) {
|
||||||
|
|
@ -920,6 +924,7 @@ import {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAssistantMic() {
|
function startAssistantMic() {
|
||||||
|
unlockAudioPlayback();
|
||||||
var input = document.getElementById('assistant-input');
|
var input = document.getElementById('assistant-input');
|
||||||
assistantMic.finalText = String(input && input.value || '');
|
assistantMic.finalText = String(input && input.value || '');
|
||||||
assistantMic.sessionFinals = '';
|
assistantMic.sessionFinals = '';
|
||||||
|
|
@ -1023,6 +1028,7 @@ import {
|
||||||
}
|
}
|
||||||
|
|
||||||
function conversationStartListening() {
|
function conversationStartListening() {
|
||||||
|
unlockAudioPlayback();
|
||||||
var status = document.getElementById('assistant-voice-status');
|
var status = document.getElementById('assistant-voice-status');
|
||||||
if (status) status.textContent = 'Listening…';
|
if (status) status.textContent = 'Listening…';
|
||||||
conversationMode.listening = true;
|
conversationMode.listening = true;
|
||||||
|
|
@ -1096,6 +1102,17 @@ import {
|
||||||
if (st) st.textContent = msg || 'Tap to speak';
|
if (st) st.textContent = msg || 'Tap to speak';
|
||||||
};
|
};
|
||||||
if (!text) { done(); return; }
|
if (!text) { done(); return; }
|
||||||
|
// Device TTS first when the browser supports it; the model voice is the fallback.
|
||||||
|
if (typeof window !== 'undefined' && window.speechSynthesis && typeof SpeechSynthesisUtterance === 'function') {
|
||||||
|
try {
|
||||||
|
var utter = new SpeechSynthesisUtterance(text);
|
||||||
|
utter.onend = function() { done('Tap to speak'); };
|
||||||
|
utter.onerror = function() { done('Tap to speak'); };
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
window.speechSynthesis.speak(utter);
|
||||||
|
return;
|
||||||
|
} catch (e) { /* fall through to the model voice */ }
|
||||||
|
}
|
||||||
fetch('/api/text-to-speech', {
|
fetch('/api/text-to-speech', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getAuthHeaders(Object.assign({ 'Content-Type': 'application/json' })),
|
headers: getAuthHeaders(Object.assign({ 'Content-Type': 'application/json' })),
|
||||||
|
|
@ -1600,6 +1617,18 @@ 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) {
|
||||||
|
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 requestMessageTranslation(row, target, provider) {
|
function requestMessageTranslation(row, target, provider) {
|
||||||
var bubble = row && row.querySelector('.assistant-bubble');
|
var bubble = row && row.querySelector('.assistant-bubble');
|
||||||
if (!bubble) return;
|
if (!bubble) return;
|
||||||
|
|
@ -1607,20 +1636,34 @@ 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 the raw markdown as text (LibreTranslate rejects rendered HTML),
|
// Translate simplified rendered HTML so headings, bold, lists and tables
|
||||||
// then re-render the translated text through the shared markdown pipeline.
|
// survive; LibreTranslate keeps those tags in html mode. Citations render
|
||||||
|
// 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;
|
||||||
translateAssistantMessage(raw, target, provider, 'text')
|
var simpleHtml = simplifyHtmlForTranslation(renderMarkdown(raw, [], {}));
|
||||||
|
translateAssistantMessage(simpleHtml, target, provider, 'html')
|
||||||
.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 = renderMarkdown(String(data.translated || ''), [], {}) +
|
bubble.innerHTML = sanitize(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>';
|
'<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) {
|
||||||
if (typeof showToast === 'function') showToast(err && err.message ? err.message : 'Translation failed', 'error');
|
// 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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ async function finalizeAssistantAnswer(ai, options) {
|
||||||
|
|
||||||
function stripModelSourcesSection(answer) {
|
function stripModelSourcesSection(answer) {
|
||||||
return cleanDanglingSourceLeadIn(String(answer || '')
|
return cleanDanglingSourceLeadIn(String(answer || '')
|
||||||
|
.replace(/(?:^|\s)(?:\bsrc\b\s*){2,}$/i, '')
|
||||||
|
.replace(/\bsrc\b(?=\s*src\b)/gi, '')
|
||||||
.replace(/\n\s*(---\s*)?(#{1,3}\s*)?(Sources|References)\s*\n[\s\S]*$/i, '')
|
.replace(/\n\s*(---\s*)?(#{1,3}\s*)?(Sources|References)\s*\n[\s\S]*$/i, '')
|
||||||
.replace(/\n\s*>?\s*(⚠️\s*)?Clinical decision support[^\n]*$/gim, '')
|
.replace(/\n\s*>?\s*(⚠️\s*)?Clinical decision support[^\n]*$/gim, '')
|
||||||
.trim());
|
.trim());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue