Skip to content

Commit e66a0f1

Browse files
committed
perf: incremental artifact updates during streaming
1 parent 229da72 commit e66a0f1

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
@@ -474,6 +474,10 @@
474474
message.content += data.content;
475475
} else if (type === 'chat:message' || type === 'replace') {
476476
message.content = data.content;
477+
// Non-current content patch; invalidate the stable cache.
478+
if (event.message_id !== history.currentId) {
479+
stableArtifactCurrentId = null;
480+
}
477481
} else if (type === 'chat:message:files' || type === 'files') {
478482
message.files = data.files;
479483
} else if (type === 'chat:message:tasks') {
@@ -509,6 +513,10 @@
509513
originalContent: existing.content,
510514
...msg
511515
};
516+
// Non-current content patch; invalidate the stable cache.
517+
if (msg.id !== history.currentId) {
518+
stableArtifactCurrentId = null;
519+
}
512520
}
513521
}
514522
}
@@ -1026,32 +1034,13 @@
10261034
}
10271035
};
10281036
1029-
const onHistoryChange = (history) => {
1030-
if (history) {
1031-
cancelAnimationFrame(contentsRAF);
1032-
contentsRAF = requestAnimationFrame(() => {
1033-
getContents();
1034-
contentsRAF = null;
1035-
});
1036-
} else {
1037-
artifactContents.set([]);
1038-
}
1039-
};
1040-
1041-
$: onHistoryChange(history);
1042-
1043-
const getContents = () => {
1044-
const messages = history ? createMessagesList(history, history.currentId) : [];
1045-
let contents = [];
1046-
messages.forEach((message) => {
1047-
if (message?.role !== 'user' && message?.content) {
1048-
const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents(
1049-
message.content
1050-
);
1037+
const extractArtifactItemsFromContent = (content: string): ArtifactItem[] => {
1038+
const { codeBlocks, htmlGroups } = getCodeBlockContents(content);
1039+
const items: ArtifactItem[] = [];
10511040
1052-
if (htmlGroups && htmlGroups.length > 0) {
1053-
htmlGroups.forEach((group) => {
1054-
const renderedContent = `
1041+
if (htmlGroups && htmlGroups.length > 0) {
1042+
htmlGroups.forEach((group) => {
1043+
const renderedContent = `
10551044
<!DOCTYPE html>
10561045
<html lang="en">
10571046
<head>
@@ -1074,22 +1063,78 @@
10741063
</body>
10751064
</html>
10761065
`;
1077-
contents = [...contents, { type: 'iframe', content: renderedContent }];
1078-
});
1079-
} else {
1080-
// Check for SVG content
1081-
for (const block of codeBlocks) {
1082-
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
1083-
contents = [...contents, { type: 'svg', content: block.code }];
1084-
}
1085-
}
1066+
items.push({ type: 'iframe', content: renderedContent });
1067+
});
1068+
} else {
1069+
for (const block of codeBlocks) {
1070+
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
1071+
items.push({ type: 'svg', content: block.code });
10861072
}
10871073
}
1074+
}
1075+
1076+
return items;
1077+
};
1078+
1079+
const onHistoryChange = (history) => {
1080+
if (!history) {
1081+
stableArtifactContents = [];
1082+
stableArtifactCurrentId = null;
1083+
artifactContents.set([]);
1084+
return;
1085+
}
1086+
1087+
const currentMsg = history.currentId ? history.messages[history.currentId] : null;
1088+
const isStreamingAssistant = currentMsg?.role === 'assistant' && !currentMsg.done;
1089+
1090+
cancelAnimationFrame(contentsRAF);
1091+
contentsRAF = requestAnimationFrame(() => {
1092+
isStreamingAssistant ? getStreamingContents() : getContents();
1093+
contentsRAF = null;
10881094
});
1095+
};
1096+
1097+
$: onHistoryChange(history);
1098+
1099+
// Full reconcile; resets stable cache so the next streaming session starts clean.
1100+
const getContents = () => {
1101+
const messages = history ? createMessagesList(history, history.currentId) : [];
1102+
const contents: ArtifactItem[] = [];
10891103
1104+
for (const message of messages) {
1105+
if (message?.role !== 'user' && message?.content) {
1106+
contents.push(...extractArtifactItemsFromContent(message.content));
1107+
}
1108+
}
1109+
1110+
stableArtifactContents = [];
1111+
stableArtifactCurrentId = null;
10901112
artifactContents.set(contents);
10911113
};
10921114
1115+
// Streaming hot path; only parses the current message per frame.
1116+
// Rebuilds the stable cache once at the start of each streaming session.
1117+
const getStreamingContents = () => {
1118+
const currentMsg = history.currentId ? history.messages[history.currentId] : null;
1119+
1120+
if (stableArtifactCurrentId !== history.currentId) {
1121+
const messages = history ? createMessagesList(history, history.currentId) : [];
1122+
stableArtifactContents = [];
1123+
for (const message of messages) {
1124+
if (message.id !== history.currentId && message?.role !== 'user' && message?.content) {
1125+
stableArtifactContents.push(...extractArtifactItemsFromContent(message.content));
1126+
}
1127+
}
1128+
stableArtifactCurrentId = history.currentId;
1129+
}
1130+
1131+
const streamingItems = currentMsg?.content
1132+
? extractArtifactItemsFromContent(currentMsg.content)
1133+
: [];
1134+
1135+
artifactContents.set([...stableArtifactContents, ...streamingItems]);
1136+
};
1137+
10931138
//////////////////////////
10941139
// Web functions
10951140
//////////////////////////
@@ -1430,8 +1475,12 @@
14301475
}
14311476
};
14321477
1478+
type ArtifactItem = { type: string; content: string };
1479+
14331480
let scrollRAF = null;
14341481
let contentsRAF = null;
1482+
let stableArtifactContents: ArtifactItem[] = [];
1483+
let stableArtifactCurrentId: string | null = null;
14351484
const scheduleScrollToBottom = () => {
14361485
if (!scrollRAF) {
14371486
scrollRAF = requestAnimationFrame(async () => {

0 commit comments

Comments
 (0)