@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.
- package/CHANGELOG.md +438 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -2
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +6 -6
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
- package/dist/components/ChatInputs/ChatInputs.js +1 -1
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/ContentPreviewModal/ContentPreviewModal.css +6 -22
- package/dist/components/FilePreview/FilePreview.css +41 -20
- package/dist/components/FilePreview/FilePreview.d.ts +2 -1
- package/dist/components/FilePreview/FilePreview.js +6 -2
- package/dist/components/FilePreview/FilePreview.js.map +1 -1
- package/dist/components/KnownFacts/KnownFacts.js +2 -2
- package/dist/components/KnownFacts/KnownFacts.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js +14 -12
- package/dist/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +1 -106
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +8 -9
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/components/TabSwitch.css +7 -0
- package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +210 -62
- package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
- package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js +65 -1
- package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
- package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
- package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
- package/dist/components/MemoriArtifactSystem/utils/applyEdits.js +117 -0
- package/dist/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
- package/dist/components/MemoriWidget/MemoriWidget.css +59 -0
- package/dist/components/Snippet/Snippet.css +6 -36
- package/dist/components/Snippet/Snippet.js +3 -1
- package/dist/components/Snippet/Snippet.js.map +1 -1
- package/dist/components/StartPanel/StartPanel.css +106 -0
- package/dist/components/StartPanel/StartPanel.js +1 -1
- package/dist/components/StartPanel/StartPanel.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.css +0 -19
- package/dist/components/UploadButton/UploadButton.js +1 -3
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.js +1 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/dist/components/layouts/chat.css +2 -0
- package/dist/components/layouts/zoomed-full-body.css +1 -0
- package/dist/helpers/message.js +3 -0
- package/dist/helpers/message.js.map +1 -1
- package/dist/locales/de.json +3 -0
- package/dist/locales/en.json +3 -0
- package/dist/locales/es.json +3 -0
- package/dist/locales/fr.json +3 -0
- package/dist/locales/it.json +3 -0
- package/dist/styles.css +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +2 -2
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -2
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +4 -5
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
- package/esm/components/ChatInputs/ChatInputs.js +1 -1
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/ContentPreviewModal/ContentPreviewModal.css +6 -22
- package/esm/components/FilePreview/FilePreview.css +41 -20
- package/esm/components/FilePreview/FilePreview.d.ts +2 -1
- package/esm/components/FilePreview/FilePreview.js +7 -3
- package/esm/components/FilePreview/FilePreview.js.map +1 -1
- package/esm/components/KnownFacts/KnownFacts.js +2 -2
- package/esm/components/KnownFacts/KnownFacts.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js +15 -13
- package/esm/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +1 -106
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +8 -9
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/components/TabSwitch.css +7 -0
- package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +211 -63
- package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
- package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js +66 -2
- package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
- package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
- package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
- package/esm/components/MemoriArtifactSystem/utils/applyEdits.js +112 -0
- package/esm/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
- package/esm/components/MemoriWidget/MemoriWidget.css +59 -0
- package/esm/components/Snippet/Snippet.css +6 -36
- package/esm/components/Snippet/Snippet.js +4 -2
- package/esm/components/Snippet/Snippet.js.map +1 -1
- package/esm/components/StartPanel/StartPanel.css +106 -0
- package/esm/components/StartPanel/StartPanel.js +2 -2
- package/esm/components/StartPanel/StartPanel.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.css +0 -19
- package/esm/components/UploadButton/UploadButton.js +1 -3
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.js +1 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/esm/components/layouts/chat.css +2 -0
- package/esm/components/layouts/zoomed-full-body.css +1 -0
- package/esm/helpers/message.js +3 -0
- package/esm/helpers/message.js.map +1 -1
- package/esm/locales/de.json +3 -0
- package/esm/locales/en.json +3 -0
- package/esm/locales/es.json +3 -0
- package/esm/locales/fr.json +3 -0
- package/esm/locales/it.json +3 -0
- package/esm/styles.css +1 -1
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +2 -2
- package/src/components/Avatar/AvatarView/AvatarComponent/constants.test.ts +19 -0
- package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +9 -6
- package/src/components/ChatInputs/ChatInputs.tsx +1 -0
- package/src/components/ContentPreviewModal/ContentPreviewModal.css +6 -22
- package/src/components/FilePreview/FilePreview.css +41 -20
- package/src/components/FilePreview/FilePreview.tsx +14 -0
- package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +14 -0
- package/src/components/KnownFacts/KnownFacts.tsx +0 -2
- package/src/components/MediaWidget/MediaPreviewModal.test.tsx +2 -3
- package/src/components/MemoriArtifactSystem/components/ArtifactActions/ArtifactActions.tsx +19 -33
- package/src/components/MemoriArtifactSystem/components/ArtifactActions/components/CopyButtonWithDropdown.tsx +44 -45
- package/src/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +1 -106
- package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +8 -9
- package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +24 -20
- package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/components/TabSwitch.css +7 -0
- package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +357 -148
- package/src/components/MemoriArtifactSystem/context/ArtifactContext.tsx +111 -9
- package/src/components/MemoriArtifactSystem/types/artifact.types.ts +20 -1
- package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.test.tsx +1 -0
- package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.tsx +19 -13
- package/src/components/MemoriArtifactSystem/utils/applyEdits.test.ts +85 -0
- package/src/components/MemoriArtifactSystem/utils/applyEdits.ts +173 -0
- package/src/components/MemoriWidget/MemoriWidget.css +59 -0
- package/src/components/Snippet/Snippet.css +6 -36
- package/src/components/Snippet/Snippet.tsx +6 -7
- package/src/components/StartPanel/StartPanel.css +106 -0
- package/src/components/StartPanel/StartPanel.stories.tsx +20 -0
- package/src/components/StartPanel/StartPanel.tsx +8 -0
- package/src/components/UploadButton/UploadButton.css +0 -19
- package/src/components/UploadButton/UploadButton.tsx +0 -11
- package/src/components/UploadButton/UploadImages/UploadImages.tsx +1 -1
- package/src/components/layouts/chat.css +2 -0
- package/src/components/layouts/zoomed-full-body.css +1 -0
- package/src/helpers/message.ts +15 -0
- package/src/locales/de.json +3 -0
- package/src/locales/en.json +3 -0
- package/src/locales/es.json +3 -0
- package/src/locales/fr.json +3 -0
- package/src/locales/it.json +3 -0
- package/src/version.ts +1 -1
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
useMemo,
|
|
5
|
+
useCallback,
|
|
6
|
+
memo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { useArtifact } from '../../context/ArtifactContext';
|
|
3
|
-
import { ArtifactData } from '../../types/artifact.types';
|
|
10
|
+
import { ArtifactData, ArtifactEdit } from '../../types/artifact.types';
|
|
4
11
|
import {
|
|
5
12
|
ChevronRight,
|
|
6
13
|
ChevronDown,
|
|
@@ -18,8 +25,9 @@ import MarkdownIcon from '../../../../icons/artifacts/MarkdownIcon';
|
|
|
18
25
|
import PythonIcon from '../../../../icons/artifacts/PythonIcon';
|
|
19
26
|
import SvgIcon from '../../../../icons/artifacts/SvgIcon';
|
|
20
27
|
import XmlIcon from '../../../../icons/artifacts/XmlIcon';
|
|
28
|
+
import { parseEdits } from '../../utils/applyEdits';
|
|
29
|
+
import { useTranslation } from 'react-i18next';
|
|
21
30
|
|
|
22
|
-
// Event type for artifact creation
|
|
23
31
|
type ArtifactCreatedEvent = CustomEvent<{
|
|
24
32
|
artifact: ArtifactData;
|
|
25
33
|
message: Message;
|
|
@@ -30,6 +38,16 @@ interface ArtifactHandlerProps {
|
|
|
30
38
|
message: Message;
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
interface DetectedUpdate {
|
|
42
|
+
artifactId: string;
|
|
43
|
+
edits: ArtifactEdit[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface FailedUpdate {
|
|
47
|
+
artifactId: string;
|
|
48
|
+
reason: 'not-found' | 'no-match' | 'invalid';
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
// ---------------------------------------------------------------------------
|
|
34
52
|
// Helpers
|
|
35
53
|
// ---------------------------------------------------------------------------
|
|
@@ -42,48 +60,72 @@ const formatBytes = (bytes: number): string => {
|
|
|
42
60
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
43
61
|
};
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
63
|
+
const CLASS_ATTR_RE = /class\s*=\s*["']([^"']*)["']/i;
|
|
64
|
+
const DATA_ACTION_RE = /data-action\s*=\s*["']([^"']+)["']/i;
|
|
65
|
+
|
|
66
|
+
const getClassList = (openingTag: string): string[] => {
|
|
67
|
+
const m = openingTag.match(CLASS_ATTR_RE);
|
|
68
|
+
if (!m) return [];
|
|
69
|
+
return m[1].split(/\s+/).filter(Boolean);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getDataAction = (openingTag: string): string | null => {
|
|
73
|
+
const m = openingTag.match(DATA_ACTION_RE);
|
|
74
|
+
return m?.[1]?.trim().toLowerCase() || null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** True for create tags: class memori-artifact, action create/absent (not update). */
|
|
78
|
+
const isCreateArtifactTag = (openingTag: string): boolean => {
|
|
79
|
+
const classes = getClassList(openingTag);
|
|
80
|
+
if (classes.includes('memori-artifact-update')) return false;
|
|
81
|
+
if (!classes.includes('memori-artifact')) return false;
|
|
82
|
+
const action = getDataAction(openingTag);
|
|
83
|
+
return action === null || action === 'create';
|
|
57
84
|
};
|
|
58
85
|
|
|
59
86
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* Examples that match:
|
|
64
|
-
* class="memori-artifact"
|
|
65
|
-
* class="memori-artifact extra-class"
|
|
66
|
-
* class="foo memori-artifact"
|
|
67
|
-
* class='memori-artifact'
|
|
68
|
-
* class = "memori-artifact extra"
|
|
87
|
+
* True for update tags:
|
|
88
|
+
* - preferred: class="memori-artifact" data-action="update"
|
|
89
|
+
* - legacy: class="memori-artifact-update"
|
|
69
90
|
*/
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
if (!
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
const isUpdateArtifactTag = (openingTag: string): boolean => {
|
|
92
|
+
const classes = getClassList(openingTag);
|
|
93
|
+
if (classes.includes('memori-artifact-update')) return true;
|
|
94
|
+
if (!classes.includes('memori-artifact')) return false;
|
|
95
|
+
return getDataAction(openingTag) === 'update';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const findMatchingOutputClose = (cleaned: string, openEnd: number): number => {
|
|
99
|
+
let depth = 1;
|
|
100
|
+
let pos = openEnd;
|
|
101
|
+
let closeStart = -1;
|
|
102
|
+
|
|
103
|
+
while (pos < cleaned.length && depth > 0) {
|
|
104
|
+
const nextOpen = cleaned.indexOf('<output', pos);
|
|
105
|
+
const nextClose = cleaned.indexOf('</output>', pos);
|
|
106
|
+
|
|
107
|
+
if (nextClose === -1) {
|
|
108
|
+
return cleaned.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
112
|
+
depth++;
|
|
113
|
+
pos = nextOpen + '<output'.length;
|
|
114
|
+
} else {
|
|
115
|
+
depth--;
|
|
116
|
+
if (depth === 0) {
|
|
117
|
+
closeStart = nextClose;
|
|
118
|
+
} else {
|
|
119
|
+
pos = nextClose + '</output>'.length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return closeStart;
|
|
76
125
|
};
|
|
77
126
|
|
|
78
127
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* Replaces the previous single-regex approach with a depth-tracking loop so
|
|
82
|
-
* that:
|
|
83
|
-
* - Multiple CSS classes are handled correctly (e.g. class="memori-artifact foo")
|
|
84
|
-
* - Nested / self-contained <output> tags inside the content don't truncate it
|
|
85
|
-
* - Unclosed tags are gracefully skipped
|
|
86
|
-
* - IDs are stable across re-renders (derived from message + content hash)
|
|
128
|
+
* Detect create artifacts (class="memori-artifact", data-action create/absent).
|
|
87
129
|
*/
|
|
88
130
|
const detectArtifacts = (
|
|
89
131
|
text: string,
|
|
@@ -97,66 +139,29 @@ const detectArtifacts = (
|
|
|
97
139
|
let searchFrom = 0;
|
|
98
140
|
let artifactNum = 0;
|
|
99
141
|
|
|
100
|
-
// Regex that matches any <output …> opening tag (capturing the full tag)
|
|
101
142
|
const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
|
|
102
143
|
|
|
103
144
|
while (searchFrom < cleaned.length) {
|
|
104
|
-
// Find the next <output …> opening tag from the current position
|
|
105
145
|
OPEN_TAG_RE.lastIndex = searchFrom;
|
|
106
146
|
const openMatch = OPEN_TAG_RE.exec(cleaned);
|
|
107
147
|
if (!openMatch) break;
|
|
108
148
|
|
|
109
|
-
const fullOpenTag = openMatch[0];
|
|
110
|
-
const
|
|
111
|
-
const openEnd = openStart + fullOpenTag.length;
|
|
149
|
+
const fullOpenTag = openMatch[0];
|
|
150
|
+
const openEnd = openMatch.index + fullOpenTag.length;
|
|
112
151
|
|
|
113
|
-
|
|
114
|
-
if (!hasMemoriArtifactClass(fullOpenTag)) {
|
|
152
|
+
if (!isCreateArtifactTag(fullOpenTag)) {
|
|
115
153
|
searchFrom = openEnd;
|
|
116
154
|
continue;
|
|
117
155
|
}
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
let depth = 1;
|
|
121
|
-
let pos = openEnd;
|
|
122
|
-
let closeStart = -1;
|
|
123
|
-
|
|
124
|
-
while (pos < cleaned.length && depth > 0) {
|
|
125
|
-
const nextOpen = cleaned.indexOf('<output', pos);
|
|
126
|
-
const nextClose = cleaned.indexOf('</output>', pos);
|
|
127
|
-
|
|
128
|
-
if (nextClose === -1) {
|
|
129
|
-
// No closing tag found — treat everything until EOF as content
|
|
130
|
-
// (handles streaming / partial messages)
|
|
131
|
-
closeStart = cleaned.length;
|
|
132
|
-
depth = 0;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
137
|
-
// There's another opening tag before the next closing tag → go deeper
|
|
138
|
-
depth++;
|
|
139
|
-
pos = nextOpen + '<output'.length;
|
|
140
|
-
} else {
|
|
141
|
-
// Found a closing tag
|
|
142
|
-
depth--;
|
|
143
|
-
if (depth === 0) {
|
|
144
|
-
closeStart = nextClose;
|
|
145
|
-
} else {
|
|
146
|
-
pos = nextClose + '</output>'.length;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
157
|
+
const closeStart = findMatchingOutputClose(cleaned, openEnd);
|
|
151
158
|
if (closeStart === -1) {
|
|
152
|
-
// Malformed — skip past this opening tag and continue
|
|
153
159
|
searchFrom = openEnd;
|
|
154
160
|
continue;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
const content = cleaned.slice(openEnd, closeStart).trim();
|
|
158
164
|
|
|
159
|
-
// Extract metadata from the opening tag
|
|
160
165
|
const mimeTypeMatch = fullOpenTag.match(
|
|
161
166
|
/data-mimetype\s*=\s*["']([^"']+)["']/i
|
|
162
167
|
);
|
|
@@ -173,13 +178,16 @@ const detectArtifacts = (
|
|
|
173
178
|
|
|
174
179
|
artifactNum++;
|
|
175
180
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
const dataArtifactIdMatch = fullOpenTag.match(
|
|
182
|
+
/data-artifact-id\s*=\s*["']([^"']+)["']/i
|
|
183
|
+
);
|
|
184
|
+
const artifactId =
|
|
185
|
+
dataArtifactIdMatch?.[1]?.trim() ||
|
|
186
|
+
`artifact-${messageKey}-${artifactNum}`;
|
|
180
187
|
|
|
181
188
|
artifacts.push({
|
|
182
|
-
id:
|
|
189
|
+
id: `artifact-ui-${artifactId}`,
|
|
190
|
+
artifactId,
|
|
183
191
|
content,
|
|
184
192
|
mimeType,
|
|
185
193
|
title,
|
|
@@ -187,7 +195,6 @@ const detectArtifacts = (
|
|
|
187
195
|
size: content.length,
|
|
188
196
|
});
|
|
189
197
|
|
|
190
|
-
// Advance past the closing tag (or EOF)
|
|
191
198
|
searchFrom =
|
|
192
199
|
closeStart === cleaned.length
|
|
193
200
|
? cleaned.length
|
|
@@ -197,6 +204,55 @@ const detectArtifacts = (
|
|
|
197
204
|
return artifacts;
|
|
198
205
|
};
|
|
199
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Detect update tags and parse their JSON edit bodies.
|
|
209
|
+
*/
|
|
210
|
+
const detectUpdates = (text: string, isFromUser: boolean): DetectedUpdate[] => {
|
|
211
|
+
if (!text || isFromUser) return [];
|
|
212
|
+
|
|
213
|
+
const cleaned = stripReasoningTags(text);
|
|
214
|
+
const updates: DetectedUpdate[] = [];
|
|
215
|
+
let searchFrom = 0;
|
|
216
|
+
|
|
217
|
+
const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
|
|
218
|
+
|
|
219
|
+
while (searchFrom < cleaned.length) {
|
|
220
|
+
OPEN_TAG_RE.lastIndex = searchFrom;
|
|
221
|
+
const openMatch = OPEN_TAG_RE.exec(cleaned);
|
|
222
|
+
if (!openMatch) break;
|
|
223
|
+
|
|
224
|
+
const fullOpenTag = openMatch[0];
|
|
225
|
+
const openEnd = openMatch.index + fullOpenTag.length;
|
|
226
|
+
|
|
227
|
+
if (!isUpdateArtifactTag(fullOpenTag)) {
|
|
228
|
+
searchFrom = openEnd;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const closeStart = cleaned.indexOf('</output>', openEnd);
|
|
233
|
+
const bodyEnd = closeStart === -1 ? cleaned.length : closeStart;
|
|
234
|
+
const body = cleaned.slice(openEnd, bodyEnd).trim();
|
|
235
|
+
|
|
236
|
+
const idMatch = fullOpenTag.match(
|
|
237
|
+
/data-artifact-id\s*=\s*["']([^"']+)["']/i
|
|
238
|
+
);
|
|
239
|
+
const artifactId = idMatch?.[1]?.trim();
|
|
240
|
+
const edits = parseEdits(body);
|
|
241
|
+
|
|
242
|
+
if (artifactId && edits) {
|
|
243
|
+
updates.push({ artifactId, edits });
|
|
244
|
+
} else if (artifactId) {
|
|
245
|
+
// Invalid body — still surface as a failed update for UI feedback
|
|
246
|
+
updates.push({ artifactId, edits: [] });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
searchFrom =
|
|
250
|
+
closeStart === -1 ? cleaned.length : closeStart + '</output>'.length;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return updates;
|
|
254
|
+
};
|
|
255
|
+
|
|
200
256
|
// ---------------------------------------------------------------------------
|
|
201
257
|
// Component
|
|
202
258
|
// ---------------------------------------------------------------------------
|
|
@@ -205,22 +261,21 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
205
261
|
isChatlogPanel = false,
|
|
206
262
|
message,
|
|
207
263
|
}) => {
|
|
208
|
-
const {
|
|
264
|
+
const {
|
|
265
|
+
openArtifact,
|
|
266
|
+
state,
|
|
267
|
+
closeArtifact,
|
|
268
|
+
registerArtifact,
|
|
269
|
+
applyArtifactUpdate,
|
|
270
|
+
} = useArtifact();
|
|
271
|
+
const { t } = useTranslation();
|
|
209
272
|
|
|
210
|
-
/**
|
|
211
|
-
* Use raw text for artifact detection.
|
|
212
|
-
* translatedText may lose <output> tags — keep it as fallback only.
|
|
213
|
-
*/
|
|
214
273
|
const messageText = useMemo(() => message.text || '', [message.text]);
|
|
215
274
|
const translatedMessageText = useMemo(
|
|
216
275
|
() => message.translatedText || '',
|
|
217
276
|
[message.translatedText]
|
|
218
277
|
);
|
|
219
278
|
|
|
220
|
-
/**
|
|
221
|
-
* Stable key that identifies this specific message.
|
|
222
|
-
* Used both for effect gating and for stable artifact ID generation.
|
|
223
|
-
*/
|
|
224
279
|
const messageKey = useMemo(
|
|
225
280
|
() => `${message.timestamp}-${message.fromUser ? '1' : '0'}`,
|
|
226
281
|
[message.timestamp, message.fromUser]
|
|
@@ -240,11 +295,6 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
240
295
|
[message]
|
|
241
296
|
);
|
|
242
297
|
|
|
243
|
-
/**
|
|
244
|
-
* Memoised artifact list.
|
|
245
|
-
* Falls back to translatedText only when the primary text yields nothing.
|
|
246
|
-
* Both branches use the same messageKey so IDs remain stable.
|
|
247
|
-
*/
|
|
248
298
|
const artifacts = useMemo<ArtifactData[]>(() => {
|
|
249
299
|
const fromUser = message.fromUser || false;
|
|
250
300
|
const primary = detectArtifacts(messageText, fromUser, messageKey);
|
|
@@ -252,29 +302,95 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
252
302
|
return detectArtifacts(translatedMessageText, fromUser, messageKey);
|
|
253
303
|
}, [messageText, translatedMessageText, message.fromUser, messageKey]);
|
|
254
304
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
305
|
+
const detectedUpdates = useMemo<DetectedUpdate[]>(() => {
|
|
306
|
+
const fromUser = message.fromUser || false;
|
|
307
|
+
const primary = detectUpdates(messageText, fromUser);
|
|
308
|
+
if (primary.length > 0) return primary;
|
|
309
|
+
return detectUpdates(translatedMessageText, fromUser);
|
|
310
|
+
}, [messageText, translatedMessageText, message.fromUser]);
|
|
311
|
+
|
|
312
|
+
const [updateResultMap, setUpdateResultMap] = useState<
|
|
313
|
+
Record<number, ArtifactData>
|
|
314
|
+
>({});
|
|
315
|
+
const [failedUpdates, setFailedUpdates] = useState<FailedUpdate[]>([]);
|
|
316
|
+
|
|
317
|
+
const processedUpdatesRef = useRef<string>('');
|
|
318
|
+
const hasAutoOpenedRef = useRef<string>('');
|
|
319
|
+
|
|
262
320
|
useEffect(() => {
|
|
263
|
-
if (!messageText
|
|
321
|
+
if (!messageText && !translatedMessageText) return;
|
|
264
322
|
|
|
265
|
-
artifacts.forEach(artifact =>
|
|
323
|
+
artifacts.forEach(artifact => {
|
|
324
|
+
registerArtifact(artifact);
|
|
325
|
+
dispatchArtifactCreatedEvent(artifact);
|
|
326
|
+
});
|
|
266
327
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
328
|
+
const updatesSignature = `${messageKey}:${detectedUpdates
|
|
329
|
+
.map(u => `${u.artifactId}:${JSON.stringify(u.edits)}`)
|
|
330
|
+
.join('|')}`;
|
|
331
|
+
|
|
332
|
+
let lastUpdated: ArtifactData | null = null;
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
detectedUpdates.length > 0 &&
|
|
336
|
+
processedUpdatesRef.current !== updatesSignature
|
|
337
|
+
) {
|
|
338
|
+
processedUpdatesRef.current = updatesSignature;
|
|
339
|
+
const nextMap: Record<number, ArtifactData> = {};
|
|
340
|
+
const nextFailed: FailedUpdate[] = [];
|
|
341
|
+
|
|
342
|
+
detectedUpdates.forEach((update, idx) => {
|
|
343
|
+
if (!update.edits.length) {
|
|
344
|
+
nextFailed.push({
|
|
345
|
+
artifactId: update.artifactId,
|
|
346
|
+
reason: 'invalid',
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const result = applyArtifactUpdate(update.artifactId, update.edits);
|
|
352
|
+
if (result.updatedArtifact) {
|
|
353
|
+
nextMap[idx] = result.updatedArtifact;
|
|
354
|
+
lastUpdated = result.updatedArtifact;
|
|
355
|
+
dispatchArtifactCreatedEvent(result.updatedArtifact);
|
|
356
|
+
} else {
|
|
357
|
+
nextFailed.push({
|
|
358
|
+
artifactId: update.artifactId,
|
|
359
|
+
reason: result.failureReason ?? 'no-match',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
setUpdateResultMap(nextMap);
|
|
365
|
+
setFailedUpdates(nextFailed);
|
|
273
366
|
}
|
|
367
|
+
|
|
368
|
+
if (isChatlogPanel) return;
|
|
369
|
+
|
|
370
|
+
const openTarget =
|
|
371
|
+
lastUpdated ??
|
|
372
|
+
(Object.keys(updateResultMap).length === 0 && artifacts.length > 0
|
|
373
|
+
? artifacts[0]
|
|
374
|
+
: null);
|
|
375
|
+
if (!openTarget) return;
|
|
376
|
+
|
|
377
|
+
if (hasAutoOpenedRef.current === messageKey) return;
|
|
378
|
+
hasAutoOpenedRef.current = messageKey;
|
|
379
|
+
|
|
380
|
+
const timer = setTimeout(
|
|
381
|
+
() => openArtifact(openTarget, { isChatLogPanel: false }),
|
|
382
|
+
100
|
|
383
|
+
);
|
|
384
|
+
return () => clearTimeout(timer);
|
|
274
385
|
}, [
|
|
275
386
|
messageKey,
|
|
276
387
|
messageText,
|
|
388
|
+
translatedMessageText,
|
|
277
389
|
artifacts,
|
|
390
|
+
detectedUpdates,
|
|
391
|
+
updateResultMap,
|
|
392
|
+
registerArtifact,
|
|
393
|
+
applyArtifactUpdate,
|
|
278
394
|
dispatchArtifactCreatedEvent,
|
|
279
395
|
isChatlogPanel,
|
|
280
396
|
openArtifact,
|
|
@@ -288,25 +404,66 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
288
404
|
openArtifact(artifact, { isChatLogPanel: isChatlogPanel });
|
|
289
405
|
}
|
|
290
406
|
},
|
|
291
|
-
[
|
|
407
|
+
[
|
|
408
|
+
state.isDrawerOpen,
|
|
409
|
+
state.currentArtifact?.id,
|
|
410
|
+
closeArtifact,
|
|
411
|
+
openArtifact,
|
|
412
|
+
isChatlogPanel,
|
|
413
|
+
]
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const getIconForMimeType = useCallback(
|
|
417
|
+
(mimeType: string): React.ReactElement => {
|
|
418
|
+
const m = mimeType.toLowerCase();
|
|
419
|
+
const iconProps = { 'aria-hidden': true as const };
|
|
420
|
+
if (m.includes('html')) return <HtmlIcon {...iconProps} />;
|
|
421
|
+
if (m.includes('markdown')) return <MarkdownIcon {...iconProps} />;
|
|
422
|
+
if (m.includes('javascript') || m.includes('typescript'))
|
|
423
|
+
return <JavascriptIcon {...iconProps} />;
|
|
424
|
+
if (m.includes('python')) return <PythonIcon {...iconProps} />;
|
|
425
|
+
if (m.includes('json')) return <DocumentIcon {...iconProps} />;
|
|
426
|
+
if (m.includes('css')) return <CssIcon {...iconProps} />;
|
|
427
|
+
if (m.includes('xml')) return <XmlIcon {...iconProps} />;
|
|
428
|
+
if (m.includes('svg')) return <SvgIcon {...iconProps} />;
|
|
429
|
+
return <DocumentIcon {...iconProps} />;
|
|
430
|
+
},
|
|
431
|
+
[]
|
|
292
432
|
);
|
|
293
433
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
434
|
+
const updatedLabel = t('artifact.updated') || 'updated';
|
|
435
|
+
const updateFailedLabel =
|
|
436
|
+
t('artifact.updateFailed') || 'Update could not be applied';
|
|
437
|
+
const updateFailedHint =
|
|
438
|
+
t('artifact.updateFailedHint') ||
|
|
439
|
+
'The patch did not match the current content. Ask to rewrite the artifact.';
|
|
440
|
+
|
|
441
|
+
const updateEntries = Object.entries(updateResultMap);
|
|
442
|
+
const hasCreates = artifacts.length > 0;
|
|
443
|
+
const hasUpdates = updateEntries.length > 0;
|
|
444
|
+
const hasFailed = failedUpdates.length > 0;
|
|
445
|
+
|
|
446
|
+
if (!hasCreates && !hasUpdates && !hasFailed) return null;
|
|
447
|
+
|
|
448
|
+
const renderChevron = (isSelected: boolean) => {
|
|
449
|
+
if (isChatlogPanel) {
|
|
450
|
+
return isSelected ? (
|
|
451
|
+
<ChevronUp className="memori-artifact-handler-action-icon" />
|
|
452
|
+
) : (
|
|
453
|
+
<ChevronDown className="memori-artifact-handler-action-icon" />
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return isSelected ? (
|
|
457
|
+
<ChevronLeft className="memori-artifact-handler-action-icon" />
|
|
458
|
+
) : (
|
|
459
|
+
<ChevronRight className="memori-artifact-handler-action-icon" />
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const renderInlineDrawer = (isSelected: boolean) =>
|
|
464
|
+
isChatlogPanel && isSelected ? (
|
|
465
|
+
<ArtifactDrawer isChatLogPanel={true} />
|
|
466
|
+
) : null;
|
|
310
467
|
|
|
311
468
|
return (
|
|
312
469
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
@@ -345,34 +502,86 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
345
502
|
</div>
|
|
346
503
|
</div>
|
|
347
504
|
<div className="memori-artifact-handler-action">
|
|
348
|
-
{
|
|
349
|
-
state.isDrawerOpen &&
|
|
350
|
-
state.currentArtifact?.id === artifact.id ? (
|
|
351
|
-
<ChevronUp className="memori-artifact-handler-action-icon" />
|
|
352
|
-
) : (
|
|
353
|
-
<ChevronDown className="memori-artifact-handler-action-icon" />
|
|
354
|
-
)
|
|
355
|
-
) : state.isDrawerOpen &&
|
|
356
|
-
state.currentArtifact?.id === artifact.id ? (
|
|
357
|
-
<ChevronLeft className="memori-artifact-handler-action-icon" />
|
|
358
|
-
) : (
|
|
359
|
-
<ChevronRight className="memori-artifact-handler-action-icon" />
|
|
360
|
-
)}
|
|
505
|
+
{renderChevron(isSelected)}
|
|
361
506
|
</div>
|
|
362
507
|
</button>
|
|
363
508
|
|
|
364
509
|
{/* Render inline drawer only for chatlog-panel mode.
|
|
365
510
|
In layout-column mode the drawer is mounted by the page layout. */}
|
|
366
|
-
{
|
|
367
|
-
state.currentArtifact?.id === artifact.id && (
|
|
368
|
-
<ArtifactDrawer isChatLogPanel={true} />
|
|
369
|
-
)}
|
|
511
|
+
{renderInlineDrawer(isSelected)}
|
|
370
512
|
</React.Fragment>
|
|
371
513
|
);
|
|
372
514
|
})}
|
|
515
|
+
|
|
516
|
+
{updateEntries.map(([, artifact]) => {
|
|
517
|
+
const isSelected =
|
|
518
|
+
state.isDrawerOpen && state.currentArtifact?.id === artifact.id;
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<React.Fragment key={artifact.id}>
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
className={`memori-artifact-handler${
|
|
525
|
+
isSelected ? ' memori-artifact-handler--selected' : ''
|
|
526
|
+
}`}
|
|
527
|
+
onClick={() => handleArtifactClick(artifact)}
|
|
528
|
+
aria-pressed={isSelected}
|
|
529
|
+
aria-label={`${artifact.title} — ${updatedLabel}`}
|
|
530
|
+
style={
|
|
531
|
+
isSelected
|
|
532
|
+
? {
|
|
533
|
+
border: '2px solid var(--memori-border-primary)',
|
|
534
|
+
}
|
|
535
|
+
: undefined
|
|
536
|
+
}
|
|
537
|
+
>
|
|
538
|
+
<div className="memori-artifact-handler-icon">
|
|
539
|
+
{getIconForMimeType(artifact.mimeType)}
|
|
540
|
+
</div>
|
|
541
|
+
<div className="memori-artifact-handler-info">
|
|
542
|
+
<div className="memori-artifact-handler-title">
|
|
543
|
+
{artifact.title} — {updatedLabel}
|
|
544
|
+
</div>
|
|
545
|
+
<div className="memori-artifact-handler-meta">
|
|
546
|
+
{artifact.mimeType} • {formatBytes(artifact.size || 0)}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
<div className="memori-artifact-handler-action">
|
|
550
|
+
{renderChevron(isSelected)}
|
|
551
|
+
</div>
|
|
552
|
+
</button>
|
|
553
|
+
|
|
554
|
+
{renderInlineDrawer(isSelected)}
|
|
555
|
+
</React.Fragment>
|
|
556
|
+
);
|
|
557
|
+
})}
|
|
558
|
+
|
|
559
|
+
{failedUpdates.map((failed, idx) => (
|
|
560
|
+
<div
|
|
561
|
+
key={`failed-${failed.artifactId}-${idx}`}
|
|
562
|
+
className="memori-artifact-handler memori-artifact-handler--failed"
|
|
563
|
+
style={{
|
|
564
|
+
opacity: 0.85,
|
|
565
|
+
border: '1px dashed var(--memori-danger, #ef4444)',
|
|
566
|
+
cursor: 'default',
|
|
567
|
+
}}
|
|
568
|
+
role="status"
|
|
569
|
+
>
|
|
570
|
+
<div className="memori-artifact-handler-icon">⚠️</div>
|
|
571
|
+
<div className="memori-artifact-handler-info">
|
|
572
|
+
<div className="memori-artifact-handler-title">
|
|
573
|
+
{updateFailedLabel}
|
|
574
|
+
</div>
|
|
575
|
+
<div className="memori-artifact-handler-meta">
|
|
576
|
+
{failed.artifactId} • {updateFailedHint}
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
))}
|
|
373
581
|
</div>
|
|
374
582
|
);
|
|
375
583
|
};
|
|
584
|
+
|
|
376
585
|
// ---------------------------------------------------------------------------
|
|
377
586
|
// Memoised export
|
|
378
587
|
// ---------------------------------------------------------------------------
|