@memori.ai/memori-react 8.8.5 → 8.9.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 (176) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +28 -0
  3. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +0 -1
  4. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -1
  5. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +0 -10
  6. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  7. package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/AvatarAnimator.js +0 -9
  8. package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/AvatarAnimator.js.map +1 -1
  9. package/dist/components/Chat/Chat.css +31 -0
  10. package/dist/components/Chat/Chat.js +18 -4
  11. package/dist/components/Chat/Chat.js.map +1 -1
  12. package/dist/components/ChatBubble/ChatBubble.js +1 -2
  13. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  14. package/dist/components/ChatInputs/ChatInputs.css +23 -0
  15. package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
  16. package/dist/components/ChatInputs/ChatInputs.js +37 -21
  17. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  18. package/dist/components/ChatTextArea/ChatTextArea.css +31 -0
  19. package/dist/components/ChatTextArea/ChatTextArea.d.ts +1 -0
  20. package/dist/components/ChatTextArea/ChatTextArea.js +9 -2
  21. package/dist/components/ChatTextArea/ChatTextArea.js.map +1 -1
  22. package/dist/components/FilePreview/FilePreview.css +39 -0
  23. package/dist/components/Header/Header.js +3 -16
  24. package/dist/components/Header/Header.js.map +1 -1
  25. package/dist/components/MediaWidget/LinkItemWidget.js +1 -1
  26. package/dist/components/MediaWidget/LinkItemWidget.js.map +1 -1
  27. package/dist/components/MediaWidget/MediaItemWidget.js +5 -9
  28. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  29. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +65 -51
  30. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  31. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.d.ts +5 -0
  32. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js +287 -0
  33. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -0
  34. package/dist/components/MemoriWidget/MemoriWidget.d.ts +12 -0
  35. package/dist/components/MemoriWidget/MemoriWidget.js +12 -3
  36. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  37. package/dist/components/Snippet/Snippet.js +4 -3
  38. package/dist/components/Snippet/Snippet.js.map +1 -1
  39. package/dist/components/StartPanel/StartPanel.css +14 -0
  40. package/dist/components/StartPanel/StartPanel.js +2 -2
  41. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  42. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +0 -21
  43. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  44. package/dist/components/VenueWidget/VenueWidget.js +0 -1
  45. package/dist/components/VenueWidget/VenueWidget.js.map +1 -1
  46. package/dist/components/layouts/HiddenChat.js +0 -15
  47. package/dist/components/layouts/HiddenChat.js.map +1 -1
  48. package/dist/components/layouts/chat.css +2 -2
  49. package/dist/context/visemeContext.js +0 -6
  50. package/dist/context/visemeContext.js.map +1 -1
  51. package/dist/helpers/constants.d.ts +11 -0
  52. package/dist/helpers/constants.js +24 -2
  53. package/dist/helpers/constants.js.map +1 -1
  54. package/dist/helpers/tts/useTTS.js +0 -3
  55. package/dist/helpers/tts/useTTS.js.map +1 -1
  56. package/dist/helpers/utils.d.ts +1 -0
  57. package/dist/helpers/utils.js +6 -1
  58. package/dist/helpers/utils.js.map +1 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/locales/de.json +2 -0
  61. package/dist/locales/en.json +2 -0
  62. package/dist/locales/es.json +2 -0
  63. package/dist/locales/fr.json +2 -0
  64. package/dist/locales/it.json +2 -0
  65. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +0 -1
  66. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -1
  67. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +0 -10
  68. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  69. package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/AvatarAnimator.js +0 -9
  70. package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/AvatarAnimator.js.map +1 -1
  71. package/esm/components/Chat/Chat.css +31 -0
  72. package/esm/components/Chat/Chat.js +19 -5
  73. package/esm/components/Chat/Chat.js.map +1 -1
  74. package/esm/components/ChatBubble/ChatBubble.js +1 -2
  75. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  76. package/esm/components/ChatInputs/ChatInputs.css +23 -0
  77. package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
  78. package/esm/components/ChatInputs/ChatInputs.js +37 -21
  79. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  80. package/esm/components/ChatTextArea/ChatTextArea.css +31 -0
  81. package/esm/components/ChatTextArea/ChatTextArea.d.ts +1 -0
  82. package/esm/components/ChatTextArea/ChatTextArea.js +9 -2
  83. package/esm/components/ChatTextArea/ChatTextArea.js.map +1 -1
  84. package/esm/components/FilePreview/FilePreview.css +39 -0
  85. package/esm/components/Header/Header.js +3 -16
  86. package/esm/components/Header/Header.js.map +1 -1
  87. package/esm/components/MediaWidget/LinkItemWidget.js +1 -1
  88. package/esm/components/MediaWidget/LinkItemWidget.js.map +1 -1
  89. package/esm/components/MediaWidget/MediaItemWidget.js +5 -9
  90. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  91. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +65 -51
  92. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  93. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.d.ts +5 -0
  94. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js +282 -0
  95. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -0
  96. package/esm/components/MemoriWidget/MemoriWidget.d.ts +12 -0
  97. package/esm/components/MemoriWidget/MemoriWidget.js +12 -3
  98. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  99. package/esm/components/Snippet/Snippet.js +4 -3
  100. package/esm/components/Snippet/Snippet.js.map +1 -1
  101. package/esm/components/StartPanel/StartPanel.css +14 -0
  102. package/esm/components/StartPanel/StartPanel.js +3 -3
  103. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  104. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +0 -21
  105. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  106. package/esm/components/VenueWidget/VenueWidget.js +0 -1
  107. package/esm/components/VenueWidget/VenueWidget.js.map +1 -1
  108. package/esm/components/layouts/HiddenChat.js +0 -15
  109. package/esm/components/layouts/HiddenChat.js.map +1 -1
  110. package/esm/components/layouts/chat.css +2 -2
  111. package/esm/context/visemeContext.js +0 -6
  112. package/esm/context/visemeContext.js.map +1 -1
  113. package/esm/helpers/constants.d.ts +11 -0
  114. package/esm/helpers/constants.js +22 -1
  115. package/esm/helpers/constants.js.map +1 -1
  116. package/esm/helpers/tts/useTTS.js +0 -3
  117. package/esm/helpers/tts/useTTS.js.map +1 -1
  118. package/esm/helpers/utils.d.ts +1 -0
  119. package/esm/helpers/utils.js +4 -0
  120. package/esm/helpers/utils.js.map +1 -1
  121. package/esm/index.js.map +1 -1
  122. package/esm/locales/de.json +2 -0
  123. package/esm/locales/en.json +2 -0
  124. package/esm/locales/es.json +2 -0
  125. package/esm/locales/fr.json +2 -0
  126. package/esm/locales/it.json +2 -0
  127. package/package.json +1 -1
  128. package/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx +0 -1
  129. package/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +0 -17
  130. package/src/components/Avatar/AvatarView/AvatarComponent/components/controllers/AvatarAnimator.ts +0 -20
  131. package/src/components/Chat/Chat.css +31 -0
  132. package/src/components/Chat/Chat.stories.tsx +503 -9
  133. package/src/components/Chat/Chat.tsx +23 -3
  134. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +73 -73
  135. package/src/components/ChatBubble/ChatBubble.tsx +1 -2
  136. package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +25 -25
  137. package/src/components/ChatInputs/ChatInputs.css +23 -0
  138. package/src/components/ChatInputs/ChatInputs.tsx +36 -14
  139. package/src/components/ChatTextArea/ChatTextArea.css +31 -0
  140. package/src/components/ChatTextArea/ChatTextArea.tsx +11 -1
  141. package/src/components/FilePreview/FilePreview.css +39 -0
  142. package/src/components/Header/Header.tsx +0 -13
  143. package/src/components/MediaWidget/LinkItemWidget.tsx +1 -1
  144. package/src/components/MediaWidget/MediaItemWidget.stories.tsx +33 -0
  145. package/src/components/MediaWidget/MediaItemWidget.tsx +7 -10
  146. package/src/components/MediaWidget/__snapshots__/LinkItemWidget.test.tsx.snap +4 -4
  147. package/src/components/MediaWidget/__snapshots__/MediaItemWidget.test.tsx.snap +6 -6
  148. package/src/components/MediaWidget/__snapshots__/MediaWidget.test.tsx.snap +2 -2
  149. package/src/components/MemoriArtifactSystem/ArtifactDrawer.stories.tsx +766 -2
  150. package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +103 -89
  151. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.test.tsx +307 -0
  152. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.tsx +373 -0
  153. package/src/components/MemoriWidget/MemoriWidget.tsx +26 -4
  154. package/src/components/Snippet/Snippet.tsx +3 -2
  155. package/src/components/StartPanel/StartPanel.css +14 -0
  156. package/src/components/StartPanel/StartPanel.tsx +23 -10
  157. package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +206 -84
  158. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +0 -23
  159. package/src/components/VenueWidget/VenueWidget.tsx +0 -1
  160. package/src/components/layouts/HiddenChat.tsx +0 -16
  161. package/src/components/layouts/__snapshots__/Chat.test.tsx.snap +204 -82
  162. package/src/components/layouts/__snapshots__/FullPage.test.tsx.snap +408 -164
  163. package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +204 -82
  164. package/src/components/layouts/__snapshots__/Totem.test.tsx.snap +204 -82
  165. package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +204 -82
  166. package/src/components/layouts/chat.css +2 -2
  167. package/src/context/visemeContext.tsx +0 -7
  168. package/src/helpers/constants.ts +28 -3
  169. package/src/helpers/tts/useTTS.ts +0 -2
  170. package/src/helpers/utils.ts +5 -0
  171. package/src/index.tsx +0 -1
  172. package/src/locales/de.json +2 -0
  173. package/src/locales/en.json +2 -0
  174. package/src/locales/es.json +2 -0
  175. package/src/locales/fr.json +2 -0
  176. package/src/locales/it.json +2 -0
@@ -25,9 +25,6 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
25
25
  message,
26
26
  }) => {
27
27
  const { openArtifact, state, closeArtifact } = useArtifact();
28
- const [currentArtifact, setCurrentArtifact] = useState<ArtifactData | null>(
29
- null
30
- );
31
28
 
32
29
  // Memoize the message text to prevent unnecessary recalculations
33
30
  const messageText = useMemo(() => {
@@ -53,63 +50,56 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
53
50
  // Simple artifact detection - look for <output class="memori-artifact"> tags
54
51
  // Remove message dependency to prevent recreation on every message change
55
52
  const detectArtifacts = useCallback((text: string, isFromUser: boolean): ArtifactData[] => {
56
- console.log('Detecting artifacts from text:', text?.substring(0, 100) + '...');
57
-
58
53
  if (!text || isFromUser) {
59
- console.log('No text or message is from user, returning empty array');
60
54
  return [];
61
55
  }
62
56
 
63
- //first strip the output tag isnide the think tag, if there is one
64
57
  text = stripReasoningTags(text);
65
58
 
66
59
  const artifacts: ArtifactData[] = [];
67
60
 
68
- // Find complete opening and closing tags
69
61
  const artifactRegex = /<output\s+class="memori-artifact"[^>]*data-mimetype="([^"]+)"[^>]*>([\s\S]*?)<\/output>/gi;
70
62
  const titleRegex = {
71
63
  dataTitle: /data-title\s*=\s*["\']([^"']+)["\']/i,
72
64
  htmlTitle: /<title>([^<]+)<\/title>/gi
73
65
  };
74
66
 
75
- const findTitle = (mimeType: string, content: string) => {
76
- console.log('Finding title for mimetype:', mimeType);
77
- return titleRegex.dataTitle.exec(content)?.[1] || titleRegex.htmlTitle.exec(content)?.[1] || `${mimeType.toUpperCase()} Artifact`;
67
+ const findTitle = (mimeType: string, content: string, outputTag: string) => {
68
+ // First try to find data-title in the output tag
69
+ const dataTitleMatch = outputTag.match(/data-title\s*=\s*["\']([^"']+)["\']/i);
70
+ if (dataTitleMatch) {
71
+ return dataTitleMatch[1];
72
+ }
73
+
74
+ // Then try to find title in the content
75
+ const htmlTitleMatch = content.match(/<title>([^<]+)<\/title>/i);
76
+ if (htmlTitleMatch) {
77
+ return htmlTitleMatch[1];
78
+ }
79
+
80
+ // Default title based on mimeType
81
+ return `${mimeType.toUpperCase()} Artifact`;
78
82
  };
79
83
 
80
84
  let match;
85
+ let artifactNum = 0;
81
86
  while ((match = artifactRegex.exec(text)) !== null) {
82
- console.log('Found complete artifact match', match);
87
+ artifactNum++;
83
88
  const mimeType = match[1];
84
89
  const content = match[2].trim();
85
-
86
- console.log('Artifact mimetype:', mimeType);
87
- console.log('Content length:', content.length);
88
-
89
- if (content.length > 50) {
90
- const artifact = {
91
- id: `artifact-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
92
- content,
93
- mimeType,
94
- title: findTitle(mimeType, text),
95
- timestamp: new Date(),
96
- size: content.length,
97
- };
98
-
99
- console.log('Created artifact:', {
100
- id: artifact.id,
101
- mimeType: artifact.mimeType,
102
- title: artifact.title,
103
- size: artifact.size
104
- });
105
-
106
- artifacts.push(artifact);
107
- } else {
108
- console.log('Skipping artifact - content too short');
109
- }
90
+ const outputTag = match[0]; // Full output tag for title extraction
91
+
92
+ const artifact = {
93
+ id: `artifact-${Date.now()}-${artifactNum}-${Math.random().toString(36).substr(2, 9)}`,
94
+ content,
95
+ mimeType,
96
+ title: findTitle(mimeType, content, outputTag),
97
+ timestamp: new Date(),
98
+ size: content.length,
99
+ };
100
+ artifacts.push(artifact);
110
101
  }
111
102
 
112
- console.log('Detected artifacts:', artifacts.length);
113
103
  return artifacts;
114
104
  }, []); // Remove message dependency
115
105
 
@@ -118,7 +108,7 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
118
108
  return detectArtifacts(messageText, message.fromUser || false);
119
109
  }, [messageText, message.fromUser, detectArtifacts]);
120
110
 
121
- // Auto-open artifacts when detected in new messages
111
+ // Auto-open first artifact when detected in new messages
122
112
  // Only run when messageId changes (actual new message), not on every render
123
113
  useEffect(() => {
124
114
  if (messageText.length > 0 && artifacts.length > 0) {
@@ -127,68 +117,92 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
127
117
  dispatchArtifactCreatedEvent(artifact);
128
118
  });
129
119
 
130
- if(isChatlogPanel){
131
- // openArtifact(artifacts[0]);
132
- setCurrentArtifact(artifacts[0]);
133
- } else {
120
+ // Only auto-open the first artifact
121
+ if (!isChatlogPanel) {
134
122
  setTimeout(() => {
135
123
  openArtifact(artifacts[0]);
136
- setCurrentArtifact(artifacts[0]);
137
124
  }, 100);
138
125
  }
139
126
  }
140
127
  }, [messageId, artifacts, dispatchArtifactCreatedEvent, isChatlogPanel, openArtifact]);
141
128
 
129
+ const handleArtifactClick = useCallback((artifact: ArtifactData) => {
130
+ if (
131
+ state.isDrawerOpen &&
132
+ state.currentArtifact?.id === artifact.id
133
+ ) {
134
+ closeArtifact();
135
+ } else {
136
+ openArtifact(artifact);
137
+ }
138
+ }, [state.isDrawerOpen, state.currentArtifact?.id, closeArtifact, openArtifact]);
139
+
140
+ const getIconForMimeType = useCallback((mimeType: string): string => {
141
+ if (mimeType.includes('html')) return '🌐';
142
+ if (mimeType.includes('markdown')) return '📝';
143
+ if (mimeType.includes('javascript') || mimeType.includes('typescript')) return '📜';
144
+ if (mimeType.includes('python')) return '🐍';
145
+ if (mimeType.includes('json')) return '📊';
146
+ if (mimeType.includes('css')) return '🎨';
147
+ if (mimeType.includes('xml')) return '📋';
148
+ if (mimeType.includes('svg')) return '🖼️';
149
+ return '📄';
150
+ }, []);
151
+
142
152
  if (artifacts.length === 0) return null;
143
153
 
144
154
  return (
145
155
  <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
146
- <div
147
- key={currentArtifact?.id}
148
- className="memori-artifact-handler"
149
- onClick={() => {
150
- if (
151
- state.isDrawerOpen &&
152
- state.currentArtifact?.id === currentArtifact?.id
153
- ) {
154
- closeArtifact();
155
- } else {
156
- openArtifact(currentArtifact as ArtifactData);
157
- }
158
- }}
159
- >
160
- <div className="memori-artifact-handler-icon">📄</div>
161
- <div className="memori-artifact-handler-info">
162
- <div className="memori-artifact-handler-title">
163
- {currentArtifact?.title}
156
+ {artifacts.map((artifact) => {
157
+ const isSelected = state.isDrawerOpen && state.currentArtifact?.id === artifact.id;
158
+
159
+ return (
160
+ <React.Fragment key={artifact.id}>
161
+ <div
162
+ className={`memori-artifact-handler${isSelected ? ' memori-artifact-handler--selected' : ''}`}
163
+ onClick={() => handleArtifactClick(artifact)}
164
+ style={isSelected ? {
165
+ border: '2px solid var(--memori-primary, #3b82f6)',
166
+ boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
167
+ } : undefined}
168
+ >
169
+ <div className="memori-artifact-handler-icon">
170
+ {getIconForMimeType(artifact.mimeType)}
171
+ </div>
172
+ <div className="memori-artifact-handler-info">
173
+ <div className="memori-artifact-handler-title">
174
+ {artifact.title}
175
+ </div>
176
+ <div className="memori-artifact-handler-meta">
177
+ {artifact.mimeType} •{' '}
178
+ {formatBytes(artifact.size || 0)}
179
+ </div>
180
+ </div>
181
+ <div className="memori-artifact-handler-action">
182
+ {isChatlogPanel ? (
183
+ state.isDrawerOpen &&
184
+ state.currentArtifact?.id === artifact.id ? (
185
+ <ChevronUp className="memori-artifact-handler-action-icon" />
186
+ ) : (
187
+ <ChevronDown className="memori-artifact-handler-action-icon" />
188
+ )
189
+ ) : state.isDrawerOpen &&
190
+ state.currentArtifact?.id === artifact.id ? (
191
+ <ChevronLeft className="memori-artifact-handler-action-icon" />
192
+ ) : (
193
+ <ChevronRight className="memori-artifact-handler-action-icon" />
194
+ )}
195
+ </div>
164
196
  </div>
165
- <div className="memori-artifact-handler-meta">
166
- {currentArtifact?.mimeType} •{' '}
167
- {formatBytes(currentArtifact?.size || 0)}
168
- </div>
169
- </div>
170
- <div className="memori-artifact-handler-action">
171
- {isChatlogPanel ? (
172
- state.isDrawerOpen &&
173
- state.currentArtifact?.id === currentArtifact?.id ? (
174
- <ChevronUp className="memori-artifact-handler-action-icon" />
175
- ) : (
176
- <ChevronDown className="memori-artifact-handler-action-icon" />
177
- )
178
- ) : state.isDrawerOpen &&
179
- state.currentArtifact?.id === currentArtifact?.id ? (
180
- <ChevronLeft className="memori-artifact-handler-action-icon" />
181
- ) : (
182
- <ChevronRight className="memori-artifact-handler-action-icon" />
183
- )}
184
- </div>
185
- </div>
186
-
187
- {/* Render ArtifactDrawer inline when in chatlog panel */}
188
- {state.isDrawerOpen &&
189
- state.currentArtifact?.id === currentArtifact?.id && (
190
- <ArtifactDrawer isChatLogPanel={isChatlogPanel} />
191
- )}
197
+
198
+ {/* Render ArtifactDrawer inline when in chatlog panel */}
199
+ {state.isDrawerOpen &&
200
+ state.currentArtifact?.id === artifact.id && (
201
+ <ArtifactDrawer isChatLogPanel={isChatlogPanel} />
202
+ )}
203
+ </React.Fragment>
204
+ );
205
+ })}
192
206
  </div>
193
207
  );
194
208
  };
@@ -0,0 +1,307 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { ArtifactProvider } from '../context/ArtifactContext';
4
+ import { ArtifactAPIBridge } from './ArtifactAPI';
5
+ import { ArtifactData } from '../types/artifact.types';
6
+ import MemoriWidget from '../../MemoriWidget/MemoriWidget';
7
+ import { Memori } from '@memori.ai/memori-api-client/dist/types';
8
+ import { VisemeProvider } from '../../../context/visemeContext';
9
+
10
+ const mockMemori: Memori = {
11
+ memoriID: 'test-memori-id',
12
+ name: 'Test Memori',
13
+ culture: 'en-US',
14
+ coverURL: '',
15
+ enableBoardOfExperts: false,
16
+ } as Memori;
17
+
18
+ // Helper function to wrap component with all required providers
19
+ const renderWithProviders = (component: React.ReactElement) => {
20
+ return render(
21
+ <VisemeProvider>
22
+ <ArtifactProvider>
23
+ {component}
24
+ </ArtifactProvider>
25
+ </VisemeProvider>
26
+ );
27
+ };
28
+
29
+ describe('ArtifactAPIBridge', () => {
30
+ beforeEach(() => {
31
+ // Clean up any previous API instance
32
+ if (window.MemoriArtifactAPI) {
33
+ delete window.MemoriArtifactAPI;
34
+ }
35
+
36
+ // Mock window.matchMedia
37
+ Object.defineProperty(window, 'matchMedia', {
38
+ writable: true,
39
+ value: jest.fn().mockImplementation(query => ({
40
+ matches: false,
41
+ media: query,
42
+ onchange: null,
43
+ addListener: jest.fn(), // deprecated
44
+ removeListener: jest.fn(), // deprecated
45
+ addEventListener: jest.fn(),
46
+ removeEventListener: jest.fn(),
47
+ dispatchEvent: jest.fn(),
48
+ })),
49
+ });
50
+ });
51
+
52
+ afterEach(() => {
53
+ // Clean up after each test
54
+ if (window.MemoriArtifactAPI) {
55
+ delete window.MemoriArtifactAPI;
56
+ }
57
+ });
58
+
59
+ it('should expose MemoriArtifactAPI on window', () => {
60
+ renderWithProviders(
61
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
62
+ );
63
+
64
+ expect(window.MemoriArtifactAPI).toBeDefined();
65
+ expect(typeof window.MemoriArtifactAPI?.createAndOpenArtifact).toBe('function');
66
+ expect(typeof window.MemoriArtifactAPI?.openArtifact).toBe('function');
67
+ expect(typeof window.MemoriArtifactAPI?.closeArtifact).toBe('function');
68
+ expect(typeof window.MemoriArtifactAPI?.toggleFullscreen).toBe('function');
69
+ expect(typeof window.MemoriArtifactAPI?.getState).toBe('function');
70
+ expect(typeof window.MemoriArtifactAPI?.createFromOutputElement).toBe('function');
71
+ });
72
+
73
+ it('should create and open artifact with simple parameters', async () => {
74
+ renderWithProviders(
75
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
76
+ );
77
+
78
+ const testContent = '<h1>Test Content</h1><p>This is a test</p>';
79
+ const testTitle = 'Test Artifact';
80
+
81
+ window.MemoriArtifactAPI!.createAndOpenArtifact(
82
+ testContent,
83
+ 'html',
84
+ testTitle
85
+ );
86
+
87
+ await waitFor(() => {
88
+ const state = window.MemoriArtifactAPI!.getState();
89
+ expect(state.isDrawerOpen).toBe(true);
90
+ expect(state.currentArtifact?.title).toBe(testTitle);
91
+ expect(state.currentArtifact?.content).toBe(testContent);
92
+ expect(state.currentArtifact?.mimeType).toBe('html');
93
+ });
94
+ });
95
+
96
+ it('should generate default title from mime type when not provided', async () => {
97
+ renderWithProviders(
98
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
99
+ );
100
+
101
+ window.MemoriArtifactAPI!.createAndOpenArtifact(
102
+ 'console.log("test")',
103
+ 'javascript'
104
+ );
105
+
106
+ await waitFor(() => {
107
+ const state = window.MemoriArtifactAPI!.getState();
108
+ expect(state.currentArtifact?.title).toBe('JavaScript Code');
109
+ });
110
+ });
111
+
112
+ it('should open artifact with full ArtifactData object', async () => {
113
+ renderWithProviders(
114
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
115
+ );
116
+
117
+ const artifact: ArtifactData = {
118
+ id: 'test-artifact-123',
119
+ content: '<div>Full artifact</div>',
120
+ mimeType: 'html',
121
+ title: 'Full Artifact Test',
122
+ timestamp: new Date(),
123
+ size: 100,
124
+ };
125
+
126
+ window.MemoriArtifactAPI!.openArtifact(artifact);
127
+
128
+ await waitFor(() => {
129
+ const state = window.MemoriArtifactAPI!.getState();
130
+ expect(state.isDrawerOpen).toBe(true);
131
+ expect(state.currentArtifact?.id).toBe('test-artifact-123');
132
+ expect(state.currentArtifact?.title).toBe('Full Artifact Test');
133
+ });
134
+ });
135
+
136
+ it('should close artifact', async () => {
137
+ renderWithProviders(
138
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
139
+ );
140
+
141
+ // First open an artifact
142
+ window.MemoriArtifactAPI!.createAndOpenArtifact(
143
+ '<h1>Test</h1>',
144
+ 'html',
145
+ 'Test'
146
+ );
147
+
148
+ await waitFor(() => {
149
+ const state = window.MemoriArtifactAPI!.getState();
150
+ expect(state.isDrawerOpen).toBe(true);
151
+ });
152
+
153
+ // Then close it
154
+ window.MemoriArtifactAPI!.closeArtifact();
155
+
156
+ await waitFor(() => {
157
+ const state = window.MemoriArtifactAPI!.getState();
158
+ expect(state.isDrawerOpen).toBe(false);
159
+ expect(state.currentArtifact).toBeNull();
160
+ });
161
+ });
162
+
163
+ it('should toggle fullscreen', async () => {
164
+ renderWithProviders(
165
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
166
+ );
167
+
168
+ // Open an artifact first
169
+ window.MemoriArtifactAPI!.createAndOpenArtifact(
170
+ '<h1>Test</h1>',
171
+ 'html',
172
+ 'Test'
173
+ );
174
+
175
+ await waitFor(() => {
176
+ const state = window.MemoriArtifactAPI!.getState();
177
+ expect(state.isFullscreen).toBe(false);
178
+ });
179
+
180
+ // Toggle fullscreen
181
+ window.MemoriArtifactAPI!.toggleFullscreen();
182
+
183
+ await waitFor(() => {
184
+ const state = window.MemoriArtifactAPI!.getState();
185
+ expect(state.isFullscreen).toBe(true);
186
+ });
187
+
188
+ // Toggle back
189
+ window.MemoriArtifactAPI!.toggleFullscreen();
190
+
191
+ await waitFor(() => {
192
+ const state = window.MemoriArtifactAPI!.getState();
193
+ expect(state.isFullscreen).toBe(false);
194
+ });
195
+ });
196
+
197
+ it('should create artifact from output element', async () => {
198
+ renderWithProviders(
199
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
200
+ );
201
+
202
+ // Create a mock output element
203
+ const outputElement = document.createElement('output');
204
+ outputElement.className = 'memori-artifact';
205
+ outputElement.innerHTML = '<div>Content from output</div>';
206
+ outputElement.setAttribute('data-mimetype', 'html');
207
+ outputElement.setAttribute('data-title', 'Output Artifact');
208
+ document.body.appendChild(outputElement);
209
+
210
+ const artifactId = window.MemoriArtifactAPI!.createFromOutputElement(outputElement);
211
+
212
+ expect(artifactId).toBeDefined();
213
+ expect(outputElement.style.display).toBe('none');
214
+ expect(outputElement.getAttribute('data-memori-processed')).toBe('true');
215
+
216
+ await waitFor(() => {
217
+ const state = window.MemoriArtifactAPI!.getState();
218
+ expect(state.isDrawerOpen).toBe(true);
219
+ expect(state.currentArtifact?.content).toBe('<div>Content from output</div>');
220
+ expect(state.currentArtifact?.title).toBe('Output Artifact');
221
+ });
222
+
223
+ // Clean up
224
+ document.body.removeChild(outputElement);
225
+ });
226
+
227
+ it('should handle multiple artifacts in the same message', async () => {
228
+ renderWithProviders(
229
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
230
+ );
231
+
232
+ // Create a message with multiple output elements
233
+ const messageContent = `
234
+ <output class="memori-artifact" data-mimetype="html" data-title="Artifact 1">
235
+ <div>Content 1</div>
236
+ </output>
237
+ <output class="memori-artifact" data-mimetype="javascript" data-title="Artifact 2">
238
+ console.log("test");
239
+ </output>
240
+ `;
241
+
242
+ window.MemoriArtifactAPI!.createAndOpenArtifact(messageContent, 'html');
243
+
244
+ await waitFor(() => {
245
+ const state = window.MemoriArtifactAPI!.getState();
246
+ // The first artifact should be opened
247
+ expect(state.isDrawerOpen).toBe(true);
248
+ expect(state.currentArtifact).toBeDefined();
249
+ });
250
+ });
251
+
252
+ it('should return initial state correctly', () => {
253
+ renderWithProviders(
254
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
255
+ );
256
+
257
+ const state = window.MemoriArtifactAPI!.getState();
258
+
259
+ expect(state.currentArtifact).toBeNull();
260
+ expect(state.isDrawerOpen).toBe(false);
261
+ expect(state.isFullscreen).toBe(false);
262
+ });
263
+
264
+ it('should clean up API on unmount', () => {
265
+ const { unmount } = renderWithProviders(
266
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
267
+ );
268
+
269
+ expect(window.MemoriArtifactAPI).toBeDefined();
270
+
271
+ unmount();
272
+
273
+ expect(window.MemoriArtifactAPI).toBeUndefined();
274
+ });
275
+
276
+ it('should handle different mime types with correct titles', async () => {
277
+ renderWithProviders(
278
+ <MemoriWidget memori={mockMemori} tenantID="test-tenant-id" />
279
+ );
280
+
281
+ const testCases = [
282
+ { mimeType: 'html', expectedTitle: 'HTML Document' },
283
+ { mimeType: 'markdown', expectedTitle: 'Markdown Document' },
284
+ { mimeType: 'javascript', expectedTitle: 'JavaScript Code' },
285
+ { mimeType: 'python', expectedTitle: 'Python Code' },
286
+ { mimeType: 'json', expectedTitle: 'JSON Data' },
287
+ { mimeType: 'css', expectedTitle: 'CSS Stylesheet' },
288
+ { mimeType: 'typescript', expectedTitle: 'TypeScript Code' },
289
+ { mimeType: 'xml', expectedTitle: 'XML Document' },
290
+ { mimeType: 'svg', expectedTitle: 'SVG Image' },
291
+ { mimeType: 'unknown', expectedTitle: 'Document' },
292
+ ];
293
+
294
+ for (const testCase of testCases) {
295
+ window.MemoriArtifactAPI!.createAndOpenArtifact(
296
+ 'test content',
297
+ testCase.mimeType
298
+ );
299
+
300
+ await waitFor(() => {
301
+ const state = window.MemoriArtifactAPI!.getState();
302
+ expect(state.currentArtifact?.title).toBe(testCase.expectedTitle);
303
+ });
304
+ }
305
+ });
306
+ });
307
+