@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,5 +1,19 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useCallback,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import {
|
|
11
|
+
ArtifactData,
|
|
12
|
+
ArtifactEdit,
|
|
13
|
+
ArtifactSystemState,
|
|
14
|
+
ApplyUpdateResult,
|
|
15
|
+
} from '../types/artifact.types';
|
|
16
|
+
import { applyEdits } from '../utils/applyEdits';
|
|
3
17
|
|
|
4
18
|
export type OpenArtifactOptions = {
|
|
5
19
|
/** Inline expansion in chat; layouts should not hide avatar / use side-drawer chrome. */
|
|
@@ -11,6 +25,11 @@ interface ArtifactContextType {
|
|
|
11
25
|
openArtifact: (artifact: ArtifactData, options?: OpenArtifactOptions) => void;
|
|
12
26
|
closeArtifact: () => void;
|
|
13
27
|
toggleFullscreen: () => void;
|
|
28
|
+
registerArtifact: (artifact: ArtifactData) => void;
|
|
29
|
+
applyArtifactUpdate: (
|
|
30
|
+
artifactId: string,
|
|
31
|
+
edits: ArtifactEdit[]
|
|
32
|
+
) => ApplyUpdateResult;
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
const ArtifactContext = createContext<ArtifactContextType | null>(null);
|
|
@@ -23,6 +42,11 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
|
|
|
23
42
|
isChatLogPanelPresentation: false,
|
|
24
43
|
});
|
|
25
44
|
|
|
45
|
+
// Synchronous registry keyed by stable artifactId → latest version.
|
|
46
|
+
// useRef avoids race where update handlers read empty React state
|
|
47
|
+
// right after a create in the same render cycle.
|
|
48
|
+
const registryRef = useRef<Record<string, ArtifactData>>({});
|
|
49
|
+
|
|
26
50
|
const openArtifact = useCallback(
|
|
27
51
|
(artifact: ArtifactData, options?: OpenArtifactOptions) => {
|
|
28
52
|
setState(() => {
|
|
@@ -39,7 +63,6 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
|
|
|
39
63
|
|
|
40
64
|
const closeArtifact = useCallback(() => {
|
|
41
65
|
setState(prev => {
|
|
42
|
-
// if (!prev.isDrawerOpen) return prev;
|
|
43
66
|
return {
|
|
44
67
|
...prev,
|
|
45
68
|
currentArtifact: null,
|
|
@@ -57,12 +80,91 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
|
|
|
57
80
|
}));
|
|
58
81
|
}, []);
|
|
59
82
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
const registerArtifact = useCallback((artifact: ArtifactData) => {
|
|
84
|
+
if (!artifact?.artifactId) return;
|
|
85
|
+
registryRef.current[artifact.artifactId] = artifact;
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const applyArtifactUpdate = useCallback(
|
|
89
|
+
(artifactId: string, edits: ArtifactEdit[]): ApplyUpdateResult => {
|
|
90
|
+
const current = registryRef.current[artifactId];
|
|
91
|
+
if (!current) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
failedEdits: edits,
|
|
95
|
+
updatedArtifact: null,
|
|
96
|
+
failureReason: 'not-found',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { content, failedEdits, appliedCount } = applyEdits(
|
|
101
|
+
current.content,
|
|
102
|
+
edits
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (appliedCount === 0) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
failedEdits,
|
|
109
|
+
updatedArtifact: null,
|
|
110
|
+
failureReason: 'no-match',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const updatedArtifact: ArtifactData = {
|
|
115
|
+
id: `artifact-ui-${artifactId}-${Date.now()}-${Math.random()
|
|
116
|
+
.toString(36)
|
|
117
|
+
.slice(2, 8)}`,
|
|
118
|
+
artifactId,
|
|
119
|
+
content,
|
|
120
|
+
mimeType: current.mimeType,
|
|
121
|
+
title: current.title,
|
|
122
|
+
timestamp: new Date(),
|
|
123
|
+
size: content.length,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
registryRef.current[artifactId] = updatedArtifact;
|
|
127
|
+
|
|
128
|
+
setState(prev => {
|
|
129
|
+
if (
|
|
130
|
+
prev.isDrawerOpen &&
|
|
131
|
+
prev.currentArtifact?.artifactId === artifactId
|
|
132
|
+
) {
|
|
133
|
+
return {
|
|
134
|
+
...prev,
|
|
135
|
+
currentArtifact: updatedArtifact,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return prev;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success: failedEdits.length === 0,
|
|
143
|
+
failedEdits,
|
|
144
|
+
updatedArtifact,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
[]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const contextValue = useMemo(
|
|
151
|
+
() => ({
|
|
152
|
+
state,
|
|
153
|
+
openArtifact,
|
|
154
|
+
closeArtifact,
|
|
155
|
+
toggleFullscreen,
|
|
156
|
+
registerArtifact,
|
|
157
|
+
applyArtifactUpdate,
|
|
158
|
+
}),
|
|
159
|
+
[
|
|
160
|
+
state,
|
|
161
|
+
openArtifact,
|
|
162
|
+
closeArtifact,
|
|
163
|
+
toggleFullscreen,
|
|
164
|
+
registerArtifact,
|
|
165
|
+
applyArtifactUpdate,
|
|
166
|
+
]
|
|
167
|
+
);
|
|
66
168
|
|
|
67
169
|
return (
|
|
68
170
|
<ArtifactContext.Provider value={contextValue}>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export interface ArtifactData {
|
|
2
|
+
/** Unique UI identifier for this version instance */
|
|
2
3
|
id: string;
|
|
4
|
+
/** Stable lineage id (from data-artifact-id); constant across versions */
|
|
5
|
+
artifactId: string;
|
|
3
6
|
content: string;
|
|
4
7
|
mimeType: string;
|
|
5
8
|
title: string;
|
|
@@ -7,6 +10,22 @@ export interface ArtifactData {
|
|
|
7
10
|
size: number;
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
export interface ArtifactEdit {
|
|
14
|
+
/** Exact string to find and replace (first occurrence) */
|
|
15
|
+
old: string;
|
|
16
|
+
/** Replacement string */
|
|
17
|
+
new: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ApplyUpdateResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
failedEdits: ArtifactEdit[];
|
|
23
|
+
updatedArtifact: ArtifactData | null;
|
|
24
|
+
/** Why the update failed when updatedArtifact is null */
|
|
25
|
+
failureReason?: 'not-found' | 'no-match';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
10
29
|
export interface ArtifactSystemState {
|
|
11
30
|
currentArtifact: ArtifactData | null;
|
|
12
31
|
isDrawerOpen: boolean;
|
|
@@ -15,4 +34,4 @@ export interface ArtifactSystemState {
|
|
|
15
34
|
isChatLogPanelPresentation: boolean;
|
|
16
35
|
}
|
|
17
36
|
|
|
18
|
-
export type ArtifactTab = 'code' | 'preview';
|
|
37
|
+
export type ArtifactTab = 'code' | 'preview';
|
|
@@ -83,7 +83,7 @@ export const ArtifactAPIBridge = ({
|
|
|
83
83
|
}: {
|
|
84
84
|
pushMessage: (message: Message) => void;
|
|
85
85
|
}) => {
|
|
86
|
-
const { openArtifact, closeArtifact, toggleFullscreen, state } =
|
|
86
|
+
const { openArtifact, closeArtifact, toggleFullscreen, state, registerArtifact } =
|
|
87
87
|
useArtifact();
|
|
88
88
|
|
|
89
89
|
const apiRef = useRef<any>(null);
|
|
@@ -115,12 +115,14 @@ export const ArtifactAPIBridge = ({
|
|
|
115
115
|
title?: string
|
|
116
116
|
) => {
|
|
117
117
|
const autoTitle = title || getTitleFromMimeType(mimeType);
|
|
118
|
+
const artifactId = `api-${Date.now()}-${Math.random()
|
|
119
|
+
.toString(36)
|
|
120
|
+
.substr(2, 9)}`;
|
|
118
121
|
|
|
119
122
|
// Create the artifact object
|
|
120
123
|
const artifact: ArtifactData = {
|
|
121
|
-
id: `artifact-${
|
|
122
|
-
|
|
123
|
-
.substr(2, 9)}`,
|
|
124
|
+
id: `artifact-ui-${artifactId}`,
|
|
125
|
+
artifactId,
|
|
124
126
|
content,
|
|
125
127
|
mimeType,
|
|
126
128
|
title: autoTitle,
|
|
@@ -131,8 +133,8 @@ export const ArtifactAPIBridge = ({
|
|
|
131
133
|
// Create wrapped message text for artifact detection
|
|
132
134
|
// Don't reassign content parameter - use a new variable
|
|
133
135
|
let messageText = content;
|
|
134
|
-
if (!messageText.includes('
|
|
135
|
-
messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
|
|
136
|
+
if (!messageText.includes('class="memori-artifact"')) {
|
|
137
|
+
messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
//we have to push in the history the artifact as message
|
|
@@ -153,6 +155,7 @@ export const ArtifactAPIBridge = ({
|
|
|
153
155
|
placeUncertaintyKm: undefined,
|
|
154
156
|
});
|
|
155
157
|
|
|
158
|
+
registerArtifact(artifact);
|
|
156
159
|
// Open the artifact immediately
|
|
157
160
|
openArtifact(artifact);
|
|
158
161
|
},
|
|
@@ -169,15 +172,17 @@ export const ArtifactAPIBridge = ({
|
|
|
169
172
|
const title =
|
|
170
173
|
outputElement.getAttribute('data-title') ||
|
|
171
174
|
getTitleFromMimeType(mimeType);
|
|
175
|
+
const artifactId =
|
|
176
|
+
outputElement.getAttribute('data-artifact-id') ||
|
|
177
|
+
`api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
172
178
|
|
|
173
179
|
// Nascondi l'elemento originale
|
|
174
180
|
outputElement.style.display = 'none';
|
|
175
181
|
outputElement.setAttribute('data-memori-processed', 'true');
|
|
176
182
|
|
|
177
183
|
const artifact: ArtifactData = {
|
|
178
|
-
id: `artifact-${
|
|
179
|
-
|
|
180
|
-
.substr(2, 9)}`,
|
|
184
|
+
id: `artifact-ui-${artifactId}`,
|
|
185
|
+
artifactId,
|
|
181
186
|
content,
|
|
182
187
|
mimeType,
|
|
183
188
|
title,
|
|
@@ -188,8 +193,8 @@ export const ArtifactAPIBridge = ({
|
|
|
188
193
|
// Create wrapped message text for artifact detection
|
|
189
194
|
// Don't reassign content - use a new variable
|
|
190
195
|
let messageText = content;
|
|
191
|
-
if (!messageText.includes('
|
|
192
|
-
messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
|
|
196
|
+
if (!messageText.includes('class="memori-artifact"')) {
|
|
197
|
+
messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
pushMessage({
|
|
@@ -209,10 +214,11 @@ export const ArtifactAPIBridge = ({
|
|
|
209
214
|
placeUncertaintyKm: undefined,
|
|
210
215
|
});
|
|
211
216
|
|
|
217
|
+
registerArtifact(artifact);
|
|
212
218
|
// Open the artifact immediately
|
|
213
219
|
openArtifact(artifact);
|
|
214
220
|
|
|
215
|
-
return artifact.
|
|
221
|
+
return artifact.artifactId;
|
|
216
222
|
},
|
|
217
223
|
|
|
218
224
|
/**
|
|
@@ -294,7 +300,7 @@ export const ArtifactAPIBridge = ({
|
|
|
294
300
|
delete (window as any).MemoriArtifactAPI;
|
|
295
301
|
}
|
|
296
302
|
};
|
|
297
|
-
}, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage]);
|
|
303
|
+
}, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage, registerArtifact]);
|
|
298
304
|
|
|
299
305
|
return null;
|
|
300
306
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { applyEdits, parseEdits } from './applyEdits';
|
|
2
|
+
|
|
3
|
+
describe('applyEdits', () => {
|
|
4
|
+
it('applies an exact string replacement', () => {
|
|
5
|
+
const result = applyEdits('Hello 🌰 World', [{ old: '🌰', new: '🥜' }]);
|
|
6
|
+
expect(result.content).toBe('Hello 🥜 World');
|
|
7
|
+
expect(result.appliedCount).toBe(1);
|
|
8
|
+
expect(result.failedEdits).toEqual([]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('replaces only the first occurrence', () => {
|
|
12
|
+
const result = applyEdits('aa bb aa', [{ old: 'aa', new: 'XX' }]);
|
|
13
|
+
expect(result.content).toBe('XX bb aa');
|
|
14
|
+
expect(result.appliedCount).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('applies multiple edits sequentially', () => {
|
|
18
|
+
const result = applyEdits('<h1>Title</h1><p>Body</p>', [
|
|
19
|
+
{ old: 'Title', new: 'Heading' },
|
|
20
|
+
{ old: 'Body', new: 'Content' },
|
|
21
|
+
]);
|
|
22
|
+
expect(result.content).toBe('<h1>Heading</h1><p>Content</p>');
|
|
23
|
+
expect(result.appliedCount).toBe(2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('falls back to whitespace-tolerant matching', () => {
|
|
27
|
+
const content = '<span class="x">\n hello world\n</span>';
|
|
28
|
+
const result = applyEdits(content, [
|
|
29
|
+
{ old: 'hello world', new: 'hi there' },
|
|
30
|
+
]);
|
|
31
|
+
expect(result.content).toContain('hi there');
|
|
32
|
+
expect(result.appliedCount).toBe(1);
|
|
33
|
+
expect(result.failedEdits).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('collects failed edits without blocking others', () => {
|
|
37
|
+
const result = applyEdits('alpha beta', [
|
|
38
|
+
{ old: 'missing', new: 'nope' },
|
|
39
|
+
{ old: 'beta', new: 'gamma' },
|
|
40
|
+
]);
|
|
41
|
+
expect(result.content).toBe('alpha gamma');
|
|
42
|
+
expect(result.appliedCount).toBe(1);
|
|
43
|
+
expect(result.failedEdits).toEqual([{ old: 'missing', new: 'nope' }]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns original content when all edits fail', () => {
|
|
47
|
+
const result = applyEdits('unchanged', [
|
|
48
|
+
{ old: 'foo', new: 'bar' },
|
|
49
|
+
]);
|
|
50
|
+
expect(result.content).toBe('unchanged');
|
|
51
|
+
expect(result.appliedCount).toBe(0);
|
|
52
|
+
expect(result.failedEdits).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('parseEdits', () => {
|
|
57
|
+
it('parses a valid JSON edits array', () => {
|
|
58
|
+
const edits = parseEdits('[{"old": "a", "new": "b"}]');
|
|
59
|
+
expect(edits).toEqual([{ old: 'a', new: 'b' }]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('unescapes HTML entities before parsing', () => {
|
|
63
|
+
const edits = parseEdits(
|
|
64
|
+
'[{"old": "🌰", "new": "🥜"}]'
|
|
65
|
+
);
|
|
66
|
+
expect(edits).toEqual([{ old: '🌰', new: '🥜' }]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns null for invalid JSON', () => {
|
|
70
|
+
expect(parseEdits('not json')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns null for non-array JSON', () => {
|
|
74
|
+
expect(parseEdits('{"old":"a","new":"b"}')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns null for array items missing old/new', () => {
|
|
78
|
+
expect(parseEdits('[{"foo":"bar"}]')).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns null for empty body', () => {
|
|
82
|
+
expect(parseEdits('')).toBeNull();
|
|
83
|
+
expect(parseEdits(' ')).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ArtifactEdit } from '../types/artifact.types';
|
|
2
|
+
|
|
3
|
+
export interface ApplyEditsResult {
|
|
4
|
+
content: string;
|
|
5
|
+
failedEdits: ArtifactEdit[];
|
|
6
|
+
appliedCount: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalize runs of whitespace to a single space for tolerant matching.
|
|
11
|
+
*/
|
|
12
|
+
const normalizeWhitespace = (s: string): string => s.replace(/\s+/g, ' ');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find the first occurrence of `needle` in `haystack` after normalizing
|
|
16
|
+
* whitespace in both, then map the match back to original haystack indices.
|
|
17
|
+
* Returns [start, end] in original string coords, or null if not found.
|
|
18
|
+
*/
|
|
19
|
+
const findWhitespaceTolerant = (
|
|
20
|
+
haystack: string,
|
|
21
|
+
needle: string
|
|
22
|
+
): [number, number] | null => {
|
|
23
|
+
const normNeedle = normalizeWhitespace(needle.trim());
|
|
24
|
+
if (!normNeedle) return null;
|
|
25
|
+
|
|
26
|
+
// Build mapping from normalized index → original index
|
|
27
|
+
const normChars: string[] = [];
|
|
28
|
+
const normToOrig: number[] = [];
|
|
29
|
+
let lastWasSpace = false;
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < haystack.length; i++) {
|
|
32
|
+
const ch = haystack[i];
|
|
33
|
+
if (/\s/.test(ch)) {
|
|
34
|
+
if (!lastWasSpace && normChars.length > 0) {
|
|
35
|
+
normChars.push(' ');
|
|
36
|
+
normToOrig.push(i);
|
|
37
|
+
lastWasSpace = true;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
normChars.push(ch);
|
|
41
|
+
normToOrig.push(i);
|
|
42
|
+
lastWasSpace = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Trim trailing space from normalized form
|
|
47
|
+
while (normChars.length > 0 && normChars[normChars.length - 1] === ' ') {
|
|
48
|
+
normChars.pop();
|
|
49
|
+
normToOrig.pop();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const normHaystack = normChars.join('');
|
|
53
|
+
const idx = normHaystack.indexOf(normNeedle);
|
|
54
|
+
if (idx === -1) return null;
|
|
55
|
+
|
|
56
|
+
const startOrig = normToOrig[idx];
|
|
57
|
+
const endNormIdx = idx + normNeedle.length - 1;
|
|
58
|
+
// End is exclusive: one past the last matched original char
|
|
59
|
+
const lastOrig = normToOrig[endNormIdx];
|
|
60
|
+
// Include any trailing whitespace that was collapsed after the last char
|
|
61
|
+
let endOrig = lastOrig + 1;
|
|
62
|
+
while (endOrig < haystack.length && /\s/.test(haystack[endOrig])) {
|
|
63
|
+
// Only consume whitespace that was part of a collapsed run inside the match
|
|
64
|
+
// Stop if we've already covered the normalized span
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [startOrig, endOrig];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Apply a list of string replacements to content.
|
|
73
|
+
* Each edit replaces the first occurrence of `old` with `new`.
|
|
74
|
+
* Exact match is tried first; whitespace-tolerant fallback second.
|
|
75
|
+
* Failed edits are collected without aborting the rest.
|
|
76
|
+
*/
|
|
77
|
+
export const applyEdits = (
|
|
78
|
+
content: string,
|
|
79
|
+
edits: ArtifactEdit[]
|
|
80
|
+
): ApplyEditsResult => {
|
|
81
|
+
let result = content;
|
|
82
|
+
const failedEdits: ArtifactEdit[] = [];
|
|
83
|
+
let appliedCount = 0;
|
|
84
|
+
|
|
85
|
+
for (const edit of edits) {
|
|
86
|
+
if (!edit || typeof edit.old !== 'string' || typeof edit.new !== 'string') {
|
|
87
|
+
failedEdits.push(edit);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const exactIdx = result.indexOf(edit.old);
|
|
92
|
+
if (exactIdx !== -1) {
|
|
93
|
+
result =
|
|
94
|
+
result.slice(0, exactIdx) +
|
|
95
|
+
edit.new +
|
|
96
|
+
result.slice(exactIdx + edit.old.length);
|
|
97
|
+
appliedCount++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const tolerant = findWhitespaceTolerant(result, edit.old);
|
|
102
|
+
if (tolerant) {
|
|
103
|
+
const [start, end] = tolerant;
|
|
104
|
+
result = result.slice(0, start) + edit.new + result.slice(end);
|
|
105
|
+
appliedCount++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
failedEdits.push(edit);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { content: result, failedEdits, appliedCount };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const HTML_ENTITIES: Record<string, string> = {
|
|
116
|
+
'"': '"',
|
|
117
|
+
'"': '"',
|
|
118
|
+
''': "'",
|
|
119
|
+
''': "'",
|
|
120
|
+
'&': '&',
|
|
121
|
+
'<': '<',
|
|
122
|
+
'>': '>',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Unescape common HTML entities that LLMs sometimes emit inside JSON bodies.
|
|
127
|
+
*/
|
|
128
|
+
const unescapeHtmlEntities = (s: string): string =>
|
|
129
|
+
s.replace(
|
|
130
|
+
/"|"|'|'|&|<|>/g,
|
|
131
|
+
m => HTML_ENTITIES[m] ?? m
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const isValidEditsArray = (value: unknown): value is ArtifactEdit[] => {
|
|
135
|
+
if (!Array.isArray(value)) return false;
|
|
136
|
+
return value.every(
|
|
137
|
+
item =>
|
|
138
|
+
item &&
|
|
139
|
+
typeof item === 'object' &&
|
|
140
|
+
typeof (item as ArtifactEdit).old === 'string' &&
|
|
141
|
+
typeof (item as ArtifactEdit).new === 'string'
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse the body of a memori-artifact update tag into an edits array.
|
|
147
|
+
* Handles valid JSON and HTML-entity-encoded JSON. Returns null on failure.
|
|
148
|
+
*/
|
|
149
|
+
export const parseEdits = (body: string): ArtifactEdit[] | null => {
|
|
150
|
+
if (!body || typeof body !== 'string') return null;
|
|
151
|
+
|
|
152
|
+
const trimmed = body.trim();
|
|
153
|
+
if (!trimmed) return null;
|
|
154
|
+
|
|
155
|
+
const tryParse = (raw: string): ArtifactEdit[] | null => {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(raw);
|
|
158
|
+
return isValidEditsArray(parsed) ? parsed : null;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const direct = tryParse(trimmed);
|
|
165
|
+
if (direct) return direct;
|
|
166
|
+
|
|
167
|
+
const unescaped = unescapeHtmlEntities(trimmed);
|
|
168
|
+
if (unescaped !== trimmed) {
|
|
169
|
+
return tryParse(unescaped);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
@@ -164,3 +164,62 @@
|
|
|
164
164
|
.memori.memori--auto-start .memori--start-panel {
|
|
165
165
|
display: none;
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
/* Close buttons (modals, drawers, popovers) — same square bordered chrome
|
|
169
|
+
as header / website-assistant / artifact-drawer IconButton.
|
|
170
|
+
@memori.ai/ui defaults these to borderless / circular. */
|
|
171
|
+
.memori-modal__close,
|
|
172
|
+
.memori-drawer__close,
|
|
173
|
+
.memori-popover__close {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
width: var(--memori-icon-button-size, 40px);
|
|
176
|
+
min-width: var(--memori-icon-button-size, 40px);
|
|
177
|
+
height: var(--memori-icon-button-size, 40px);
|
|
178
|
+
min-height: var(--memori-icon-button-size, 40px);
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
flex-shrink: 0;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
padding: 0;
|
|
184
|
+
border: var(--memori-border, 1px) solid color-mix(in srgb, var(--memori-text-color, #111827) 18%, transparent);
|
|
185
|
+
border-radius: var(--memori-radius-box);
|
|
186
|
+
margin: 0;
|
|
187
|
+
background: transparent;
|
|
188
|
+
box-shadow: none;
|
|
189
|
+
color: var(--memori-icon-button-icon, color-mix(in oklch, var(--memori-text-color, #111827) 72%, transparent));
|
|
190
|
+
transform: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.memori-popover__close {
|
|
194
|
+
block-size: var(--memori-icon-button-size, 40px);
|
|
195
|
+
inline-size: var(--memori-icon-button-size, 40px);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.memori-modal__close:hover,
|
|
199
|
+
.memori-drawer__close:hover,
|
|
200
|
+
.memori-popover__close:hover {
|
|
201
|
+
border-color: color-mix(in srgb, var(--memori-text-color, #111827) 18%, transparent);
|
|
202
|
+
background: color-mix(in srgb, var(--memori-text-color, #111827) 6%, transparent);
|
|
203
|
+
box-shadow: none;
|
|
204
|
+
color: var(--memori-text-color, #111827);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.memori-modal__close:focus-visible,
|
|
208
|
+
.memori-drawer__close:focus-visible,
|
|
209
|
+
.memori-popover__close:focus-visible {
|
|
210
|
+
border-color: var(--memori-primary);
|
|
211
|
+
box-shadow: var(--memori-focus-ring, 0 0 0 3px color-mix(in oklch, var(--memori-primary) 18%, transparent));
|
|
212
|
+
outline: 2px solid var(--memori-primary);
|
|
213
|
+
outline-offset: var(--memori-focus-ring-offset, 2px);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.memori-modal__close svg,
|
|
217
|
+
.memori-drawer__close svg,
|
|
218
|
+
.memori-popover__close svg {
|
|
219
|
+
width: var(--memori-icon-button-icon-size, 20px);
|
|
220
|
+
height: var(--memori-icon-button-icon-size, 20px);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.memori-drawer__header {
|
|
224
|
+
align-items: center;
|
|
225
|
+
}
|
|
@@ -105,36 +105,22 @@ pre[class*='language-'] {
|
|
|
105
105
|
display: flex;
|
|
106
106
|
align-items: center;
|
|
107
107
|
padding: 0.5rem;
|
|
108
|
-
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
109
108
|
border-radius: 0.5rem;
|
|
110
|
-
border-radius: var(--memori-radius-box);
|
|
111
|
-
-webkit-backdrop-filter: blur(10px);
|
|
112
|
-
backdrop-filter: blur(10px);
|
|
113
|
-
background: rgba(0, 0, 0, 0.05);
|
|
114
|
-
background-color: color-mix(in oklch, var(--memori-main-background) 38%, transparent);
|
|
115
|
-
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
|
116
109
|
gap: var(--memori-spacing-sm);
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
button.memori-snippet--copy-button {
|
|
120
|
-
width: 32px !important;
|
|
121
|
-
height: 32px !important;
|
|
113
|
+
/* width: 32px !important;
|
|
114
|
+
height: 32px !important; */
|
|
122
115
|
padding: 0;
|
|
123
116
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
124
117
|
border-radius: 0.5rem !important;
|
|
125
|
-
background:
|
|
118
|
+
background: var(--memori-secondary-background);
|
|
126
119
|
transition: all 0.2s ease;
|
|
127
120
|
}
|
|
128
121
|
|
|
129
|
-
button.memori-snippet--copy-button:hover,
|
|
130
|
-
button.memori-snippet--copy-button:focus {
|
|
131
|
-
border-color: rgba(0, 0, 0, 0.2);
|
|
132
|
-
background: rgba(0, 0, 0, 0.1);
|
|
133
|
-
transform: scale(1.05);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
122
|
button.memori-snippet--copy-button:active {
|
|
137
|
-
background: rgba(0, 0, 0, 0.15);
|
|
123
|
+
/* background: rgba(0, 0, 0, 0.15); */
|
|
138
124
|
transform: scale(0.95);
|
|
139
125
|
}
|
|
140
126
|
|
|
@@ -144,24 +130,8 @@ button.memori-snippet--copy-button svg {
|
|
|
144
130
|
color: var(--memori-text-color);
|
|
145
131
|
}
|
|
146
132
|
|
|
147
|
-
.memori-snippet--copied
|
|
148
|
-
|
|
149
|
-
color: var(--memori-text-color);
|
|
150
|
-
font-size: 0.75rem;
|
|
151
|
-
font-weight: var(--memori-text-weight-semibold);
|
|
152
|
-
letter-spacing: 0.05em;
|
|
153
|
-
text-transform: uppercase;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
@keyframes fadeInOut {
|
|
157
|
-
0% {
|
|
158
|
-
opacity: 0;
|
|
159
|
-
transform: translateX(-4px);
|
|
160
|
-
}
|
|
161
|
-
100% {
|
|
162
|
-
opacity: 1;
|
|
163
|
-
transform: translateX(0);
|
|
164
|
-
}
|
|
133
|
+
.memori-snippet button.memori-snippet--copy-button--copied svg {
|
|
134
|
+
color: var(--memori-success-color, #22c55e);
|
|
165
135
|
}
|
|
166
136
|
|
|
167
137
|
.memori-snippet--caption {
|