fix: prevent broken HTML during streaming by deferring render to completion
All checks were successful
BotUI CI/CD / build (push) Successful in 59s
All checks were successful
BotUI CI/CD / build (push) Successful in 59s
When HTML content is streamed incrementally, injecting partial HTML into the DOM causes the browser to malform or discard incomplete tags. Changes: - In addMessage(): For streaming HTML (has msgId), show as escaped text initially - In updateStreaming(): For HTML content, show plain text during streaming - In finalizeStreaming(): Render complete HTML only when streaming is done - This ensures HTML is only rendered when the full content is received Applies to both chat.html and partials/chat.html
This commit is contained in:
parent
87fcb02b60
commit
91e9701c3e
2 changed files with 144 additions and 103 deletions
|
|
@ -283,40 +283,49 @@
|
|||
);
|
||||
}
|
||||
|
||||
function addMessage(sender, content, msgId) {
|
||||
var messages = document.getElementById("messages");
|
||||
if (!messages) return;
|
||||
function addMessage(sender, content, msgId) {
|
||||
var messages = document.getElementById("messages");
|
||||
if (!messages) return;
|
||||
|
||||
var div = document.createElement("div");
|
||||
div.className = "message " + sender;
|
||||
if (msgId) div.id = msgId;
|
||||
var div = document.createElement("div");
|
||||
div.className = "message " + sender;
|
||||
if (msgId) div.id = msgId;
|
||||
|
||||
if (sender === "user") {
|
||||
var processedContent = renderMentionInMessage(
|
||||
escapeHtml(content),
|
||||
);
|
||||
div.innerHTML =
|
||||
'<div class="message-content user-message">' +
|
||||
processedContent +
|
||||
"</div>";
|
||||
} else {
|
||||
// Check if content has HTML (any tag, including comments)
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(content);
|
||||
console.log("Bot message - hasHtmlTags:", hasHtmlTags, "content length:", content.length);
|
||||
|
||||
var parsed = hasHtmlTags
|
||||
? content // Use HTML directly (no escaping!)
|
||||
: (typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content));
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
div.innerHTML =
|
||||
'<div class="message-content bot-message">' +
|
||||
parsed +
|
||||
"</div>";
|
||||
}
|
||||
if (sender === "user") {
|
||||
var processedContent = renderMentionInMessage(
|
||||
escapeHtml(content),
|
||||
);
|
||||
div.innerHTML =
|
||||
'<div class="message-content user-message">' +
|
||||
processedContent +
|
||||
"</div>";
|
||||
} else {
|
||||
// Check if content has HTML (any tag, including comments)
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(content);
|
||||
console.log("Bot message - hasHtmlTags:", hasHtmlTags, "content length:", content.length, "msgId:", msgId);
|
||||
|
||||
messages.appendChild(div);
|
||||
var parsed;
|
||||
if (hasHtmlTags && msgId) {
|
||||
// Streaming HTML content - show as text initially to avoid broken tags
|
||||
// Will be rendered as HTML at finalizeStreaming
|
||||
parsed = escapeHtml(content);
|
||||
} else if (hasHtmlTags) {
|
||||
// Complete HTML content - render directly
|
||||
parsed = content;
|
||||
} else {
|
||||
// Markdown content
|
||||
parsed = typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content);
|
||||
}
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
div.innerHTML =
|
||||
'<div class="message-content bot-message">' +
|
||||
parsed +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
messages.appendChild(div);
|
||||
|
||||
// Auto-scroll to bottom unless user is manually scrolling
|
||||
if (!isUserScrolling) {
|
||||
|
|
@ -735,36 +744,52 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function updateStreaming(content) {
|
||||
var el = document.getElementById(streamingMessageId);
|
||||
if (el) {
|
||||
// Check if content has HTML tags
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(content);
|
||||
var parsed = hasHtmlTags
|
||||
? content // Use HTML directly
|
||||
: (typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content));
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
el.querySelector(".message-content").innerHTML = parsed;
|
||||
}
|
||||
function updateStreaming(content) {
|
||||
var el = document.getElementById(streamingMessageId);
|
||||
if (el) {
|
||||
var msgContent = el.querySelector(".message-content");
|
||||
// Check if final content will be HTML (full accumulated content)
|
||||
var willBeHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(currentStreamingContent);
|
||||
if (willBeHtml) {
|
||||
// For HTML content, show plain text during streaming to avoid broken tags
|
||||
// HTML will be rendered at finalizeStreaming when complete
|
||||
msgContent.textContent = content;
|
||||
} else {
|
||||
// For markdown, render incrementally
|
||||
var parsed = typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content);
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
msgContent.innerHTML = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeStreaming() {
|
||||
var el = document.getElementById(streamingMessageId);
|
||||
if (el) {
|
||||
// Check if content has HTML tags
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(currentStreamingContent);
|
||||
var parsed = hasHtmlTags
|
||||
? currentStreamingContent // Use HTML directly
|
||||
: (typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(currentStreamingContent)
|
||||
: escapeHtml(currentStreamingContent));
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
el.querySelector(".message-content").innerHTML = parsed;
|
||||
el.removeAttribute("id");
|
||||
setupMentionClickHandlers(el);
|
||||
}
|
||||
function finalizeStreaming() {
|
||||
var el = document.getElementById(streamingMessageId);
|
||||
if (el) {
|
||||
var msgContent = el.querySelector(".message-content");
|
||||
// Check if content has HTML tags
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(currentStreamingContent);
|
||||
if (hasHtmlTags) {
|
||||
// Render complete HTML at the end
|
||||
var parsed = renderMentionInMessage(currentStreamingContent);
|
||||
msgContent.innerHTML = parsed;
|
||||
} else {
|
||||
// Render markdown
|
||||
var parsed = typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(currentStreamingContent)
|
||||
: escapeHtml(currentStreamingContent);
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
msgContent.innerHTML = parsed;
|
||||
}
|
||||
el.removeAttribute("id");
|
||||
setupMentionClickHandlers(el);
|
||||
}
|
||||
streamingMessageId = null;
|
||||
currentStreamingContent = "";
|
||||
}
|
||||
streamingMessageId = null;
|
||||
currentStreamingContent = "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,40 +487,49 @@
|
|||
);
|
||||
}
|
||||
|
||||
function addMessage(sender, content, msgId) {
|
||||
var messages = document.getElementById("messages");
|
||||
if (!messages) return;
|
||||
function addMessage(sender, content, msgId) {
|
||||
var messages = document.getElementById("messages");
|
||||
if (!messages) return;
|
||||
|
||||
var div = document.createElement("div");
|
||||
div.className = "message " + sender;
|
||||
if (msgId) div.id = msgId;
|
||||
var div = document.createElement("div");
|
||||
div.className = "message " + sender;
|
||||
if (msgId) div.id = msgId;
|
||||
|
||||
if (sender === "user") {
|
||||
var processedContent = renderMentionInMessage(
|
||||
escapeHtml(content),
|
||||
);
|
||||
div.innerHTML =
|
||||
'<div class="message-content user-message">' +
|
||||
processedContent +
|
||||
"</div>";
|
||||
} else {
|
||||
// Check if content has HTML (any tag, including comments)
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(content);
|
||||
console.log("Bot message - hasHtmlTags:", hasHtmlTags, "content length:", content.length);
|
||||
|
||||
var parsed = hasHtmlTags
|
||||
? content // Use HTML directly (no escaping!)
|
||||
: (typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content));
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
div.innerHTML =
|
||||
'<div class="message-content bot-message">' +
|
||||
parsed +
|
||||
"</div>";
|
||||
}
|
||||
if (sender === "user") {
|
||||
var processedContent = renderMentionInMessage(
|
||||
escapeHtml(content),
|
||||
);
|
||||
div.innerHTML =
|
||||
'<div class="message-content user-message">' +
|
||||
processedContent +
|
||||
"</div>";
|
||||
} else {
|
||||
// Check if content has HTML (any tag, including comments)
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(content);
|
||||
console.log("Bot message - hasHtmlTags:", hasHtmlTags, "content length:", content.length, "msgId:", msgId);
|
||||
|
||||
messages.appendChild(div);
|
||||
var parsed;
|
||||
if (hasHtmlTags && msgId) {
|
||||
// Streaming HTML content - show as text initially to avoid broken tags
|
||||
// Will be rendered as HTML at finalizeStreaming
|
||||
parsed = escapeHtml(content);
|
||||
} else if (hasHtmlTags) {
|
||||
// Complete HTML content - render directly
|
||||
parsed = content;
|
||||
} else {
|
||||
// Markdown content
|
||||
parsed = typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content);
|
||||
}
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
div.innerHTML =
|
||||
'<div class="message-content bot-message">' +
|
||||
parsed +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
messages.appendChild(div);
|
||||
|
||||
// Auto-scroll to bottom unless user is manually scrolling
|
||||
if (!isUserScrolling) {
|
||||
|
|
@ -939,19 +948,26 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function updateStreaming(content) {
|
||||
var el = document.getElementById(streamingMessageId);
|
||||
if (el) {
|
||||
// Check if content has HTML tags
|
||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(content);
|
||||
var parsed = hasHtmlTags
|
||||
? content // Use HTML directly
|
||||
: (typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content));
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
el.querySelector(".message-content").innerHTML = parsed;
|
||||
}
|
||||
function updateStreaming(content) {
|
||||
var el = document.getElementById(streamingMessageId);
|
||||
if (el) {
|
||||
var msgContent = el.querySelector(".message-content");
|
||||
// Check if final content will be HTML (full accumulated content)
|
||||
var willBeHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(currentStreamingContent);
|
||||
if (willBeHtml) {
|
||||
// For HTML content, show plain text during streaming to avoid broken tags
|
||||
// HTML will be rendered at finalizeStreaming when complete
|
||||
msgContent.textContent = content;
|
||||
} else {
|
||||
// For markdown, render incrementally
|
||||
var parsed = typeof marked !== "undefined" && marked.parse
|
||||
? marked.parse(content)
|
||||
: escapeHtml(content);
|
||||
parsed = renderMentionInMessage(parsed);
|
||||
msgContent.innerHTML = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeStreaming() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue