@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
package/esm/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const version = '8.
|
|
1
|
+
export const version = '8.44.0';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "8.
|
|
2
|
+
"version": "8.44.0",
|
|
3
3
|
"name": "@memori.ai/memori-react",
|
|
4
4
|
"author": "Memori Srl",
|
|
5
5
|
"main": "dist/index.js",
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
"@react-three/drei": "8.20.2",
|
|
300
300
|
"@react-three/fiber": "7.0.25",
|
|
301
301
|
"classnames": "2.5.1",
|
|
302
|
-
"dompurify": "^3.4.
|
|
302
|
+
"dompurify": "^3.4.13",
|
|
303
303
|
"ellipsed": "1.6.0",
|
|
304
304
|
"i18next": "22.0.6",
|
|
305
305
|
"katex": "^0.16.11",
|
|
@@ -138,4 +138,4 @@ export const DEFAULT_CONFIG: AnimationConfig = {
|
|
|
138
138
|
export const EMOTION_SMOOTHING = 0.3;
|
|
139
139
|
export const VISEME_SMOOTHING = 0.5;
|
|
140
140
|
/** Multiplier applied to emotion morph target values before clamping (1 = raw mapping). */
|
|
141
|
-
export const EMOTION_INTENSITY =
|
|
141
|
+
export const EMOTION_INTENSITY = 1.5;
|
|
@@ -619,6 +619,7 @@ const Chat: React.FC<Props> = ({
|
|
|
619
619
|
setSendOnEnter={setSendOnEnter}
|
|
620
620
|
client={client}
|
|
621
621
|
sessionID={sessionID}
|
|
622
|
+
baseUrl={baseUrl}
|
|
622
623
|
showUpload={showUpload}
|
|
623
624
|
attachmentsMenuOpen={attachmentsMenuOpen}
|
|
624
625
|
setAttachmentsMenuOpen={setAttachmentsMenuOpen}
|
|
@@ -35,6 +35,7 @@ export interface Props {
|
|
|
35
35
|
isTyping?: boolean;
|
|
36
36
|
sessionID?: string;
|
|
37
37
|
memoriID?: string;
|
|
38
|
+
baseUrl?: string;
|
|
38
39
|
client?: ReturnType<typeof memoriApiClient>;
|
|
39
40
|
onTextareaExpanded?: (expanded: boolean) => void;
|
|
40
41
|
/** Override total document payload limit (character count). */
|
|
@@ -70,6 +71,7 @@ const ChatInputs: React.FC<Props> = ({
|
|
|
70
71
|
sessionID,
|
|
71
72
|
authToken,
|
|
72
73
|
memoriID,
|
|
74
|
+
baseUrl,
|
|
73
75
|
client,
|
|
74
76
|
onTextareaExpanded,
|
|
75
77
|
maxTotalMessagePayload,
|
|
@@ -367,6 +369,7 @@ ${text}
|
|
|
367
369
|
authToken={authToken}
|
|
368
370
|
client={client}
|
|
369
371
|
sessionID={sessionID}
|
|
372
|
+
baseUrl={baseUrl}
|
|
370
373
|
isMediaAccepted={dialogState?.acceptsMedia || false}
|
|
371
374
|
setDocumentPreviewFiles={setDocumentPreviewFiles}
|
|
372
375
|
documentPreviewFiles={documentPreviewFiles}
|
|
@@ -23,7 +23,6 @@ const meta: Meta = {
|
|
|
23
23
|
|
|
24
24
|
type Props = {
|
|
25
25
|
onUpload: (text: string) => void;
|
|
26
|
-
convertapiToken: string;
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
export default meta;
|
|
@@ -55,12 +54,10 @@ const Template: Story<Props> = args => {
|
|
|
55
54
|
export const Default = Template.bind({});
|
|
56
55
|
Default.args = {
|
|
57
56
|
onUpload: () => {},
|
|
58
|
-
convertapiToken: '',
|
|
59
57
|
};
|
|
60
58
|
|
|
61
59
|
export const Disabled = Template.bind({});
|
|
62
60
|
Disabled.args = {
|
|
63
61
|
onUpload: () => {},
|
|
64
|
-
convertapiToken: '',
|
|
65
62
|
disabled: true,
|
|
66
63
|
};
|
|
@@ -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 ChevronRight from '../../../icons/ChevronRight';
|
|
5
12
|
import ArtifactDrawer from '../ArtifactDrawer/ArtifactDrawer';
|
|
6
13
|
import ChevronDown from '../../../icons/ChevronDown';
|
|
@@ -8,8 +15,9 @@ import ChevronLeft from '../../../icons/ChevronLeft';
|
|
|
8
15
|
import ChevronUp from '../../../icons/ChevronUp';
|
|
9
16
|
import { Message } from '@memori.ai/memori-api-client/dist/types';
|
|
10
17
|
import { stripReasoningTags } from '../../../../helpers/utils';
|
|
18
|
+
import { parseEdits } from '../../utils/applyEdits';
|
|
19
|
+
import { useTranslation } from 'react-i18next';
|
|
11
20
|
|
|
12
|
-
// Event type for artifact creation
|
|
13
21
|
type ArtifactCreatedEvent = CustomEvent<{
|
|
14
22
|
artifact: ArtifactData;
|
|
15
23
|
message: Message;
|
|
@@ -20,6 +28,16 @@ interface ArtifactHandlerProps {
|
|
|
20
28
|
message: Message;
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
interface DetectedUpdate {
|
|
32
|
+
artifactId: string;
|
|
33
|
+
edits: ArtifactEdit[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FailedUpdate {
|
|
37
|
+
artifactId: string;
|
|
38
|
+
reason: 'not-found' | 'no-match' | 'invalid';
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
// ---------------------------------------------------------------------------
|
|
24
42
|
// Helpers
|
|
25
43
|
// ---------------------------------------------------------------------------
|
|
@@ -32,48 +50,75 @@ const formatBytes = (bytes: number): string => {
|
|
|
32
50
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
33
51
|
};
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
53
|
+
const CLASS_ATTR_RE = /class\s*=\s*["']([^"']*)["']/i;
|
|
54
|
+
const DATA_ACTION_RE = /data-action\s*=\s*["']([^"']+)["']/i;
|
|
55
|
+
|
|
56
|
+
const getClassList = (openingTag: string): string[] => {
|
|
57
|
+
const m = openingTag.match(CLASS_ATTR_RE);
|
|
58
|
+
if (!m) return [];
|
|
59
|
+
return m[1].split(/\s+/).filter(Boolean);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getDataAction = (openingTag: string): string | null => {
|
|
63
|
+
const m = openingTag.match(DATA_ACTION_RE);
|
|
64
|
+
return m?.[1]?.trim().toLowerCase() || null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** True for create tags: class memori-artifact, action create/absent (not update). */
|
|
68
|
+
const isCreateArtifactTag = (openingTag: string): boolean => {
|
|
69
|
+
const classes = getClassList(openingTag);
|
|
70
|
+
if (classes.includes('memori-artifact-update')) return false;
|
|
71
|
+
if (!classes.includes('memori-artifact')) return false;
|
|
72
|
+
const action = getDataAction(openingTag);
|
|
73
|
+
return action === null || action === 'create';
|
|
47
74
|
};
|
|
48
75
|
|
|
49
76
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* Examples that match:
|
|
54
|
-
* class="memori-artifact"
|
|
55
|
-
* class="memori-artifact extra-class"
|
|
56
|
-
* class="foo memori-artifact"
|
|
57
|
-
* class='memori-artifact'
|
|
58
|
-
* class = "memori-artifact extra"
|
|
77
|
+
* True for update tags:
|
|
78
|
+
* - preferred: class="memori-artifact" data-action="update"
|
|
79
|
+
* - legacy: class="memori-artifact-update"
|
|
59
80
|
*/
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
if (!
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const isUpdateArtifactTag = (openingTag: string): boolean => {
|
|
82
|
+
const classes = getClassList(openingTag);
|
|
83
|
+
if (classes.includes('memori-artifact-update')) return true;
|
|
84
|
+
if (!classes.includes('memori-artifact')) return false;
|
|
85
|
+
return getDataAction(openingTag) === 'update';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const findMatchingOutputClose = (
|
|
89
|
+
cleaned: string,
|
|
90
|
+
openEnd: number
|
|
91
|
+
): number => {
|
|
92
|
+
let depth = 1;
|
|
93
|
+
let pos = openEnd;
|
|
94
|
+
let closeStart = -1;
|
|
95
|
+
|
|
96
|
+
while (pos < cleaned.length && depth > 0) {
|
|
97
|
+
const nextOpen = cleaned.indexOf('<output', pos);
|
|
98
|
+
const nextClose = cleaned.indexOf('</output>', pos);
|
|
99
|
+
|
|
100
|
+
if (nextClose === -1) {
|
|
101
|
+
return cleaned.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
105
|
+
depth++;
|
|
106
|
+
pos = nextOpen + '<output'.length;
|
|
107
|
+
} else {
|
|
108
|
+
depth--;
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
closeStart = nextClose;
|
|
111
|
+
} else {
|
|
112
|
+
pos = nextClose + '</output>'.length;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return closeStart;
|
|
66
118
|
};
|
|
67
119
|
|
|
68
120
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* Replaces the previous single-regex approach with a depth-tracking loop so
|
|
72
|
-
* that:
|
|
73
|
-
* - Multiple CSS classes are handled correctly (e.g. class="memori-artifact foo")
|
|
74
|
-
* - Nested / self-contained <output> tags inside the content don't truncate it
|
|
75
|
-
* - Unclosed tags are gracefully skipped
|
|
76
|
-
* - IDs are stable across re-renders (derived from message + content hash)
|
|
121
|
+
* Detect create artifacts (class="memori-artifact", data-action create/absent).
|
|
77
122
|
*/
|
|
78
123
|
const detectArtifacts = (
|
|
79
124
|
text: string,
|
|
@@ -87,66 +132,29 @@ const detectArtifacts = (
|
|
|
87
132
|
let searchFrom = 0;
|
|
88
133
|
let artifactNum = 0;
|
|
89
134
|
|
|
90
|
-
// Regex that matches any <output …> opening tag (capturing the full tag)
|
|
91
135
|
const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
|
|
92
136
|
|
|
93
137
|
while (searchFrom < cleaned.length) {
|
|
94
|
-
// Find the next <output …> opening tag from the current position
|
|
95
138
|
OPEN_TAG_RE.lastIndex = searchFrom;
|
|
96
139
|
const openMatch = OPEN_TAG_RE.exec(cleaned);
|
|
97
140
|
if (!openMatch) break;
|
|
98
141
|
|
|
99
|
-
const fullOpenTag = openMatch[0];
|
|
100
|
-
const
|
|
101
|
-
const openEnd = openStart + fullOpenTag.length;
|
|
142
|
+
const fullOpenTag = openMatch[0];
|
|
143
|
+
const openEnd = openMatch.index + fullOpenTag.length;
|
|
102
144
|
|
|
103
|
-
|
|
104
|
-
if (!hasMemoriArtifactClass(fullOpenTag)) {
|
|
145
|
+
if (!isCreateArtifactTag(fullOpenTag)) {
|
|
105
146
|
searchFrom = openEnd;
|
|
106
147
|
continue;
|
|
107
148
|
}
|
|
108
149
|
|
|
109
|
-
|
|
110
|
-
let depth = 1;
|
|
111
|
-
let pos = openEnd;
|
|
112
|
-
let closeStart = -1;
|
|
113
|
-
|
|
114
|
-
while (pos < cleaned.length && depth > 0) {
|
|
115
|
-
const nextOpen = cleaned.indexOf('<output', pos);
|
|
116
|
-
const nextClose = cleaned.indexOf('</output>', pos);
|
|
117
|
-
|
|
118
|
-
if (nextClose === -1) {
|
|
119
|
-
// No closing tag found — treat everything until EOF as content
|
|
120
|
-
// (handles streaming / partial messages)
|
|
121
|
-
closeStart = cleaned.length;
|
|
122
|
-
depth = 0;
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
127
|
-
// There's another opening tag before the next closing tag → go deeper
|
|
128
|
-
depth++;
|
|
129
|
-
pos = nextOpen + '<output'.length;
|
|
130
|
-
} else {
|
|
131
|
-
// Found a closing tag
|
|
132
|
-
depth--;
|
|
133
|
-
if (depth === 0) {
|
|
134
|
-
closeStart = nextClose;
|
|
135
|
-
} else {
|
|
136
|
-
pos = nextClose + '</output>'.length;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
150
|
+
const closeStart = findMatchingOutputClose(cleaned, openEnd);
|
|
141
151
|
if (closeStart === -1) {
|
|
142
|
-
// Malformed — skip past this opening tag and continue
|
|
143
152
|
searchFrom = openEnd;
|
|
144
153
|
continue;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
const content = cleaned.slice(openEnd, closeStart).trim();
|
|
148
157
|
|
|
149
|
-
// Extract metadata from the opening tag
|
|
150
158
|
const mimeTypeMatch = fullOpenTag.match(
|
|
151
159
|
/data-mimetype\s*=\s*["']([^"']+)["']/i
|
|
152
160
|
);
|
|
@@ -163,13 +171,16 @@ const detectArtifacts = (
|
|
|
163
171
|
|
|
164
172
|
artifactNum++;
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
const dataArtifactIdMatch = fullOpenTag.match(
|
|
175
|
+
/data-artifact-id\s*=\s*["']([^"']+)["']/i
|
|
176
|
+
);
|
|
177
|
+
const artifactId =
|
|
178
|
+
dataArtifactIdMatch?.[1]?.trim() ||
|
|
179
|
+
`artifact-${messageKey}-${artifactNum}`;
|
|
170
180
|
|
|
171
181
|
artifacts.push({
|
|
172
|
-
id:
|
|
182
|
+
id: `artifact-ui-${artifactId}`,
|
|
183
|
+
artifactId,
|
|
173
184
|
content,
|
|
174
185
|
mimeType,
|
|
175
186
|
title,
|
|
@@ -177,7 +188,6 @@ const detectArtifacts = (
|
|
|
177
188
|
size: content.length,
|
|
178
189
|
});
|
|
179
190
|
|
|
180
|
-
// Advance past the closing tag (or EOF)
|
|
181
191
|
searchFrom =
|
|
182
192
|
closeStart === cleaned.length
|
|
183
193
|
? cleaned.length
|
|
@@ -187,6 +197,58 @@ const detectArtifacts = (
|
|
|
187
197
|
return artifacts;
|
|
188
198
|
};
|
|
189
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Detect update tags and parse their JSON edit bodies.
|
|
202
|
+
*/
|
|
203
|
+
const detectUpdates = (
|
|
204
|
+
text: string,
|
|
205
|
+
isFromUser: boolean
|
|
206
|
+
): DetectedUpdate[] => {
|
|
207
|
+
if (!text || isFromUser) return [];
|
|
208
|
+
|
|
209
|
+
const cleaned = stripReasoningTags(text);
|
|
210
|
+
const updates: DetectedUpdate[] = [];
|
|
211
|
+
let searchFrom = 0;
|
|
212
|
+
|
|
213
|
+
const OPEN_TAG_RE = /<output\b([^>]*)>/gi;
|
|
214
|
+
|
|
215
|
+
while (searchFrom < cleaned.length) {
|
|
216
|
+
OPEN_TAG_RE.lastIndex = searchFrom;
|
|
217
|
+
const openMatch = OPEN_TAG_RE.exec(cleaned);
|
|
218
|
+
if (!openMatch) break;
|
|
219
|
+
|
|
220
|
+
const fullOpenTag = openMatch[0];
|
|
221
|
+
const openEnd = openMatch.index + fullOpenTag.length;
|
|
222
|
+
|
|
223
|
+
if (!isUpdateArtifactTag(fullOpenTag)) {
|
|
224
|
+
searchFrom = openEnd;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const closeStart = cleaned.indexOf('</output>', openEnd);
|
|
229
|
+
const bodyEnd = closeStart === -1 ? cleaned.length : closeStart;
|
|
230
|
+
const body = cleaned.slice(openEnd, bodyEnd).trim();
|
|
231
|
+
|
|
232
|
+
const idMatch = fullOpenTag.match(
|
|
233
|
+
/data-artifact-id\s*=\s*["']([^"']+)["']/i
|
|
234
|
+
);
|
|
235
|
+
const artifactId = idMatch?.[1]?.trim();
|
|
236
|
+
const edits = parseEdits(body);
|
|
237
|
+
|
|
238
|
+
if (artifactId && edits) {
|
|
239
|
+
updates.push({ artifactId, edits });
|
|
240
|
+
} else if (artifactId) {
|
|
241
|
+
// Invalid body — still surface as a failed update for UI feedback
|
|
242
|
+
updates.push({ artifactId, edits: [] });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
searchFrom =
|
|
246
|
+
closeStart === -1 ? cleaned.length : closeStart + '</output>'.length;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return updates;
|
|
250
|
+
};
|
|
251
|
+
|
|
190
252
|
// ---------------------------------------------------------------------------
|
|
191
253
|
// Component
|
|
192
254
|
// ---------------------------------------------------------------------------
|
|
@@ -195,22 +257,21 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
195
257
|
isChatlogPanel = false,
|
|
196
258
|
message,
|
|
197
259
|
}) => {
|
|
198
|
-
const {
|
|
260
|
+
const {
|
|
261
|
+
openArtifact,
|
|
262
|
+
state,
|
|
263
|
+
closeArtifact,
|
|
264
|
+
registerArtifact,
|
|
265
|
+
applyArtifactUpdate,
|
|
266
|
+
} = useArtifact();
|
|
267
|
+
const { t } = useTranslation();
|
|
199
268
|
|
|
200
|
-
/**
|
|
201
|
-
* Use raw text for artifact detection.
|
|
202
|
-
* translatedText may lose <output> tags — keep it as fallback only.
|
|
203
|
-
*/
|
|
204
269
|
const messageText = useMemo(() => message.text || '', [message.text]);
|
|
205
270
|
const translatedMessageText = useMemo(
|
|
206
271
|
() => message.translatedText || '',
|
|
207
272
|
[message.translatedText]
|
|
208
273
|
);
|
|
209
274
|
|
|
210
|
-
/**
|
|
211
|
-
* Stable key that identifies this specific message.
|
|
212
|
-
* Used both for effect gating and for stable artifact ID generation.
|
|
213
|
-
*/
|
|
214
275
|
const messageKey = useMemo(
|
|
215
276
|
() => `${message.timestamp}-${message.fromUser ? '1' : '0'}`,
|
|
216
277
|
[message.timestamp, message.fromUser]
|
|
@@ -226,11 +287,6 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
226
287
|
[message]
|
|
227
288
|
);
|
|
228
289
|
|
|
229
|
-
/**
|
|
230
|
-
* Memoised artifact list.
|
|
231
|
-
* Falls back to translatedText only when the primary text yields nothing.
|
|
232
|
-
* Both branches use the same messageKey so IDs remain stable.
|
|
233
|
-
*/
|
|
234
290
|
const artifacts = useMemo<ArtifactData[]>(() => {
|
|
235
291
|
const fromUser = message.fromUser || false;
|
|
236
292
|
const primary = detectArtifacts(messageText, fromUser, messageKey);
|
|
@@ -238,26 +294,92 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
238
294
|
return detectArtifacts(translatedMessageText, fromUser, messageKey);
|
|
239
295
|
}, [messageText, translatedMessageText, message.fromUser, messageKey]);
|
|
240
296
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
297
|
+
const detectedUpdates = useMemo<DetectedUpdate[]>(() => {
|
|
298
|
+
const fromUser = message.fromUser || false;
|
|
299
|
+
const primary = detectUpdates(messageText, fromUser);
|
|
300
|
+
if (primary.length > 0) return primary;
|
|
301
|
+
return detectUpdates(translatedMessageText, fromUser);
|
|
302
|
+
}, [messageText, translatedMessageText, message.fromUser]);
|
|
303
|
+
|
|
304
|
+
const [updateResultMap, setUpdateResultMap] = useState<
|
|
305
|
+
Record<number, ArtifactData>
|
|
306
|
+
>({});
|
|
307
|
+
const [failedUpdates, setFailedUpdates] = useState<FailedUpdate[]>([]);
|
|
308
|
+
|
|
309
|
+
const processedUpdatesRef = useRef<string>('');
|
|
310
|
+
const hasAutoOpenedRef = useRef<string>('');
|
|
311
|
+
|
|
248
312
|
useEffect(() => {
|
|
249
|
-
if (!messageText
|
|
313
|
+
if (!messageText && !translatedMessageText) return;
|
|
314
|
+
|
|
315
|
+
artifacts.forEach(artifact => {
|
|
316
|
+
registerArtifact(artifact);
|
|
317
|
+
dispatchArtifactCreatedEvent(artifact);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const updatesSignature = `${messageKey}:${detectedUpdates
|
|
321
|
+
.map(u => `${u.artifactId}:${JSON.stringify(u.edits)}`)
|
|
322
|
+
.join('|')}`;
|
|
323
|
+
|
|
324
|
+
let lastUpdated: ArtifactData | null = null;
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
detectedUpdates.length > 0 &&
|
|
328
|
+
processedUpdatesRef.current !== updatesSignature
|
|
329
|
+
) {
|
|
330
|
+
processedUpdatesRef.current = updatesSignature;
|
|
331
|
+
const nextMap: Record<number, ArtifactData> = {};
|
|
332
|
+
const nextFailed: FailedUpdate[] = [];
|
|
333
|
+
|
|
334
|
+
detectedUpdates.forEach((update, idx) => {
|
|
335
|
+
if (!update.edits.length) {
|
|
336
|
+
nextFailed.push({
|
|
337
|
+
artifactId: update.artifactId,
|
|
338
|
+
reason: 'invalid',
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
250
342
|
|
|
251
|
-
|
|
343
|
+
const result = applyArtifactUpdate(update.artifactId, update.edits);
|
|
344
|
+
if (result.updatedArtifact) {
|
|
345
|
+
nextMap[idx] = result.updatedArtifact;
|
|
346
|
+
lastUpdated = result.updatedArtifact;
|
|
347
|
+
dispatchArtifactCreatedEvent(result.updatedArtifact);
|
|
348
|
+
} else {
|
|
349
|
+
nextFailed.push({
|
|
350
|
+
artifactId: update.artifactId,
|
|
351
|
+
reason: result.failureReason ?? 'no-match',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
});
|
|
252
355
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return () => clearTimeout(timer);
|
|
356
|
+
setUpdateResultMap(nextMap);
|
|
357
|
+
setFailedUpdates(nextFailed);
|
|
256
358
|
}
|
|
359
|
+
|
|
360
|
+
if (isChatlogPanel) return;
|
|
361
|
+
|
|
362
|
+
const openTarget =
|
|
363
|
+
lastUpdated ??
|
|
364
|
+
(Object.keys(updateResultMap).length === 0 && artifacts.length > 0
|
|
365
|
+
? artifacts[0]
|
|
366
|
+
: null);
|
|
367
|
+
if (!openTarget) return;
|
|
368
|
+
|
|
369
|
+
if (hasAutoOpenedRef.current === messageKey) return;
|
|
370
|
+
hasAutoOpenedRef.current = messageKey;
|
|
371
|
+
|
|
372
|
+
const timer = setTimeout(() => openArtifact(openTarget), 100);
|
|
373
|
+
return () => clearTimeout(timer);
|
|
257
374
|
}, [
|
|
258
375
|
messageKey,
|
|
259
376
|
messageText,
|
|
377
|
+
translatedMessageText,
|
|
260
378
|
artifacts,
|
|
379
|
+
detectedUpdates,
|
|
380
|
+
updateResultMap,
|
|
381
|
+
registerArtifact,
|
|
382
|
+
applyArtifactUpdate,
|
|
261
383
|
dispatchArtifactCreatedEvent,
|
|
262
384
|
isChatlogPanel,
|
|
263
385
|
openArtifact,
|
|
@@ -287,7 +409,34 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
287
409
|
return '📄';
|
|
288
410
|
}, []);
|
|
289
411
|
|
|
290
|
-
|
|
412
|
+
const updatedLabel = t('artifact.updated') || 'updated';
|
|
413
|
+
const updateFailedLabel =
|
|
414
|
+
t('artifact.updateFailed') || 'Update could not be applied';
|
|
415
|
+
const updateFailedHint =
|
|
416
|
+
t('artifact.updateFailedHint') ||
|
|
417
|
+
'The patch did not match the current content. Ask to rewrite the artifact.';
|
|
418
|
+
|
|
419
|
+
const updateEntries = Object.entries(updateResultMap);
|
|
420
|
+
const hasCreates = artifacts.length > 0;
|
|
421
|
+
const hasUpdates = updateEntries.length > 0;
|
|
422
|
+
const hasFailed = failedUpdates.length > 0;
|
|
423
|
+
|
|
424
|
+
if (!hasCreates && !hasUpdates && !hasFailed) return null;
|
|
425
|
+
|
|
426
|
+
const renderChevron = (isSelected: boolean) => {
|
|
427
|
+
if (isChatlogPanel) {
|
|
428
|
+
return isSelected ? (
|
|
429
|
+
<ChevronUp className="memori-artifact-handler-action-icon" />
|
|
430
|
+
) : (
|
|
431
|
+
<ChevronDown className="memori-artifact-handler-action-icon" />
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return isSelected ? (
|
|
435
|
+
<ChevronLeft className="memori-artifact-handler-action-icon" />
|
|
436
|
+
) : (
|
|
437
|
+
<ChevronRight className="memori-artifact-handler-action-icon" />
|
|
438
|
+
);
|
|
439
|
+
};
|
|
291
440
|
|
|
292
441
|
return (
|
|
293
442
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
@@ -323,38 +472,80 @@ const ArtifactHandler: React.FC<ArtifactHandlerProps> = ({
|
|
|
323
472
|
</div>
|
|
324
473
|
</div>
|
|
325
474
|
<div className="memori-artifact-handler-action">
|
|
326
|
-
{
|
|
327
|
-
isSelected ? (
|
|
328
|
-
<ChevronUp className="memori-artifact-handler-action-icon" />
|
|
329
|
-
) : (
|
|
330
|
-
<ChevronDown className="memori-artifact-handler-action-icon" />
|
|
331
|
-
)
|
|
332
|
-
) : isSelected ? (
|
|
333
|
-
<ChevronLeft className="memori-artifact-handler-action-icon" />
|
|
334
|
-
) : (
|
|
335
|
-
<ChevronRight className="memori-artifact-handler-action-icon" />
|
|
336
|
-
)}
|
|
475
|
+
{renderChevron(isSelected)}
|
|
337
476
|
</div>
|
|
338
477
|
</div>
|
|
339
478
|
|
|
340
|
-
{/* Render ArtifactDrawer inline when in chatlog panel */}
|
|
341
479
|
{isSelected && <ArtifactDrawer isChatLogPanel={isChatlogPanel} />}
|
|
342
480
|
</React.Fragment>
|
|
343
481
|
);
|
|
344
482
|
})}
|
|
483
|
+
|
|
484
|
+
{updateEntries.map(([, artifact]) => {
|
|
485
|
+
const isSelected =
|
|
486
|
+
state.isDrawerOpen && state.currentArtifact?.id === artifact.id;
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<React.Fragment key={artifact.id}>
|
|
490
|
+
<div
|
|
491
|
+
className={`memori-artifact-handler${
|
|
492
|
+
isSelected ? ' memori-artifact-handler--selected' : ''
|
|
493
|
+
}`}
|
|
494
|
+
onClick={() => handleArtifactClick(artifact)}
|
|
495
|
+
style={
|
|
496
|
+
isSelected
|
|
497
|
+
? {
|
|
498
|
+
border: '2px solid var(--memori-primary, #3b82f6)',
|
|
499
|
+
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
|
|
500
|
+
}
|
|
501
|
+
: undefined
|
|
502
|
+
}
|
|
503
|
+
>
|
|
504
|
+
<div className="memori-artifact-handler-icon">✏️</div>
|
|
505
|
+
<div className="memori-artifact-handler-info">
|
|
506
|
+
<div className="memori-artifact-handler-title">
|
|
507
|
+
{artifact.title} — {updatedLabel}
|
|
508
|
+
</div>
|
|
509
|
+
<div className="memori-artifact-handler-meta">
|
|
510
|
+
{artifact.mimeType} • {formatBytes(artifact.size || 0)}
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div className="memori-artifact-handler-action">
|
|
514
|
+
{renderChevron(isSelected)}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
{isSelected && <ArtifactDrawer isChatLogPanel={isChatlogPanel} />}
|
|
519
|
+
</React.Fragment>
|
|
520
|
+
);
|
|
521
|
+
})}
|
|
522
|
+
|
|
523
|
+
{failedUpdates.map((failed, idx) => (
|
|
524
|
+
<div
|
|
525
|
+
key={`failed-${failed.artifactId}-${idx}`}
|
|
526
|
+
className="memori-artifact-handler memori-artifact-handler--failed"
|
|
527
|
+
style={{
|
|
528
|
+
opacity: 0.85,
|
|
529
|
+
border: '1px dashed var(--memori-danger, #ef4444)',
|
|
530
|
+
cursor: 'default',
|
|
531
|
+
}}
|
|
532
|
+
role="status"
|
|
533
|
+
>
|
|
534
|
+
<div className="memori-artifact-handler-icon">⚠️</div>
|
|
535
|
+
<div className="memori-artifact-handler-info">
|
|
536
|
+
<div className="memori-artifact-handler-title">
|
|
537
|
+
{updateFailedLabel}
|
|
538
|
+
</div>
|
|
539
|
+
<div className="memori-artifact-handler-meta">
|
|
540
|
+
{failed.artifactId} • {updateFailedHint}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
))}
|
|
345
545
|
</div>
|
|
346
546
|
);
|
|
347
547
|
};
|
|
348
548
|
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
// Memoised export
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* FIX: comparison now uses the same field priority as detectArtifacts
|
|
355
|
-
* (message.text first, translatedText as fallback) to avoid asymmetric
|
|
356
|
-
* re-render skipping.
|
|
357
|
-
*/
|
|
358
549
|
const MemoizedArtifactHandler = memo(ArtifactHandler, (prev, next) => {
|
|
359
550
|
const prevText = prev.message.text || prev.message.translatedText || '';
|
|
360
551
|
const nextText = next.message.text || next.message.translatedText || '';
|