|
474 | 474 | message.content += data.content; |
475 | 475 | } else if (type === 'chat:message' || type === 'replace') { |
476 | 476 | message.content = data.content; |
| 477 | + // Non-current content patch; invalidate the stable cache. |
| 478 | + if (event.message_id !== history.currentId) { |
| 479 | + stableArtifactCurrentId = null; |
| 480 | + } |
477 | 481 | } else if (type === 'chat:message:files' || type === 'files') { |
478 | 482 | message.files = data.files; |
479 | 483 | } else if (type === 'chat:message:tasks') { |
|
509 | 513 | originalContent: existing.content, |
510 | 514 | ...msg |
511 | 515 | }; |
| 516 | + // Non-current content patch; invalidate the stable cache. |
| 517 | + if (msg.id !== history.currentId) { |
| 518 | + stableArtifactCurrentId = null; |
| 519 | + } |
512 | 520 | } |
513 | 521 | } |
514 | 522 | } |
|
1026 | 1034 | } |
1027 | 1035 | }; |
1028 | 1036 |
|
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[] = []; |
1051 | 1040 |
|
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 = ` |
1055 | 1044 | <!DOCTYPE html> |
1056 | 1045 | <html lang="en"> |
1057 | 1046 | <head> |
|
1074 | 1063 | </body> |
1075 | 1064 | </html> |
1076 | 1065 | `; |
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 }); |
1086 | 1072 | } |
1087 | 1073 | } |
| 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; |
1088 | 1094 | }); |
| 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[] = []; |
1089 | 1103 |
|
| 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; |
1090 | 1112 | artifactContents.set(contents); |
1091 | 1113 | }; |
1092 | 1114 |
|
| 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 | +
|
1093 | 1138 | ////////////////////////// |
1094 | 1139 | // Web functions |
1095 | 1140 | ////////////////////////// |
|
1430 | 1475 | } |
1431 | 1476 | }; |
1432 | 1477 |
|
| 1478 | + type ArtifactItem = { type: string; content: string }; |
| 1479 | +
|
1433 | 1480 | let scrollRAF = null; |
1434 | 1481 | let contentsRAF = null; |
| 1482 | + let stableArtifactContents: ArtifactItem[] = []; |
| 1483 | + let stableArtifactCurrentId: string | null = null; |
1435 | 1484 | const scheduleScrollToBottom = () => { |
1436 | 1485 | if (!scrollRAF) { |
1437 | 1486 | scrollRAF = requestAnimationFrame(async () => { |
|
0 commit comments