Skip to content

Commit 0caaccf

Browse files
Algorithm5838github-actions[bot]
authored andcommitted
perf: incremental artifact updates during streaming
1 parent 66c23c2 commit 0caaccf

1 file changed

Lines changed: 83 additions & 34 deletions

File tree

src/lib/components/chat/Chat.svelte

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,10 @@
498498
message.content += data.content;
499499
} else if (type === 'chat:message' || type === 'replace') {
500500
message.content = data.content;
501+
// Non-current content patch; invalidate the stable cache.
502+
if (event.message_id !== history.currentId) {
503+
stableArtifactCurrentId = null;
504+
}
501505
} else if (type === 'chat:message:files' || type === 'files') {
502506
message.files = data.files;
503507
} else if (type === 'chat:message:tasks') {
@@ -533,6 +537,10 @@
533537
originalContent: existing.content,
534538
...msg
535539
};
540+
// Non-current content patch; invalidate the stable cache.
541+
if (msg.id !== history.currentId) {
542+
stableArtifactCurrentId = null;
543+
}
536544
}
537545
}
538546
}
@@ -1055,32 +1063,13 @@
10551063
}
10561064
};
10571065
1058-
const onHistoryChange = (history) => {
1059-
if (history) {
1060-
cancelAnimationFrame(contentsRAF);
1061-
contentsRAF = requestAnimationFrame(() => {
1062-
getContents();
1063-
contentsRAF = null;
1064-
});
1065-
} else {
1066-
artifactContents.set([]);
1067-
}
1068-
};
1069-
1070-
$: onHistoryChange(history);
1071-
1072-
const getContents = () => {
1073-
const messages = history ? createMessagesList(history, history.currentId) : [];
1074-
let contents = [];
1075-
messages.forEach((message) => {
1076-
if (message?.role !== 'user' && message?.content) {
1077-
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1078-
message.content
1079-
);
1066+
const extractArtifactItemsFromContent = (content: string): ArtifactItem[] => {
1067+
const { codeBlocks, htmlGroups } = getCodeBlockContents(content);
1068+
const items: ArtifactItem[] = [];
10801069
1081-
if (htmlGroups && htmlGroups.length > 0) {
1082-
htmlGroups.forEach((group) => {
1083-
const renderedContent = `
1070+
if (htmlGroups && htmlGroups.length > 0) {
1071+
htmlGroups.forEach((group) => {
1072+
const renderedContent = `
10841073
<!DOCTYPE html>
10851074
<html lang="en">
10861075
<head>
@@ -1103,22 +1092,78 @@
11031092
</body>
11041093
</html>
11051094
`;
1106-
contents = [...contents, { type: 'iframe', content: renderedContent }];
1107-
});
1108-
} else {
1109-
// Check for SVG content
1110-
for (const block of codeBlocks) {
1111-
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
1112-
contents = [...contents, { type: 'svg', content: block.code }];
1113-
}
1114-
}
1095+
items.push({ type: 'iframe', content: renderedContent });
1096+
});
1097+
} else {
1098+
for (const block of codeBlocks) {
1099+
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
1100+
items.push({ type: 'svg', content: block.code });
11151101
}
11161102
}
1103+
}
1104+
1105+
return items;
1106+
};
1107+
1108+
const onHistoryChange = (history) => {
1109+
if (!history) {
1110+
stableArtifactContents = [];
1111+
stableArtifactCurrentId = null;
1112+
artifactContents.set([]);
1113+
return;
1114+
}
1115+
1116+
const currentMsg = history.currentId ? history.messages[history.currentId] : null;
1117+
const isStreamingAssistant = currentMsg?.role === 'assistant' && !currentMsg.done;
1118+
1119+
cancelAnimationFrame(contentsRAF);
1120+
contentsRAF = requestAnimationFrame(() => {
1121+
isStreamingAssistant ? getStreamingContents() : getContents();
1122+
contentsRAF = null;
11171123
});
1124+
};
1125+
1126+
$: onHistoryChange(history);
1127+
1128+
// Full reconcile; resets stable cache so the next streaming session starts clean.
1129+
const getContents = () => {
1130+
const messages = history ? createMessagesList(history, history.currentId) : [];
1131+
const contents: ArtifactItem[] = [];
11181132
1133+
for (const message of messages) {
1134+
if (message?.role !== 'user' && message?.content) {
1135+
contents.push(...extractArtifactItemsFromContent(message.content));
1136+
}
1137+
}
1138+
1139+
stableArtifactContents = [];
1140+
stableArtifactCurrentId = null;
11191141
artifactContents.set(contents);
11201142
};
11211143
1144+
// Streaming hot path; only parses the current message per frame.
1145+
// Rebuilds the stable cache once at the start of each streaming session.
1146+
const getStreamingContents = () => {
1147+
const currentMsg = history.currentId ? history.messages[history.currentId] : null;
1148+
1149+
if (stableArtifactCurrentId !== history.currentId) {
1150+
const messages = history ? createMessagesList(history, history.currentId) : [];
1151+
stableArtifactContents = [];
1152+
for (const message of messages) {
1153+
if (message.id !== history.currentId && message?.role !== 'user' && message?.content) {
1154+
stableArtifactContents.push(...extractArtifactItemsFromContent(message.content));
1155+
}
1156+
}
1157+
stableArtifactCurrentId = history.currentId;
1158+
}
1159+
1160+
const streamingItems = currentMsg?.content
1161+
? extractArtifactItemsFromContent(currentMsg.content)
1162+
: [];
1163+
1164+
artifactContents.set([...stableArtifactContents, ...streamingItems]);
1165+
};
1166+
11221167
//////////////////////////
11231168
// Web functions
11241169
//////////////////////////
@@ -1459,8 +1504,12 @@
14591504
}
14601505
};
14611506
1507+
type ArtifactItem = { type: string; content: string };
1508+
14621509
let scrollRAF = null;
14631510
let contentsRAF = null;
1511+
let stableArtifactContents: ArtifactItem[] = [];
1512+
let stableArtifactCurrentId: string | null = null;
14641513
const scheduleScrollToBottom = () => {
14651514
if (!scrollRAF) {
14661515
scrollRAF = requestAnimationFrame(async () => {

0 commit comments

Comments
 (0)