|
498 | 498 | message.content += data.content; |
499 | 499 | } else if (type === 'chat:message' || type === 'replace') { |
500 | 500 | message.content = data.content; |
| 501 | + // Non-current content patch; invalidate the stable cache. |
| 502 | + if (event.message_id !== history.currentId) { |
| 503 | + stableArtifactCurrentId = null; |
| 504 | + } |
501 | 505 | } else if (type === 'chat:message:files' || type === 'files') { |
502 | 506 | message.files = data.files; |
503 | 507 | } else if (type === 'chat:message:tasks') { |
|
533 | 537 | originalContent: existing.content, |
534 | 538 | ...msg |
535 | 539 | }; |
| 540 | + // Non-current content patch; invalidate the stable cache. |
| 541 | + if (msg.id !== history.currentId) { |
| 542 | + stableArtifactCurrentId = null; |
| 543 | + } |
536 | 544 | } |
537 | 545 | } |
538 | 546 | } |
|
1055 | 1063 | } |
1056 | 1064 | }; |
1057 | 1065 |
|
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[] = []; |
1080 | 1069 |
|
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 = ` |
1084 | 1073 | <!DOCTYPE html> |
1085 | 1074 | <html lang="en"> |
1086 | 1075 | <head> |
|
1103 | 1092 | </body> |
1104 | 1093 | </html> |
1105 | 1094 | `; |
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 }); |
1115 | 1101 | } |
1116 | 1102 | } |
| 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; |
1117 | 1123 | }); |
| 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[] = []; |
1118 | 1132 |
|
| 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; |
1119 | 1141 | artifactContents.set(contents); |
1120 | 1142 | }; |
1121 | 1143 |
|
| 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 | +
|
1122 | 1167 | ////////////////////////// |
1123 | 1168 | // Web functions |
1124 | 1169 | ////////////////////////// |
|
1459 | 1504 | } |
1460 | 1505 | }; |
1461 | 1506 |
|
| 1507 | + type ArtifactItem = { type: string; content: string }; |
| 1508 | +
|
1462 | 1509 | let scrollRAF = null; |
1463 | 1510 | let contentsRAF = null; |
| 1511 | + let stableArtifactContents: ArtifactItem[] = []; |
| 1512 | + let stableArtifactCurrentId: string | null = null; |
1464 | 1513 | const scheduleScrollToBottom = () => { |
1465 | 1514 | if (!scrollRAF) { |
1466 | 1515 | scrollRAF = requestAnimationFrame(async () => { |
|
0 commit comments