@memori.ai/memori-react 8.33.0 → 8.35.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.
- package/CHANGELOG.md +20 -0
- package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.css +7 -1
- package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +4 -1
- package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js +3 -3
- package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
- package/dist/components/Header/ChatConsumptionDropdown.d.ts +1 -0
- package/dist/components/Header/ChatConsumptionDropdown.js +3 -2
- package/dist/components/Header/ChatConsumptionDropdown.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +139 -89
- package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
- package/dist/components/MobileSessionPanel/MobileSessionPanel.css +377 -0
- package/dist/components/MobileSessionPanel/MobileSessionPanel.d.ts +57 -0
- package/dist/components/MobileSessionPanel/MobileSessionPanel.js +159 -0
- package/dist/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -0
- package/dist/components/PositionPopover/PositionPopover.js +6 -1
- package/dist/components/PositionPopover/PositionPopover.js.map +1 -1
- package/dist/components/StartPanel/StartPanel.js +1 -0
- package/dist/components/StartPanel/StartPanel.js.map +1 -1
- package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +2 -2
- package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
- package/dist/components/layouts/WebsiteAssistant/website-assistant.css +23 -25
- package/dist/components/layouts/fullpage.css +21 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +7 -1
- package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +4 -1
- package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js +4 -4
- package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
- package/esm/components/Header/ChatConsumptionDropdown.d.ts +1 -0
- package/esm/components/Header/ChatConsumptionDropdown.js +3 -2
- package/esm/components/Header/ChatConsumptionDropdown.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +139 -89
- package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
- package/esm/components/MobileSessionPanel/MobileSessionPanel.css +377 -0
- package/esm/components/MobileSessionPanel/MobileSessionPanel.d.ts +57 -0
- package/esm/components/MobileSessionPanel/MobileSessionPanel.js +157 -0
- package/esm/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -0
- package/esm/components/PositionPopover/PositionPopover.js +6 -1
- package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
- package/esm/components/StartPanel/StartPanel.js +1 -0
- package/esm/components/StartPanel/StartPanel.js.map +1 -1
- package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +2 -2
- package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
- package/esm/components/layouts/WebsiteAssistant/website-assistant.css +23 -25
- package/esm/components/layouts/fullpage.css +21 -9
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +2 -2
- package/src/components/Chat/Chat.stories.tsx +127 -1
- package/src/components/Header/ChatConsumptionDropdown.test.tsx +31 -0
- package/src/components/Header/ChatConsumptionDropdown.tsx +20 -11
- package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +300 -162
- package/src/components/StartPanel/StartPanel.tsx +1 -0
- package/src/mocks/data.ts +143 -13
- 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 {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
}, [
|
|
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'))
|
|
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(
|
|
157
|
-
const isSelected =
|
|
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${
|
|
301
|
+
className={`memori-artifact-handler${
|
|
302
|
+
isSelected ? ' memori-artifact-handler--selected' : ''
|
|
303
|
+
}`}
|
|
163
304
|
onClick={() => handleArtifactClick(artifact)}
|
|
164
|
-
style={
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
<
|
|
188
|
-
)
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|