@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,5 +1,19 @@
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
  export type OpenArtifactOptions = {
5
19
  /** Inline expansion in chat; layouts should not hide avatar / use side-drawer chrome. */
@@ -11,6 +25,11 @@ interface ArtifactContextType {
11
25
  openArtifact: (artifact: ArtifactData, options?: OpenArtifactOptions) => void;
12
26
  closeArtifact: () => void;
13
27
  toggleFullscreen: () => void;
28
+ registerArtifact: (artifact: ArtifactData) => void;
29
+ applyArtifactUpdate: (
30
+ artifactId: string,
31
+ edits: ArtifactEdit[]
32
+ ) => ApplyUpdateResult;
14
33
  }
15
34
 
16
35
  const ArtifactContext = createContext<ArtifactContextType | null>(null);
@@ -23,6 +42,11 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
23
42
  isChatLogPanelPresentation: false,
24
43
  });
25
44
 
45
+ // Synchronous registry keyed by stable artifactId → latest version.
46
+ // useRef avoids race where update handlers read empty React state
47
+ // right after a create in the same render cycle.
48
+ const registryRef = useRef<Record<string, ArtifactData>>({});
49
+
26
50
  const openArtifact = useCallback(
27
51
  (artifact: ArtifactData, options?: OpenArtifactOptions) => {
28
52
  setState(() => {
@@ -39,7 +63,6 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
39
63
 
40
64
  const closeArtifact = useCallback(() => {
41
65
  setState(prev => {
42
- // if (!prev.isDrawerOpen) return prev;
43
66
  return {
44
67
  ...prev,
45
68
  currentArtifact: null,
@@ -57,12 +80,91 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
57
80
  }));
58
81
  }, []);
59
82
 
60
- const contextValue = useMemo(() => ({
61
- state,
62
- openArtifact,
63
- closeArtifact,
64
- toggleFullscreen,
65
- }), [state, openArtifact, closeArtifact, toggleFullscreen]);
83
+ const registerArtifact = useCallback((artifact: ArtifactData) => {
84
+ if (!artifact?.artifactId) return;
85
+ registryRef.current[artifact.artifactId] = artifact;
86
+ }, []);
87
+
88
+ const applyArtifactUpdate = useCallback(
89
+ (artifactId: string, edits: ArtifactEdit[]): ApplyUpdateResult => {
90
+ const current = registryRef.current[artifactId];
91
+ if (!current) {
92
+ return {
93
+ success: false,
94
+ failedEdits: edits,
95
+ updatedArtifact: null,
96
+ failureReason: 'not-found',
97
+ };
98
+ }
99
+
100
+ const { content, failedEdits, appliedCount } = applyEdits(
101
+ current.content,
102
+ edits
103
+ );
104
+
105
+ if (appliedCount === 0) {
106
+ return {
107
+ success: false,
108
+ failedEdits,
109
+ updatedArtifact: null,
110
+ failureReason: 'no-match',
111
+ };
112
+ }
113
+
114
+ const updatedArtifact: ArtifactData = {
115
+ id: `artifact-ui-${artifactId}-${Date.now()}-${Math.random()
116
+ .toString(36)
117
+ .slice(2, 8)}`,
118
+ artifactId,
119
+ content,
120
+ mimeType: current.mimeType,
121
+ title: current.title,
122
+ timestamp: new Date(),
123
+ size: content.length,
124
+ };
125
+
126
+ registryRef.current[artifactId] = updatedArtifact;
127
+
128
+ setState(prev => {
129
+ if (
130
+ prev.isDrawerOpen &&
131
+ prev.currentArtifact?.artifactId === artifactId
132
+ ) {
133
+ return {
134
+ ...prev,
135
+ currentArtifact: updatedArtifact,
136
+ };
137
+ }
138
+ return prev;
139
+ });
140
+
141
+ return {
142
+ success: failedEdits.length === 0,
143
+ failedEdits,
144
+ updatedArtifact,
145
+ };
146
+ },
147
+ []
148
+ );
149
+
150
+ const contextValue = useMemo(
151
+ () => ({
152
+ state,
153
+ openArtifact,
154
+ closeArtifact,
155
+ toggleFullscreen,
156
+ registerArtifact,
157
+ applyArtifactUpdate,
158
+ }),
159
+ [
160
+ state,
161
+ openArtifact,
162
+ closeArtifact,
163
+ toggleFullscreen,
164
+ registerArtifact,
165
+ applyArtifactUpdate,
166
+ ]
167
+ );
66
168
 
67
169
  return (
68
170
  <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,6 +10,22 @@ 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;
@@ -15,4 +34,4 @@ export interface ArtifactSystemState {
15
34
  isChatLogPanelPresentation: boolean;
16
35
  }
17
36
 
18
- export type ArtifactTab = 'code' | 'preview';
37
+ 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',
@@ -83,7 +83,7 @@ export const ArtifactAPIBridge = ({
83
83
  }: {
84
84
  pushMessage: (message: Message) => void;
85
85
  }) => {
86
- const { openArtifact, closeArtifact, toggleFullscreen, state } =
86
+ const { openArtifact, closeArtifact, toggleFullscreen, state, registerArtifact } =
87
87
  useArtifact();
88
88
 
89
89
  const apiRef = useRef<any>(null);
@@ -115,12 +115,14 @@ export const ArtifactAPIBridge = ({
115
115
  title?: string
116
116
  ) => {
117
117
  const autoTitle = title || getTitleFromMimeType(mimeType);
118
+ const artifactId = `api-${Date.now()}-${Math.random()
119
+ .toString(36)
120
+ .substr(2, 9)}`;
118
121
 
119
122
  // Create the artifact object
120
123
  const artifact: ArtifactData = {
121
- id: `artifact-${Date.now()}-${Math.random()
122
- .toString(36)
123
- .substr(2, 9)}`,
124
+ id: `artifact-ui-${artifactId}`,
125
+ artifactId,
124
126
  content,
125
127
  mimeType,
126
128
  title: autoTitle,
@@ -131,8 +133,8 @@ export const ArtifactAPIBridge = ({
131
133
  // Create wrapped message text for artifact detection
132
134
  // Don't reassign content parameter - use a new variable
133
135
  let messageText = content;
134
- if (!messageText.includes('<output class="memori-artifact">')) {
135
- messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
136
+ if (!messageText.includes('class="memori-artifact"')) {
137
+ messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
136
138
  }
137
139
 
138
140
  //we have to push in the history the artifact as message
@@ -153,6 +155,7 @@ export const ArtifactAPIBridge = ({
153
155
  placeUncertaintyKm: undefined,
154
156
  });
155
157
 
158
+ registerArtifact(artifact);
156
159
  // Open the artifact immediately
157
160
  openArtifact(artifact);
158
161
  },
@@ -169,15 +172,17 @@ export const ArtifactAPIBridge = ({
169
172
  const title =
170
173
  outputElement.getAttribute('data-title') ||
171
174
  getTitleFromMimeType(mimeType);
175
+ const artifactId =
176
+ outputElement.getAttribute('data-artifact-id') ||
177
+ `api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
172
178
 
173
179
  // Nascondi l'elemento originale
174
180
  outputElement.style.display = 'none';
175
181
  outputElement.setAttribute('data-memori-processed', 'true');
176
182
 
177
183
  const artifact: ArtifactData = {
178
- id: `artifact-${Date.now()}-${Math.random()
179
- .toString(36)
180
- .substr(2, 9)}`,
184
+ id: `artifact-ui-${artifactId}`,
185
+ artifactId,
181
186
  content,
182
187
  mimeType,
183
188
  title,
@@ -188,8 +193,8 @@ export const ArtifactAPIBridge = ({
188
193
  // Create wrapped message text for artifact detection
189
194
  // Don't reassign content - use a new variable
190
195
  let messageText = content;
191
- if (!messageText.includes('<output class="memori-artifact">')) {
192
- messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
196
+ if (!messageText.includes('class="memori-artifact"')) {
197
+ messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
193
198
  }
194
199
 
195
200
  pushMessage({
@@ -209,10 +214,11 @@ export const ArtifactAPIBridge = ({
209
214
  placeUncertaintyKm: undefined,
210
215
  });
211
216
 
217
+ registerArtifact(artifact);
212
218
  // Open the artifact immediately
213
219
  openArtifact(artifact);
214
220
 
215
- return artifact.id;
221
+ return artifact.artifactId;
216
222
  },
217
223
 
218
224
  /**
@@ -294,7 +300,7 @@ export const ArtifactAPIBridge = ({
294
300
  delete (window as any).MemoriArtifactAPI;
295
301
  }
296
302
  };
297
- }, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage]);
303
+ }, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage, registerArtifact]);
298
304
 
299
305
  return null;
300
306
  };
@@ -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
+ };
@@ -164,3 +164,62 @@
164
164
  .memori.memori--auto-start .memori--start-panel {
165
165
  display: none;
166
166
  }
167
+
168
+ /* Close buttons (modals, drawers, popovers) — same square bordered chrome
169
+ as header / website-assistant / artifact-drawer IconButton.
170
+ @memori.ai/ui defaults these to borderless / circular. */
171
+ .memori-modal__close,
172
+ .memori-drawer__close,
173
+ .memori-popover__close {
174
+ display: inline-flex;
175
+ width: var(--memori-icon-button-size, 40px);
176
+ min-width: var(--memori-icon-button-size, 40px);
177
+ height: var(--memori-icon-button-size, 40px);
178
+ min-height: var(--memori-icon-button-size, 40px);
179
+ box-sizing: border-box;
180
+ flex-shrink: 0;
181
+ align-items: center;
182
+ justify-content: center;
183
+ padding: 0;
184
+ border: var(--memori-border, 1px) solid color-mix(in srgb, var(--memori-text-color, #111827) 18%, transparent);
185
+ border-radius: var(--memori-radius-box);
186
+ margin: 0;
187
+ background: transparent;
188
+ box-shadow: none;
189
+ color: var(--memori-icon-button-icon, color-mix(in oklch, var(--memori-text-color, #111827) 72%, transparent));
190
+ transform: none;
191
+ }
192
+
193
+ .memori-popover__close {
194
+ block-size: var(--memori-icon-button-size, 40px);
195
+ inline-size: var(--memori-icon-button-size, 40px);
196
+ }
197
+
198
+ .memori-modal__close:hover,
199
+ .memori-drawer__close:hover,
200
+ .memori-popover__close:hover {
201
+ border-color: color-mix(in srgb, var(--memori-text-color, #111827) 18%, transparent);
202
+ background: color-mix(in srgb, var(--memori-text-color, #111827) 6%, transparent);
203
+ box-shadow: none;
204
+ color: var(--memori-text-color, #111827);
205
+ }
206
+
207
+ .memori-modal__close:focus-visible,
208
+ .memori-drawer__close:focus-visible,
209
+ .memori-popover__close:focus-visible {
210
+ border-color: var(--memori-primary);
211
+ box-shadow: var(--memori-focus-ring, 0 0 0 3px color-mix(in oklch, var(--memori-primary) 18%, transparent));
212
+ outline: 2px solid var(--memori-primary);
213
+ outline-offset: var(--memori-focus-ring-offset, 2px);
214
+ }
215
+
216
+ .memori-modal__close svg,
217
+ .memori-drawer__close svg,
218
+ .memori-popover__close svg {
219
+ width: var(--memori-icon-button-icon-size, 20px);
220
+ height: var(--memori-icon-button-icon-size, 20px);
221
+ }
222
+
223
+ .memori-drawer__header {
224
+ align-items: center;
225
+ }
@@ -105,36 +105,22 @@ pre[class*='language-'] {
105
105
  display: flex;
106
106
  align-items: center;
107
107
  padding: 0.5rem;
108
- border: 1px solid rgba(0, 0, 0, 0.1);
109
108
  border-radius: 0.5rem;
110
- border-radius: var(--memori-radius-box);
111
- -webkit-backdrop-filter: blur(10px);
112
- backdrop-filter: blur(10px);
113
- background: rgba(0, 0, 0, 0.05);
114
- background-color: color-mix(in oklch, var(--memori-main-background) 38%, transparent);
115
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
116
109
  gap: var(--memori-spacing-sm);
117
110
  }
118
111
 
119
112
  button.memori-snippet--copy-button {
120
- width: 32px !important;
121
- height: 32px !important;
113
+ /* width: 32px !important;
114
+ height: 32px !important; */
122
115
  padding: 0;
123
116
  border: 1px solid rgba(0, 0, 0, 0.1);
124
117
  border-radius: 0.5rem !important;
125
- background: rgba(0, 0, 0, 0.05);
118
+ background: var(--memori-secondary-background);
126
119
  transition: all 0.2s ease;
127
120
  }
128
121
 
129
- button.memori-snippet--copy-button:hover,
130
- button.memori-snippet--copy-button:focus {
131
- border-color: rgba(0, 0, 0, 0.2);
132
- background: rgba(0, 0, 0, 0.1);
133
- transform: scale(1.05);
134
- }
135
-
136
122
  button.memori-snippet--copy-button:active {
137
- background: rgba(0, 0, 0, 0.15);
123
+ /* background: rgba(0, 0, 0, 0.15); */
138
124
  transform: scale(0.95);
139
125
  }
140
126
 
@@ -144,24 +130,8 @@ button.memori-snippet--copy-button svg {
144
130
  color: var(--memori-text-color);
145
131
  }
146
132
 
147
- .memori-snippet--copied-text {
148
- animation: fadeInOut 0.3s ease;
149
- color: var(--memori-text-color);
150
- font-size: 0.75rem;
151
- font-weight: var(--memori-text-weight-semibold);
152
- letter-spacing: 0.05em;
153
- text-transform: uppercase;
154
- }
155
-
156
- @keyframes fadeInOut {
157
- 0% {
158
- opacity: 0;
159
- transform: translateX(-4px);
160
- }
161
- 100% {
162
- opacity: 1;
163
- transform: translateX(0);
164
- }
133
+ .memori-snippet button.memori-snippet--copy-button--copied svg {
134
+ color: var(--memori-success-color, #22c55e);
165
135
  }
166
136
 
167
137
  .memori-snippet--caption {