@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +1 -1
  3. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +1 -1
  4. package/dist/components/Chat/Chat.js +1 -1
  5. package/dist/components/Chat/Chat.js.map +1 -1
  6. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.css +6 -1
  7. package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
  8. package/dist/components/ChatInputs/ChatInputs.js +2 -2
  9. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  10. package/dist/components/IconButton/IconButton.css +144 -0
  11. package/dist/components/IconButton/IconButton.d.ts +9 -0
  12. package/dist/components/IconButton/IconButton.js +36 -0
  13. package/dist/components/IconButton/IconButton.js.map +1 -0
  14. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
  15. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  16. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +204 -59
  17. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  18. package/dist/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js +1 -1
  19. package/dist/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js.map +1 -1
  20. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
  21. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js +76 -16
  22. package/dist/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
  23. package/dist/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
  24. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
  25. package/dist/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
  26. package/dist/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
  27. package/dist/components/MemoriArtifactSystem/utils/applyEdits.js +117 -0
  28. package/dist/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
  29. package/dist/components/MobileSessionPanel/MobileSessionPanel.css +29 -7
  30. package/dist/components/MobileSessionPanel/MobileSessionPanel.d.ts +3 -0
  31. package/dist/components/MobileSessionPanel/MobileSessionPanel.js +8 -6
  32. package/dist/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
  33. package/dist/components/PositionPopover/PositionPopover.d.ts +2 -0
  34. package/dist/components/PositionPopover/PositionPopover.js +19 -11
  35. package/dist/components/PositionPopover/PositionPopover.js.map +1 -1
  36. package/dist/components/UploadButton/UploadButton.d.ts +1 -0
  37. package/dist/components/UploadButton/UploadButton.js +12 -18
  38. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  39. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +1 -6
  40. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +7 -139
  41. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  42. package/dist/components/icons/Print.js +1 -1
  43. package/dist/components/icons/Print.js.map +1 -1
  44. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +24 -3
  45. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  46. package/dist/components/layouts/WebsiteAssistant/website-assistant.css +137 -55
  47. package/dist/components/layouts/fullpage.css +165 -68
  48. package/dist/helpers/constants.d.ts +1 -0
  49. package/dist/helpers/constants.js +24 -1
  50. package/dist/helpers/constants.js.map +1 -1
  51. package/dist/helpers/convertDocument.d.ts +1 -0
  52. package/dist/helpers/convertDocument.js +24 -0
  53. package/dist/helpers/convertDocument.js.map +1 -0
  54. package/dist/helpers/message.js +3 -0
  55. package/dist/helpers/message.js.map +1 -1
  56. package/dist/helpers/positionPopover.d.ts +1 -1
  57. package/dist/helpers/positionPopover.js +2 -2
  58. package/dist/helpers/positionPopover.js.map +1 -1
  59. package/dist/helpers/tts/muteSpeaker.d.ts +13 -0
  60. package/dist/helpers/tts/muteSpeaker.js +26 -0
  61. package/dist/helpers/tts/muteSpeaker.js.map +1 -0
  62. package/dist/locales/de.json +3 -0
  63. package/dist/locales/en.json +3 -0
  64. package/dist/locales/es.json +3 -0
  65. package/dist/locales/fr.json +3 -0
  66. package/dist/locales/it.json +3 -0
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +1 -1
  70. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +1 -1
  71. package/esm/components/Chat/Chat.js +1 -1
  72. package/esm/components/Chat/Chat.js.map +1 -1
  73. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +6 -1
  74. package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
  75. package/esm/components/ChatInputs/ChatInputs.js +2 -2
  76. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  77. package/esm/components/IconButton/IconButton.css +144 -0
  78. package/esm/components/IconButton/IconButton.d.ts +9 -0
  79. package/esm/components/IconButton/IconButton.js +33 -0
  80. package/esm/components/IconButton/IconButton.js.map +1 -0
  81. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +1 -0
  82. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  83. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +205 -60
  84. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  85. package/esm/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js +2 -2
  86. package/esm/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.js.map +1 -1
  87. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.d.ts +3 -1
  88. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js +77 -17
  89. package/esm/components/MemoriArtifactSystem/context/ArtifactContext.js.map +1 -1
  90. package/esm/components/MemoriArtifactSystem/types/artifact.types.d.ts +11 -0
  91. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js +18 -13
  92. package/esm/components/MemoriArtifactSystem/utils/ArtifactAPI.js.map +1 -1
  93. package/esm/components/MemoriArtifactSystem/utils/applyEdits.d.ts +8 -0
  94. package/esm/components/MemoriArtifactSystem/utils/applyEdits.js +112 -0
  95. package/esm/components/MemoriArtifactSystem/utils/applyEdits.js.map +1 -0
  96. package/esm/components/MobileSessionPanel/MobileSessionPanel.css +29 -7
  97. package/esm/components/MobileSessionPanel/MobileSessionPanel.d.ts +3 -0
  98. package/esm/components/MobileSessionPanel/MobileSessionPanel.js +8 -6
  99. package/esm/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
  100. package/esm/components/PositionPopover/PositionPopover.d.ts +2 -0
  101. package/esm/components/PositionPopover/PositionPopover.js +20 -12
  102. package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
  103. package/esm/components/UploadButton/UploadButton.d.ts +1 -0
  104. package/esm/components/UploadButton/UploadButton.js +13 -19
  105. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  106. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +1 -6
  107. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +8 -140
  108. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  109. package/esm/components/icons/Print.js +1 -1
  110. package/esm/components/icons/Print.js.map +1 -1
  111. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +24 -3
  112. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  113. package/esm/components/layouts/WebsiteAssistant/website-assistant.css +137 -55
  114. package/esm/components/layouts/fullpage.css +165 -68
  115. package/esm/helpers/constants.d.ts +1 -0
  116. package/esm/helpers/constants.js +23 -0
  117. package/esm/helpers/constants.js.map +1 -1
  118. package/esm/helpers/convertDocument.d.ts +1 -0
  119. package/esm/helpers/convertDocument.js +20 -0
  120. package/esm/helpers/convertDocument.js.map +1 -0
  121. package/esm/helpers/message.js +3 -0
  122. package/esm/helpers/message.js.map +1 -1
  123. package/esm/helpers/positionPopover.d.ts +1 -1
  124. package/esm/helpers/positionPopover.js +2 -2
  125. package/esm/helpers/positionPopover.js.map +1 -1
  126. package/esm/helpers/tts/muteSpeaker.d.ts +13 -0
  127. package/esm/helpers/tts/muteSpeaker.js +21 -0
  128. package/esm/helpers/tts/muteSpeaker.js.map +1 -0
  129. package/esm/locales/de.json +3 -0
  130. package/esm/locales/en.json +3 -0
  131. package/esm/locales/es.json +3 -0
  132. package/esm/locales/fr.json +3 -0
  133. package/esm/locales/it.json +3 -0
  134. package/esm/version.d.ts +1 -1
  135. package/esm/version.js +1 -1
  136. package/package.json +2 -2
  137. package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +1 -1
  138. package/src/components/Chat/Chat.tsx +1 -0
  139. package/src/components/ChatInputs/ChatInputs.tsx +3 -0
  140. package/src/components/FilePreview/FilePreview.stories.tsx +0 -3
  141. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +1 -0
  142. package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +324 -133
  143. package/src/components/MemoriArtifactSystem/components/ArtifactPreview/ArtifactPreview.tsx +2 -2
  144. package/src/components/MemoriArtifactSystem/context/ArtifactContext.tsx +122 -24
  145. package/src/components/MemoriArtifactSystem/types/artifact.types.ts +20 -1
  146. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.test.tsx +1 -0
  147. package/src/components/MemoriArtifactSystem/utils/ArtifactAPI.tsx +19 -13
  148. package/src/components/MemoriArtifactSystem/utils/applyEdits.test.ts +85 -0
  149. package/src/components/MemoriArtifactSystem/utils/applyEdits.ts +173 -0
  150. package/src/components/UploadButton/UploadButton.stories.tsx +2 -2
  151. package/src/components/UploadButton/UploadButton.tsx +19 -19
  152. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +13 -187
  153. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
  154. package/src/components/icons/Print.tsx +6 -2
  155. package/src/helpers/constants.ts +24 -0
  156. package/src/helpers/convertDocument.test.ts +67 -0
  157. package/src/helpers/convertDocument.ts +35 -0
  158. package/src/helpers/message.ts +15 -0
  159. package/src/locales/de.json +3 -0
  160. package/src/locales/en.json +3 -0
  161. package/src/locales/es.json +3 -0
  162. package/src/locales/fr.json +3 -0
  163. package/src/locales/it.json +3 -0
  164. package/src/version.ts +1 -1
package/esm/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '8.42.0';
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.42.0",
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.12",
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 = 2.5;
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
  };
@@ -378,6 +378,7 @@ const ArtifactDrawer: React.FC<{ isChatLogPanel?: boolean }> = ({
378
378
  timestamp: new Date(),
379
379
  size: 0,
380
380
  id: '',
381
+ artifactId: '',
381
382
  })}
382
383
  disabled={false}
383
384
  className="memori-artifact-action-btn"
@@ -1,6 +1,13 @@
1
- import React, { useEffect, useState, useMemo, useCallback, memo } from '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
- * Lightweight deterministic hash of the first 200 chars of a string.
37
- * Used to produce stable artifact IDs that survive re-renders.
38
- */
39
- const hashContent = (str: string): string => {
40
- let hash = 0;
41
- const len = Math.min(str.length, 200);
42
- for (let i = 0; i < len; i++) {
43
- hash = (hash << 5) - hash + str.charCodeAt(i);
44
- hash |= 0; // Convert to 32-bit int
45
- }
46
- return Math.abs(hash).toString(36);
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
- * Returns true when the `class` attribute value of an opening tag contains
51
- * the word "memori-artifact" as a whole word (handles multiple classes).
52
- *
53
- * Examples that match:
54
- * class="memori-artifact"
55
- * class="memori-artifact extra-class"
56
- * class="foo memori-artifact"
57
- * class='memori-artifact'
58
- * class = "memori-artifact extra"
77
+ * True for update tags:
78
+ * - preferred: class="memori-artifact" data-action="update"
79
+ * - legacy: class="memori-artifact-update"
59
80
  */
60
- const CLASS_ATTR_RE = /class\s*=\s*["']([^"']*)["']/i;
61
- const hasMemoriArtifactClass = (openingTag: string): boolean => {
62
- const m = openingTag.match(CLASS_ATTR_RE);
63
- if (!m) return false;
64
- // \b word-boundary ensures we don't match "foo-memori-artifact-bar"
65
- return /\bmemori-artifact\b/.test(m[1]);
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
- * Parser-style artifact detector.
70
- *
71
- * Replaces the previous single-regex approach with a depth-tracking loop so
72
- * that:
73
- * - Multiple CSS classes are handled correctly (e.g. class="memori-artifact foo")
74
- * - Nested / self-contained <output> tags inside the content don't truncate it
75
- * - Unclosed tags are gracefully skipped
76
- * - IDs are stable across re-renders (derived from message + content hash)
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]; // e.g. <output class="memori-artifact" data-mimetype="text/html">
100
- const openStart = openMatch.index;
101
- const openEnd = openStart + fullOpenTag.length;
142
+ const fullOpenTag = openMatch[0];
143
+ const openEnd = openMatch.index + fullOpenTag.length;
102
144
 
103
- // Only process tags that carry the required class
104
- if (!hasMemoriArtifactClass(fullOpenTag)) {
145
+ if (!isCreateArtifactTag(fullOpenTag)) {
105
146
  searchFrom = openEnd;
106
147
  continue;
107
148
  }
108
149
 
109
- // Depth-tracking scan to find the matching </output>
110
- let depth = 1;
111
- let pos = openEnd;
112
- let closeStart = -1;
113
-
114
- while (pos < cleaned.length && depth > 0) {
115
- const nextOpen = cleaned.indexOf('<output', pos);
116
- const nextClose = cleaned.indexOf('</output>', pos);
117
-
118
- if (nextClose === -1) {
119
- // No closing tag found — treat everything until EOF as content
120
- // (handles streaming / partial messages)
121
- closeStart = cleaned.length;
122
- depth = 0;
123
- break;
124
- }
125
-
126
- if (nextOpen !== -1 && nextOpen < nextClose) {
127
- // There's another opening tag before the next closing tag → go deeper
128
- depth++;
129
- pos = nextOpen + '<output'.length;
130
- } else {
131
- // Found a closing tag
132
- depth--;
133
- if (depth === 0) {
134
- closeStart = nextClose;
135
- } else {
136
- pos = nextClose + '</output>'.length;
137
- }
138
- }
139
- }
140
-
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
- // Stable ID: does not change across re-renders for the same content
167
- const stableId = `artifact-${messageKey}-${artifactNum}-${hashContent(
168
- content
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: stableId,
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 { openArtifact, state, closeArtifact } = useArtifact();
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
- * Auto-open the first artifact when a new message arrives.
243
- *
244
- * FIX: messageText is now included in the dependency array so the effect
245
- * re-fires correctly if the text changes without the messageKey changing
246
- * (e.g. streaming updates).
247
- */
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 || artifacts.length === 0) return;
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
- artifacts.forEach(artifact => dispatchArtifactCreatedEvent(artifact));
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
- if (!isChatlogPanel) {
254
- const timer = setTimeout(() => openArtifact(artifacts[0]), 100);
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
- if (artifacts.length === 0) return null;
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
- {isChatlogPanel ? (
327
- isSelected ? (
328
- <ChevronUp className="memori-artifact-handler-action-icon" />
329
- ) : (
330
- <ChevronDown className="memori-artifact-handler-action-icon" />
331
- )
332
- ) : isSelected ? (
333
- <ChevronLeft className="memori-artifact-handler-action-icon" />
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 || '';