Skip to content

Commit df1a4ac

Browse files
committed
perf: incremental artifact updates during streaming
1 parent c0e32c6 commit df1a4ac

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
@@ -486,6 +486,10 @@
486486
message.content += data.content;
487487
} else if (type === 'chat:message' || type === 'replace') {
488488
message.content = data.content;
489+
// Non-current content patch; invalidate the stable cache.
490+
if (event.message_id !== history.currentId) {
491+
stableArtifactCurrentId = null;
492+
}
489493
} else if (type === 'chat:message:files' || type === 'files') {
490494
message.files = data.files;
491495
} else if (type === 'chat:message:tasks') {
@@ -521,6 +525,10 @@
521525
originalContent: existing.content,
522526
...msg
523527
};
528+
// Non-current content patch; invalidate the stable cache.
529+
if (msg.id !== history.currentId) {
530+
stableArtifactCurrentId = null;
531+
}
524532
}
525533
}
526534
}
@@ -1040,32 +1048,13 @@
10401048
}
10411049
};
10421050
1043-
const onHistoryChange = (history) => {
1044-
if (history) {
1045-
cancelAnimationFrame(contentsRAF);
1046-
contentsRAF = requestAnimationFrame(() => {
1047-
getContents();
1048-
contentsRAF = null;
1049-
});
1050-
} else {
1051-
artifactContents.set([]);
1052-
}
1053-
};
1054-
1055-
$: onHistoryChange(history);
1056-
1057-
const getContents = () => {
1058-
const messages = history ? createMessagesList(history, history.currentId) : [];
1059-
let contents = [];
1060-
messages.forEach((message) => {
1061-
if (message?.role !== 'user' && message?.content) {
1062-
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1063-
message.content
1064-
);
1051+
const extractArtifactItemsFromContent = (content: string): ArtifactItem[] => {
1052+
const { codeBlocks, htmlGroups } = getCodeBlockContents(content);
1053+
const items: ArtifactItem[] = [];
10651054
1066-
if (htmlGroups && htmlGroups.length > 0) {
1067-
htmlGroups.forEach((group) => {
1068-
const renderedContent = `
1055+
if (htmlGroups && htmlGroups.length > 0) {
1056+
htmlGroups.forEach((group) => {
1057+
const renderedContent = `
10691058
<!DOCTYPE html>
10701059
<html lang="en">
10711060
<head>
@@ -1088,22 +1077,78 @@
10881077
</body>
10891078
</html>
10901079
`;
1091-
contents = [...contents, { type: 'iframe', content: renderedContent }];
1092-
});
1093-
} else {
1094-
// Check for SVG content
1095-
for (const block of codeBlocks) {
1096-
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
1097-
contents = [...contents, { type: 'svg', content: block.code }];
1098-
}
1099-
}
1080+
items.push({ type: 'iframe', content: renderedContent });
1081+
});
1082+
} else {
1083+
for (const block of codeBlocks) {
1084+
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
1085+
items.push({ type: 'svg', content: block.code });
11001086
}
11011087
}
1088+
}
1089+
1090+
return items;
1091+
};
1092+
1093+
const onHistoryChange = (history) => {
1094+
if (!history) {
1095+
stableArtifactContents = [];
1096+
stableArtifactCurrentId = null;
1097+
artifactContents.set([]);
1098+
return;
1099+
}
1100+
1101+
const currentMsg = history.currentId ? history.messages[history.currentId] : null;
1102+
const isStreamingAssistant = currentMsg?.role === 'assistant' && !currentMsg.done;
1103+
1104+
cancelAnimationFrame(contentsRAF);
1105+
contentsRAF = requestAnimationFrame(() => {
1106+
isStreamingAssistant ? getStreamingContents() : getContents();
1107+
contentsRAF = null;
11021108
});
1109+
};
1110+
1111+
$: onHistoryChange(history);
1112+
1113+
// Full reconcile; resets stable cache so the next streaming session starts clean.
1114+
const getContents = () => {
1115+
const messages = history ? createMessagesList(history, history.currentId) : [];
1116+
const contents: ArtifactItem[] = [];
11031117
1118+
for (const message of messages) {
1119+
if (message?.role !== 'user' && message?.content) {
1120+
contents.push(...extractArtifactItemsFromContent(message.content));
1121+
}
1122+
}
1123+
1124+
stableArtifactContents = [];
1125+
stableArtifactCurrentId = null;
11041126
artifactContents.set(contents);
11051127
};
11061128
1129+
// Streaming hot path; only parses the current message per frame.
1130+
// Rebuilds the stable cache once at the start of each streaming session.
1131+
const getStreamingContents = () => {
1132+
const currentMsg = history.currentId ? history.messages[history.currentId] : null;
1133+
1134+
if (stableArtifactCurrentId !== history.currentId) {
1135+
const messages = history ? createMessagesList(history, history.currentId) : [];
1136+
stableArtifactContents = [];
1137+
for (const message of messages) {
1138+
if (message.id !== history.currentId && message?.role !== 'user' && message?.content) {
1139+
stableArtifactContents.push(...extractArtifactItemsFromContent(message.content));
1140+
}
1141+
}
1142+
stableArtifactCurrentId = history.currentId;
1143+
}
1144+
1145+
const streamingItems = currentMsg?.content
1146+
? extractArtifactItemsFromContent(currentMsg.content)
1147+
: [];
1148+
1149+
artifactContents.set([...stableArtifactContents, ...streamingItems]);
1150+
};
1151+
11071152
//////////////////////////
11081153
// Web functions
11091154
//////////////////////////
@@ -1473,8 +1518,12 @@
14731518
await messagesRef?.scrollToTop();
14741519
};
14751520
1521+
type ArtifactItem = { type: string; content: string };
1522+
14761523
let scrollRAF = null;
14771524
let contentsRAF = null;
1525+
let stableArtifactContents: ArtifactItem[] = [];
1526+
let stableArtifactCurrentId: string | null = null;
14781527
const scheduleScrollToBottom = () => {
14791528
if (!scrollRAF) {
14801529
scrollRAF = requestAnimationFrame(async () => {

0 commit comments

Comments
 (0)