@memori.ai/memori-react 9.0.0-alpha.5 → 9.0.0-alpha.7

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 (286) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/components/AgeVerificationModal/AgeVerificationModal.js +8 -3
  3. package/dist/components/AgeVerificationModal/AgeVerificationModal.js.map +1 -1
  4. package/dist/components/Auth/Auth.js +3 -1
  5. package/dist/components/Auth/Auth.js.map +1 -1
  6. package/dist/components/Avatar/Avatar.js +5 -10
  7. package/dist/components/Avatar/Avatar.js.map +1 -1
  8. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +8 -1
  9. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  10. package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js +1 -1
  11. package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js.map +1 -1
  12. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -0
  13. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +14 -5
  14. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
  15. package/dist/components/Chat/Chat.js +3 -1
  16. package/dist/components/Chat/Chat.js.map +1 -1
  17. package/dist/components/ChatBubble/ChatBubble.js +21 -15
  18. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  19. package/dist/components/ChatHistoryDrawer/ChatHistory.js +3 -1
  20. package/dist/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
  21. package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js +6 -3
  22. package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
  23. package/dist/components/ContentPreviewModal/ContentPreviewModal.js +3 -1
  24. package/dist/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
  25. package/dist/components/DateSelector/DateSelector.d.ts +2 -1
  26. package/dist/components/DateSelector/DateSelector.js +17 -7
  27. package/dist/components/DateSelector/DateSelector.js.map +1 -1
  28. package/dist/components/ExpertsDrawer/ExpertsDrawer.js +3 -1
  29. package/dist/components/ExpertsDrawer/ExpertsDrawer.js.map +1 -1
  30. package/dist/components/FilePreview/FilePreview.js +22 -5
  31. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  32. package/dist/components/KnownFacts/KnownFacts.js +5 -3
  33. package/dist/components/KnownFacts/KnownFacts.js.map +1 -1
  34. package/dist/components/LoginDrawer/LoginDrawer.js +3 -1
  35. package/dist/components/LoginDrawer/LoginDrawer.js.map +1 -1
  36. package/dist/components/MediaWidget/MediaItemWidget.js +14 -12
  37. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  38. package/dist/components/MediaWidget/MediaItemWidget.utils.d.ts +3 -2
  39. package/dist/components/MediaWidget/MediaItemWidget.utils.js +28 -6
  40. package/dist/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  41. package/dist/components/MediaWidget/MediaPreviewModal.js +6 -7
  42. package/dist/components/MediaWidget/MediaPreviewModal.js.map +1 -1
  43. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +19 -5
  44. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +4 -1
  45. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  46. package/dist/components/MemoriWidget/MemoriWidget.css +4 -2
  47. package/dist/components/MemoriWidget/MemoriWidget.js +760 -404
  48. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  49. package/dist/components/SettingsDrawer/SettingsDrawer.js +3 -1
  50. package/dist/components/SettingsDrawer/SettingsDrawer.js.map +1 -1
  51. package/dist/components/StartPanel/StartPanel.js +3 -1
  52. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  53. package/dist/components/UploadButton/UploadButton.js +21 -5
  54. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  55. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +126 -60
  56. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  57. package/dist/components/UploadButton/UploadImages/UploadImages.js +3 -1
  58. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  59. package/dist/components/WhyThisAnswer/WhyThisAnswer.js +3 -1
  60. package/dist/components/WhyThisAnswer/WhyThisAnswer.js.map +1 -1
  61. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +3 -1
  62. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  63. package/dist/components/layouts/WebsiteAssistant/website-assistant.css +21 -1
  64. package/dist/components/layouts/chat.css +2 -1
  65. package/dist/components/layouts/fullpage.css +31 -8
  66. package/dist/components/layouts/hidden-chat.css +1 -0
  67. package/dist/components/layouts/totem.css +2 -1
  68. package/dist/components/layouts/zoomed-full-body.css +32 -7
  69. package/dist/context/widgetSurfaceContext.d.ts +3 -0
  70. package/dist/context/widgetSurfaceContext.js +11 -0
  71. package/dist/context/widgetSurfaceContext.js.map +1 -0
  72. package/dist/helpers/constants.d.ts +3 -0
  73. package/dist/helpers/constants.js +27 -1
  74. package/dist/helpers/constants.js.map +1 -1
  75. package/dist/helpers/credits.d.ts +5 -2
  76. package/dist/helpers/credits.js +5 -1
  77. package/dist/helpers/credits.js.map +1 -1
  78. package/dist/helpers/message.d.ts +1 -1
  79. package/dist/helpers/message.js +4 -4
  80. package/dist/helpers/message.js.map +1 -1
  81. package/dist/helpers/nats/getNatsConfig.d.ts +9 -0
  82. package/dist/helpers/nats/getNatsConfig.js +65 -0
  83. package/dist/helpers/nats/getNatsConfig.js.map +1 -0
  84. package/dist/helpers/nats/isSessionExpiredError.d.ts +3 -0
  85. package/dist/helpers/nats/isSessionExpiredError.js +36 -0
  86. package/dist/helpers/nats/isSessionExpiredError.js.map +1 -0
  87. package/dist/helpers/nats/useNats.d.ts +12 -0
  88. package/dist/helpers/nats/useNats.js +75 -0
  89. package/dist/helpers/nats/useNats.js.map +1 -0
  90. package/dist/helpers/nats/useNatsSession.d.ts +28 -0
  91. package/dist/helpers/nats/useNatsSession.js +212 -0
  92. package/dist/helpers/nats/useNatsSession.js.map +1 -0
  93. package/dist/helpers/utils.d.ts +21 -0
  94. package/dist/helpers/utils.js +75 -4
  95. package/dist/helpers/utils.js.map +1 -1
  96. package/dist/locales/de.json +1 -0
  97. package/dist/locales/en.json +1 -0
  98. package/dist/locales/es.json +1 -0
  99. package/dist/locales/fr.json +1 -0
  100. package/dist/locales/it.json +1 -0
  101. package/dist/styles.css +1 -1
  102. package/dist/types/integration.d.ts +3 -0
  103. package/dist/types/integration.js +16 -0
  104. package/dist/types/integration.js.map +1 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/esm/components/AgeVerificationModal/AgeVerificationModal.js +8 -3
  108. package/esm/components/AgeVerificationModal/AgeVerificationModal.js.map +1 -1
  109. package/esm/components/Auth/Auth.js +3 -1
  110. package/esm/components/Auth/Auth.js.map +1 -1
  111. package/esm/components/Avatar/Avatar.js +5 -10
  112. package/esm/components/Avatar/Avatar.js.map +1 -1
  113. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +9 -2
  114. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  115. package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js +2 -2
  116. package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js.map +1 -1
  117. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -0
  118. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +13 -4
  119. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
  120. package/esm/components/Chat/Chat.js +3 -1
  121. package/esm/components/Chat/Chat.js.map +1 -1
  122. package/esm/components/ChatBubble/ChatBubble.js +21 -15
  123. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  124. package/esm/components/ChatHistoryDrawer/ChatHistory.js +3 -1
  125. package/esm/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
  126. package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js +6 -3
  127. package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
  128. package/esm/components/ContentPreviewModal/ContentPreviewModal.js +3 -1
  129. package/esm/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
  130. package/esm/components/DateSelector/DateSelector.d.ts +2 -1
  131. package/esm/components/DateSelector/DateSelector.js +17 -7
  132. package/esm/components/DateSelector/DateSelector.js.map +1 -1
  133. package/esm/components/ExpertsDrawer/ExpertsDrawer.js +3 -1
  134. package/esm/components/ExpertsDrawer/ExpertsDrawer.js.map +1 -1
  135. package/esm/components/FilePreview/FilePreview.js +24 -7
  136. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  137. package/esm/components/KnownFacts/KnownFacts.js +5 -3
  138. package/esm/components/KnownFacts/KnownFacts.js.map +1 -1
  139. package/esm/components/LoginDrawer/LoginDrawer.js +3 -1
  140. package/esm/components/LoginDrawer/LoginDrawer.js.map +1 -1
  141. package/esm/components/MediaWidget/MediaItemWidget.js +16 -14
  142. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  143. package/esm/components/MediaWidget/MediaItemWidget.utils.d.ts +3 -2
  144. package/esm/components/MediaWidget/MediaItemWidget.utils.js +27 -6
  145. package/esm/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  146. package/esm/components/MediaWidget/MediaPreviewModal.js +6 -7
  147. package/esm/components/MediaWidget/MediaPreviewModal.js.map +1 -1
  148. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +19 -5
  149. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +4 -1
  150. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  151. package/esm/components/MemoriWidget/MemoriWidget.css +4 -2
  152. package/esm/components/MemoriWidget/MemoriWidget.js +760 -404
  153. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  154. package/esm/components/SettingsDrawer/SettingsDrawer.js +3 -1
  155. package/esm/components/SettingsDrawer/SettingsDrawer.js.map +1 -1
  156. package/esm/components/StartPanel/StartPanel.js +3 -1
  157. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  158. package/esm/components/UploadButton/UploadButton.js +21 -5
  159. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  160. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +126 -60
  161. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  162. package/esm/components/UploadButton/UploadImages/UploadImages.js +3 -1
  163. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  164. package/esm/components/WhyThisAnswer/WhyThisAnswer.js +3 -1
  165. package/esm/components/WhyThisAnswer/WhyThisAnswer.js.map +1 -1
  166. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +3 -1
  167. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  168. package/esm/components/layouts/WebsiteAssistant/website-assistant.css +21 -1
  169. package/esm/components/layouts/chat.css +2 -1
  170. package/esm/components/layouts/fullpage.css +31 -8
  171. package/esm/components/layouts/hidden-chat.css +1 -0
  172. package/esm/components/layouts/totem.css +2 -1
  173. package/esm/components/layouts/zoomed-full-body.css +32 -7
  174. package/esm/context/widgetSurfaceContext.d.ts +3 -0
  175. package/esm/context/widgetSurfaceContext.js +7 -0
  176. package/esm/context/widgetSurfaceContext.js.map +1 -0
  177. package/esm/helpers/constants.d.ts +3 -0
  178. package/esm/helpers/constants.js +26 -0
  179. package/esm/helpers/constants.js.map +1 -1
  180. package/esm/helpers/credits.d.ts +5 -2
  181. package/esm/helpers/credits.js +5 -1
  182. package/esm/helpers/credits.js.map +1 -1
  183. package/esm/helpers/message.d.ts +1 -1
  184. package/esm/helpers/message.js +2 -2
  185. package/esm/helpers/message.js.map +1 -1
  186. package/esm/helpers/nats/getNatsConfig.d.ts +9 -0
  187. package/esm/helpers/nats/getNatsConfig.js +61 -0
  188. package/esm/helpers/nats/getNatsConfig.js.map +1 -0
  189. package/esm/helpers/nats/isSessionExpiredError.d.ts +3 -0
  190. package/esm/helpers/nats/isSessionExpiredError.js +32 -0
  191. package/esm/helpers/nats/isSessionExpiredError.js.map +1 -0
  192. package/esm/helpers/nats/useNats.d.ts +12 -0
  193. package/esm/helpers/nats/useNats.js +72 -0
  194. package/esm/helpers/nats/useNats.js.map +1 -0
  195. package/esm/helpers/nats/useNatsSession.d.ts +28 -0
  196. package/esm/helpers/nats/useNatsSession.js +208 -0
  197. package/esm/helpers/nats/useNatsSession.js.map +1 -0
  198. package/esm/helpers/utils.d.ts +21 -0
  199. package/esm/helpers/utils.js +68 -3
  200. package/esm/helpers/utils.js.map +1 -1
  201. package/esm/locales/de.json +1 -0
  202. package/esm/locales/en.json +1 -0
  203. package/esm/locales/es.json +1 -0
  204. package/esm/locales/fr.json +1 -0
  205. package/esm/locales/it.json +1 -0
  206. package/esm/styles.css +1 -1
  207. package/esm/types/integration.d.ts +3 -0
  208. package/esm/types/integration.js +11 -0
  209. package/esm/types/integration.js.map +1 -0
  210. package/esm/version.d.ts +1 -1
  211. package/esm/version.js +1 -1
  212. package/package.json +5 -3
  213. package/src/components/AgeVerificationModal/AgeVerificationModal.tsx +9 -1
  214. package/src/components/Auth/Auth.tsx +3 -0
  215. package/src/components/Avatar/Avatar.stories.tsx +45 -28
  216. package/src/components/Avatar/Avatar.test.tsx +54 -0
  217. package/src/components/Avatar/Avatar.tsx +8 -12
  218. package/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +12 -5
  219. package/src/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.ts +2 -1
  220. package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +16 -4
  221. package/src/components/Avatar/__snapshots__/Avatar.test.tsx.snap +164 -0
  222. package/src/components/Chat/Chat.tsx +3 -0
  223. package/src/components/ChatBubble/ChatBubble.tsx +40 -12
  224. package/src/components/ChatHistoryDrawer/ChatHistory.tsx +3 -0
  225. package/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx +8 -4
  226. package/src/components/ContentPreviewModal/ContentPreviewModal.tsx +3 -0
  227. package/src/components/DateSelector/DateSelector.stories.tsx +11 -2
  228. package/src/components/DateSelector/DateSelector.tsx +28 -9
  229. package/src/components/ExpertsDrawer/ExpertsDrawer.tsx +3 -0
  230. package/src/components/FilePreview/FilePreview.tsx +28 -7
  231. package/src/components/KnownFacts/KnownFacts.tsx +5 -0
  232. package/src/components/LoginDrawer/LoginDrawer.tsx +3 -0
  233. package/src/components/MediaWidget/MediaItemWidget.tsx +26 -13
  234. package/src/components/MediaWidget/MediaItemWidget.utils.test.ts +45 -2
  235. package/src/components/MediaWidget/MediaItemWidget.utils.ts +39 -6
  236. package/src/components/MediaWidget/MediaPreviewModal.tsx +9 -7
  237. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +19 -5
  238. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +4 -0
  239. package/src/components/MemoriWidget/MemoriWidget.css +4 -2
  240. package/src/components/MemoriWidget/MemoriWidget.tsx +818 -349
  241. package/src/components/SettingsDrawer/SettingsDrawer.tsx +3 -0
  242. package/src/components/StartPanel/StartPanel.stories.tsx +22 -0
  243. package/src/components/StartPanel/StartPanel.test.tsx +64 -1
  244. package/src/components/StartPanel/StartPanel.tsx +3 -0
  245. package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +261 -0
  246. package/src/components/UploadButton/UploadButton.tsx +30 -11
  247. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +151 -61
  248. package/src/components/UploadButton/UploadImages/UploadImages.tsx +3 -0
  249. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
  250. package/src/components/WhyThisAnswer/WhyThisAnswer.tsx +3 -0
  251. package/src/components/layouts/WebsiteAssistant/WebsiteAssistant.tsx +4 -0
  252. package/src/components/layouts/WebsiteAssistant/__snapshots__/WebsiteAssistant.test.tsx.snap +48 -44
  253. package/src/components/layouts/WebsiteAssistant/website-assistant.css +21 -1
  254. package/src/components/layouts/__snapshots__/Chat.test.tsx.snap +533 -529
  255. package/src/components/layouts/__snapshots__/FullPage.test.tsx.snap +1058 -1050
  256. package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +443 -439
  257. package/src/components/layouts/__snapshots__/Totem.test.tsx.snap +584 -580
  258. package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +528 -524
  259. package/src/components/layouts/chat.css +2 -1
  260. package/src/components/layouts/fullpage.css +31 -8
  261. package/src/components/layouts/hidden-chat.css +1 -0
  262. package/src/components/layouts/layouts.stories.tsx +131 -12
  263. package/src/components/layouts/totem.css +2 -1
  264. package/src/components/layouts/zoomed-full-body.css +32 -7
  265. package/src/context/widgetSurfaceContext.tsx +14 -0
  266. package/src/helpers/constants.ts +32 -1
  267. package/src/helpers/credits.ts +16 -1
  268. package/src/helpers/message.test.ts +28 -1
  269. package/src/helpers/message.ts +4 -4
  270. package/src/helpers/nats/getNatsConfig.ts +145 -0
  271. package/src/helpers/nats/isSessionExpiredError.test.ts +38 -0
  272. package/src/helpers/nats/isSessionExpiredError.ts +51 -0
  273. package/src/helpers/nats/useNats.ts +124 -0
  274. package/src/helpers/nats/useNatsSession.ts +394 -0
  275. package/src/helpers/utils.test.ts +162 -0
  276. package/src/helpers/utils.ts +108 -3
  277. package/src/index.stories.tsx +32 -0
  278. package/src/locales/de.json +1 -0
  279. package/src/locales/en.json +1 -0
  280. package/src/locales/es.json +1 -0
  281. package/src/locales/fr.json +1 -0
  282. package/src/locales/it.json +1 -0
  283. package/src/styles.css +61 -1
  284. package/src/types/integration.test.ts +32 -0
  285. package/src/types/integration.ts +31 -0
  286. package/src/version.ts +1 -1
@@ -36,6 +36,7 @@ import {
36
36
  import { Expandable, Modal } from '@memori.ai/ui';
37
37
  import memoriApiClient from '@memori.ai/memori-api-client';
38
38
  import { usePressTooltip } from '../../helpers/usePressTooltip';
39
+ import { useWidgetSurfaceEl } from '../../context/widgetSurfaceContext';
39
40
  const ASSET_URL_PATTERN = /https?:\/\/\S*\/api\/v\d+\/asset\/\S+/gi;
40
41
 
41
42
  const sanitizeRawCopyText = (text: string) =>
@@ -122,6 +123,7 @@ const ChatBubble: React.FC<Props> = ({
122
123
  customMediaRenderer,
123
124
  }) => {
124
125
  const { t, i18n } = useTranslation();
126
+ const surfaceEl = useWidgetSurfaceEl();
125
127
  const lang = i18n.language || 'en';
126
128
  const generatedByAiTooltip = usePressTooltip();
127
129
  const [showingWhyThisAnswer, setShowingWhyThisAnswer] = useState(false);
@@ -129,13 +131,16 @@ const ChatBubble: React.FC<Props> = ({
129
131
  const [copyStatus, setCopyStatus] = useState<{
130
132
  plain: 'idle' | 'success' | 'error';
131
133
  raw: 'idle' | 'success' | 'error';
132
- }>({ plain: 'idle', raw: 'idle' });
134
+ functionCache: 'idle' | 'success' | 'error';
135
+ }>({ plain: 'idle', raw: 'idle', functionCache: 'idle' });
133
136
  const copyResetTimers = useRef<{
134
137
  plain: ReturnType<typeof setTimeout> | null;
135
138
  raw: ReturnType<typeof setTimeout> | null;
139
+ functionCache: ReturnType<typeof setTimeout> | null;
136
140
  }>({
137
141
  plain: null,
138
142
  raw: null,
143
+ functionCache: null,
139
144
  });
140
145
  // Initialize MathJax on component mount
141
146
  useEffect(() => {
@@ -244,26 +249,33 @@ const ChatBubble: React.FC<Props> = ({
244
249
 
245
250
  useEffect(() => {
246
251
  return () => {
247
- (Object.keys(copyResetTimers.current) as Array<'plain' | 'raw'>).forEach(
248
- key => {
249
- const timer = copyResetTimers.current[key];
250
- if (timer) {
251
- clearTimeout(timer);
252
- copyResetTimers.current[key] = null;
253
- }
252
+ (
253
+ Object.keys(copyResetTimers.current) as Array<
254
+ 'plain' | 'raw' | 'functionCache'
255
+ >
256
+ ).forEach(key => {
257
+ const timer = copyResetTimers.current[key];
258
+ if (timer) {
259
+ clearTimeout(timer);
260
+ copyResetTimers.current[key] = null;
254
261
  }
255
- );
262
+ });
256
263
  };
257
264
  }, []);
258
265
 
259
- const clearCopyReset = (type: 'plain' | 'raw') => {
266
+ const functionCacheCopyText =
267
+ functionCacheData
268
+ ?.map(f => `${f.title}\n\n${f.content}`)
269
+ .join('\n\n---\n\n') ?? '';
270
+
271
+ const clearCopyReset = (type: 'plain' | 'raw' | 'functionCache') => {
260
272
  if (copyResetTimers.current[type]) {
261
273
  clearTimeout(copyResetTimers.current[type]!);
262
274
  copyResetTimers.current[type] = null;
263
275
  }
264
276
  };
265
277
 
266
- const scheduleCopyReset = (type: 'plain' | 'raw') => {
278
+ const scheduleCopyReset = (type: 'plain' | 'raw' | 'functionCache') => {
267
279
  clearCopyReset(type);
268
280
  copyResetTimers.current[type] = setTimeout(() => {
269
281
  setCopyStatus(prev => ({ ...prev, [type]: 'idle' }));
@@ -271,7 +283,10 @@ const ChatBubble: React.FC<Props> = ({
271
283
  }, COPY_FEEDBACK_MS);
272
284
  };
273
285
 
274
- const handleCopyClick = async (type: 'plain' | 'raw', text: string) => {
286
+ const handleCopyClick = async (
287
+ type: 'plain' | 'raw' | 'functionCache',
288
+ text: string
289
+ ) => {
275
290
  if (!text?.length) return;
276
291
  try {
277
292
  if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
@@ -821,12 +836,25 @@ const ChatBubble: React.FC<Props> = ({
821
836
  )}
822
837
 
823
838
  <Modal
839
+ container={surfaceEl ?? undefined}
824
840
  open={openFunctionCache}
825
841
  className="memori-chat--function-cache-modal"
826
842
  title={t('functionCache') || 'Function Cache'}
827
843
  closable={true}
828
844
  onOpenChange={setOpenFunctionCache}
829
845
  >
846
+ <div style={{ marginBottom: '1rem' }}>
847
+ <Button
848
+ icon={<Copy aria-hidden />}
849
+ onClick={() =>
850
+ handleCopyClick('functionCache', functionCacheCopyText)
851
+ }
852
+ >
853
+ {copyStatus.functionCache === 'success'
854
+ ? copiedLabel
855
+ : t('copy') || 'Copy'}
856
+ </Button>
857
+ </div>
830
858
  {functionCacheData?.map((f, i) => (
831
859
  <div
832
860
  key={f.mediumID}
@@ -21,6 +21,7 @@ import { stripAllInternalTags } from '../../helpers/message';
21
21
  import debounce from 'lodash/debounce';
22
22
  import { Spin } from '@memori.ai/ui';
23
23
  import { SelectBox } from '@memori.ai/ui';
24
+ import { useWidgetSurfaceEl } from '../../context/widgetSurfaceContext';
24
25
  // Helpers / Utils
25
26
  import { getTranslation } from '../../helpers/translations';
26
27
  import ChatResumeDrawer from './ChatResumeDrawer';
@@ -440,6 +441,7 @@ const ChatHistoryDrawer = ({
440
441
  userLang,
441
442
  isMultilanguageEnabled = false,
442
443
  }: Props) => {
444
+ const surfaceEl = useWidgetSurfaceEl();
443
445
  const { t } = useTranslation();
444
446
  const { add } = useAlertManager();
445
447
  const { getUserChatLogsByTokenPaged } = apiClient.chatLogs;
@@ -1136,6 +1138,7 @@ const ChatHistoryDrawer = ({
1136
1138
 
1137
1139
  return (
1138
1140
  <Drawer
1141
+ container={surfaceEl ?? undefined}
1139
1142
  className="memori-chat-history-drawer"
1140
1143
  open={open}
1141
1144
  onClose={onClose}
@@ -33,17 +33,21 @@ const PROVIDER_CONFIGS: Record<string, ProviderConfig> = {
33
33
  statusPage: 'https://status.mistral-data.com/',
34
34
  apiComponentName: ['Mistral'],
35
35
  },
36
+ // Use status.claude.com directly: status.anthropic.com 302-redirects and
37
+ // browsers block the redirected response (missing CORS on the redirect).
36
38
  Anthropic: {
37
- statusUrl: 'https://status.anthropic.com/api/v2/summary.json',
38
- statusPage: 'https://status.anthropic.com/',
39
- apiComponentName: ['api.anthropic.com'],
39
+ statusUrl: 'https://status.claude.com/api/v2/summary.json',
40
+ statusPage: 'https://status.claude.com/',
41
+ apiComponentName: [
42
+ 'Claude API (api.anthropic.com)',
43
+ 'api.anthropic.com',
44
+ ],
40
45
  },
41
46
  };
42
47
 
43
48
  // Modified to handle fetch errors, different component names, and check incidents
44
49
  const fetchProviderStatus = async (config: ProviderConfig): Promise<Status> => {
45
50
  try {
46
- // Use cross-fetch instead of native fetch
47
51
  const response = await fetch(config.statusUrl);
48
52
  if (!response.ok) {
49
53
  console.warn(`Status API returned ${response.status}`);
@@ -1,6 +1,7 @@
1
1
  import React, { FC } from 'react';
2
2
  import { Modal } from '@memori.ai/ui';
3
3
  import cx from 'classnames';
4
+ import { useWidgetSurfaceEl } from '../../context/widgetSurfaceContext';
4
5
 
5
6
  export interface ContentPreviewModalProps {
6
7
  open: boolean;
@@ -25,10 +26,12 @@ const ContentPreviewModal: FC<ContentPreviewModalProps> = ({
25
26
  children,
26
27
  className,
27
28
  }) => {
29
+ const surfaceEl = useWidgetSurfaceEl();
28
30
  const width = 'min(90vw, 800px)';
29
31
 
30
32
  return (
31
33
  <Modal
34
+ container={surfaceEl ?? undefined}
32
35
  open={open}
33
36
  onClose={() => onClose()}
34
37
  width={width}
@@ -25,14 +25,23 @@ const Template: Story<Props> = args => (
25
25
  // https://storybook.js.org/docs/react/workflows/unit-testing
26
26
  export const Default = Template.bind({});
27
27
  Default.args = {
28
- defaultDate: new Date(Date.now()),
28
+ defaultDate: '1990-06-15',
29
+ minAge: 18,
30
+ onChange: console.log,
31
+ disabled: false,
32
+ };
33
+
34
+ export const MinAge14 = Template.bind({});
35
+ MinAge14.args = {
36
+ minAge: 14,
29
37
  onChange: console.log,
30
38
  disabled: false,
31
39
  };
32
40
 
33
41
  export const Disabled = Template.bind({});
34
42
  Disabled.args = {
35
- defaultDate: new Date(Date.now()),
43
+ defaultDate: '1990-06-15',
44
+ minAge: 18,
36
45
  onChange: () => {},
37
46
  disabled: true,
38
47
  };
@@ -5,16 +5,23 @@ import { useTranslation } from 'react-i18next';
5
5
  export interface Props {
6
6
  defaultDate?: string | Date;
7
7
  disabled?: boolean;
8
+ /** Minimum age; latest selectable birth date is today minus this many years. */
9
+ minAge?: number;
8
10
  onChange: (date: DateTime | undefined) => void;
9
11
  }
10
12
 
11
13
  const DateSelector = memo(
12
- ({ defaultDate, onChange, disabled = false }: Props) => {
14
+ ({ defaultDate, onChange, disabled = false, minAge = 18 }: Props) => {
13
15
  const { t } = useTranslation();
14
16
 
15
- // Calculate constraints for birth date (18 years ago to 1900)
16
- const maxDate = useMemo(() => DateTime.now().minus({ years: 18 }), []);
17
- const minDate = useMemo(() => DateTime.fromObject({ year: 1900, month: 1, day: 1 }), []);
17
+ const maxDate = useMemo(
18
+ () => DateTime.now().minus({ years: minAge }).startOf('day'),
19
+ [minAge]
20
+ );
21
+ const minDate = useMemo(
22
+ () => DateTime.fromObject({ year: 1900, month: 1, day: 1 }),
23
+ []
24
+ );
18
25
 
19
26
  // State for individual fields as strings to allow empty values
20
27
  const [day, setDay] = useState<string>('');
@@ -54,10 +61,22 @@ const DateSelector = memo(
54
61
  return () => window.removeEventListener('resize', checkMobile);
55
62
  }, []);
56
63
 
64
+ // Avoid re-running when parent passes an unstable onChange (inline arrow).
65
+ const onChangeRef = useRef(onChange);
66
+ onChangeRef.current = onChange;
67
+ const lastEmittedRef = useRef<string | null | undefined>(undefined);
68
+
57
69
  // Validate and propagate changes
58
70
  useEffect(() => {
71
+ const emit = (next: DateTime | undefined) => {
72
+ const key = next?.isValid ? next.toISODate() : null;
73
+ if (key === lastEmittedRef.current) return;
74
+ lastEmittedRef.current = key;
75
+ onChangeRef.current(next);
76
+ };
77
+
59
78
  if (!day || !month || !year) {
60
- onChange(undefined);
79
+ emit(undefined);
61
80
  return;
62
81
  }
63
82
 
@@ -66,7 +85,7 @@ const DateSelector = memo(
66
85
  const y = parseInt(year);
67
86
 
68
87
  if (isNaN(d) || isNaN(m) || isNaN(y)) {
69
- onChange(undefined);
88
+ emit(undefined);
70
89
  return;
71
90
  }
72
91
 
@@ -75,11 +94,11 @@ const DateSelector = memo(
75
94
  // Basic validation: check if it's a valid date (e.g. not Feb 30)
76
95
  // and within range.
77
96
  if (newDate.isValid && newDate >= minDate && newDate <= maxDate) {
78
- onChange(newDate);
97
+ emit(newDate);
79
98
  } else {
80
- onChange(undefined);
99
+ emit(undefined);
81
100
  }
82
- }, [day, month, year, onChange, minDate, maxDate]);
101
+ }, [day, month, year, minDate, maxDate]);
83
102
 
84
103
  // Handle mobile native input change
85
104
  const handleMobileChange = (e: ChangeEvent<HTMLInputElement>) => {
@@ -3,6 +3,7 @@ import {
3
3
  Tenant,
4
4
  } from '@memori.ai/memori-api-client/dist/types';
5
5
  import { Drawer } from '@memori.ai/ui';
6
+ import { useWidgetSurfaceEl } from '../../context/widgetSurfaceContext';
6
7
  import { useTranslation } from 'react-i18next';
7
8
  import { getResourceUrl } from '../../helpers/media';
8
9
 
@@ -24,9 +25,11 @@ const ExpertsDrawer = ({
24
25
  onClose,
25
26
  }: Props) => {
26
27
  const { t } = useTranslation();
28
+ const surfaceEl = useWidgetSurfaceEl();
27
29
 
28
30
  return (
29
31
  <Drawer
32
+ container={surfaceEl ?? undefined}
30
33
  open={open}
31
34
  onClose={onClose}
32
35
  className="memori--experts-drawer"
@@ -3,8 +3,13 @@ import { File, X } from 'lucide-react';
3
3
  import { Button } from '@memori.ai/ui';
4
4
  import ContentPreviewModal from '../ContentPreviewModal';
5
5
  import Snippet from '../Snippet/Snippet';
6
- import { stripHTML, stripDocumentAttachmentTags } from '../../helpers/utils';
7
- import { getFileExtensionFromMime } from '../MediaWidget/MediaItemWidget.utils';
6
+ import {
7
+ stripHTML,
8
+ stripDocumentAttachmentTags,
9
+ isOfficeNativeFilename,
10
+ getDocumentAttachmentAssetUrl,
11
+ } from '../../helpers/utils';
12
+ import { getDocumentBadgeLabel } from '../MediaWidget/MediaItemWidget.utils';
8
13
  import { useTranslation } from 'react-i18next';
9
14
 
10
15
  type FilePreviewProps = {
@@ -60,6 +65,15 @@ const FilePreview = ({
60
65
  return 'CSV';
61
66
  case 'html':
62
67
  return 'HTML';
68
+ case 'doc':
69
+ case 'docx':
70
+ case 'dotx':
71
+ return 'Word';
72
+ case 'xls':
73
+ case 'xltx':
74
+ return 'Excel';
75
+ case 'potx':
76
+ return 'PowerPoint';
63
77
  case 'jpg':
64
78
  case 'jpeg':
65
79
  return 'JPEG';
@@ -111,9 +125,7 @@ const FilePreview = ({
111
125
  if (isHtmlFile(file)) {
112
126
  let htmlContent = content;
113
127
  if (htmlContent.includes('&lt;') || htmlContent.includes('&quot;')) {
114
- const div = document.createElement('div');
115
- div.innerHTML = htmlContent;
116
- htmlContent = div.textContent || div.innerText || htmlContent;
128
+ htmlContent = stripHTML(htmlContent) || htmlContent;
117
129
  }
118
130
  htmlContent = stripDocumentAttachmentTags(htmlContent);
119
131
  return htmlContent;
@@ -169,7 +181,16 @@ const FilePreview = ({
169
181
  <button
170
182
  type="button"
171
183
  className="memori--preview-item-trigger"
172
- onClick={() => setSelectedFile(file)}
184
+ onClick={() => {
185
+ if (isOfficeNativeFilename(file.name || '')) {
186
+ const url = getDocumentAttachmentAssetUrl(file);
187
+ if (url) {
188
+ window.open(url, '_blank', 'noopener,noreferrer');
189
+ }
190
+ } else {
191
+ setSelectedFile(file);
192
+ }
193
+ }}
173
194
  aria-label={file.name}
174
195
  >
175
196
  {isImageContent(file.content, file.type) ? (
@@ -184,7 +205,7 @@ const FilePreview = ({
184
205
  <span className="memori--preview-filename">{file.name}</span>
185
206
  <span className="memori--preview-filetype">
186
207
  {file.mimeType
187
- ? getFileExtensionFromMime(file.mimeType)
208
+ ? getDocumentBadgeLabel(file.mimeType, file.name)
188
209
  : getFileType(file.name, file.type)}
189
210
  </span>
190
211
  </div>
@@ -23,6 +23,7 @@ import type {
23
23
  } from '@tanstack/react-table';
24
24
  import { getErrori18nKey } from '../../helpers/error';
25
25
  import { useTranslation } from 'react-i18next';
26
+ import { useWidgetSurfaceEl } from '../../context/widgetSurfaceContext';
26
27
  import { Trash2 } from 'lucide-react';
27
28
 
28
29
  export interface Props {
@@ -45,6 +46,7 @@ const KnownFacts = ({
45
46
  initialKnownFacts = [],
46
47
  closeDrawer,
47
48
  }: Props) => {
49
+ const surfaceEl = useWidgetSurfaceEl();
48
50
  const { t, i18n } = useTranslation();
49
51
  const { add } = useAlertManager();
50
52
  const { getKnownFactsPaginated, deleteKnownFact } = apiClient.knownFacts;
@@ -168,6 +170,7 @@ const KnownFacts = ({
168
170
 
169
171
  return (
170
172
  <Drawer
173
+ container={surfaceEl ?? undefined}
171
174
  open={visible}
172
175
  anchor="right"
173
176
  className="memori-known-facts-drawer"
@@ -180,6 +183,7 @@ const KnownFacts = ({
180
183
  >
181
184
  <div className="memori-known-facts-body">
182
185
  <Modal
186
+ container={surfaceEl ?? undefined}
183
187
  className="memori-known-facts-modal"
184
188
  open={bulkDeleteModalVisible}
185
189
  closable
@@ -316,6 +320,7 @@ const KnownFacts = ({
316
320
  />
317
321
 
318
322
  <Modal
323
+ container={surfaceEl ?? undefined}
319
324
  className="memori-known-facts-modal"
320
325
  open={deleteModalVisibleFor !== undefined}
321
326
  closable
@@ -11,6 +11,7 @@ import {
11
11
  Form,
12
12
  } from '@memori.ai/ui';
13
13
  import { useTranslation } from 'react-i18next';
14
+ import { useWidgetSurfaceEl } from '../../context/widgetSurfaceContext';
14
15
  import cx from 'classnames';
15
16
  import memoriApiClient from '@memori.ai/memori-api-client';
16
17
  import { getErrori18nKey } from '../../helpers/error';
@@ -50,6 +51,7 @@ const LoginDrawer = ({
50
51
  __TEST__signup = false,
51
52
  __TEST__needMissingData = false,
52
53
  }: Props) => {
54
+ const surfaceEl = useWidgetSurfaceEl();
53
55
  const { t, i18n } = useTranslation();
54
56
  const { add } = useAlertManager();
55
57
  const lang = i18n.language === 'it' ? 'it' : 'en';
@@ -436,6 +438,7 @@ const LoginDrawer = ({
436
438
 
437
439
  return (
438
440
  <Drawer
441
+ container={surfaceEl ?? undefined}
439
442
  open={open}
440
443
  onClose={onClose}
441
444
  className={cx('memori--login-drawer', {
@@ -11,7 +11,10 @@ import React, {
11
11
  import { getResourceUrl } from '../../helpers/media';
12
12
  import {
13
13
  withLinksOpenInNewTab,
14
+ stripHTML,
14
15
  stripDocumentAttachmentTags,
16
+ isAssetOnlyDocumentAttachment,
17
+ getDocumentAttachmentAssetUrl,
15
18
  } from '../../helpers/utils';
16
19
  import { getTranslation } from '../../helpers/translations';
17
20
  import { prismSyntaxLangs } from '../../helpers/constants';
@@ -32,7 +35,7 @@ import type {
32
35
  import {
33
36
  formatBytes,
34
37
  getFileExtensionFromUrl,
35
- getFileExtensionFromMime,
38
+ getDocumentBadgeLabel,
36
39
  countLines,
37
40
  shouldUseDarkFileCard,
38
41
  fetchLinkPreview,
@@ -268,7 +271,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
268
271
  ) : (
269
272
  <DocumentCard
270
273
  title={medium.title || 'File'}
271
- badge={getFileExtensionFromMime(medium.mimeType)}
274
+ badge={getDocumentBadgeLabel(medium.mimeType, medium.title)}
272
275
  meta={(() => {
273
276
  const size = getContentSize(medium);
274
277
  return size != null && size > 0 ? formatBytes(size) : null;
@@ -284,7 +287,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
284
287
  return (
285
288
  <DocumentCard
286
289
  title={medium.title || 'File'}
287
- badge={getFileExtensionFromMime(medium.mimeType)}
290
+ badge={getDocumentBadgeLabel(medium.mimeType, medium.title)}
288
291
  meta={(() => {
289
292
  const size = getContentSize(medium);
290
293
  return size != null && size > 0 ? formatBytes(size) : null;
@@ -298,7 +301,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
298
301
  return (
299
302
  <DocumentCard
300
303
  title={medium.title || 'File'}
301
- badge={getFileExtensionFromMime(medium.mimeType)}
304
+ badge={getDocumentBadgeLabel(medium.mimeType, medium.title)}
302
305
  meta={(() => {
303
306
  const size = getContentSize(medium);
304
307
  return size != null && size > 0 ? formatBytes(size) : null;
@@ -324,8 +327,11 @@ export const RenderMediaItem = memo(function RenderMediaItem({
324
327
 
325
328
  // Extension and file detection helpers
326
329
  const fileExtensionFromUrl = getFileExtensionFromUrl(normURL || item.url);
327
- const fileExtensionFromMime = getFileExtensionFromMime(item.mimeType);
328
- const fileExtension = fileExtensionFromUrl || fileExtensionFromMime;
330
+ const fileExtension = getDocumentBadgeLabel(
331
+ item.mimeType,
332
+ item.title,
333
+ normURL || item.url
334
+ );
329
335
  const isFile = shouldUseDarkFileCard(
330
336
  item,
331
337
  fileExtensionFromUrl,
@@ -355,8 +361,16 @@ export const RenderMediaItem = memo(function RenderMediaItem({
355
361
  const metaParts = [lineText, sizeText].filter(Boolean);
356
362
  const metaLine = metaParts.length > 0 ? metaParts.join(' · ') : null;
357
363
 
364
+ // Asset-only attachments (e.g. Office native files) open via URL, not modal
365
+ const isAssetOnlyAttachment = isAssetOnlyDocumentAttachment(item);
366
+
358
367
  // Document attachments and attached files should open in modal, not as links
359
- if ((isDocumentAttachment || isAttachedFile) && item.mediumID && _onClick) {
368
+ if (
369
+ (isDocumentAttachment || isAttachedFile) &&
370
+ item.mediumID &&
371
+ _onClick &&
372
+ !isAssetOnlyAttachment
373
+ ) {
360
374
  return (
361
375
  <div
362
376
  onClick={() => _onClick(item)}
@@ -395,16 +409,17 @@ export const RenderMediaItem = memo(function RenderMediaItem({
395
409
 
396
410
  // Build href: open in new tab (never modal). Use URL, or blob for content-only items.
397
411
  const getFileCardHref = (): string => {
398
- if (item.url) {
412
+ const assetUrl = getDocumentAttachmentAssetUrl(item);
413
+ if (assetUrl) {
399
414
  return (
400
415
  getResourceUrl({
401
- resourceURI: item.url,
416
+ resourceURI: assetUrl,
402
417
  sessionID,
403
418
  tenantID,
404
419
  baseURL,
405
420
  apiURL,
406
421
  }) ||
407
- item.url ||
422
+ assetUrl ||
408
423
  '#'
409
424
  );
410
425
  }
@@ -416,9 +431,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
416
431
  htmlContent.includes('<document_attachment')
417
432
  ) {
418
433
  if (htmlContent.includes('&lt;') || htmlContent.includes('&quot;')) {
419
- const div = document.createElement('div');
420
- div.innerHTML = htmlContent;
421
- htmlContent = div.textContent || div.innerText || htmlContent;
434
+ htmlContent = stripHTML(htmlContent) || htmlContent;
422
435
  }
423
436
  htmlContent = stripDocumentAttachmentTags(htmlContent);
424
437
  }
@@ -2,6 +2,7 @@ import {
2
2
  formatBytes,
3
3
  getFileExtensionFromUrl,
4
4
  getFileExtensionFromMime,
5
+ getDocumentBadgeLabel,
5
6
  countLines,
6
7
  shouldUseDarkFileCard,
7
8
  fetchLinkPreview,
@@ -57,8 +58,25 @@ describe('MediaItemWidget.utils', () => {
57
58
  expect(getFileExtensionFromMime('text/html')).toBe('HTML');
58
59
  expect(getFileExtensionFromMime('text/plain')).toBe('TXT');
59
60
  expect(getFileExtensionFromMime('application/json')).toBe('JSON');
60
- expect(getFileExtensionFromMime('application/vnd.ms-excel')).toBe('XLS');
61
- expect(getFileExtensionFromMime('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')).toBe('XLSX');
61
+ expect(getFileExtensionFromMime('application/vnd.ms-excel')).toBe('Excel');
62
+ expect(
63
+ getFileExtensionFromMime(
64
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
65
+ )
66
+ ).toBe('Excel');
67
+ });
68
+
69
+ it('maps office mime types with parameters to short labels', () => {
70
+ expect(
71
+ getFileExtensionFromMime(
72
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document; charset=binary'
73
+ )
74
+ ).toBe('Word');
75
+ expect(
76
+ getFileExtensionFromMime(
77
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'
78
+ )
79
+ ).toBe('Excel');
62
80
  });
63
81
 
64
82
  it('uses subtype when not in MIME_TO_EXT map', () => {
@@ -71,6 +89,31 @@ describe('MediaItemWidget.utils', () => {
71
89
  });
72
90
  });
73
91
 
92
+ describe('getDocumentBadgeLabel', () => {
93
+ it('prefers URL extension, then filename, then mime type', () => {
94
+ expect(
95
+ getDocumentBadgeLabel(
96
+ 'application/pdf',
97
+ 'report.docx',
98
+ 'https://example.com/file.pdf'
99
+ )
100
+ ).toBe('PDF');
101
+ expect(
102
+ getDocumentBadgeLabel(
103
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
104
+ 'report.docx',
105
+ 'https://example.com/asset/123'
106
+ )
107
+ ).toBe('Word');
108
+ expect(
109
+ getDocumentBadgeLabel(
110
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
111
+ 'budget.xlsx'
112
+ )
113
+ ).toBe('Excel');
114
+ });
115
+ });
116
+
74
117
  describe('countLines', () => {
75
118
  it('returns 0 for undefined or empty', () => {
76
119
  expect(countLines(undefined)).toBe(0);