@memori.ai/memori-react 8.43.0 → 8.44.0

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 (66) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +1 -1
  3. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +1 -1
  4. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
  5. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  6. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +204 -59
  7. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  8. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
  9. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js +76 -16
  10. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
  11. package/dist/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
  12. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
  13. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
  14. package/dist/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
  15. package/dist/components/MemoriArtifactSystem/utils/applyEdits.js +117 -0
  16. package/dist/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
  17. package/dist/helpers/message.js +3 -0
  18. package/dist/helpers/message.js.map +1 -1
  19. package/dist/locales/de.json +3 -0
  20. package/dist/locales/en.json +3 -0
  21. package/dist/locales/es.json +3 -0
  22. package/dist/locales/fr.json +3 -0
  23. package/dist/locales/it.json +3 -0
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +1 -1
  27. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +1 -1
  28. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
  29. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  30. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +205 -60
  31. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  32. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
  33. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js +77 -17
  34. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
  35. package/esm/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
  36. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
  37. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
  38. package/esm/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
  39. package/esm/components/MemoriArtifactSystem/utils/applyEdits.js +112 -0
  40. package/esm/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
  41. package/esm/helpers/message.js +3 -0
  42. package/esm/helpers/message.js.map +1 -1
  43. package/esm/locales/de.json +3 -0
  44. package/esm/locales/en.json +3 -0
  45. package/esm/locales/es.json +3 -0
  46. package/esm/locales/fr.json +3 -0
  47. package/esm/locales/it.json +3 -0
  48. package/esm/version.d.ts +1 -1
  49. package/esm/version.js +1 -1
  50. package/package.json +1 -1
  51. package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +1 -1
  52. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +1 -0
  53. package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +324 -133
  54. package/src/components/MemoriArtifactSystem/context/ArtifactContext.tsx +122 -24
  55. package/src/components/MemoriArtifactSystem/types/artifact.types.ts +20 -1
  56. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.test.tsx +1 -0
  57. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.tsx +19 -13
  58. package/src/components/MemoriArtifactSystem/utils/applyEdits.test.ts +85 -0
  59. package/src/components/MemoriArtifactSystem/utils/applyEdits.ts +173 -0
  60. package/src/helpers/message.ts +15 -0
  61. package/src/locales/de.json +3 -0
  62. package/src/locales/en.json +3 -0
  63. package/src/locales/es.json +3 -0
  64. package/src/locales/fr.json +3 -0
  65. package/src/locales/it.json +3 -0
  66. package/src/version.ts +1 -1
@@ -1,11 +1,30 @@
1
- import React, { createContext, useContext, useState, ReactNode, useCallback, useMemo } from 'react';
2
- import { ArtifactData, ArtifactSystemState } from '../types/artifact.types';
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ ReactNode,
6
+ useCallback,
7
+ useMemo,
8
+ useRef,
9
+ } from 'react';
10
+ import {
11
+ ArtifactData,
12
+ ArtifactEdit,
13
+ ArtifactSystemState,
14
+ ApplyUpdateResult,
15
+ } from '../types/artifact.types';
16
+ import { applyEdits } from '../utils/applyEdits';
3
17
 
4
18
  interface ArtifactContextType {
5
19
  state: ArtifactSystemState;
6
20
  openArtifact: (artifact: ArtifactData) => void;
7
21
  closeArtifact: () => void;
8
22
  toggleFullscreen: () => void;
23
+ registerArtifact: (artifact: ArtifactData) => void;
24
+ applyArtifactUpdate: (
25
+ artifactId: string,
26
+ edits: ArtifactEdit[]
27
+ ) => ApplyUpdateResult;
9
28
  }
10
29
 
11
30
  const ArtifactContext = createContext<ArtifactContextType | null>(null);
@@ -17,26 +36,26 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
17
36
  isFullscreen: false,
18
37
  });
19
38
 
39
+ // Synchronous registry keyed by stable artifactId → latest version.
40
+ // useRef avoids race where update handlers read empty React state
41
+ // right after a create in the same render cycle.
42
+ const registryRef = useRef<Record<string, ArtifactData>>({});
43
+
20
44
  const openArtifact = useCallback((artifact: ArtifactData) => {
21
- setState(() => {
22
- return {
23
- currentArtifact: artifact,
24
- isDrawerOpen: true,
25
- isFullscreen: false,
26
- };
27
- });
45
+ setState(() => ({
46
+ currentArtifact: artifact,
47
+ isDrawerOpen: true,
48
+ isFullscreen: false,
49
+ }));
28
50
  }, []);
29
51
 
30
52
  const closeArtifact = useCallback(() => {
31
- setState(prev => {
32
- // if (!prev.isDrawerOpen) return prev;
33
- return {
34
- ...prev,
35
- currentArtifact: null,
36
- isDrawerOpen: false,
37
- isFullscreen: false,
38
- };
39
- });
53
+ setState(prev => ({
54
+ ...prev,
55
+ currentArtifact: null,
56
+ isDrawerOpen: false,
57
+ isFullscreen: false,
58
+ }));
40
59
  }, []);
41
60
 
42
61
  const toggleFullscreen = useCallback(() => {
@@ -46,12 +65,91 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
46
65
  }));
47
66
  }, []);
48
67
 
49
- const contextValue = useMemo(() => ({
50
- state,
51
- openArtifact,
52
- closeArtifact,
53
- toggleFullscreen,
54
- }), [state, openArtifact, closeArtifact, toggleFullscreen]);
68
+ const registerArtifact = useCallback((artifact: ArtifactData) => {
69
+ if (!artifact?.artifactId) return;
70
+ registryRef.current[artifact.artifactId] = artifact;
71
+ }, []);
72
+
73
+ const applyArtifactUpdate = useCallback(
74
+ (artifactId: string, edits: ArtifactEdit[]): ApplyUpdateResult => {
75
+ const current = registryRef.current[artifactId];
76
+ if (!current) {
77
+ return {
78
+ success: false,
79
+ failedEdits: edits,
80
+ updatedArtifact: null,
81
+ failureReason: 'not-found',
82
+ };
83
+ }
84
+
85
+ const { content, failedEdits, appliedCount } = applyEdits(
86
+ current.content,
87
+ edits
88
+ );
89
+
90
+ if (appliedCount === 0) {
91
+ return {
92
+ success: false,
93
+ failedEdits,
94
+ updatedArtifact: null,
95
+ failureReason: 'no-match',
96
+ };
97
+ }
98
+
99
+ const updatedArtifact: ArtifactData = {
100
+ id: `artifact-ui-${artifactId}-${Date.now()}-${Math.random()
101
+ .toString(36)
102
+ .slice(2, 8)}`,
103
+ artifactId,
104
+ content,
105
+ mimeType: current.mimeType,
106
+ title: current.title,
107
+ timestamp: new Date(),
108
+ size: content.length,
109
+ };
110
+
111
+ registryRef.current[artifactId] = updatedArtifact;
112
+
113
+ setState(prev => {
114
+ if (
115
+ prev.isDrawerOpen &&
116
+ prev.currentArtifact?.artifactId === artifactId
117
+ ) {
118
+ return {
119
+ ...prev,
120
+ currentArtifact: updatedArtifact,
121
+ };
122
+ }
123
+ return prev;
124
+ });
125
+
126
+ return {
127
+ success: failedEdits.length === 0,
128
+ failedEdits,
129
+ updatedArtifact,
130
+ };
131
+ },
132
+ []
133
+ );
134
+
135
+ const contextValue = useMemo(
136
+ () => ({
137
+ state,
138
+ openArtifact,
139
+ closeArtifact,
140
+ toggleFullscreen,
141
+ registerArtifact,
142
+ applyArtifactUpdate,
143
+ }),
144
+ [
145
+ state,
146
+ openArtifact,
147
+ closeArtifact,
148
+ toggleFullscreen,
149
+ registerArtifact,
150
+ applyArtifactUpdate,
151
+ ]
152
+ );
55
153
 
56
154
  return (
57
155
  <ArtifactContext.Provider value={contextValue}>
@@ -1,5 +1,8 @@
1
1
  export interface ArtifactData {
2
+ /** Unique UI identifier for this version instance */
2
3
  id: string;
4
+ /** Stable lineage id (from data-artifact-id); constant across versions */
5
+ artifactId: string;
3
6
  content: string;
4
7
  mimeType: string;
5
8
  title: string;
@@ -7,10 +10,26 @@ export interface ArtifactData {
7
10
  size: number;
8
11
  }
9
12
 
13
+ export interface ArtifactEdit {
14
+ /** Exact string to find and replace (first occurrence) */
15
+ old: string;
16
+ /** Replacement string */
17
+ new: string;
18
+ }
19
+
20
+ export interface ApplyUpdateResult {
21
+ success: boolean;
22
+ failedEdits: ArtifactEdit[];
23
+ updatedArtifact: ArtifactData | null;
24
+ /** Why the update failed when updatedArtifact is null */
25
+ failureReason?: 'not-found' | 'no-match';
26
+ }
27
+
28
+
10
29
  export interface ArtifactSystemState {
11
30
  currentArtifact: ArtifactData | null;
12
31
  isDrawerOpen: boolean;
13
32
  isFullscreen: boolean;
14
33
  }
15
34
 
16
- export type ArtifactTab = 'code' | 'preview';
35
+ export type ArtifactTab = 'code' | 'preview';
@@ -116,6 +116,7 @@ describe('ArtifactAPIBridge', () => {
116
116
 
117
117
  const artifact: ArtifactData = {
118
118
  id: 'test-artifact-123',
119
+ artifactId: 'test-artifact',
119
120
  content: '<div>Full artifact</div>',
120
121
  mimeType: 'html',
121
122
  title: 'Full Artifact Test',
@@ -73,7 +73,7 @@ export const ArtifactAPIBridge = ({
73
73
  }: {
74
74
  pushMessage: (message: Message) => void;
75
75
  }) => {
76
- const { openArtifact, closeArtifact, toggleFullscreen, state } =
76
+ const { openArtifact, closeArtifact, toggleFullscreen, state, registerArtifact } =
77
77
  useArtifact();
78
78
 
79
79
  const apiRef = useRef<any>(null);
@@ -105,12 +105,14 @@ export const ArtifactAPIBridge = ({
105
105
  title?: string
106
106
  ) => {
107
107
  const autoTitle = title || getTitleFromMimeType(mimeType);
108
+ const artifactId = `api-${Date.now()}-${Math.random()
109
+ .toString(36)
110
+ .substr(2, 9)}`;
108
111
 
109
112
  // Create the artifact object
110
113
  const artifact: ArtifactData = {
111
- id: `artifact-${Date.now()}-${Math.random()
112
- .toString(36)
113
- .substr(2, 9)}`,
114
+ id: `artifact-ui-${artifactId}`,
115
+ artifactId,
114
116
  content,
115
117
  mimeType,
116
118
  title: autoTitle,
@@ -121,8 +123,8 @@ export const ArtifactAPIBridge = ({
121
123
  // Create wrapped message text for artifact detection
122
124
  // Don't reassign content parameter - use a new variable
123
125
  let messageText = content;
124
- if (!messageText.includes('<output class="memori-artifact">')) {
125
- messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
126
+ if (!messageText.includes('class="memori-artifact"')) {
127
+ messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
126
128
  }
127
129
 
128
130
  //we have to push in the history the artifact as message
@@ -143,6 +145,7 @@ export const ArtifactAPIBridge = ({
143
145
  placeUncertaintyKm: undefined,
144
146
  });
145
147
 
148
+ registerArtifact(artifact);
146
149
  // Open the artifact immediately
147
150
  openArtifact(artifact);
148
151
  },
@@ -159,15 +162,17 @@ export const ArtifactAPIBridge = ({
159
162
  const title =
160
163
  outputElement.getAttribute('data-title') ||
161
164
  getTitleFromMimeType(mimeType);
165
+ const artifactId =
166
+ outputElement.getAttribute('data-artifact-id') ||
167
+ `api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
162
168
 
163
169
  // Nascondi l'elemento originale
164
170
  outputElement.style.display = 'none';
165
171
  outputElement.setAttribute('data-memori-processed', 'true');
166
172
 
167
173
  const artifact: ArtifactData = {
168
- id: `artifact-${Date.now()}-${Math.random()
169
- .toString(36)
170
- .substr(2, 9)}`,
174
+ id: `artifact-ui-${artifactId}`,
175
+ artifactId,
171
176
  content,
172
177
  mimeType,
173
178
  title,
@@ -178,8 +183,8 @@ export const ArtifactAPIBridge = ({
178
183
  // Create wrapped message text for artifact detection
179
184
  // Don't reassign content - use a new variable
180
185
  let messageText = content;
181
- if (!messageText.includes('<output class="memori-artifact">')) {
182
- messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
186
+ if (!messageText.includes('class="memori-artifact"')) {
187
+ messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
183
188
  }
184
189
 
185
190
  pushMessage({
@@ -199,10 +204,11 @@ export const ArtifactAPIBridge = ({
199
204
  placeUncertaintyKm: undefined,
200
205
  });
201
206
 
207
+ registerArtifact(artifact);
202
208
  // Open the artifact immediately
203
209
  openArtifact(artifact);
204
210
 
205
- return artifact.id;
211
+ return artifact.artifactId;
206
212
  },
207
213
 
208
214
  /**
@@ -283,7 +289,7 @@ export const ArtifactAPIBridge = ({
283
289
  delete (window as any).MemoriArtifactAPI;
284
290
  }
285
291
  };
286
- }, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage]);
292
+ }, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage, registerArtifact]);
287
293
 
288
294
  return null;
289
295
  };
@@ -0,0 +1,85 @@
1
+ import { applyEdits, parseEdits } from './applyEdits';
2
+
3
+ describe('applyEdits', () => {
4
+ it('applies an exact string replacement', () => {
5
+ const result = applyEdits('Hello 🌰 World', [{ old: '🌰', new: '🥜' }]);
6
+ expect(result.content).toBe('Hello 🥜 World');
7
+ expect(result.appliedCount).toBe(1);
8
+ expect(result.failedEdits).toEqual([]);
9
+ });
10
+
11
+ it('replaces only the first occurrence', () => {
12
+ const result = applyEdits('aa bb aa', [{ old: 'aa', new: 'XX' }]);
13
+ expect(result.content).toBe('XX bb aa');
14
+ expect(result.appliedCount).toBe(1);
15
+ });
16
+
17
+ it('applies multiple edits sequentially', () => {
18
+ const result = applyEdits('<h1>Title</h1><p>Body</p>', [
19
+ { old: 'Title', new: 'Heading' },
20
+ { old: 'Body', new: 'Content' },
21
+ ]);
22
+ expect(result.content).toBe('<h1>Heading</h1><p>Content</p>');
23
+ expect(result.appliedCount).toBe(2);
24
+ });
25
+
26
+ it('falls back to whitespace-tolerant matching', () => {
27
+ const content = '<span class="x">\n hello world\n</span>';
28
+ const result = applyEdits(content, [
29
+ { old: 'hello world', new: 'hi there' },
30
+ ]);
31
+ expect(result.content).toContain('hi there');
32
+ expect(result.appliedCount).toBe(1);
33
+ expect(result.failedEdits).toEqual([]);
34
+ });
35
+
36
+ it('collects failed edits without blocking others', () => {
37
+ const result = applyEdits('alpha beta', [
38
+ { old: 'missing', new: 'nope' },
39
+ { old: 'beta', new: 'gamma' },
40
+ ]);
41
+ expect(result.content).toBe('alpha gamma');
42
+ expect(result.appliedCount).toBe(1);
43
+ expect(result.failedEdits).toEqual([{ old: 'missing', new: 'nope' }]);
44
+ });
45
+
46
+ it('returns original content when all edits fail', () => {
47
+ const result = applyEdits('unchanged', [
48
+ { old: 'foo', new: 'bar' },
49
+ ]);
50
+ expect(result.content).toBe('unchanged');
51
+ expect(result.appliedCount).toBe(0);
52
+ expect(result.failedEdits).toHaveLength(1);
53
+ });
54
+ });
55
+
56
+ describe('parseEdits', () => {
57
+ it('parses a valid JSON edits array', () => {
58
+ const edits = parseEdits('[{"old": "a", "new": "b"}]');
59
+ expect(edits).toEqual([{ old: 'a', new: 'b' }]);
60
+ });
61
+
62
+ it('unescapes HTML entities before parsing', () => {
63
+ const edits = parseEdits(
64
+ '[{&quot;old&quot;: &quot;🌰&quot;, &quot;new&quot;: &quot;🥜&quot;}]'
65
+ );
66
+ expect(edits).toEqual([{ old: '🌰', new: '🥜' }]);
67
+ });
68
+
69
+ it('returns null for invalid JSON', () => {
70
+ expect(parseEdits('not json')).toBeNull();
71
+ });
72
+
73
+ it('returns null for non-array JSON', () => {
74
+ expect(parseEdits('{"old":"a","new":"b"}')).toBeNull();
75
+ });
76
+
77
+ it('returns null for array items missing old/new', () => {
78
+ expect(parseEdits('[{"foo":"bar"}]')).toBeNull();
79
+ });
80
+
81
+ it('returns null for empty body', () => {
82
+ expect(parseEdits('')).toBeNull();
83
+ expect(parseEdits(' ')).toBeNull();
84
+ });
85
+ });
@@ -0,0 +1,173 @@
1
+ import { ArtifactEdit } from '../types/artifact.types';
2
+
3
+ export interface ApplyEditsResult {
4
+ content: string;
5
+ failedEdits: ArtifactEdit[];
6
+ appliedCount: number;
7
+ }
8
+
9
+ /**
10
+ * Normalize runs of whitespace to a single space for tolerant matching.
11
+ */
12
+ const normalizeWhitespace = (s: string): string => s.replace(/\s+/g, ' ');
13
+
14
+ /**
15
+ * Find the first occurrence of `needle` in `haystack` after normalizing
16
+ * whitespace in both, then map the match back to original haystack indices.
17
+ * Returns [start, end] in original string coords, or null if not found.
18
+ */
19
+ const findWhitespaceTolerant = (
20
+ haystack: string,
21
+ needle: string
22
+ ): [number, number] | null => {
23
+ const normNeedle = normalizeWhitespace(needle.trim());
24
+ if (!normNeedle) return null;
25
+
26
+ // Build mapping from normalized index → original index
27
+ const normChars: string[] = [];
28
+ const normToOrig: number[] = [];
29
+ let lastWasSpace = false;
30
+
31
+ for (let i = 0; i < haystack.length; i++) {
32
+ const ch = haystack[i];
33
+ if (/\s/.test(ch)) {
34
+ if (!lastWasSpace && normChars.length > 0) {
35
+ normChars.push(' ');
36
+ normToOrig.push(i);
37
+ lastWasSpace = true;
38
+ }
39
+ } else {
40
+ normChars.push(ch);
41
+ normToOrig.push(i);
42
+ lastWasSpace = false;
43
+ }
44
+ }
45
+
46
+ // Trim trailing space from normalized form
47
+ while (normChars.length > 0 && normChars[normChars.length - 1] === ' ') {
48
+ normChars.pop();
49
+ normToOrig.pop();
50
+ }
51
+
52
+ const normHaystack = normChars.join('');
53
+ const idx = normHaystack.indexOf(normNeedle);
54
+ if (idx === -1) return null;
55
+
56
+ const startOrig = normToOrig[idx];
57
+ const endNormIdx = idx + normNeedle.length - 1;
58
+ // End is exclusive: one past the last matched original char
59
+ const lastOrig = normToOrig[endNormIdx];
60
+ // Include any trailing whitespace that was collapsed after the last char
61
+ let endOrig = lastOrig + 1;
62
+ while (endOrig < haystack.length && /\s/.test(haystack[endOrig])) {
63
+ // Only consume whitespace that was part of a collapsed run inside the match
64
+ // Stop if we've already covered the normalized span
65
+ break;
66
+ }
67
+
68
+ return [startOrig, endOrig];
69
+ };
70
+
71
+ /**
72
+ * Apply a list of string replacements to content.
73
+ * Each edit replaces the first occurrence of `old` with `new`.
74
+ * Exact match is tried first; whitespace-tolerant fallback second.
75
+ * Failed edits are collected without aborting the rest.
76
+ */
77
+ export const applyEdits = (
78
+ content: string,
79
+ edits: ArtifactEdit[]
80
+ ): ApplyEditsResult => {
81
+ let result = content;
82
+ const failedEdits: ArtifactEdit[] = [];
83
+ let appliedCount = 0;
84
+
85
+ for (const edit of edits) {
86
+ if (!edit || typeof edit.old !== 'string' || typeof edit.new !== 'string') {
87
+ failedEdits.push(edit);
88
+ continue;
89
+ }
90
+
91
+ const exactIdx = result.indexOf(edit.old);
92
+ if (exactIdx !== -1) {
93
+ result =
94
+ result.slice(0, exactIdx) +
95
+ edit.new +
96
+ result.slice(exactIdx + edit.old.length);
97
+ appliedCount++;
98
+ continue;
99
+ }
100
+
101
+ const tolerant = findWhitespaceTolerant(result, edit.old);
102
+ if (tolerant) {
103
+ const [start, end] = tolerant;
104
+ result = result.slice(0, start) + edit.new + result.slice(end);
105
+ appliedCount++;
106
+ continue;
107
+ }
108
+
109
+ failedEdits.push(edit);
110
+ }
111
+
112
+ return { content: result, failedEdits, appliedCount };
113
+ };
114
+
115
+ const HTML_ENTITIES: Record<string, string> = {
116
+ '&quot;': '"',
117
+ '&#34;': '"',
118
+ '&apos;': "'",
119
+ '&#39;': "'",
120
+ '&amp;': '&',
121
+ '&lt;': '<',
122
+ '&gt;': '>',
123
+ };
124
+
125
+ /**
126
+ * Unescape common HTML entities that LLMs sometimes emit inside JSON bodies.
127
+ */
128
+ const unescapeHtmlEntities = (s: string): string =>
129
+ s.replace(
130
+ /&quot;|&#34;|&apos;|&#39;|&amp;|&lt;|&gt;/g,
131
+ m => HTML_ENTITIES[m] ?? m
132
+ );
133
+
134
+ const isValidEditsArray = (value: unknown): value is ArtifactEdit[] => {
135
+ if (!Array.isArray(value)) return false;
136
+ return value.every(
137
+ item =>
138
+ item &&
139
+ typeof item === 'object' &&
140
+ typeof (item as ArtifactEdit).old === 'string' &&
141
+ typeof (item as ArtifactEdit).new === 'string'
142
+ );
143
+ };
144
+
145
+ /**
146
+ * Parse the body of a memori-artifact update tag into an edits array.
147
+ * Handles valid JSON and HTML-entity-encoded JSON. Returns null on failure.
148
+ */
149
+ export const parseEdits = (body: string): ArtifactEdit[] | null => {
150
+ if (!body || typeof body !== 'string') return null;
151
+
152
+ const trimmed = body.trim();
153
+ if (!trimmed) return null;
154
+
155
+ const tryParse = (raw: string): ArtifactEdit[] | null => {
156
+ try {
157
+ const parsed = JSON.parse(raw);
158
+ return isValidEditsArray(parsed) ? parsed : null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ };
163
+
164
+ const direct = tryParse(trimmed);
165
+ if (direct) return direct;
166
+
167
+ const unescaped = unescapeHtmlEntities(trimmed);
168
+ if (unescaped !== trimmed) {
169
+ return tryParse(unescaped);
170
+ }
171
+
172
+ return null;
173
+ };
@@ -114,6 +114,21 @@ export const renderMsg = (
114
114
  /<output\s+class\s*=\s*["\']memori-artifact["\'][^>]*data-mimetype\s*=\s*["\']([^"']+)["\'][^>]*>([\s\S]*?)(?:<\/output>|$)/gi,
115
115
  ''
116
116
  )
117
+ // Preferred: data-action="update" on memori-artifact
118
+ .replaceAll(
119
+ /<output\s+class\s*=\s*["\']memori-artifact["\'][^>]*data-action\s*=\s*["\']update["\'][^>]*>([\s\S]*?)(?:<\/output>|$)/gi,
120
+ ''
121
+ )
122
+ // data-action may appear before class
123
+ .replaceAll(
124
+ /<output\s+[^>]*data-action\s*=\s*["\']update["\'][^>]*class\s*=\s*["\'][^"']*memori-artifact[^"']*["\'][^>]*>([\s\S]*?)(?:<\/output>|$)/gi,
125
+ ''
126
+ )
127
+ // Legacy class name
128
+ .replaceAll(
129
+ /<output\s+class\s*=\s*["\']memori-artifact-update["\'][^>]*>([\s\S]*?)(?:<\/output>|$)/gi,
130
+ ''
131
+ )
117
132
  .replaceAll(/```markdown([^```]+)```/g, '$1')
118
133
  .replaceAll('($', '( $')
119
134
  .replaceAll(':$', ': $')
@@ -197,6 +197,9 @@
197
197
  "artifact": {
198
198
  "close": "Schließen",
199
199
  "size": "Größe",
200
+ "updated": "aktualisiert",
201
+ "updateFailed": "Update konnte nicht angewendet werden",
202
+ "updateFailedHint": "Der Patch stimmt nicht mit dem aktuellen Inhalt überein. Bitten Sie um ein Neuschreiben des Artifacts.",
200
203
  "generated": "Generiert",
201
204
  "download": "Herunterladen",
202
205
  "print": "Drucken",
@@ -210,6 +210,9 @@
210
210
  "artifact": {
211
211
  "close": "Close",
212
212
  "size": "Size",
213
+ "updated": "updated",
214
+ "updateFailed": "Update could not be applied",
215
+ "updateFailedHint": "The patch did not match the current content. Ask to rewrite the artifact.",
213
216
  "generated": "Generated",
214
217
  "download": "Download",
215
218
  "print": "Print",
@@ -197,6 +197,9 @@
197
197
  "artifact": {
198
198
  "close": "Cerrar",
199
199
  "size": "Tamaño",
200
+ "updated": "actualizado",
201
+ "updateFailed": "No se pudo aplicar la actualización",
202
+ "updateFailedHint": "El parche no coincide con el contenido actual. Pide reescribir el artifact.",
200
203
  "generated": "Generado",
201
204
  "download": "Descargar",
202
205
  "print": "Imprimir",
@@ -196,6 +196,9 @@
196
196
  "artifact": {
197
197
  "close": "Fermer",
198
198
  "size": "Taille",
199
+ "updated": "mis à jour",
200
+ "updateFailed": "Mise à jour non appliquée",
201
+ "updateFailedHint": "Le correctif ne correspond pas au contenu actuel. Demandez de réécrire l'artifact.",
199
202
  "generated": "Généré",
200
203
  "download": "Télécharger",
201
204
  "print": "Imprimer",
@@ -212,6 +212,9 @@
212
212
  "artifact": {
213
213
  "close": "Chiudi",
214
214
  "size": "Dimensione",
215
+ "updated": "aggiornato",
216
+ "updateFailed": "Aggiornamento non applicato",
217
+ "updateFailedHint": "Il patch non corrisponde al contenuto attuale. Chiedi di riscrivere l'artifact.",
215
218
  "generated": "Generato",
216
219
  "download": "Scarica",
217
220
  "print": "Stampa",
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const version = '8.43.0';
2
+ export const version = '8.44.0';