@memori.ai/memori-react 9.0.0-alpha.20 → 9.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +438 -1
  2. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +1 -1
  3. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  4. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -2
  5. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +6 -6
  6. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
  7. package/dist/components/ChatInputs/ChatInputs.js +1 -1
  8. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  9. package/dist/components/ContentPreviewModal/ContentPreviewModal.css +6 -22
  10. package/dist/components/FilePreview/FilePreview.css +41 -20
  11. package/dist/components/FilePreview/FilePreview.d.ts +2 -1
  12. package/dist/components/FilePreview/FilePreview.js +6 -2
  13. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  14. package/dist/components/KnownFacts/KnownFacts.js +2 -2
  15. package/dist/components/KnownFacts/KnownFacts.js.map +1 -1
  16. package/dist/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js +1 -1
  17. package/dist/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js.map +1 -1
  18. package/dist/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js +14 -12
  19. package/dist/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js.map +1 -1
  20. package/dist/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +1 -106
  21. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +8 -9
  22. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
  23. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  24. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/components/TabSwitch.css +7 -0
  25. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +210 -62
  26. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  27. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
  28. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js +65 -1
  29. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
  30. package/dist/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
  31. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
  32. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
  33. package/dist/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
  34. package/dist/components/MemoriArtifactSystem/utils/applyEdits.js +117 -0
  35. package/dist/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
  36. package/dist/components/MemoriWidget/MemoriWidget.css +59 -0
  37. package/dist/components/Snippet/Snippet.css +6 -36
  38. package/dist/components/Snippet/Snippet.js +3 -1
  39. package/dist/components/Snippet/Snippet.js.map +1 -1
  40. package/dist/components/StartPanel/StartPanel.css +106 -0
  41. package/dist/components/StartPanel/StartPanel.js +1 -1
  42. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  43. package/dist/components/UploadButton/UploadButton.css +0 -19
  44. package/dist/components/UploadButton/UploadButton.js +1 -3
  45. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  46. package/dist/components/UploadButton/UploadImages/UploadImages.js +1 -1
  47. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  48. package/dist/components/layouts/chat.css +2 -0
  49. package/dist/components/layouts/zoomed-full-body.css +1 -0
  50. package/dist/helpers/message.js +3 -0
  51. package/dist/helpers/message.js.map +1 -1
  52. package/dist/locales/de.json +3 -0
  53. package/dist/locales/en.json +3 -0
  54. package/dist/locales/es.json +3 -0
  55. package/dist/locales/fr.json +3 -0
  56. package/dist/locales/it.json +3 -0
  57. package/dist/styles.css +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +2 -2
  61. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  62. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -2
  63. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +4 -5
  64. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
  65. package/esm/components/ChatInputs/ChatInputs.js +1 -1
  66. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  67. package/esm/components/ContentPreviewModal/ContentPreviewModal.css +6 -22
  68. package/esm/components/FilePreview/FilePreview.css +41 -20
  69. package/esm/components/FilePreview/FilePreview.d.ts +2 -1
  70. package/esm/components/FilePreview/FilePreview.js +7 -3
  71. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  72. package/esm/components/KnownFacts/KnownFacts.js +2 -2
  73. package/esm/components/KnownFacts/KnownFacts.js.map +1 -1
  74. package/esm/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js +1 -1
  75. package/esm/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js.map +1 -1
  76. package/esm/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js +15 -13
  77. package/esm/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js.map +1 -1
  78. package/esm/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +1 -106
  79. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +8 -9
  80. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
  81. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  82. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/components/TabSwitch.css +7 -0
  83. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +211 -63
  84. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  85. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
  86. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js +66 -2
  87. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
  88. package/esm/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
  89. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
  90. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
  91. package/esm/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
  92. package/esm/components/MemoriArtifactSystem/utils/applyEdits.js +112 -0
  93. package/esm/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
  94. package/esm/components/MemoriWidget/MemoriWidget.css +59 -0
  95. package/esm/components/Snippet/Snippet.css +6 -36
  96. package/esm/components/Snippet/Snippet.js +4 -2
  97. package/esm/components/Snippet/Snippet.js.map +1 -1
  98. package/esm/components/StartPanel/StartPanel.css +106 -0
  99. package/esm/components/StartPanel/StartPanel.js +2 -2
  100. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  101. package/esm/components/UploadButton/UploadButton.css +0 -19
  102. package/esm/components/UploadButton/UploadButton.js +1 -3
  103. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  104. package/esm/components/UploadButton/UploadImages/UploadImages.js +1 -1
  105. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  106. package/esm/components/layouts/chat.css +2 -0
  107. package/esm/components/layouts/zoomed-full-body.css +1 -0
  108. package/esm/helpers/message.js +3 -0
  109. package/esm/helpers/message.js.map +1 -1
  110. package/esm/locales/de.json +3 -0
  111. package/esm/locales/en.json +3 -0
  112. package/esm/locales/es.json +3 -0
  113. package/esm/locales/fr.json +3 -0
  114. package/esm/locales/it.json +3 -0
  115. package/esm/styles.css +1 -1
  116. package/esm/version.d.ts +1 -1
  117. package/esm/version.js +1 -1
  118. package/package.json +1 -1
  119. package/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +2 -2
  120. package/src/components/Avatar/AvatarView/AvatarComponent/constants.test.ts +19 -0
  121. package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +9 -6
  122. package/src/components/ChatInputs/ChatInputs.tsx +1 -0
  123. package/src/components/ContentPreviewModal/ContentPreviewModal.css +6 -22
  124. package/src/components/FilePreview/FilePreview.css +41 -20
  125. package/src/components/FilePreview/FilePreview.tsx +14 -0
  126. package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +14 -0
  127. package/src/components/KnownFacts/KnownFacts.tsx +0 -2
  128. package/src/components/MediaWidget/MediaPreviewModal.test.tsx +2 -3
  129. package/src/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.tsx +19 -33
  130. package/src/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.tsx +44 -45
  131. package/src/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +1 -106
  132. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +8 -9
  133. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +24 -20
  134. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/components/TabSwitch.css +7 -0
  135. package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +357 -148
  136. package/src/components/MemoriArtifactSystem/context/ArtifactContext.tsx +111 -9
  137. package/src/components/MemoriArtifactSystem/types/artifact.types.ts +20 -1
  138. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.test.tsx +1 -0
  139. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.tsx +19 -13
  140. package/src/components/MemoriArtifactSystem/utils/applyEdits.test.ts +85 -0
  141. package/src/components/MemoriArtifactSystem/utils/applyEdits.ts +173 -0
  142. package/src/components/MemoriWidget/MemoriWidget.css +59 -0
  143. package/src/components/Snippet/Snippet.css +6 -36
  144. package/src/components/Snippet/Snippet.tsx +6 -7
  145. package/src/components/StartPanel/StartPanel.css +106 -0
  146. package/src/components/StartPanel/StartPanel.stories.tsx +20 -0
  147. package/src/components/StartPanel/StartPanel.tsx +8 -0
  148. package/src/components/UploadButton/UploadButton.css +0 -19
  149. package/src/components/UploadButton/UploadButton.tsx +0 -11
  150. package/src/components/UploadButton/UploadImages/UploadImages.tsx +1 -1
  151. package/src/components/layouts/chat.css +2 -0
  152. package/src/components/layouts/zoomed-full-body.css +1 -0
  153. package/src/helpers/message.ts +15 -0
  154. package/src/locales/de.json +3 -0
  155. package/src/locales/en.json +3 -0
  156. package/src/locales/es.json +3 -0
  157. package/src/locales/fr.json +3 -0
  158. package/src/locales/it.json +3 -0
  159. package/src/version.ts +1 -1
@@ -1,6 +1,13 @@
1
- import React, { useEffect, useMemo, useCallback, memo } from 'react';
1
+ import React, {
2
+ useEffect,
3
+ useState,
4
+ useMemo,
5
+ useCallback,
6
+ memo,
7
+ useRef,
8
+ } from 'react';
2
9
  import { useArtifact } from '../../context/ArtifactContext';
3
- import { ArtifactData } from '../../types/artifact.types';
10
+ import { ArtifactData, ArtifactEdit } from '../../types/artifact.types';
4
11
  import {
5
12
  ChevronRight,
6
13
  ChevronDown,
@@ -18,8 +25,9 @@ import MarkdownIcon from '../../../../icons/artifacts/MarkdownIcon';
18
25
  import PythonIcon from '../../../../icons/artifacts/PythonIcon';
19
26
  import SvgIcon from '../../../../icons/artifacts/SvgIcon';
20
27
  import XmlIcon from '../../../../icons/artifacts/XmlIcon';
28
+ import { parseEdits } from '../../utils/applyEdits';
29
+ import { useTranslation } from 'react-i18next';
21
30
 
22
- // Event type for artifact creation
23
31
  type ArtifactCreatedEvent = CustomEvent<{
24
32
  artifact: ArtifactData;
25
33
  message: Message;
@@ -30,6 +38,16 @@ interface ArtifactHandlerProps {
30
38
  message: Message;
31
39
  }
32
40
 
41
+ interface DetectedUpdate {
42
+ artifactId: string;
43
+ edits: ArtifactEdit[];
44
+ }
45
+
46
+ interface FailedUpdate {
47
+ artifactId: string;
48
+ reason: 'not-found' | 'no-match' | 'invalid';
49
+ }
50
+
33
51
  // ---------------------------------------------------------------------------
34
52
  // Helpers
35
53
  // ---------------------------------------------------------------------------
@@ -42,48 +60,72 @@ const formatBytes = (bytes: number): string => {
42
60
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
43
61
  };
44
62
 
45
- /**
46
- * Lightweight deterministic hash of the first 200 chars of a string.
47
- * Used to produce stable artifact IDs that survive re-renders.
48
- */
49
- const hashContent = (str: string): string => {
50
- let hash = 0;
51
- const len = Math.min(str.length, 200);
52
- for (let i = 0; i < len; i++) {
53
- hash = (hash << 5) - hash + str.charCodeAt(i);
54
- hash |= 0; // Convert to 32-bit int
55
- }
56
- return Math.abs(hash).toString(36);
63
+ const CLASS_ATTR_RE = /class\s*=\s*["']([^"']*)["']/i;
64
+ const DATA_ACTION_RE = /data-action\s*=\s*["']([^"']+)["']/i;
65
+
66
+ const getClassList = (openingTag: string): string[] => {
67
+ const m = openingTag.match(CLASS_ATTR_RE);
68
+ if (!m) return [];
69
+ return m[1].split(/\s+/).filter(Boolean);
70
+ };
71
+
72
+ const getDataAction = (openingTag: string): string | null => {
73
+ const m = openingTag.match(DATA_ACTION_RE);
74
+ return m?.[1]?.trim().toLowerCase() || null;
75
+ };
76
+
77
+ /** True for create tags: class memori-artifact, action create/absent (not update). */
78
+ const isCreateArtifactTag = (openingTag: string): boolean => {
79
+ const classes = getClassList(openingTag);
80
+ if (classes.includes('memori-artifact-update')) return false;
81
+ if (!classes.includes('memori-artifact')) return false;
82
+ const action = getDataAction(openingTag);
83
+ return action === null || action === 'create';
57
84
  };
58
85
 
59
86
  /**
60
- * Returns true when the `class` attribute value of an opening tag contains
61
- * the word "memori-artifact" as a whole word (handles multiple classes).
62
- *
63
- * Examples that match:
64
- * class="memori-artifact"
65
- * class="memori-artifact extra-class"
66
- * class="foo memori-artifact"
67
- * class='memori-artifact'
68
- * class = "memori-artifact extra"
87
+ * True for update tags:
88
+ * - preferred: class="memori-artifact" data-action="update"
89
+ * - legacy: class="memori-artifact-update"
69
90
  */
70
- const CLASS_ATTR_RE = /class\s*=\s*["']([^"']*)["']/i;
71
- const hasMemoriArtifactClass = (openingTag: string): boolean => {
72
- const m = openingTag.match(CLASS_ATTR_RE);
73
- if (!m) return false;
74
- // \b word-boundary ensures we don't match "foo-memori-artifact-bar"
75
- return /\bmemori-artifact\b/.test(m[1]);
91
+ const isUpdateArtifactTag = (openingTag: string): boolean => {
92
+ const classes = getClassList(openingTag);
93
+ if (classes.includes('memori-artifact-update')) return true;
94
+ if (!classes.includes('memori-artifact')) return false;
95
+ return getDataAction(openingTag) === 'update';
96
+ };
97
+
98
+ const findMatchingOutputClose = (cleaned: string, openEnd: number): number => {
99
+ let depth = 1;
100
+ let pos = openEnd;
101
+ let closeStart = -1;
102
+
103
+ while (pos < cleaned.length && depth > 0) {
104
+ const nextOpen = cleaned.indexOf('<output', pos);
105
+ const nextClose = cleaned.indexOf('</output>', pos);
106
+
107
+ if (nextClose === -1) {
108
+ return cleaned.length;
109
+ }
110
+
111
+ if (nextOpen !== -1 && nextOpen < nextClose) {
112
+ depth++;
113
+ pos = nextOpen + '<output'.length;
114
+ } else {
115
+ depth--;
116
+ if (depth === 0) {
117
+ closeStart = nextClose;
118
+ } else {
119
+ pos = nextClose + '</output>'.length;
120
+ }
121
+ }
122
+ }
123
+
124
+ return closeStart;
76
125
  };
77
126
 
78
127
  /**
79
- * Parser-style artifact detector.
80
- *
81
- * Replaces the previous single-regex approach with a depth-tracking loop so
82
- * that:
83
- * - Multiple CSS classes are handled correctly (e.g. class="memori-artifact foo")
84
- * - Nested / self-contained <output> tags inside the content don't truncate it
85
- * - Unclosed tags are gracefully skipped
86
- * - IDs are stable across re-renders (derived from message + content hash)
128
+ * Detect create artifacts (class="memori-artifact", data-action create/absent).
87
129
  */
88
130
  const detectArtifacts = (
89
131
  text: string,
@@ -97,66 +139,29 @@ const detectArtifacts = (
97
139
  let searchFrom = 0;
98
140
  let artifactNum = 0;
99
141
 
100
- // Regex that matches any <output …> opening tag (capturing the full tag)
101
142
  const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
102
143
 
103
144
  while (searchFrom < cleaned.length) {
104
- // Find the next <output …> opening tag from the current position
105
145
  OPEN_TAG_RE.lastIndex = searchFrom;
106
146
  const openMatch = OPEN_TAG_RE.exec(cleaned);
107
147
  if (!openMatch) break;
108
148
 
109
- const fullOpenTag = openMatch[0]; // e.g. <output class="memori-artifact" data-mimetype="text/html">
110
- const openStart = openMatch.index;
111
- const openEnd = openStart + fullOpenTag.length;
149
+ const fullOpenTag = openMatch[0];
150
+ const openEnd = openMatch.index + fullOpenTag.length;
112
151
 
113
- // Only process tags that carry the required class
114
- if (!hasMemoriArtifactClass(fullOpenTag)) {
152
+ if (!isCreateArtifactTag(fullOpenTag)) {
115
153
  searchFrom = openEnd;
116
154
  continue;
117
155
  }
118
156
 
119
- // Depth-tracking scan to find the matching </output>
120
- let depth = 1;
121
- let pos = openEnd;
122
- let closeStart = -1;
123
-
124
- while (pos < cleaned.length && depth > 0) {
125
- const nextOpen = cleaned.indexOf('<output', pos);
126
- const nextClose = cleaned.indexOf('</output>', pos);
127
-
128
- if (nextClose === -1) {
129
- // No closing tag found — treat everything until EOF as content
130
- // (handles streaming / partial messages)
131
- closeStart = cleaned.length;
132
- depth = 0;
133
- break;
134
- }
135
-
136
- if (nextOpen !== -1 && nextOpen < nextClose) {
137
- // There's another opening tag before the next closing tag → go deeper
138
- depth++;
139
- pos = nextOpen + '<output'.length;
140
- } else {
141
- // Found a closing tag
142
- depth--;
143
- if (depth === 0) {
144
- closeStart = nextClose;
145
- } else {
146
- pos = nextClose + '</output>'.length;
147
- }
148
- }
149
- }
150
-
157
+ const closeStart = findMatchingOutputClose(cleaned, openEnd);
151
158
  if (closeStart === -1) {
152
- // Malformed — skip past this opening tag and continue
153
159
  searchFrom = openEnd;
154
160
  continue;
155
161
  }
156
162
 
157
163
  const content = cleaned.slice(openEnd, closeStart).trim();
158
164
 
159
- // Extract metadata from the opening tag
160
165
  const mimeTypeMatch = fullOpenTag.match(
161
166
  /data-mimetype\s*=\s*["']([^"']+)["']/i
162
167
  );
@@ -173,13 +178,16 @@ const detectArtifacts = (
173
178
 
174
179
  artifactNum++;
175
180
 
176
- // Stable ID: does not change across re-renders for the same content
177
- const stableId = `artifact-${messageKey}-${artifactNum}-${hashContent(
178
- content
179
- )}`;
181
+ const dataArtifactIdMatch = fullOpenTag.match(
182
+ /data-artifact-id\s*=\s*["']([^"']+)["']/i
183
+ );
184
+ const artifactId =
185
+ dataArtifactIdMatch?.[1]?.trim() ||
186
+ `artifact-${messageKey}-${artifactNum}`;
180
187
 
181
188
  artifacts.push({
182
- id: stableId,
189
+ id: `artifact-ui-${artifactId}`,
190
+ artifactId,
183
191
  content,
184
192
  mimeType,
185
193
  title,
@@ -187,7 +195,6 @@ const detectArtifacts = (
187
195
  size: content.length,
188
196
  });
189
197
 
190
- // Advance past the closing tag (or EOF)
191
198
  searchFrom =
192
199
  closeStart === cleaned.length
193
200
  ? cleaned.length
@@ -197,6 +204,55 @@ const detectArtifacts = (
197
204
  return artifacts;
198
205
  };
199
206
 
207
+ /**
208
+ * Detect update tags and parse their JSON edit bodies.
209
+ */
210
+ const detectUpdates = (text: string, isFromUser: boolean): DetectedUpdate[] => {
211
+ if (!text || isFromUser) return [];
212
+
213
+ const cleaned = stripReasoningTags(text);
214
+ const updates: DetectedUpdate[] = [];
215
+ let searchFrom = 0;
216
+
217
+ const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
218
+
219
+ while (searchFrom < cleaned.length) {
220
+ OPEN_TAG_RE.lastIndex = searchFrom;
221
+ const openMatch = OPEN_TAG_RE.exec(cleaned);
222
+ if (!openMatch) break;
223
+
224
+ const fullOpenTag = openMatch[0];
225
+ const openEnd = openMatch.index + fullOpenTag.length;
226
+
227
+ if (!isUpdateArtifactTag(fullOpenTag)) {
228
+ searchFrom = openEnd;
229
+ continue;
230
+ }
231
+
232
+ const closeStart = cleaned.indexOf('</output>', openEnd);
233
+ const bodyEnd = closeStart === -1 ? cleaned.length : closeStart;
234
+ const body = cleaned.slice(openEnd, bodyEnd).trim();
235
+
236
+ const idMatch = fullOpenTag.match(
237
+ /data-artifact-id\s*=\s*["']([^"']+)["']/i
238
+ );
239
+ const artifactId = idMatch?.[1]?.trim();
240
+ const edits = parseEdits(body);
241
+
242
+ if (artifactId && edits) {
243
+ updates.push({ artifactId, edits });
244
+ } else if (artifactId) {
245
+ // Invalid body — still surface as a failed update for UI feedback
246
+ updates.push({ artifactId, edits: [] });
247
+ }
248
+
249
+ searchFrom =
250
+ closeStart === -1 ? cleaned.length : closeStart + '</output>'.length;
251
+ }
252
+
253
+ return updates;
254
+ };
255
+
200
256
  // ---------------------------------------------------------------------------
201
257
  // Component
202
258
  // ---------------------------------------------------------------------------
@@ -205,22 +261,21 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
205
261
  isChatlogPanel = false,
206
262
  message,
207
263
  }) => {
208
- const { openArtifact, state, closeArtifact } = useArtifact();
264
+ const {
265
+ openArtifact,
266
+ state,
267
+ closeArtifact,
268
+ registerArtifact,
269
+ applyArtifactUpdate,
270
+ } = useArtifact();
271
+ const { t } = useTranslation();
209
272
 
210
- /**
211
- * Use raw text for artifact detection.
212
- * translatedText may lose <output> tags — keep it as fallback only.
213
- */
214
273
  const messageText = useMemo(() => message.text || '', [message.text]);
215
274
  const translatedMessageText = useMemo(
216
275
  () => message.translatedText || '',
217
276
  [message.translatedText]
218
277
  );
219
278
 
220
- /**
221
- * Stable key that identifies this specific message.
222
- * Used both for effect gating and for stable artifact ID generation.
223
- */
224
279
  const messageKey = useMemo(
225
280
  () => `${message.timestamp}-${message.fromUser ? '1' : '0'}`,
226
281
  [message.timestamp, message.fromUser]
@@ -240,11 +295,6 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
240
295
  [message]
241
296
  );
242
297
 
243
- /**
244
- * Memoised artifact list.
245
- * Falls back to translatedText only when the primary text yields nothing.
246
- * Both branches use the same messageKey so IDs remain stable.
247
- */
248
298
  const artifacts = useMemo<ArtifactData[]>(() => {
249
299
  const fromUser = message.fromUser || false;
250
300
  const primary = detectArtifacts(messageText, fromUser, messageKey);
@@ -252,29 +302,95 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
252
302
  return detectArtifacts(translatedMessageText, fromUser, messageKey);
253
303
  }, [messageText, translatedMessageText, message.fromUser, messageKey]);
254
304
 
255
- /**
256
- * Auto-open the first artifact when a new message arrives.
257
- *
258
- * FIX: messageText is now included in the dependency array so the effect
259
- * re-fires correctly if the text changes without the messageKey changing
260
- * (e.g. streaming updates).
261
- */
305
+ const detectedUpdates = useMemo<DetectedUpdate[]>(() => {
306
+ const fromUser = message.fromUser || false;
307
+ const primary = detectUpdates(messageText, fromUser);
308
+ if (primary.length > 0) return primary;
309
+ return detectUpdates(translatedMessageText, fromUser);
310
+ }, [messageText, translatedMessageText, message.fromUser]);
311
+
312
+ const [updateResultMap, setUpdateResultMap] = useState<
313
+ Record<number, ArtifactData>
314
+ >({});
315
+ const [failedUpdates, setFailedUpdates] = useState<FailedUpdate[]>([]);
316
+
317
+ const processedUpdatesRef = useRef<string>('');
318
+ const hasAutoOpenedRef = useRef<string>('');
319
+
262
320
  useEffect(() => {
263
- if (!messageText || artifacts.length === 0) return;
321
+ if (!messageText && !translatedMessageText) return;
264
322
 
265
- artifacts.forEach(artifact => dispatchArtifactCreatedEvent(artifact));
323
+ artifacts.forEach(artifact => {
324
+ registerArtifact(artifact);
325
+ dispatchArtifactCreatedEvent(artifact);
326
+ });
266
327
 
267
- if (!isChatlogPanel) {
268
- const timer = setTimeout(
269
- () => openArtifact(artifacts[0], { isChatLogPanel: false }),
270
- 100
271
- );
272
- return () => clearTimeout(timer);
328
+ const updatesSignature = `${messageKey}:${detectedUpdates
329
+ .map(u => `${u.artifactId}:${JSON.stringify(u.edits)}`)
330
+ .join('|')}`;
331
+
332
+ let lastUpdated: ArtifactData | null = null;
333
+
334
+ if (
335
+ detectedUpdates.length > 0 &&
336
+ processedUpdatesRef.current !== updatesSignature
337
+ ) {
338
+ processedUpdatesRef.current = updatesSignature;
339
+ const nextMap: Record<number, ArtifactData> = {};
340
+ const nextFailed: FailedUpdate[] = [];
341
+
342
+ detectedUpdates.forEach((update, idx) => {
343
+ if (!update.edits.length) {
344
+ nextFailed.push({
345
+ artifactId: update.artifactId,
346
+ reason: 'invalid',
347
+ });
348
+ return;
349
+ }
350
+
351
+ const result = applyArtifactUpdate(update.artifactId, update.edits);
352
+ if (result.updatedArtifact) {
353
+ nextMap[idx] = result.updatedArtifact;
354
+ lastUpdated = result.updatedArtifact;
355
+ dispatchArtifactCreatedEvent(result.updatedArtifact);
356
+ } else {
357
+ nextFailed.push({
358
+ artifactId: update.artifactId,
359
+ reason: result.failureReason ?? 'no-match',
360
+ });
361
+ }
362
+ });
363
+
364
+ setUpdateResultMap(nextMap);
365
+ setFailedUpdates(nextFailed);
273
366
  }
367
+
368
+ if (isChatlogPanel) return;
369
+
370
+ const openTarget =
371
+ lastUpdated ??
372
+ (Object.keys(updateResultMap).length === 0 && artifacts.length > 0
373
+ ? artifacts[0]
374
+ : null);
375
+ if (!openTarget) return;
376
+
377
+ if (hasAutoOpenedRef.current === messageKey) return;
378
+ hasAutoOpenedRef.current = messageKey;
379
+
380
+ const timer = setTimeout(
381
+ () => openArtifact(openTarget, { isChatLogPanel: false }),
382
+ 100
383
+ );
384
+ return () => clearTimeout(timer);
274
385
  }, [
275
386
  messageKey,
276
387
  messageText,
388
+ translatedMessageText,
277
389
  artifacts,
390
+ detectedUpdates,
391
+ updateResultMap,
392
+ registerArtifact,
393
+ applyArtifactUpdate,
278
394
  dispatchArtifactCreatedEvent,
279
395
  isChatlogPanel,
280
396
  openArtifact,
@@ -288,25 +404,66 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
288
404
  openArtifact(artifact, { isChatLogPanel: isChatlogPanel });
289
405
  }
290
406
  },
291
- [state.isDrawerOpen, state.currentArtifact?.id, closeArtifact, openArtifact]
407
+ [
408
+ state.isDrawerOpen,
409
+ state.currentArtifact?.id,
410
+ closeArtifact,
411
+ openArtifact,
412
+ isChatlogPanel,
413
+ ]
414
+ );
415
+
416
+ const getIconForMimeType = useCallback(
417
+ (mimeType: string): React.ReactElement => {
418
+ const m = mimeType.toLowerCase();
419
+ const iconProps = { 'aria-hidden': true as const };
420
+ if (m.includes('html')) return <HtmlIcon {...iconProps} />;
421
+ if (m.includes('markdown')) return <MarkdownIcon {...iconProps} />;
422
+ if (m.includes('javascript') || m.includes('typescript'))
423
+ return <JavascriptIcon {...iconProps} />;
424
+ if (m.includes('python')) return <PythonIcon {...iconProps} />;
425
+ if (m.includes('json')) return <DocumentIcon {...iconProps} />;
426
+ if (m.includes('css')) return <CssIcon {...iconProps} />;
427
+ if (m.includes('xml')) return <XmlIcon {...iconProps} />;
428
+ if (m.includes('svg')) return <SvgIcon {...iconProps} />;
429
+ return <DocumentIcon {...iconProps} />;
430
+ },
431
+ []
292
432
  );
293
433
 
294
- const getIconForMimeType = useCallback((mimeType: string): React.ReactElement => {
295
- const m = mimeType.toLowerCase();
296
- const iconProps = { 'aria-hidden': true as const };
297
- if (m.includes('html')) return <HtmlIcon {...iconProps} />;
298
- if (m.includes('markdown')) return <MarkdownIcon {...iconProps} />;
299
- if (m.includes('javascript') || m.includes('typescript'))
300
- return <JavascriptIcon {...iconProps} />;
301
- if (m.includes('python')) return <PythonIcon {...iconProps} />;
302
- if (m.includes('json')) return <DocumentIcon {...iconProps} />;
303
- if (m.includes('css')) return <CssIcon {...iconProps} />;
304
- if (m.includes('xml')) return <XmlIcon {...iconProps} />;
305
- if (m.includes('svg')) return <SvgIcon {...iconProps} />;
306
- return <DocumentIcon {...iconProps} />;
307
- }, []);
308
-
309
- if (artifacts.length === 0) return null;
434
+ const updatedLabel = t('artifact.updated') || 'updated';
435
+ const updateFailedLabel =
436
+ t('artifact.updateFailed') || 'Update could not be applied';
437
+ const updateFailedHint =
438
+ t('artifact.updateFailedHint') ||
439
+ 'The patch did not match the current content. Ask to rewrite the artifact.';
440
+
441
+ const updateEntries = Object.entries(updateResultMap);
442
+ const hasCreates = artifacts.length > 0;
443
+ const hasUpdates = updateEntries.length > 0;
444
+ const hasFailed = failedUpdates.length > 0;
445
+
446
+ if (!hasCreates && !hasUpdates && !hasFailed) return null;
447
+
448
+ const renderChevron = (isSelected: boolean) => {
449
+ if (isChatlogPanel) {
450
+ return isSelected ? (
451
+ <ChevronUp className="memori-artifact-handler-action-icon" />
452
+ ) : (
453
+ <ChevronDown className="memori-artifact-handler-action-icon" />
454
+ );
455
+ }
456
+ return isSelected ? (
457
+ <ChevronLeft className="memori-artifact-handler-action-icon" />
458
+ ) : (
459
+ <ChevronRight className="memori-artifact-handler-action-icon" />
460
+ );
461
+ };
462
+
463
+ const renderInlineDrawer = (isSelected: boolean) =>
464
+ isChatlogPanel && isSelected ? (
465
+ <ArtifactDrawer isChatLogPanel={true} />
466
+ ) : null;
310
467
 
311
468
  return (
312
469
  <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
@@ -345,34 +502,86 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
345
502
  </div>
346
503
  </div>
347
504
  <div className="memori-artifact-handler-action">
348
- {isChatlogPanel ? (
349
- state.isDrawerOpen &&
350
- state.currentArtifact?.id === artifact.id ? (
351
- <ChevronUp className="memori-artifact-handler-action-icon" />
352
- ) : (
353
- <ChevronDown className="memori-artifact-handler-action-icon" />
354
- )
355
- ) : state.isDrawerOpen &&
356
- state.currentArtifact?.id === artifact.id ? (
357
- <ChevronLeft className="memori-artifact-handler-action-icon" />
358
- ) : (
359
- <ChevronRight className="memori-artifact-handler-action-icon" />
360
- )}
505
+ {renderChevron(isSelected)}
361
506
  </div>
362
507
  </button>
363
508
 
364
509
  {/* Render inline drawer only for chatlog-panel mode.
365
510
  In layout-column mode the drawer is mounted by the page layout. */}
366
- {isChatlogPanel && state.isDrawerOpen &&
367
- state.currentArtifact?.id === artifact.id && (
368
- <ArtifactDrawer isChatLogPanel={true} />
369
- )}
511
+ {renderInlineDrawer(isSelected)}
370
512
  </React.Fragment>
371
513
  );
372
514
  })}
515
+
516
+ {updateEntries.map(([, artifact]) => {
517
+ const isSelected =
518
+ state.isDrawerOpen && state.currentArtifact?.id === artifact.id;
519
+
520
+ return (
521
+ <React.Fragment key={artifact.id}>
522
+ <button
523
+ type="button"
524
+ className={`memori-artifact-handler${
525
+ isSelected ? ' memori-artifact-handler--selected' : ''
526
+ }`}
527
+ onClick={() => handleArtifactClick(artifact)}
528
+ aria-pressed={isSelected}
529
+ aria-label={`${artifact.title} — ${updatedLabel}`}
530
+ style={
531
+ isSelected
532
+ ? {
533
+ border: '2px solid var(--memori-border-primary)',
534
+ }
535
+ : undefined
536
+ }
537
+ >
538
+ <div className="memori-artifact-handler-icon">
539
+ {getIconForMimeType(artifact.mimeType)}
540
+ </div>
541
+ <div className="memori-artifact-handler-info">
542
+ <div className="memori-artifact-handler-title">
543
+ {artifact.title} — {updatedLabel}
544
+ </div>
545
+ <div className="memori-artifact-handler-meta">
546
+ {artifact.mimeType} • {formatBytes(artifact.size || 0)}
547
+ </div>
548
+ </div>
549
+ <div className="memori-artifact-handler-action">
550
+ {renderChevron(isSelected)}
551
+ </div>
552
+ </button>
553
+
554
+ {renderInlineDrawer(isSelected)}
555
+ </React.Fragment>
556
+ );
557
+ })}
558
+
559
+ {failedUpdates.map((failed, idx) => (
560
+ <div
561
+ key={`failed-${failed.artifactId}-${idx}`}
562
+ className="memori-artifact-handler memori-artifact-handler--failed"
563
+ style={{
564
+ opacity: 0.85,
565
+ border: '1px dashed var(--memori-danger, #ef4444)',
566
+ cursor: 'default',
567
+ }}
568
+ role="status"
569
+ >
570
+ <div className="memori-artifact-handler-icon">⚠️</div>
571
+ <div className="memori-artifact-handler-info">
572
+ <div className="memori-artifact-handler-title">
573
+ {updateFailedLabel}
574
+ </div>
575
+ <div className="memori-artifact-handler-meta">
576
+ {failed.artifactId} • {updateFailedHint}
577
+ </div>
578
+ </div>
579
+ </div>
580
+ ))}
373
581
  </div>
374
582
  );
375
583
  };
584
+
376
585
  // ---------------------------------------------------------------------------
377
586
  // Memoised export
378
587
  // ---------------------------------------------------------------------------