Fix streaming message display and TTS issues

- Separate username/model header from message content using distinct DOM elements
- Fix button positioning to appear on left side of messages
- Ensure TTS only reads clean message content, not username/model header
- Add support for stopping current TTS when auto-play is toggled off
- Improve DOM structure with message-body wrapper for proper layout
- Fix streaming messages to maintain header display throughout entire stream
This commit is contained in:
Russell Ballestrini 2025-08-11 15:26:52 -04:00
parent dada6b3f22
commit 88f68e4adc
2 changed files with 66 additions and 17 deletions

View file

@ -76,6 +76,19 @@
display: block;
}
/* Styling for the message body wrapper that contains header and content */
.message-body {
width: 100%;
display: flex;
flex-direction: column;
}
/* Styling for the message header (username/model) */
.message-header {
width: 100%;
margin-bottom: 0;
}
/* Styling for the message div holding html/markdown content */
.message-content {
width: 100%;

View file

@ -101,6 +101,7 @@ const dompurify_config = {
// keeping track of scrolling to prevent autoscrolling.
let userHasScrolledUp = false;
let currentAudio = null; // To keep track of the currently playing audio
let currentQueuedAudio = null; // To keep track of currently playing queued TTS audio
let audioCache = {}; // Cache to store audio blobs
// Flag to prevent mutual updates on desktop/mobile
@ -417,12 +418,15 @@ async function speakTextQueued(text, playButton, messageId) {
const cleanText = text.replace(/[^a-zA-Z0-9\s.,!?]/g, '');
const playAudio = (audio) => {
currentQueuedAudio = audio; // Track the currently playing queued audio
audio.onended = () => {
console.log("TTS finished for:", messageId);
currentQueuedAudio = null; // Clear when finished
resolve();
};
audio.onerror = () => {
console.error("TTS audio error for:", messageId);
currentQueuedAudio = null; // Clear on error
reject(new Error("Audio playback failed"));
};
audio.play().catch(reject);
@ -540,6 +544,13 @@ function toggleAutoPlayTTS() {
}
currentAudio = null;
}
// Stop any currently playing queued TTS audio
if (currentQueuedAudio) {
currentQueuedAudio.pause();
currentQueuedAudio.currentTime = 0;
currentQueuedAudio = null;
}
}
updateAutoPlayTTSDisplay();
@ -725,12 +736,15 @@ socket.on("delete_processing_message", (msg_id) => {
tempMessages.forEach((tempMessage) => {
tempMessage.remove();
});
// Clear the message buffer for the corresponding message ID
// Clear the message buffer and header for the corresponding message ID
delete messageBuffers[msg_id];
delete messageHeaders[msg_id];
});
// A dictionary to hold buffers for each message ID
const messageBuffers = {};
// A dictionary to track message headers (username/model) for each message ID
const messageHeaders = {};
// Socket event for receiving chunks of a message
socket.on("message_chunk", (data) => {
@ -748,9 +762,20 @@ socket.on("message_chunk", (data) => {
// If the message-content div doesn't exist, create it
if (!messageWrapper.querySelector(".message-content")) {
// Create a message body wrapper to contain both header and content
const messageBodyWrapper = document.createElement("div");
messageBodyWrapper.className = "message-body";
messageWrapper.appendChild(messageBodyWrapper);
// Create header element for username/model
const headerElement = document.createElement("div");
headerElement.className = "message-header";
messageBodyWrapper.appendChild(headerElement);
// Create content element for actual message content
targetMessageElement = document.createElement("div");
targetMessageElement.className = "message-content";
messageWrapper.appendChild(targetMessageElement);
messageBodyWrapper.appendChild(targetMessageElement);
} else {
targetMessageElement = messageWrapper.querySelector(".message-content");
}
@ -760,17 +785,26 @@ socket.on("message_chunk", (data) => {
messageBuffers[data.id] = "";
}
// Store header info on first chunk and update header element
if (data.is_first_chunk && data.username && data.model_name) {
messageHeaders[data.id] = {
username: data.username,
model_name: data.model_name
};
// Update header element
const headerElement = messageWrapper.querySelector(".message-header");
if (headerElement) {
const headerContent = `**${data.username} (${data.model_name}):**`;
headerElement.innerHTML = DOMPurify.sanitize(marked.marked(headerContent), dompurify_config);
}
}
// Append the chunk to the buffer
messageBuffers[data.id] += data.content;
// Build the content for display (includes header for first chunk)
let displayContent = messageBuffers[data.id];
if (data.is_first_chunk && data.username && data.model_name) {
displayContent = `**${data.username} (${data.model_name}):**\n\n${displayContent}`;
}
// Process the display content with marked and set it as the content of the target element
const sanitizedContent = DOMPurify.sanitize(marked.marked(displayContent), dompurify_config);
// Process just the content and set it in the content element
const sanitizedContent = DOMPurify.sanitize(marked.marked(messageBuffers[data.id]), dompurify_config);
targetMessageElement.innerHTML = sanitizedContent;
// Store the raw markdown in a data attribute for later use in editing (without header for clean editing)
@ -811,13 +845,14 @@ socket.on("message_chunk", (data) => {
const playButton = document.createElement("button");
playButton.textContent = "Play";
playButton.onclick = () => {
const fullText = targetMessageElement.textContent || targetMessageElement.innerText;
speakText(fullText, playButton, data.id);
// Use content from the content element (clean text without header)
const cleanText = targetMessageElement.textContent || targetMessageElement.innerText || "";
speakText(cleanText, playButton, data.id);
};
buttonContainer.appendChild(playButton);
// Append the button container before the message content
messageWrapper.insertBefore(buttonContainer, targetMessageElement);
// Insert the button container at the beginning of the message wrapper (before header and content)
messageWrapper.insertBefore(buttonContainer, messageWrapper.firstChild);
// Auto-play TTS if enabled and message is complete (only when streaming finishes)
console.log("DEBUG: Streaming complete check:", {
@ -832,9 +867,10 @@ socket.on("message_chunk", (data) => {
console.log("DEBUG: Found play button for streaming TTS:", playButton ? "YES" : "NO", playButton?.textContent);
if (playButton) {
setTimeout(() => {
const fullText = targetMessageElement.textContent || targetMessageElement.innerText;
console.log("DEBUG: Queueing streaming TTS:", data.id, fullText.substring(0, 50) + "...");
queueTTS(fullText, playButton, data.id);
// Use content from the content element (clean text without header)
const cleanText = targetMessageElement.textContent || targetMessageElement.innerText || "";
console.log("DEBUG: Queueing streaming TTS:", data.id, cleanText.substring(0, 50) + "...");
queueTTS(cleanText, playButton, data.id);
}, 50); // Small delay to let the message render
}
}