diff --git a/templates/base.html b/templates/base.html
index d0f5fb7..65560ea 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -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%;
diff --git a/templates/chat.html b/templates/chat.html
index 2d4e57c..e0c1dbb 100644
--- a/templates/chat.html
+++ b/templates/chat.html
@@ -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
}
}