|
486 | 486 | message.content += data.content; |
487 | 487 | } else if (type === 'chat:message' || type === 'replace') { |
488 | 488 | message.content = data.content; |
| 489 | + // Non-current content patch; invalidate the stable cache. |
| 490 | + if (event.message_id !== history.currentId) { |
| 491 | + stableArtifactCurrentId = null; |
| 492 | + } |
489 | 493 | } else if (type === 'chat:message:files' || type === 'files') { |
490 | 494 | message.files = data.files; |
491 | 495 | } else if (type === 'chat:message:tasks') { |
|
521 | 525 | originalContent: existing.content, |
522 | 526 | ...msg |
523 | 527 | }; |
| 528 | + // Non-current content patch; invalidate the stable cache. |
| 529 | + if (msg.id !== history.currentId) { |
| 530 | + stableArtifactCurrentId = null; |
| 531 | + } |
524 | 532 | } |
525 | 533 | } |
526 | 534 | } |
|
1040 | 1048 | } |
1041 | 1049 | }; |
1042 | 1050 |
|
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[] = []; |
1065 | 1054 |
|
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 = ` |
1069 | 1058 | <!DOCTYPE html> |
1070 | 1059 | <html lang="en"> |
1071 | 1060 | <head> |
|
1088 | 1077 | </body> |
1089 | 1078 | </html> |
1090 | 1079 | `; |
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 }); |
1100 | 1086 | } |
1101 | 1087 | } |
| 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; |
1102 | 1108 | }); |
| 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[] = []; |
1103 | 1117 |
|
| 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; |
1104 | 1126 | artifactContents.set(contents); |
1105 | 1127 | }; |
1106 | 1128 |
|
| 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 | +
|
1107 | 1152 | ////////////////////////// |
1108 | 1153 | // Web functions |
1109 | 1154 | ////////////////////////// |
|
1473 | 1518 | await messagesRef?.scrollToTop(); |
1474 | 1519 | }; |
1475 | 1520 |
|
| 1521 | + type ArtifactItem = { type: string; content: string }; |
| 1522 | +
|
1476 | 1523 | let scrollRAF = null; |
1477 | 1524 | let contentsRAF = null; |
| 1525 | + let stableArtifactContents: ArtifactItem[] = []; |
| 1526 | + let stableArtifactCurrentId: string | null = null; |
1478 | 1527 | const scheduleScrollToBottom = () => { |
1479 | 1528 | if (!scrollRAF) { |
1480 | 1529 | scrollRAF = requestAnimationFrame(async () => { |
|
0 commit comments