@memori.ai/memori-react 8.34.0 → 8.35.1

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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.css +7 -1
  3. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +4 -1
  4. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js +3 -3
  5. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
  6. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +139 -89
  7. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  8. package/dist/components/MemoriWidget/MemoriWidget.js +10 -8
  9. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  10. package/dist/components/MicrophoneButton/MicrophoneButton.css +1 -0
  11. package/dist/components/MobileSessionPanel/MobileSessionPanel.css +377 -0
  12. package/dist/components/MobileSessionPanel/MobileSessionPanel.d.ts +57 -0
  13. package/dist/components/MobileSessionPanel/MobileSessionPanel.js +159 -0
  14. package/dist/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -0
  15. package/dist/components/PositionPopover/PositionPopover.css +107 -112
  16. package/dist/components/PositionPopover/PositionPopover.js +23 -18
  17. package/dist/components/PositionPopover/PositionPopover.js.map +1 -1
  18. package/dist/components/StartPanel/StartPanel.js +1 -0
  19. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  20. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +2 -2
  21. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  22. package/dist/components/layouts/WebsiteAssistant/website-assistant.css +23 -25
  23. package/dist/components/layouts/fullpage.css +21 -9
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +7 -1
  27. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +4 -1
  28. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js +4 -4
  29. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
  30. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +139 -89
  31. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  32. package/esm/components/MemoriWidget/MemoriWidget.js +10 -8
  33. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  34. package/esm/components/MicrophoneButton/MicrophoneButton.css +1 -0
  35. package/esm/components/MobileSessionPanel/MobileSessionPanel.css +377 -0
  36. package/esm/components/MobileSessionPanel/MobileSessionPanel.d.ts +57 -0
  37. package/esm/components/MobileSessionPanel/MobileSessionPanel.js +157 -0
  38. package/esm/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -0
  39. package/esm/components/PositionPopover/PositionPopover.css +107 -112
  40. package/esm/components/PositionPopover/PositionPopover.js +25 -20
  41. package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
  42. package/esm/components/StartPanel/StartPanel.js +1 -0
  43. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  44. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +2 -2
  45. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  46. package/esm/components/layouts/WebsiteAssistant/website-assistant.css +23 -25
  47. package/esm/components/layouts/fullpage.css +21 -9
  48. package/esm/version.d.ts +1 -1
  49. package/esm/version.js +1 -1
  50. package/package.json +2 -2
  51. package/src/components/Chat/Chat.stories.tsx +127 -1
  52. package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +300 -162
  53. package/src/components/MemoriWidget/MemoriWidget.tsx +5 -3
  54. package/src/components/MicrophoneButton/MicrophoneButton.css +1 -0
  55. package/src/components/StartPanel/StartPanel.tsx +1 -0
  56. package/src/mocks/data.ts +143 -13
  57. package/src/version.ts +1 -1
@@ -7,7 +7,7 @@ import ChevronDown from '../../../icons/ChevronDown';
7
7
  import ChevronLeft from '../../../icons/ChevronLeft';
8
8
  import ChevronUp from '../../../icons/ChevronUp';
9
9
  import { Message } from '@memori.ai/memori-api-client/dist/types';
10
- import { stripOutputTags, stripReasoningTags } from '../../../../helpers/utils';
10
+ import { stripReasoningTags } from '../../../../helpers/utils';
11
11
 
12
12
  // Event type for artifact creation
13
13
  type ArtifactCreatedEvent = CustomEvent<{
@@ -20,127 +20,265 @@ interface ArtifactHandlerProps {
20
20
  message: Message;
21
21
  }
22
22
 
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const formatBytes = (bytes: number): string => {
28
+ if (bytes === 0) return '0 Bytes';
29
+ const k = 1024;
30
+ const sizes = ['Bytes', 'KB', 'MB'];
31
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
32
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
33
+ };
34
+
35
+ /**
36
+ * Lightweight deterministic hash of the first 200 chars of a string.
37
+ * Used to produce stable artifact IDs that survive re-renders.
38
+ */
39
+ const hashContent = (str: string): string => {
40
+ let hash = 0;
41
+ const len = Math.min(str.length, 200);
42
+ for (let i = 0; i < len; i++) {
43
+ hash = (hash << 5) - hash + str.charCodeAt(i);
44
+ hash |= 0; // Convert to 32-bit int
45
+ }
46
+ return Math.abs(hash).toString(36);
47
+ };
48
+
49
+ /**
50
+ * Returns true when the `class` attribute value of an opening tag contains
51
+ * the word "memori-artifact" as a whole word (handles multiple classes).
52
+ *
53
+ * Examples that match:
54
+ * class="memori-artifact"
55
+ * class="memori-artifact extra-class"
56
+ * class="foo memori-artifact"
57
+ * class='memori-artifact'
58
+ * class = "memori-artifact extra"
59
+ */
60
+ const CLASS_ATTR_RE = /class\s*=\s*["']([^"']*)["']/i;
61
+ const hasMemoriArtifactClass = (openingTag: string): boolean => {
62
+ const m = openingTag.match(CLASS_ATTR_RE);
63
+ if (!m) return false;
64
+ // \b word-boundary ensures we don't match "foo-memori-artifact-bar"
65
+ return /\bmemori-artifact\b/.test(m[1]);
66
+ };
67
+
68
+ /**
69
+ * Parser-style artifact detector.
70
+ *
71
+ * Replaces the previous single-regex approach with a depth-tracking loop so
72
+ * that:
73
+ * - Multiple CSS classes are handled correctly (e.g. class="memori-artifact foo")
74
+ * - Nested / self-contained <output> tags inside the content don't truncate it
75
+ * - Unclosed tags are gracefully skipped
76
+ * - IDs are stable across re-renders (derived from message + content hash)
77
+ */
78
+ const detectArtifacts = (
79
+ text: string,
80
+ isFromUser: boolean,
81
+ messageKey: string
82
+ ): ArtifactData[] => {
83
+ if (!text || isFromUser) return [];
84
+
85
+ const cleaned = stripReasoningTags(text);
86
+ const artifacts: ArtifactData[] = [];
87
+ let searchFrom = 0;
88
+ let artifactNum = 0;
89
+
90
+ // Regex that matches any <output …> opening tag (capturing the full tag)
91
+ const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
92
+
93
+ while (searchFrom < cleaned.length) {
94
+ // Find the next <output …> opening tag from the current position
95
+ OPEN_TAG_RE.lastIndex = searchFrom;
96
+ const openMatch = OPEN_TAG_RE.exec(cleaned);
97
+ if (!openMatch) break;
98
+
99
+ const fullOpenTag = openMatch[0]; // e.g. <output class="memori-artifact" data-mimetype="text/html">
100
+ const openStart = openMatch.index;
101
+ const openEnd = openStart + fullOpenTag.length;
102
+
103
+ // Only process tags that carry the required class
104
+ if (!hasMemoriArtifactClass(fullOpenTag)) {
105
+ searchFrom = openEnd;
106
+ continue;
107
+ }
108
+
109
+ // Depth-tracking scan to find the matching </output>
110
+ let depth = 1;
111
+ let pos = openEnd;
112
+ let closeStart = -1;
113
+
114
+ while (pos < cleaned.length && depth > 0) {
115
+ const nextOpen = cleaned.indexOf('<output', pos);
116
+ const nextClose = cleaned.indexOf('</output>', pos);
117
+
118
+ if (nextClose === -1) {
119
+ // No closing tag found — treat everything until EOF as content
120
+ // (handles streaming / partial messages)
121
+ closeStart = cleaned.length;
122
+ depth = 0;
123
+ break;
124
+ }
125
+
126
+ if (nextOpen !== -1 && nextOpen < nextClose) {
127
+ // There's another opening tag before the next closing tag → go deeper
128
+ depth++;
129
+ pos = nextOpen + '<output'.length;
130
+ } else {
131
+ // Found a closing tag
132
+ depth--;
133
+ if (depth === 0) {
134
+ closeStart = nextClose;
135
+ } else {
136
+ pos = nextClose + '</output>'.length;
137
+ }
138
+ }
139
+ }
140
+
141
+ if (closeStart === -1) {
142
+ // Malformed — skip past this opening tag and continue
143
+ searchFrom = openEnd;
144
+ continue;
145
+ }
146
+
147
+ const content = cleaned.slice(openEnd, closeStart).trim();
148
+
149
+ // Extract metadata from the opening tag
150
+ const mimeTypeMatch = fullOpenTag.match(
151
+ /data-mimetype\s*=\s*["']([^"']+)["']/i
152
+ );
153
+ const mimeType = mimeTypeMatch?.[1]?.trim() || 'text/plain';
154
+
155
+ const dataTitleMatch = fullOpenTag.match(
156
+ /data-title\s*=\s*["']([^"']+)["']/i
157
+ );
158
+ const htmlTitleMatch = content.match(/<title>([^<]+)<\/title>/i);
159
+ const title =
160
+ dataTitleMatch?.[1] ||
161
+ htmlTitleMatch?.[1] ||
162
+ `${mimeType.toUpperCase()} Artifact`;
163
+
164
+ artifactNum++;
165
+
166
+ // Stable ID: does not change across re-renders for the same content
167
+ const stableId = `artifact-${messageKey}-${artifactNum}-${hashContent(
168
+ content
169
+ )}`;
170
+
171
+ artifacts.push({
172
+ id: stableId,
173
+ content,
174
+ mimeType,
175
+ title,
176
+ timestamp: new Date(),
177
+ size: content.length,
178
+ });
179
+
180
+ // Advance past the closing tag (or EOF)
181
+ searchFrom =
182
+ closeStart === cleaned.length
183
+ ? cleaned.length
184
+ : closeStart + '</output>'.length;
185
+ }
186
+
187
+ return artifacts;
188
+ };
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Component
192
+ // ---------------------------------------------------------------------------
193
+
23
194
  const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
24
195
  isChatlogPanel = false,
25
196
  message,
26
197
  }) => {
27
198
  const { openArtifact, state, closeArtifact } = useArtifact();
28
199
 
29
- // Memoize the message text to prevent unnecessary recalculations
30
- const messageText = useMemo(() => {
31
- return message.translatedText || message.text || '';
32
- }, [message.translatedText, message.text]);
33
-
34
- // Memoize the message ID to track when the actual message changes
35
- const messageId = useMemo(() => {
36
- return `${message.timestamp}-${message.fromUser}`;
37
- }, [message.timestamp, message.fromUser]);
38
-
39
- // Function to dispatch artifact created event
40
- const dispatchArtifactCreatedEvent = useCallback((artifact: ArtifactData) => {
41
- const event: ArtifactCreatedEvent = new CustomEvent('artifactCreated', {
42
- detail: {
43
- artifact,
44
- message,
45
- },
46
- });
47
- document.dispatchEvent(event);
48
- }, [message]);
49
-
50
- // Simple artifact detection - look for <output class="memori-artifact"> tags
51
- // Remove message dependency to prevent recreation on every message change
52
- const detectArtifacts = useCallback((text: string, isFromUser: boolean): ArtifactData[] => {
53
- if (!text || isFromUser) {
54
- return [];
55
- }
200
+ /**
201
+ * Use raw text for artifact detection.
202
+ * translatedText may lose <output> tags — keep it as fallback only.
203
+ */
204
+ const messageText = useMemo(() => message.text || '', [message.text]);
205
+ const translatedMessageText = useMemo(
206
+ () => message.translatedText || '',
207
+ [message.translatedText]
208
+ );
56
209
 
57
- text = stripReasoningTags(text);
58
-
59
- const artifacts: ArtifactData[] = [];
60
-
61
- const artifactRegex = /<output\s+class="memori-artifact"[^>]*data-mimetype="([^"]+)"[^>]*>([\s\S]*?)<\/output>/gi;
62
- const titleRegex = {
63
- dataTitle: /data-title\s*=\s*["\']([^"']+)["\']/i,
64
- htmlTitle: /<title>([^<]+)<\/title>/gi
65
- };
66
-
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`;
82
- };
83
-
84
- let match;
85
- let artifactNum = 0;
86
- while ((match = artifactRegex.exec(text)) !== null) {
87
- artifactNum++;
88
- const mimeType = match[1];
89
- const content = match[2].trim();
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);
101
- }
210
+ /**
211
+ * Stable key that identifies this specific message.
212
+ * Used both for effect gating and for stable artifact ID generation.
213
+ */
214
+ const messageKey = useMemo(
215
+ () => `${message.timestamp}-${message.fromUser ? '1' : '0'}`,
216
+ [message.timestamp, message.fromUser]
217
+ );
102
218
 
103
- return artifacts;
104
- }, []); // Remove message dependency
219
+ const dispatchArtifactCreatedEvent = useCallback(
220
+ (artifact: ArtifactData) => {
221
+ const event: ArtifactCreatedEvent = new CustomEvent('artifactCreated', {
222
+ detail: { artifact, message },
223
+ });
224
+ document.dispatchEvent(event);
225
+ },
226
+ [message]
227
+ );
105
228
 
106
- // Memoize artifacts detection to prevent recalculation on every render
107
- const artifacts = useMemo(() => {
108
- return detectArtifacts(messageText, message.fromUser || false);
109
- }, [messageText, message.fromUser, detectArtifacts]);
229
+ /**
230
+ * Memoised artifact list.
231
+ * Falls back to translatedText only when the primary text yields nothing.
232
+ * Both branches use the same messageKey so IDs remain stable.
233
+ */
234
+ const artifacts = useMemo<ArtifactData[]>(() => {
235
+ const fromUser = message.fromUser || false;
236
+ const primary = detectArtifacts(messageText, fromUser, messageKey);
237
+ if (primary.length > 0) return primary;
238
+ return detectArtifacts(translatedMessageText, fromUser, messageKey);
239
+ }, [messageText, translatedMessageText, message.fromUser, messageKey]);
110
240
 
111
- // Auto-open first artifact when detected in new messages
112
- // Only run when messageId changes (actual new message), not on every render
241
+ /**
242
+ * Auto-open the first artifact when a new message arrives.
243
+ *
244
+ * FIX: messageText is now included in the dependency array so the effect
245
+ * re-fires correctly if the text changes without the messageKey changing
246
+ * (e.g. streaming updates).
247
+ */
113
248
  useEffect(() => {
114
- if (messageText.length > 0 && artifacts.length > 0) {
115
- // Dispatch event for each artifact created
116
- artifacts.forEach(artifact => {
117
- dispatchArtifactCreatedEvent(artifact);
118
- });
249
+ if (!messageText || artifacts.length === 0) return;
119
250
 
120
- // Only auto-open the first artifact
121
- if (!isChatlogPanel) {
122
- setTimeout(() => {
123
- openArtifact(artifacts[0]);
124
- }, 100);
125
- }
126
- }
127
- }, [messageId, artifacts, dispatchArtifactCreatedEvent, isChatlogPanel, openArtifact]);
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);
251
+ artifacts.forEach(artifact => dispatchArtifactCreatedEvent(artifact));
252
+
253
+ if (!isChatlogPanel) {
254
+ const timer = setTimeout(() => openArtifact(artifacts[0]), 100);
255
+ return () => clearTimeout(timer);
137
256
  }
138
- }, [state.isDrawerOpen, state.currentArtifact?.id, closeArtifact, openArtifact]);
257
+ }, [
258
+ messageKey,
259
+ messageText,
260
+ artifacts,
261
+ dispatchArtifactCreatedEvent,
262
+ isChatlogPanel,
263
+ openArtifact,
264
+ ]);
265
+
266
+ const handleArtifactClick = useCallback(
267
+ (artifact: ArtifactData) => {
268
+ if (state.isDrawerOpen && state.currentArtifact?.id === artifact.id) {
269
+ closeArtifact();
270
+ } else {
271
+ openArtifact(artifact);
272
+ }
273
+ },
274
+ [state.isDrawerOpen, state.currentArtifact?.id, closeArtifact, openArtifact]
275
+ );
139
276
 
140
277
  const getIconForMimeType = useCallback((mimeType: string): string => {
141
278
  if (mimeType.includes('html')) return '🌐';
142
279
  if (mimeType.includes('markdown')) return '📝';
143
- if (mimeType.includes('javascript') || mimeType.includes('typescript')) return '📜';
280
+ if (mimeType.includes('javascript') || mimeType.includes('typescript'))
281
+ return '📜';
144
282
  if (mimeType.includes('python')) return '🐍';
145
283
  if (mimeType.includes('json')) return '📊';
146
284
  if (mimeType.includes('css')) return '🎨';
@@ -153,79 +291,79 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
153
291
 
154
292
  return (
155
293
  <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
156
- {artifacts.map((artifact) => {
157
- const isSelected = state.isDrawerOpen && state.currentArtifact?.id === artifact.id;
158
-
294
+ {artifacts.map(artifact => {
295
+ const isSelected =
296
+ state.isDrawerOpen && state.currentArtifact?.id === artifact.id;
297
+
159
298
  return (
160
299
  <React.Fragment key={artifact.id}>
161
300
  <div
162
- className={`memori-artifact-handler${isSelected ? ' memori-artifact-handler--selected' : ''}`}
301
+ className={`memori-artifact-handler${
302
+ isSelected ? ' memori-artifact-handler--selected' : ''
303
+ }`}
163
304
  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}
305
+ style={
306
+ isSelected
307
+ ? {
308
+ border: '2px solid var(--memori-primary, #3b82f6)',
309
+ boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
310
+ }
311
+ : undefined
312
+ }
168
313
  >
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}
314
+ <div className="memori-artifact-handler-icon">
315
+ {getIconForMimeType(artifact.mimeType)}
175
316
  </div>
176
- <div className="memori-artifact-handler-meta">
177
- {artifact.mimeType} •{' '}
178
- {formatBytes(artifact.size || 0)}
317
+ <div className="memori-artifact-handler-info">
318
+ <div className="memori-artifact-handler-title">
319
+ {artifact.title}
320
+ </div>
321
+ <div className="memori-artifact-handler-meta">
322
+ {artifact.mimeType} • {formatBytes(artifact.size || 0)}
323
+ </div>
179
324
  </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" />
325
+ <div className="memori-artifact-handler-action">
326
+ {isChatlogPanel ? (
327
+ isSelected ? (
328
+ <ChevronUp className="memori-artifact-handler-action-icon" />
329
+ ) : (
330
+ <ChevronDown className="memori-artifact-handler-action-icon" />
331
+ )
332
+ ) : isSelected ? (
333
+ <ChevronLeft className="memori-artifact-handler-action-icon" />
186
334
  ) : (
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
- )}
335
+ <ChevronRight className="memori-artifact-handler-action-icon" />
336
+ )}
337
+ </div>
195
338
  </div>
196
- </div>
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
- );
339
+
340
+ {/* Render ArtifactDrawer inline when in chatlog panel */}
341
+ {isSelected && <ArtifactDrawer isChatLogPanel={isChatlogPanel} />}
342
+ </React.Fragment>
343
+ );
205
344
  })}
206
345
  </div>
207
346
  );
208
347
  };
209
348
 
210
- const formatBytes = (bytes: number): string => {
211
- if (bytes === 0) return '0 Bytes';
212
- const k = 1024;
213
- const sizes = ['Bytes', 'KB', 'MB'];
214
- const i = Math.floor(Math.log(bytes) / Math.log(k));
215
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
216
- };
349
+ // ---------------------------------------------------------------------------
350
+ // Memoised export
351
+ // ---------------------------------------------------------------------------
352
+
353
+ /**
354
+ * FIX: comparison now uses the same field priority as detectArtifacts
355
+ * (message.text first, translatedText as fallback) to avoid asymmetric
356
+ * re-render skipping.
357
+ */
358
+ const MemoizedArtifactHandler = memo(ArtifactHandler, (prev, next) => {
359
+ const prevText = prev.message.text || prev.message.translatedText || '';
360
+ const nextText = next.message.text || next.message.translatedText || '';
217
361
 
218
- // Memoize the component to prevent re-renders when props haven't changed
219
- const MemoizedArtifactHandler = memo(ArtifactHandler, (prevProps, nextProps) => {
220
- // Only re-render if the message content or isChatlogPanel changes
221
- const prevMessageText = prevProps.message.translatedText || prevProps.message.text || '';
222
- const nextMessageText = nextProps.message.translatedText || nextProps.message.text || '';
223
-
224
362
  return (
225
- prevProps.isChatlogPanel === nextProps.isChatlogPanel &&
226
- prevMessageText === nextMessageText &&
227
- prevProps.message.fromUser === nextProps.message.fromUser &&
228
- prevProps.message.timestamp === nextProps.message.timestamp
363
+ prev.isChatlogPanel === next.isChatlogPanel &&
364
+ prevText === nextText &&
365
+ prev.message.fromUser === next.message.fromUser &&
366
+ prev.message.timestamp === next.message.timestamp
229
367
  );
230
368
  });
231
369
 
@@ -2870,6 +2870,9 @@ const MemoriWidget = ({
2870
2870
  showOnlyLastMessages === undefined
2871
2871
  ? selectedLayout !== 'TOTEM' && selectedLayout !== 'WEBSITE_ASSISTANT'
2872
2872
  : !showOnlyLastMessages;
2873
+ const canShowLoginButton =
2874
+ !tenant?.ssoLogin &&
2875
+ (showLogin ?? integrationConfig?.showLogin ?? memori.requireLoginToken);
2873
2876
 
2874
2877
  const headerProps: HeaderProps = {
2875
2878
  memori: {
@@ -2901,8 +2904,7 @@ const MemoriWidget = ({
2901
2904
  showReload: selectedLayout === 'TOTEM',
2902
2905
  showClear: showClear ?? integrationConfig?.showClear ?? false,
2903
2906
  clearHistory: () => setHistory(h => h.slice(-1)),
2904
- showLogin:
2905
- showLogin ?? integrationConfig?.showLogin ?? memori.requireLoginToken,
2907
+ showLogin: canShowLoginButton,
2906
2908
  setShowLoginDrawer,
2907
2909
  loginToken,
2908
2910
  user,
@@ -2961,7 +2963,7 @@ const MemoriWidget = ({
2961
2963
  isUserLoggedIn: !!loginToken && !!user?.userID,
2962
2964
  hasInitialSession: !!initialSessionID,
2963
2965
  notEnoughCredits: needsCredits && !hasEnoughCredits,
2964
- showLogin: showLogin ?? memori.requireLoginToken,
2966
+ showLogin: canShowLoginButton,
2965
2967
  setShowLoginDrawer,
2966
2968
  user,
2967
2969
  };
@@ -119,6 +119,7 @@
119
119
  right: 115%;
120
120
  width: 100%;
121
121
  height: 100%;
122
+ min-height: 35px;
122
123
  background: rgba(0, 0, 0, 0.8);
123
124
  color: #fff;
124
125
  font-size: 0.85em;
@@ -194,6 +194,7 @@ const StartPanel: React.FC<Props> = ({
194
194
  )}
195
195
  {((memori.needsPosition && position) || !memori.needsPosition) &&
196
196
  !!memori.requireLoginToken &&
197
+ !tenant?.ssoLogin &&
197
198
  !isUserLoggedIn && (
198
199
  <div className="memori--needsLogin">
199
200
  <p>