@memori.ai/memori-react 8.42.0 → 8.44.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 +52 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +1 -1
- package/dist/components/Chat/Chat.js +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.css +6 -1
- package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
- package/dist/components/ChatInputs/ChatInputs.js +2 -2
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/IconButton/IconButton.css +144 -0
- package/dist/components/IconButton/IconButton.d.ts +9 -0
- package/dist/components/IconButton/IconButton.js +36 -0
- package/dist/components/IconButton/IconButton.js.map +1 -0
- 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/ArtifactHandler/ArtifactHandler.js +204 -59
- package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
- package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js +76 -16
- 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/MobileSessionPanel/MobileSessionPanel.css +29 -7
- package/dist/components/MobileSessionPanel/MobileSessionPanel.d.ts +3 -0
- package/dist/components/MobileSessionPanel/MobileSessionPanel.js +8 -6
- package/dist/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
- package/dist/components/PositionPopover/PositionPopover.d.ts +2 -0
- package/dist/components/PositionPopover/PositionPopover.js +19 -11
- package/dist/components/PositionPopover/PositionPopover.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.d.ts +1 -0
- package/dist/components/UploadButton/UploadButton.js +12 -18
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +1 -6
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +7 -139
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/dist/components/icons/Print.js +1 -1
- package/dist/components/icons/Print.js.map +1 -1
- package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +24 -3
- package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
- package/dist/components/layouts/WebsiteAssistant/website-assistant.css +137 -55
- package/dist/components/layouts/fullpage.css +165 -68
- package/dist/helpers/constants.d.ts +1 -0
- package/dist/helpers/constants.js +24 -1
- package/dist/helpers/constants.js.map +1 -1
- package/dist/helpers/convertDocument.d.ts +1 -0
- package/dist/helpers/convertDocument.js +24 -0
- package/dist/helpers/convertDocument.js.map +1 -0
- package/dist/helpers/message.js +3 -0
- package/dist/helpers/message.js.map +1 -1
- package/dist/helpers/positionPopover.d.ts +1 -1
- package/dist/helpers/positionPopover.js +2 -2
- package/dist/helpers/positionPopover.js.map +1 -1
- package/dist/helpers/tts/muteSpeaker.d.ts +13 -0
- package/dist/helpers/tts/muteSpeaker.js +26 -0
- package/dist/helpers/tts/muteSpeaker.js.map +1 -0
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +1 -1
- package/esm/components/Chat/Chat.js +1 -1
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +6 -1
- package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
- package/esm/components/ChatInputs/ChatInputs.js +2 -2
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/IconButton/IconButton.css +144 -0
- package/esm/components/IconButton/IconButton.d.ts +9 -0
- package/esm/components/IconButton/IconButton.js +33 -0
- package/esm/components/IconButton/IconButton.js.map +1 -0
- 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/ArtifactHandler/ArtifactHandler.js +205 -60
- package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js +2 -2
- package/esm/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
- package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js +77 -17
- 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/MobileSessionPanel/MobileSessionPanel.css +29 -7
- package/esm/components/MobileSessionPanel/MobileSessionPanel.d.ts +3 -0
- package/esm/components/MobileSessionPanel/MobileSessionPanel.js +8 -6
- package/esm/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
- package/esm/components/PositionPopover/PositionPopover.d.ts +2 -0
- package/esm/components/PositionPopover/PositionPopover.js +20 -12
- package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.d.ts +1 -0
- package/esm/components/UploadButton/UploadButton.js +13 -19
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +1 -6
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +8 -140
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/esm/components/icons/Print.js +1 -1
- package/esm/components/icons/Print.js.map +1 -1
- package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +24 -3
- package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
- package/esm/components/layouts/WebsiteAssistant/website-assistant.css +137 -55
- package/esm/components/layouts/fullpage.css +165 -68
- package/esm/helpers/constants.d.ts +1 -0
- package/esm/helpers/constants.js +23 -0
- package/esm/helpers/constants.js.map +1 -1
- package/esm/helpers/convertDocument.d.ts +1 -0
- package/esm/helpers/convertDocument.js +20 -0
- package/esm/helpers/convertDocument.js.map +1 -0
- package/esm/helpers/message.js +3 -0
- package/esm/helpers/message.js.map +1 -1
- package/esm/helpers/positionPopover.d.ts +1 -1
- package/esm/helpers/positionPopover.js +2 -2
- package/esm/helpers/positionPopover.js.map +1 -1
- package/esm/helpers/tts/muteSpeaker.d.ts +13 -0
- package/esm/helpers/tts/muteSpeaker.js +21 -0
- package/esm/helpers/tts/muteSpeaker.js.map +1 -0
- 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/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +2 -2
- package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +1 -1
- package/src/components/Chat/Chat.tsx +1 -0
- package/src/components/ChatInputs/ChatInputs.tsx +3 -0
- package/src/components/FilePreview/FilePreview.stories.tsx +0 -3
- package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +1 -0
- package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +324 -133
- package/src/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.tsx +2 -2
- package/src/components/MemoriArtifactSystem/context/ArtifactContext.tsx +122 -24
- 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/UploadButton/UploadButton.stories.tsx +2 -2
- package/src/components/UploadButton/UploadButton.tsx +19 -19
- package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +13 -187
- package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
- package/src/components/icons/Print.tsx +6 -2
- package/src/helpers/constants.ts +24 -0
- package/src/helpers/convertDocument.test.ts +67 -0
- package/src/helpers/convertDocument.ts +35 -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
|
@@ -13,7 +13,7 @@ import Code from '../../../icons/Code';
|
|
|
13
13
|
import { PreviewIcon } from '../../../icons/Preview';
|
|
14
14
|
import Snippet from '../../../Snippet/Snippet';
|
|
15
15
|
import { Medium } from '@memori.ai/memori-api-client/dist/types';
|
|
16
|
-
import { renderMsg } from '../../../../helpers/message';
|
|
16
|
+
import { renderMsg, sanitizeMsg } from '../../../../helpers/message';
|
|
17
17
|
|
|
18
18
|
const ArtifactPreview: React.FC<{
|
|
19
19
|
artifact: ArtifactData;
|
|
@@ -64,7 +64,7 @@ const ArtifactPreview: React.FC<{
|
|
|
64
64
|
<div
|
|
65
65
|
className="memori-artifact-preview-text"
|
|
66
66
|
dangerouslySetInnerHTML={{
|
|
67
|
-
__html: artifact.content,
|
|
67
|
+
__html: sanitizeMsg(artifact.content),
|
|
68
68
|
}}
|
|
69
69
|
/>
|
|
70
70
|
);
|
|
@@ -1,11 +1,30 @@
|
|
|
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
|
interface ArtifactContextType {
|
|
5
19
|
state: ArtifactSystemState;
|
|
6
20
|
openArtifact: (artifact: ArtifactData) => void;
|
|
7
21
|
closeArtifact: () => void;
|
|
8
22
|
toggleFullscreen: () => void;
|
|
23
|
+
registerArtifact: (artifact: ArtifactData) => void;
|
|
24
|
+
applyArtifactUpdate: (
|
|
25
|
+
artifactId: string,
|
|
26
|
+
edits: ArtifactEdit[]
|
|
27
|
+
) => ApplyUpdateResult;
|
|
9
28
|
}
|
|
10
29
|
|
|
11
30
|
const ArtifactContext = createContext<ArtifactContextType | null>(null);
|
|
@@ -17,26 +36,26 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
|
|
|
17
36
|
isFullscreen: false,
|
|
18
37
|
});
|
|
19
38
|
|
|
39
|
+
// Synchronous registry keyed by stable artifactId → latest version.
|
|
40
|
+
// useRef avoids race where update handlers read empty React state
|
|
41
|
+
// right after a create in the same render cycle.
|
|
42
|
+
const registryRef = useRef<Record<string, ArtifactData>>({});
|
|
43
|
+
|
|
20
44
|
const openArtifact = useCallback((artifact: ArtifactData) => {
|
|
21
|
-
setState(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
});
|
|
45
|
+
setState(() => ({
|
|
46
|
+
currentArtifact: artifact,
|
|
47
|
+
isDrawerOpen: true,
|
|
48
|
+
isFullscreen: false,
|
|
49
|
+
}));
|
|
28
50
|
}, []);
|
|
29
51
|
|
|
30
52
|
const closeArtifact = useCallback(() => {
|
|
31
|
-
setState(prev => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
isFullscreen: false,
|
|
38
|
-
};
|
|
39
|
-
});
|
|
53
|
+
setState(prev => ({
|
|
54
|
+
...prev,
|
|
55
|
+
currentArtifact: null,
|
|
56
|
+
isDrawerOpen: false,
|
|
57
|
+
isFullscreen: false,
|
|
58
|
+
}));
|
|
40
59
|
}, []);
|
|
41
60
|
|
|
42
61
|
const toggleFullscreen = useCallback(() => {
|
|
@@ -46,12 +65,91 @@ export const ArtifactProvider = ({ children }: { children: ReactNode }) => {
|
|
|
46
65
|
}));
|
|
47
66
|
}, []);
|
|
48
67
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
const registerArtifact = useCallback((artifact: ArtifactData) => {
|
|
69
|
+
if (!artifact?.artifactId) return;
|
|
70
|
+
registryRef.current[artifact.artifactId] = artifact;
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const applyArtifactUpdate = useCallback(
|
|
74
|
+
(artifactId: string, edits: ArtifactEdit[]): ApplyUpdateResult => {
|
|
75
|
+
const current = registryRef.current[artifactId];
|
|
76
|
+
if (!current) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
failedEdits: edits,
|
|
80
|
+
updatedArtifact: null,
|
|
81
|
+
failureReason: 'not-found',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { content, failedEdits, appliedCount } = applyEdits(
|
|
86
|
+
current.content,
|
|
87
|
+
edits
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (appliedCount === 0) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
failedEdits,
|
|
94
|
+
updatedArtifact: null,
|
|
95
|
+
failureReason: 'no-match',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const updatedArtifact: ArtifactData = {
|
|
100
|
+
id: `artifact-ui-${artifactId}-${Date.now()}-${Math.random()
|
|
101
|
+
.toString(36)
|
|
102
|
+
.slice(2, 8)}`,
|
|
103
|
+
artifactId,
|
|
104
|
+
content,
|
|
105
|
+
mimeType: current.mimeType,
|
|
106
|
+
title: current.title,
|
|
107
|
+
timestamp: new Date(),
|
|
108
|
+
size: content.length,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
registryRef.current[artifactId] = updatedArtifact;
|
|
112
|
+
|
|
113
|
+
setState(prev => {
|
|
114
|
+
if (
|
|
115
|
+
prev.isDrawerOpen &&
|
|
116
|
+
prev.currentArtifact?.artifactId === artifactId
|
|
117
|
+
) {
|
|
118
|
+
return {
|
|
119
|
+
...prev,
|
|
120
|
+
currentArtifact: updatedArtifact,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return prev;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
success: failedEdits.length === 0,
|
|
128
|
+
failedEdits,
|
|
129
|
+
updatedArtifact,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
[]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const contextValue = useMemo(
|
|
136
|
+
() => ({
|
|
137
|
+
state,
|
|
138
|
+
openArtifact,
|
|
139
|
+
closeArtifact,
|
|
140
|
+
toggleFullscreen,
|
|
141
|
+
registerArtifact,
|
|
142
|
+
applyArtifactUpdate,
|
|
143
|
+
}),
|
|
144
|
+
[
|
|
145
|
+
state,
|
|
146
|
+
openArtifact,
|
|
147
|
+
closeArtifact,
|
|
148
|
+
toggleFullscreen,
|
|
149
|
+
registerArtifact,
|
|
150
|
+
applyArtifactUpdate,
|
|
151
|
+
]
|
|
152
|
+
);
|
|
55
153
|
|
|
56
154
|
return (
|
|
57
155
|
<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,10 +10,26 @@ 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;
|
|
13
32
|
isFullscreen: boolean;
|
|
14
33
|
}
|
|
15
34
|
|
|
16
|
-
export type ArtifactTab = 'code' | 'preview';
|
|
35
|
+
export type ArtifactTab = 'code' | 'preview';
|
|
@@ -73,7 +73,7 @@ export const ArtifactAPIBridge = ({
|
|
|
73
73
|
}: {
|
|
74
74
|
pushMessage: (message: Message) => void;
|
|
75
75
|
}) => {
|
|
76
|
-
const { openArtifact, closeArtifact, toggleFullscreen, state } =
|
|
76
|
+
const { openArtifact, closeArtifact, toggleFullscreen, state, registerArtifact } =
|
|
77
77
|
useArtifact();
|
|
78
78
|
|
|
79
79
|
const apiRef = useRef<any>(null);
|
|
@@ -105,12 +105,14 @@ export const ArtifactAPIBridge = ({
|
|
|
105
105
|
title?: string
|
|
106
106
|
) => {
|
|
107
107
|
const autoTitle = title || getTitleFromMimeType(mimeType);
|
|
108
|
+
const artifactId = `api-${Date.now()}-${Math.random()
|
|
109
|
+
.toString(36)
|
|
110
|
+
.substr(2, 9)}`;
|
|
108
111
|
|
|
109
112
|
// Create the artifact object
|
|
110
113
|
const artifact: ArtifactData = {
|
|
111
|
-
id: `artifact-${
|
|
112
|
-
|
|
113
|
-
.substr(2, 9)}`,
|
|
114
|
+
id: `artifact-ui-${artifactId}`,
|
|
115
|
+
artifactId,
|
|
114
116
|
content,
|
|
115
117
|
mimeType,
|
|
116
118
|
title: autoTitle,
|
|
@@ -121,8 +123,8 @@ export const ArtifactAPIBridge = ({
|
|
|
121
123
|
// Create wrapped message text for artifact detection
|
|
122
124
|
// Don't reassign content parameter - use a new variable
|
|
123
125
|
let messageText = content;
|
|
124
|
-
if (!messageText.includes('
|
|
125
|
-
messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
|
|
126
|
+
if (!messageText.includes('class="memori-artifact"')) {
|
|
127
|
+
messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${autoTitle}">${content}</output>`;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
//we have to push in the history the artifact as message
|
|
@@ -143,6 +145,7 @@ export const ArtifactAPIBridge = ({
|
|
|
143
145
|
placeUncertaintyKm: undefined,
|
|
144
146
|
});
|
|
145
147
|
|
|
148
|
+
registerArtifact(artifact);
|
|
146
149
|
// Open the artifact immediately
|
|
147
150
|
openArtifact(artifact);
|
|
148
151
|
},
|
|
@@ -159,15 +162,17 @@ export const ArtifactAPIBridge = ({
|
|
|
159
162
|
const title =
|
|
160
163
|
outputElement.getAttribute('data-title') ||
|
|
161
164
|
getTitleFromMimeType(mimeType);
|
|
165
|
+
const artifactId =
|
|
166
|
+
outputElement.getAttribute('data-artifact-id') ||
|
|
167
|
+
`api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
162
168
|
|
|
163
169
|
// Nascondi l'elemento originale
|
|
164
170
|
outputElement.style.display = 'none';
|
|
165
171
|
outputElement.setAttribute('data-memori-processed', 'true');
|
|
166
172
|
|
|
167
173
|
const artifact: ArtifactData = {
|
|
168
|
-
id: `artifact-${
|
|
169
|
-
|
|
170
|
-
.substr(2, 9)}`,
|
|
174
|
+
id: `artifact-ui-${artifactId}`,
|
|
175
|
+
artifactId,
|
|
171
176
|
content,
|
|
172
177
|
mimeType,
|
|
173
178
|
title,
|
|
@@ -178,8 +183,8 @@ export const ArtifactAPIBridge = ({
|
|
|
178
183
|
// Create wrapped message text for artifact detection
|
|
179
184
|
// Don't reassign content - use a new variable
|
|
180
185
|
let messageText = content;
|
|
181
|
-
if (!messageText.includes('
|
|
182
|
-
messageText = `<output class="memori-artifact" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
|
|
186
|
+
if (!messageText.includes('class="memori-artifact"')) {
|
|
187
|
+
messageText = `<output class="memori-artifact" data-action="create" data-artifact-id="${artifactId}" data-mimetype="${mimeType}" data-title="${title}">${content}</output>`;
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
pushMessage({
|
|
@@ -199,10 +204,11 @@ export const ArtifactAPIBridge = ({
|
|
|
199
204
|
placeUncertaintyKm: undefined,
|
|
200
205
|
});
|
|
201
206
|
|
|
207
|
+
registerArtifact(artifact);
|
|
202
208
|
// Open the artifact immediately
|
|
203
209
|
openArtifact(artifact);
|
|
204
210
|
|
|
205
|
-
return artifact.
|
|
211
|
+
return artifact.artifactId;
|
|
206
212
|
},
|
|
207
213
|
|
|
208
214
|
/**
|
|
@@ -283,7 +289,7 @@ export const ArtifactAPIBridge = ({
|
|
|
283
289
|
delete (window as any).MemoriArtifactAPI;
|
|
284
290
|
}
|
|
285
291
|
};
|
|
286
|
-
}, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage]);
|
|
292
|
+
}, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage, registerArtifact]);
|
|
287
293
|
|
|
288
294
|
return null;
|
|
289
295
|
};
|
|
@@ -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
|
+
};
|
|
@@ -121,7 +121,7 @@ A unified upload button that supports both images and documents with the followi
|
|
|
121
121
|
## Limits
|
|
122
122
|
|
|
123
123
|
- **Images**: Maximum 5 images, 15MB per file, formats: .jpg, .jpeg, .png
|
|
124
|
-
- **Documents**: Maximum 5 documents,
|
|
124
|
+
- **Documents**: Maximum 5 documents, 25MB per file; PDF, Office, OpenDocument, HTML, structured text, and plain-text formats
|
|
125
125
|
- **Content**: Document content is limited to 300,000 characters per document (truncated if exceeded)
|
|
126
126
|
`,
|
|
127
127
|
},
|
|
@@ -206,7 +206,7 @@ const Template: Story<UploadButtonProps> = (args) => {
|
|
|
206
206
|
<li>Drag and drop files anywhere in this area</li>
|
|
207
207
|
<li>Press <kbd style={{ padding: '2px 6px', backgroundColor: '#f0f0f0', borderRadius: '3px' }}>Cmd+V</kbd> / <kbd style={{ padding: '2px 6px', backgroundColor: '#f0f0f0', borderRadius: '3px' }}>Ctrl+V</kbd> to open file chooser</li>
|
|
208
208
|
<li>Paste files from clipboard using <kbd style={{ padding: '2px 6px', backgroundColor: '#f0f0f0', borderRadius: '3px' }}>Cmd+V</kbd> / <kbd style={{ padding: '2px 6px', backgroundColor: '#f0f0f0', borderRadius: '3px' }}>Ctrl+V</kbd></li>
|
|
209
|
-
<li>Upload
|
|
209
|
+
<li>Upload images and supported PDF, Office, OpenDocument, HTML, or text documents</li>
|
|
210
210
|
</ul>
|
|
211
211
|
</div>
|
|
212
212
|
</div>
|