Merge pull request #2029 from ksylvan/kayvan/web-cleanup

Web UI: Fix streaming chat: tokens now accumulate in order into a single message
This commit is contained in:
Kayvan Sylvan 2026-02-25 18:58:08 -08:00 committed by GitHub
commit abf045ac81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 61 additions and 33 deletions

View file

@ -0,0 +1,6 @@
### PR [#2029](https://github.com/danielmiessler/Fabric/pull/2029) by [ksylvan](https://github.com/ksylvan): Web UI: Fix streaming chat: tokens now accumulate in order into a single message
- Feat: improve streaming message handling and SSE buffer parsing
- Append content to existing assistant messages instead of replacing, and fix loading message removal to search by index rather than position
- Refactor SSE buffer splitting to always retain incomplete segments, and trim segments before parsing to handle whitespace edge cases
- Consolidate duplicate message update logic across chat components and process remaining buffer after stream completion more reliably

View file

@ -329,15 +329,22 @@ async function readFileContent(file: File): Promise<string> {
messageStore.update(messages => {
const newMessages = [...messages];
// Replace the processing message with actual content
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.format === 'loading') {
newMessages.pop();
const loadingIndex = newMessages.findIndex(m => m.format === 'loading');
if (loadingIndex !== -1) {
newMessages.splice(loadingIndex, 1);
}
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === 'assistant') {
lastMessage.content += content;
lastMessage.format = response?.format;
} else {
newMessages.push({
role: 'assistant',
content,
format: response?.format
});
}
newMessages.push({
role: 'assistant',
content,
format: response?.format
});
return newMessages;
});
},
@ -442,13 +449,18 @@ async function readFileContent(file: File): Promise<string> {
if (loadingIndex !== -1) {
newMessages.splice(loadingIndex, 1);
}
// Always append a new assistant message
newMessages.push({
role: 'assistant',
content,
format: response?.format
});
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === 'assistant') {
lastMessage.content += content;
lastMessage.format = response?.format;
} else {
newMessages.push({
role: 'assistant',
content,
format: response?.format
});
}
return newMessages;
});
},

View file

@ -142,31 +142,41 @@ export class ChatService {
if (done) break;
buffer += new TextDecoder().decode(value);
const messages = buffer
.split("\n\n")
.filter((msg) => msg.startsWith("data: "));
if (messages.length > 1) {
buffer = messages.pop() || "";
for (const msg of messages) {
try {
let response = JSON.parse(msg.slice(6)) as StreamResponse;
response = processResponse(response);
controller.enqueue(response);
} catch (parseError) {
console.error("Error parsing stream message:", parseError);
}
const segments = buffer.split("\n\n");
// Last segment may be incomplete; keep it as buffer
buffer = segments.pop() || "";
for (const segment of segments) {
const trimmed = segment.trim();
if (!trimmed.startsWith("data: ")) continue;
try {
let response = JSON.parse(
trimmed.slice(6),
) as StreamResponse;
response = processResponse(response);
controller.enqueue(response);
} catch (parseError) {
console.error(
"Error parsing stream message:",
parseError,
);
}
}
}
if (buffer.startsWith("data: ")) {
// Process any remaining complete message in the buffer
const trimmed = buffer.trim();
if (trimmed.startsWith("data: ")) {
try {
let response = JSON.parse(buffer.slice(6)) as StreamResponse;
let response = JSON.parse(
trimmed.slice(6),
) as StreamResponse;
response = processResponse(response);
controller.enqueue(response);
} catch (parseError) {
console.error("Error parsing final message:", parseError);
console.error(
"Error parsing final message:",
parseError,
);
}
}
} catch (error) {

View file

@ -132,7 +132,7 @@ export async function sendMessage(
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === "assistant") {
lastMessage.content = content;
lastMessage.content += content;
lastMessage.format = response?.format;
console.log("Message updated:", {
role: "assistant",