@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.
- package/CHANGELOG.md +56 -0
- package/dist/components/AgeVerificationModal/AgeVerificationModal.js +8 -3
- package/dist/components/AgeVerificationModal/AgeVerificationModal.js.map +1 -1
- package/dist/components/Auth/Auth.js +3 -1
- package/dist/components/Auth/Auth.js.map +1 -1
- package/dist/components/Avatar/Avatar.js +5 -10
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +8 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js.map +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +14 -5
- package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
- package/dist/components/Chat/Chat.js +3 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatBubble/ChatBubble.js +21 -15
- package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
- package/dist/components/ChatHistoryDrawer/ChatHistory.js +3 -1
- package/dist/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
- package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js +6 -3
- package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
- package/dist/components/ContentPreviewModal/ContentPreviewModal.js +3 -1
- package/dist/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
- package/dist/components/DateSelector/DateSelector.d.ts +2 -1
- package/dist/components/DateSelector/DateSelector.js +17 -7
- package/dist/components/DateSelector/DateSelector.js.map +1 -1
- package/dist/components/ExpertsDrawer/ExpertsDrawer.js +3 -1
- package/dist/components/ExpertsDrawer/ExpertsDrawer.js.map +1 -1
- package/dist/components/FilePreview/FilePreview.js +22 -5
- package/dist/components/FilePreview/FilePreview.js.map +1 -1
- package/dist/components/KnownFacts/KnownFacts.js +5 -3
- package/dist/components/KnownFacts/KnownFacts.js.map +1 -1
- package/dist/components/LoginDrawer/LoginDrawer.js +3 -1
- package/dist/components/LoginDrawer/LoginDrawer.js.map +1 -1
- package/dist/components/MediaWidget/MediaItemWidget.js +14 -12
- package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/dist/components/MediaWidget/MediaItemWidget.utils.d.ts +3 -2
- package/dist/components/MediaWidget/MediaItemWidget.utils.js +28 -6
- package/dist/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
- package/dist/components/MediaWidget/MediaPreviewModal.js +6 -7
- package/dist/components/MediaWidget/MediaPreviewModal.js.map +1 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +19 -5
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +4 -1
- package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.css +4 -2
- package/dist/components/MemoriWidget/MemoriWidget.js +760 -404
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/SettingsDrawer/SettingsDrawer.js +3 -1
- package/dist/components/SettingsDrawer/SettingsDrawer.js.map +1 -1
- package/dist/components/StartPanel/StartPanel.js +3 -1
- package/dist/components/StartPanel/StartPanel.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.js +21 -5
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +126 -60
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.js +3 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/dist/components/WhyThisAnswer/WhyThisAnswer.js +3 -1
- package/dist/components/WhyThisAnswer/WhyThisAnswer.js.map +1 -1
- package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +3 -1
- package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
- package/dist/components/layouts/WebsiteAssistant/website-assistant.css +21 -1
- package/dist/components/layouts/chat.css +2 -1
- package/dist/components/layouts/fullpage.css +31 -8
- package/dist/components/layouts/hidden-chat.css +1 -0
- package/dist/components/layouts/totem.css +2 -1
- package/dist/components/layouts/zoomed-full-body.css +32 -7
- package/dist/context/widgetSurfaceContext.d.ts +3 -0
- package/dist/context/widgetSurfaceContext.js +11 -0
- package/dist/context/widgetSurfaceContext.js.map +1 -0
- package/dist/helpers/constants.d.ts +3 -0
- package/dist/helpers/constants.js +27 -1
- package/dist/helpers/constants.js.map +1 -1
- package/dist/helpers/credits.d.ts +5 -2
- package/dist/helpers/credits.js +5 -1
- package/dist/helpers/credits.js.map +1 -1
- package/dist/helpers/message.d.ts +1 -1
- package/dist/helpers/message.js +4 -4
- package/dist/helpers/message.js.map +1 -1
- package/dist/helpers/nats/getNatsConfig.d.ts +9 -0
- package/dist/helpers/nats/getNatsConfig.js +65 -0
- package/dist/helpers/nats/getNatsConfig.js.map +1 -0
- package/dist/helpers/nats/isSessionExpiredError.d.ts +3 -0
- package/dist/helpers/nats/isSessionExpiredError.js +36 -0
- package/dist/helpers/nats/isSessionExpiredError.js.map +1 -0
- package/dist/helpers/nats/useNats.d.ts +12 -0
- package/dist/helpers/nats/useNats.js +75 -0
- package/dist/helpers/nats/useNats.js.map +1 -0
- package/dist/helpers/nats/useNatsSession.d.ts +28 -0
- package/dist/helpers/nats/useNatsSession.js +212 -0
- package/dist/helpers/nats/useNatsSession.js.map +1 -0
- package/dist/helpers/utils.d.ts +21 -0
- package/dist/helpers/utils.js +75 -4
- package/dist/helpers/utils.js.map +1 -1
- package/dist/locales/de.json +1 -0
- package/dist/locales/en.json +1 -0
- package/dist/locales/es.json +1 -0
- package/dist/locales/fr.json +1 -0
- package/dist/locales/it.json +1 -0
- package/dist/styles.css +1 -1
- package/dist/types/integration.d.ts +3 -0
- package/dist/types/integration.js +16 -0
- package/dist/types/integration.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/components/AgeVerificationModal/AgeVerificationModal.js +8 -3
- package/esm/components/AgeVerificationModal/AgeVerificationModal.js.map +1 -1
- package/esm/components/Auth/Auth.js +3 -1
- package/esm/components/Auth/Auth.js.map +1 -1
- package/esm/components/Avatar/Avatar.js +5 -10
- package/esm/components/Avatar/Avatar.js.map +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +9 -2
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js +2 -2
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js.map +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +13 -4
- package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
- package/esm/components/Chat/Chat.js +3 -1
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatBubble/ChatBubble.js +21 -15
- package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
- package/esm/components/ChatHistoryDrawer/ChatHistory.js +3 -1
- package/esm/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
- package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js +6 -3
- package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
- package/esm/components/ContentPreviewModal/ContentPreviewModal.js +3 -1
- package/esm/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
- package/esm/components/DateSelector/DateSelector.d.ts +2 -1
- package/esm/components/DateSelector/DateSelector.js +17 -7
- package/esm/components/DateSelector/DateSelector.js.map +1 -1
- package/esm/components/ExpertsDrawer/ExpertsDrawer.js +3 -1
- package/esm/components/ExpertsDrawer/ExpertsDrawer.js.map +1 -1
- package/esm/components/FilePreview/FilePreview.js +24 -7
- package/esm/components/FilePreview/FilePreview.js.map +1 -1
- package/esm/components/KnownFacts/KnownFacts.js +5 -3
- package/esm/components/KnownFacts/KnownFacts.js.map +1 -1
- package/esm/components/LoginDrawer/LoginDrawer.js +3 -1
- package/esm/components/LoginDrawer/LoginDrawer.js.map +1 -1
- package/esm/components/MediaWidget/MediaItemWidget.js +16 -14
- package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/esm/components/MediaWidget/MediaItemWidget.utils.d.ts +3 -2
- package/esm/components/MediaWidget/MediaItemWidget.utils.js +27 -6
- package/esm/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
- package/esm/components/MediaWidget/MediaPreviewModal.js +6 -7
- package/esm/components/MediaWidget/MediaPreviewModal.js.map +1 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +19 -5
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +4 -1
- package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.css +4 -2
- package/esm/components/MemoriWidget/MemoriWidget.js +760 -404
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/SettingsDrawer/SettingsDrawer.js +3 -1
- package/esm/components/SettingsDrawer/SettingsDrawer.js.map +1 -1
- package/esm/components/StartPanel/StartPanel.js +3 -1
- package/esm/components/StartPanel/StartPanel.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.js +21 -5
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +126 -60
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.js +3 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/esm/components/WhyThisAnswer/WhyThisAnswer.js +3 -1
- package/esm/components/WhyThisAnswer/WhyThisAnswer.js.map +1 -1
- package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +3 -1
- package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
- package/esm/components/layouts/WebsiteAssistant/website-assistant.css +21 -1
- package/esm/components/layouts/chat.css +2 -1
- package/esm/components/layouts/fullpage.css +31 -8
- package/esm/components/layouts/hidden-chat.css +1 -0
- package/esm/components/layouts/totem.css +2 -1
- package/esm/components/layouts/zoomed-full-body.css +32 -7
- package/esm/context/widgetSurfaceContext.d.ts +3 -0
- package/esm/context/widgetSurfaceContext.js +7 -0
- package/esm/context/widgetSurfaceContext.js.map +1 -0
- package/esm/helpers/constants.d.ts +3 -0
- package/esm/helpers/constants.js +26 -0
- package/esm/helpers/constants.js.map +1 -1
- package/esm/helpers/credits.d.ts +5 -2
- package/esm/helpers/credits.js +5 -1
- package/esm/helpers/credits.js.map +1 -1
- package/esm/helpers/message.d.ts +1 -1
- package/esm/helpers/message.js +2 -2
- package/esm/helpers/message.js.map +1 -1
- package/esm/helpers/nats/getNatsConfig.d.ts +9 -0
- package/esm/helpers/nats/getNatsConfig.js +61 -0
- package/esm/helpers/nats/getNatsConfig.js.map +1 -0
- package/esm/helpers/nats/isSessionExpiredError.d.ts +3 -0
- package/esm/helpers/nats/isSessionExpiredError.js +32 -0
- package/esm/helpers/nats/isSessionExpiredError.js.map +1 -0
- package/esm/helpers/nats/useNats.d.ts +12 -0
- package/esm/helpers/nats/useNats.js +72 -0
- package/esm/helpers/nats/useNats.js.map +1 -0
- package/esm/helpers/nats/useNatsSession.d.ts +28 -0
- package/esm/helpers/nats/useNatsSession.js +208 -0
- package/esm/helpers/nats/useNatsSession.js.map +1 -0
- package/esm/helpers/utils.d.ts +21 -0
- package/esm/helpers/utils.js +68 -3
- package/esm/helpers/utils.js.map +1 -1
- package/esm/locales/de.json +1 -0
- package/esm/locales/en.json +1 -0
- package/esm/locales/es.json +1 -0
- package/esm/locales/fr.json +1 -0
- package/esm/locales/it.json +1 -0
- package/esm/styles.css +1 -1
- package/esm/types/integration.d.ts +3 -0
- package/esm/types/integration.js +11 -0
- package/esm/types/integration.js.map +1 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +5 -3
- package/src/components/AgeVerificationModal/AgeVerificationModal.tsx +9 -1
- package/src/components/Auth/Auth.tsx +3 -0
- package/src/components/Avatar/Avatar.stories.tsx +45 -28
- package/src/components/Avatar/Avatar.test.tsx +54 -0
- package/src/components/Avatar/Avatar.tsx +8 -12
- package/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +12 -5
- package/src/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.ts +2 -1
- package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +16 -4
- package/src/components/Avatar/__snapshots__/Avatar.test.tsx.snap +164 -0
- package/src/components/Chat/Chat.tsx +3 -0
- package/src/components/ChatBubble/ChatBubble.tsx +40 -12
- package/src/components/ChatHistoryDrawer/ChatHistory.tsx +3 -0
- package/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx +8 -4
- package/src/components/ContentPreviewModal/ContentPreviewModal.tsx +3 -0
- package/src/components/DateSelector/DateSelector.stories.tsx +11 -2
- package/src/components/DateSelector/DateSelector.tsx +28 -9
- package/src/components/ExpertsDrawer/ExpertsDrawer.tsx +3 -0
- package/src/components/FilePreview/FilePreview.tsx +28 -7
- package/src/components/KnownFacts/KnownFacts.tsx +5 -0
- package/src/components/LoginDrawer/LoginDrawer.tsx +3 -0
- package/src/components/MediaWidget/MediaItemWidget.tsx +26 -13
- package/src/components/MediaWidget/MediaItemWidget.utils.test.ts +45 -2
- package/src/components/MediaWidget/MediaItemWidget.utils.ts +39 -6
- package/src/components/MediaWidget/MediaPreviewModal.tsx +9 -7
- package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +19 -5
- package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +4 -0
- package/src/components/MemoriWidget/MemoriWidget.css +4 -2
- package/src/components/MemoriWidget/MemoriWidget.tsx +818 -349
- package/src/components/SettingsDrawer/SettingsDrawer.tsx +3 -0
- package/src/components/StartPanel/StartPanel.stories.tsx +22 -0
- package/src/components/StartPanel/StartPanel.test.tsx +64 -1
- package/src/components/StartPanel/StartPanel.tsx +3 -0
- package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +261 -0
- package/src/components/UploadButton/UploadButton.tsx +30 -11
- package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +151 -61
- package/src/components/UploadButton/UploadImages/UploadImages.tsx +3 -0
- package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
- package/src/components/WhyThisAnswer/WhyThisAnswer.tsx +3 -0
- package/src/components/layouts/WebsiteAssistant/WebsiteAssistant.tsx +4 -0
- package/src/components/layouts/WebsiteAssistant/__snapshots__/WebsiteAssistant.test.tsx.snap +48 -44
- package/src/components/layouts/WebsiteAssistant/website-assistant.css +21 -1
- package/src/components/layouts/__snapshots__/Chat.test.tsx.snap +533 -529
- package/src/components/layouts/__snapshots__/FullPage.test.tsx.snap +1058 -1050
- package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +443 -439
- package/src/components/layouts/__snapshots__/Totem.test.tsx.snap +584 -580
- package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +528 -524
- package/src/components/layouts/chat.css +2 -1
- package/src/components/layouts/fullpage.css +31 -8
- package/src/components/layouts/hidden-chat.css +1 -0
- package/src/components/layouts/layouts.stories.tsx +131 -12
- package/src/components/layouts/totem.css +2 -1
- package/src/components/layouts/zoomed-full-body.css +32 -7
- package/src/context/widgetSurfaceContext.tsx +14 -0
- package/src/helpers/constants.ts +32 -1
- package/src/helpers/credits.ts +16 -1
- package/src/helpers/message.test.ts +28 -1
- package/src/helpers/message.ts +4 -4
- package/src/helpers/nats/getNatsConfig.ts +145 -0
- package/src/helpers/nats/isSessionExpiredError.test.ts +38 -0
- package/src/helpers/nats/isSessionExpiredError.ts +51 -0
- package/src/helpers/nats/useNats.ts +124 -0
- package/src/helpers/nats/useNatsSession.ts +394 -0
- package/src/helpers/utils.test.ts +162 -0
- package/src/helpers/utils.ts +108 -3
- package/src/index.stories.tsx +32 -0
- package/src/locales/de.json +1 -0
- package/src/locales/en.json +1 -0
- package/src/locales/es.json +1 -0
- package/src/locales/fr.json +1 -0
- package/src/locales/it.json +1 -0
- package/src/styles.css +61 -1
- package/src/types/integration.test.ts +32 -0
- package/src/types/integration.ts +31 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// helpers/nats/useNatsSession.ts - Subscribe to a session's NATS channel.
|
|
2
|
+
//
|
|
3
|
+
// Opens a W3C WebSocket connection (via `wsconnect`), then either:
|
|
4
|
+
// - core NATS: subscribes to the session subject directly, or
|
|
5
|
+
// - JetStream: consumes from the configured stream (ordered consumer by default).
|
|
6
|
+
//
|
|
7
|
+
// When JetStream is enabled, reconnects on tab visibility / network resume and
|
|
8
|
+
// replays the last message per subject to recover events missed while Safari
|
|
9
|
+
// (or other browsers) suspend the WebSocket in background.
|
|
10
|
+
//
|
|
11
|
+
// The cleanup (`consumerMessages.stop()` + `nc.close()`) is essential: without
|
|
12
|
+
// it, every `sessionId` change would leave orphan connections / iterators.
|
|
13
|
+
import { useEffect, useRef, useState, type MutableRefObject } from 'react';
|
|
14
|
+
import { wsconnect, NatsConnection } from '@nats-io/nats-core';
|
|
15
|
+
import {
|
|
16
|
+
jetstream,
|
|
17
|
+
DeliverPolicy,
|
|
18
|
+
type ConsumerMessages,
|
|
19
|
+
} from '@nats-io/jetstream';
|
|
20
|
+
import type { NatsConfig } from './getNatsConfig';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `progress` event: job advancement updates.
|
|
24
|
+
*/
|
|
25
|
+
export interface NatsProgressEvent {
|
|
26
|
+
eventType: 'progress';
|
|
27
|
+
jobId?: string;
|
|
28
|
+
currentStep?: number;
|
|
29
|
+
finalStep?: number;
|
|
30
|
+
message?: string;
|
|
31
|
+
startingTime?: string;
|
|
32
|
+
correlationID?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `dialog.text_entered_response` event: same body returned by
|
|
37
|
+
* `POST /memori/v2/EnterTextAsync/{sessionId}` (via NATS).
|
|
38
|
+
*/
|
|
39
|
+
export interface NatsDialogResponseEvent {
|
|
40
|
+
eventType: 'dialog_text_entered_response';
|
|
41
|
+
requestID?: string;
|
|
42
|
+
resultCode?: number;
|
|
43
|
+
resultMessage?: string;
|
|
44
|
+
currentState?: any;
|
|
45
|
+
correlationID?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `error` event.
|
|
50
|
+
*/
|
|
51
|
+
export interface NatsErrorEvent {
|
|
52
|
+
eventType: 'error';
|
|
53
|
+
errorCode?: string | number;
|
|
54
|
+
errorMessage?: string;
|
|
55
|
+
backtrace?: string;
|
|
56
|
+
correlationID?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Discriminated union of all events received on the session subject.
|
|
61
|
+
*/
|
|
62
|
+
export type NatsSessionEvent =
|
|
63
|
+
| NatsProgressEvent
|
|
64
|
+
| NatsDialogResponseEvent
|
|
65
|
+
| NatsErrorEvent;
|
|
66
|
+
|
|
67
|
+
type JetStreamConsumeMode = 'live' | 'catch-up';
|
|
68
|
+
|
|
69
|
+
function readString(
|
|
70
|
+
raw: Record<string, unknown>,
|
|
71
|
+
camelKey: string,
|
|
72
|
+
snakeKey: string
|
|
73
|
+
): string | undefined {
|
|
74
|
+
const value = raw[camelKey] ?? raw[snakeKey];
|
|
75
|
+
return typeof value === 'string' ? value : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readNumber(
|
|
79
|
+
raw: Record<string, unknown>,
|
|
80
|
+
camelKey: string,
|
|
81
|
+
snakeKey: string
|
|
82
|
+
): number | undefined {
|
|
83
|
+
const value = raw[camelKey] ?? raw[snakeKey];
|
|
84
|
+
return typeof value === 'number' ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Unwraps the `currentState` of a `dialog_text_entered_response`.
|
|
89
|
+
*
|
|
90
|
+
* The async (NATS) response wraps the dialog state one level deeper than the
|
|
91
|
+
* synchronous HTTP response: `currentState` is a `ResponseSpec` that *contains*
|
|
92
|
+
* the actual `DialogState` under a nested `currentState` key. We unwrap it so
|
|
93
|
+
* downstream consumers always receive the `DialogState` directly, while still
|
|
94
|
+
* accepting the already-flat shape for backward compatibility.
|
|
95
|
+
*/
|
|
96
|
+
function unwrapDialogState(value: unknown): unknown {
|
|
97
|
+
if (
|
|
98
|
+
value &&
|
|
99
|
+
typeof value === 'object' &&
|
|
100
|
+
'currentState' in (value as Record<string, unknown>)
|
|
101
|
+
) {
|
|
102
|
+
return (value as Record<string, unknown>).currentState;
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalizes raw NATS payloads (snake_case or camelCase) into typed events.
|
|
109
|
+
*/
|
|
110
|
+
export function normalizeNatsEvent(raw: Record<string, unknown>): NatsSessionEvent {
|
|
111
|
+
const eventType = (readString(raw, 'eventType', 'event_type') ??
|
|
112
|
+
'unknown') as NatsSessionEvent['eventType'];
|
|
113
|
+
|
|
114
|
+
const correlationID =
|
|
115
|
+
readString(raw, 'correlationID', 'correlation_id') ??
|
|
116
|
+
readString(raw, 'correlationId', 'correlation_id');
|
|
117
|
+
|
|
118
|
+
if (eventType === 'progress') {
|
|
119
|
+
return {
|
|
120
|
+
eventType: 'progress',
|
|
121
|
+
jobId: readString(raw, 'jobId', 'job_id'),
|
|
122
|
+
currentStep: readNumber(raw, 'currentStep', 'current_step'),
|
|
123
|
+
finalStep: readNumber(raw, 'finalStep', 'final_step'),
|
|
124
|
+
message: typeof raw.message === 'string' ? raw.message : undefined,
|
|
125
|
+
startingTime: readString(raw, 'startingTime', 'starting_time'),
|
|
126
|
+
correlationID,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (eventType === 'dialog_text_entered_response') {
|
|
131
|
+
return {
|
|
132
|
+
eventType: 'dialog_text_entered_response',
|
|
133
|
+
requestID:
|
|
134
|
+
typeof raw.requestID === 'string' ? raw.requestID : undefined,
|
|
135
|
+
resultCode:
|
|
136
|
+
typeof raw.resultCode === 'number' ? raw.resultCode : undefined,
|
|
137
|
+
resultMessage:
|
|
138
|
+
typeof raw.resultMessage === 'string' ? raw.resultMessage : undefined,
|
|
139
|
+
currentState: unwrapDialogState(raw.currentState),
|
|
140
|
+
correlationID,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (eventType === 'error') {
|
|
145
|
+
return {
|
|
146
|
+
eventType: 'error',
|
|
147
|
+
errorCode: (raw.errorCode ?? raw.error_code) as string | number | undefined,
|
|
148
|
+
errorMessage: readString(raw, 'errorMessage', 'error_message'),
|
|
149
|
+
backtrace: typeof raw.backtrace === 'string' ? raw.backtrace : undefined,
|
|
150
|
+
correlationID,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
eventType,
|
|
156
|
+
correlationID,
|
|
157
|
+
} as NatsSessionEvent;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function decodeMessage(
|
|
161
|
+
subject: string,
|
|
162
|
+
received: number,
|
|
163
|
+
raw: Record<string, unknown>,
|
|
164
|
+
onMessage: (event: NatsSessionEvent) => void
|
|
165
|
+
) {
|
|
166
|
+
const payload = normalizeNatsEvent(raw);
|
|
167
|
+
console.debug(
|
|
168
|
+
`[NATS] message #${received} on ${subject} (eventType=${payload.eventType})`,
|
|
169
|
+
payload
|
|
170
|
+
);
|
|
171
|
+
onMessage(payload);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function consumeCoreSubscription(
|
|
175
|
+
nc: NatsConnection,
|
|
176
|
+
subject: string,
|
|
177
|
+
onMessage: (event: NatsSessionEvent) => void
|
|
178
|
+
) {
|
|
179
|
+
const sub = nc.subscribe(subject);
|
|
180
|
+
console.info('[NATS] subscribed to subject', subject);
|
|
181
|
+
let received = 0;
|
|
182
|
+
for await (const msg of sub) {
|
|
183
|
+
received += 1;
|
|
184
|
+
try {
|
|
185
|
+
decodeMessage(
|
|
186
|
+
subject,
|
|
187
|
+
received,
|
|
188
|
+
msg.json<Record<string, unknown>>(),
|
|
189
|
+
onMessage
|
|
190
|
+
);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('[NATS] message decode error', err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.info(
|
|
196
|
+
`[NATS] subscription on ${subject} closed (received ${received} messages)`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function consumeJetStream(
|
|
201
|
+
nc: NatsConnection,
|
|
202
|
+
config: Pick<NatsConfig, 'stream' | 'consumer'>,
|
|
203
|
+
subject: string,
|
|
204
|
+
mode: JetStreamConsumeMode,
|
|
205
|
+
onMessage: (event: NatsSessionEvent) => void,
|
|
206
|
+
setConsumerMessages: (messages: ConsumerMessages) => void
|
|
207
|
+
) {
|
|
208
|
+
const js = jetstream(nc);
|
|
209
|
+
const consumer = config.consumer
|
|
210
|
+
? await js.consumers.get(config.stream!, config.consumer)
|
|
211
|
+
: await js.consumers.get(config.stream!, {
|
|
212
|
+
filter_subjects: [subject],
|
|
213
|
+
deliver_policy:
|
|
214
|
+
mode === 'catch-up'
|
|
215
|
+
? DeliverPolicy.LastPerSubject
|
|
216
|
+
: DeliverPolicy.New,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.info(
|
|
220
|
+
'[NATS] JetStream consumer ready',
|
|
221
|
+
config.consumer
|
|
222
|
+
? { stream: config.stream, consumer: config.consumer, subject, mode }
|
|
223
|
+
: { stream: config.stream, subject, mode }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const messages = await consumer.consume();
|
|
227
|
+
setConsumerMessages(messages);
|
|
228
|
+
let received = 0;
|
|
229
|
+
for await (const msg of messages) {
|
|
230
|
+
received += 1;
|
|
231
|
+
try {
|
|
232
|
+
decodeMessage(subject, received, msg.json(), onMessage);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error('[NATS] message decode error', err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.info(
|
|
238
|
+
`[NATS] JetStream consumer on ${subject} closed (received ${received} messages)`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function closeNatsSession(
|
|
243
|
+
consumerMessagesRef: MutableRefObject<ConsumerMessages | null>,
|
|
244
|
+
connRef: MutableRefObject<NatsConnection | null>
|
|
245
|
+
) {
|
|
246
|
+
consumerMessagesRef.current?.stop();
|
|
247
|
+
consumerMessagesRef.current = null;
|
|
248
|
+
connRef.current?.close();
|
|
249
|
+
connRef.current = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Open a NATS WebSocket connection, subscribe to the session subject and
|
|
254
|
+
* forward decoded events to `onMessage`.
|
|
255
|
+
*
|
|
256
|
+
* `onMessage` is kept in a ref so callers do not need to memoize it: the
|
|
257
|
+
* connection is only (re)opened when `sessionId`, `config` or `resumeGeneration`
|
|
258
|
+
* change.
|
|
259
|
+
*
|
|
260
|
+
* @param sessionId Current session UUID (subscription is skipped when falsy).
|
|
261
|
+
* @param config Connection config from `/api/nats` (url + token required).
|
|
262
|
+
* @param onMessage Callback invoked for each decoded event.
|
|
263
|
+
*/
|
|
264
|
+
export function useNatsSession(
|
|
265
|
+
sessionId: string | undefined,
|
|
266
|
+
config: NatsConfig | undefined,
|
|
267
|
+
onMessage: (event: NatsSessionEvent) => void
|
|
268
|
+
) {
|
|
269
|
+
const connRef = useRef<NatsConnection | null>(null);
|
|
270
|
+
const consumerMessagesRef = useRef<ConsumerMessages | null>(null);
|
|
271
|
+
const onMessageRef = useRef(onMessage);
|
|
272
|
+
const wasHiddenRef = useRef(false);
|
|
273
|
+
const [resumeGeneration, setResumeGeneration] = useState(0);
|
|
274
|
+
|
|
275
|
+
// Keep the latest callback without retriggering the connection effect.
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
onMessageRef.current = onMessage;
|
|
278
|
+
}, [onMessage]);
|
|
279
|
+
|
|
280
|
+
// Reconnect when the tab returns from background or the network comes back.
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (!sessionId || !config?.stream) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const requestResume = () => {
|
|
287
|
+
if (document.visibilityState !== 'visible') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
console.info('[NATS] resuming JetStream after visibility/network change');
|
|
291
|
+
setResumeGeneration(generation => generation + 1);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const onVisibilityChange = () => {
|
|
295
|
+
if (document.visibilityState === 'hidden') {
|
|
296
|
+
wasHiddenRef.current = true;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (wasHiddenRef.current) {
|
|
300
|
+
wasHiddenRef.current = false;
|
|
301
|
+
requestResume();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const onPageShow = (event: PageTransitionEvent) => {
|
|
306
|
+
if (event.persisted) {
|
|
307
|
+
requestResume();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
312
|
+
window.addEventListener('online', requestResume);
|
|
313
|
+
window.addEventListener('pageshow', onPageShow);
|
|
314
|
+
|
|
315
|
+
return () => {
|
|
316
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
317
|
+
window.removeEventListener('online', requestResume);
|
|
318
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
319
|
+
};
|
|
320
|
+
}, [sessionId, config?.stream]);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!sessionId || !config?.url || !config.token) {
|
|
324
|
+
console.debug(
|
|
325
|
+
'[NATS] subscription skipped (missing sessionId/url/token)',
|
|
326
|
+
{
|
|
327
|
+
sessionId,
|
|
328
|
+
hasUrl: !!config?.url,
|
|
329
|
+
hasToken: !!config?.token,
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let closed = false;
|
|
336
|
+
const subject = config.subject ?? `sessions.${sessionId}`;
|
|
337
|
+
const useJetStream = !!config.stream;
|
|
338
|
+
const jetStreamMode: JetStreamConsumeMode =
|
|
339
|
+
useJetStream && resumeGeneration > 0 ? 'catch-up' : 'live';
|
|
340
|
+
|
|
341
|
+
(async () => {
|
|
342
|
+
console.info(
|
|
343
|
+
'[NATS] connecting to',
|
|
344
|
+
config.url,
|
|
345
|
+
'for session',
|
|
346
|
+
sessionId,
|
|
347
|
+
useJetStream ? `(JetStream/${jetStreamMode})` : '(core)'
|
|
348
|
+
);
|
|
349
|
+
const nc = await wsconnect({
|
|
350
|
+
servers: [config.url],
|
|
351
|
+
token: config.token,
|
|
352
|
+
});
|
|
353
|
+
if (closed) {
|
|
354
|
+
console.debug(
|
|
355
|
+
'[NATS] connection established after cleanup, closing immediately'
|
|
356
|
+
);
|
|
357
|
+
await nc.close();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
connRef.current = nc;
|
|
361
|
+
console.info('[NATS] connected to', nc.getServer());
|
|
362
|
+
|
|
363
|
+
const dispatch = (event: NatsSessionEvent) => {
|
|
364
|
+
onMessageRef.current(event);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (useJetStream) {
|
|
368
|
+
await consumeJetStream(
|
|
369
|
+
nc,
|
|
370
|
+
config,
|
|
371
|
+
subject,
|
|
372
|
+
jetStreamMode,
|
|
373
|
+
dispatch,
|
|
374
|
+
messages => {
|
|
375
|
+
consumerMessagesRef.current = messages;
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
await consumeCoreSubscription(nc, subject, dispatch);
|
|
380
|
+
}
|
|
381
|
+
})().catch(err => console.error('[NATS] connection error', err));
|
|
382
|
+
|
|
383
|
+
return () => {
|
|
384
|
+
console.info('[NATS] cleanup: closing connection for subject', subject);
|
|
385
|
+
closed = true;
|
|
386
|
+
closeNatsSession(consumerMessagesRef, connRef);
|
|
387
|
+
};
|
|
388
|
+
}, [sessionId, config, resumeGeneration]);
|
|
389
|
+
|
|
390
|
+
// Reset catch-up generation when the active session changes.
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
setResumeGeneration(0);
|
|
393
|
+
}, [sessionId]);
|
|
394
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
difference,
|
|
3
3
|
stripEmojis,
|
|
4
|
+
stripHTML,
|
|
4
5
|
stripMarkdown,
|
|
5
6
|
stripOutputTags,
|
|
6
7
|
escapeHTML,
|
|
8
|
+
extractAttachmentLinks,
|
|
9
|
+
extractAttachmentLink,
|
|
10
|
+
isAssetOnlyDocumentAttachment,
|
|
11
|
+
isOfficeNativeFilename,
|
|
12
|
+
parseDocumentAttachmentsFromMessage,
|
|
7
13
|
} from './utils';
|
|
8
14
|
|
|
9
15
|
describe('Utils/difference', () => {
|
|
@@ -141,6 +147,162 @@ describe('utils/stripOutputTags', () => {
|
|
|
141
147
|
});
|
|
142
148
|
});
|
|
143
149
|
|
|
150
|
+
describe('utils/attachment helpers', () => {
|
|
151
|
+
it('extracts multiple attachment links in order', () => {
|
|
152
|
+
const input = [
|
|
153
|
+
'<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
|
|
154
|
+
'</document_attachment>',
|
|
155
|
+
'<attachment_link>',
|
|
156
|
+
'https://assets.example.com/a.docx',
|
|
157
|
+
'</attachment_link>',
|
|
158
|
+
'<document_attachment filename="b.pdf" type="application/pdf">',
|
|
159
|
+
'pdf text',
|
|
160
|
+
'</document_attachment>',
|
|
161
|
+
'<attachment_link>https://assets.example.com/b.txt</attachment_link>',
|
|
162
|
+
].join('\n');
|
|
163
|
+
|
|
164
|
+
expect(extractAttachmentLinks(input)).toEqual([
|
|
165
|
+
'https://assets.example.com/a.docx',
|
|
166
|
+
'https://assets.example.com/b.txt',
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('extracts a single attachment link', () => {
|
|
171
|
+
const input =
|
|
172
|
+
'<attachment_link>\nhttps://assets.example.com/file.docx\n</attachment_link>';
|
|
173
|
+
expect(extractAttachmentLink(input)).toBe(
|
|
174
|
+
'https://assets.example.com/file.docx'
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('detects office native filenames', () => {
|
|
179
|
+
expect(isOfficeNativeFilename('report.doc')).toBe(true);
|
|
180
|
+
expect(isOfficeNativeFilename('report.docx')).toBe(true);
|
|
181
|
+
expect(isOfficeNativeFilename('budget.xls')).toBe(false);
|
|
182
|
+
expect(isOfficeNativeFilename('template.XLTX')).toBe(true);
|
|
183
|
+
expect(isOfficeNativeFilename('slides.potx')).toBe(true);
|
|
184
|
+
expect(isOfficeNativeFilename('notes.pdf')).toBe(false);
|
|
185
|
+
expect(isOfficeNativeFilename('data.xlsx')).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('detects asset-only document attachments', () => {
|
|
189
|
+
expect(
|
|
190
|
+
isAssetOnlyDocumentAttachment({
|
|
191
|
+
title: 'report.docx',
|
|
192
|
+
content: '',
|
|
193
|
+
url: 'https://assets.example.com/file.docx',
|
|
194
|
+
})
|
|
195
|
+
).toBe(true);
|
|
196
|
+
expect(
|
|
197
|
+
isAssetOnlyDocumentAttachment({
|
|
198
|
+
title: 'report.docx',
|
|
199
|
+
content: [
|
|
200
|
+
'<document_attachment filename="report.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
|
|
201
|
+
'</document_attachment>',
|
|
202
|
+
'<attachment_link>https://assets.example.com/file.docx</attachment_link>',
|
|
203
|
+
].join('\n'),
|
|
204
|
+
url: '',
|
|
205
|
+
})
|
|
206
|
+
).toBe(true);
|
|
207
|
+
expect(
|
|
208
|
+
isAssetOnlyDocumentAttachment({
|
|
209
|
+
content: 'extracted text',
|
|
210
|
+
url: 'https://assets.example.com/file.txt',
|
|
211
|
+
})
|
|
212
|
+
).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('pairs each document attachment with its adjacent link', () => {
|
|
216
|
+
const input = [
|
|
217
|
+
'<attachment_link>https://assets.example.com/orphan.txt</attachment_link>',
|
|
218
|
+
'<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
|
|
219
|
+
'</document_attachment>',
|
|
220
|
+
'<attachment_link>https://assets.example.com/a.docx</attachment_link>',
|
|
221
|
+
'<document_attachment filename="b.pdf" type="application/pdf">',
|
|
222
|
+
'pdf text',
|
|
223
|
+
'</document_attachment>',
|
|
224
|
+
'<attachment_link>https://assets.example.com/b.txt</attachment_link>',
|
|
225
|
+
].join('\n');
|
|
226
|
+
|
|
227
|
+
expect(parseDocumentAttachmentsFromMessage(input)).toEqual([
|
|
228
|
+
{
|
|
229
|
+
filename: 'a.docx',
|
|
230
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
231
|
+
content: '',
|
|
232
|
+
url: 'https://assets.example.com/a.docx',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
filename: 'b.pdf',
|
|
236
|
+
type: 'application/pdf',
|
|
237
|
+
content: 'pdf text',
|
|
238
|
+
url: 'https://assets.example.com/b.txt',
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns an empty url when no adjacent attachment link exists', () => {
|
|
244
|
+
const input =
|
|
245
|
+
'<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document"></document_attachment>';
|
|
246
|
+
|
|
247
|
+
expect(parseDocumentAttachmentsFromMessage(input)).toEqual([
|
|
248
|
+
{
|
|
249
|
+
filename: 'a.docx',
|
|
250
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
251
|
+
content: '',
|
|
252
|
+
url: '',
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('utils/stripHTML', () => {
|
|
259
|
+
it('strips tags and returns text content', () => {
|
|
260
|
+
expect(stripHTML('<p>Hello <strong>world</strong></p>')).toBe(
|
|
261
|
+
'Hello world'
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('does not retain onerror from partner XSS payload', () => {
|
|
266
|
+
const payload =
|
|
267
|
+
'<img src="immagine-inesistente.jpg" onerror="alert(\'XSS Eseguito!\')">';
|
|
268
|
+
const result = stripHTML(payload);
|
|
269
|
+
expect(result).toBe('');
|
|
270
|
+
expect(result).not.toMatch(/onerror/i);
|
|
271
|
+
// Re-parse result: must not introduce an executable handler attribute
|
|
272
|
+
const reparsed = new DOMParser().parseFromString(result, 'text/html');
|
|
273
|
+
expect(reparsed.querySelector('[onerror]')).toBeNull();
|
|
274
|
+
expect(reparsed.querySelector('img')).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('strips svg onload payloads without leaving handlers', () => {
|
|
278
|
+
const payload = '<svg onload="alert(1)"><text>x</text></svg>';
|
|
279
|
+
const result = stripHTML(payload);
|
|
280
|
+
expect(result).toBe('x');
|
|
281
|
+
expect(result).not.toMatch(/onload/i);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('decodes HTML entities without executing nested markup', () => {
|
|
285
|
+
const encoded =
|
|
286
|
+
'<img src="x" onerror="alert(1)">hello</img>';
|
|
287
|
+
const result = stripHTML(encoded);
|
|
288
|
+
expect(result).toContain('hello');
|
|
289
|
+
// Entity-decoded text may contain the string "onerror" as text; ensure no live element
|
|
290
|
+
const reparsed = new DOMParser().parseFromString(
|
|
291
|
+
`<div>${result.replace(/</g, '<')}</div>`,
|
|
292
|
+
'text/html'
|
|
293
|
+
);
|
|
294
|
+
expect(reparsed.querySelector('[onerror]')).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('handles nested tags', () => {
|
|
298
|
+
expect(stripHTML('<div><span>a</span><b>b</b></div>')).toBe('ab');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('returns empty string for empty input', () => {
|
|
302
|
+
expect(stripHTML('')).toBe('');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
144
306
|
describe('utils/parsing combined', () => {
|
|
145
307
|
it('should remove output tag from real message', () => {
|
|
146
308
|
const result = escapeHTML(
|
package/src/helpers/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import { Material, MeshStandardMaterial, SkinnedMesh } from 'three';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
|
+
import { officeNativeExtensions } from './constants';
|
|
4
5
|
|
|
5
6
|
export const hasTouchscreen = (): boolean => {
|
|
6
7
|
let hasTouchScreen = false;
|
|
@@ -237,6 +238,67 @@ export const stripMarkdown = (text: string) => {
|
|
|
237
238
|
return text;
|
|
238
239
|
};
|
|
239
240
|
|
|
241
|
+
export const OFFICE_NATIVE_EXTENSIONS = officeNativeExtensions;
|
|
242
|
+
|
|
243
|
+
export const isOfficeNativeFilename = (filename: string): boolean => {
|
|
244
|
+
const ext = `.${filename.split('.').pop()?.toLowerCase() || ''}`;
|
|
245
|
+
return (officeNativeExtensions as readonly string[]).includes(ext);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export type ParsedDocumentAttachment = {
|
|
249
|
+
filename: string;
|
|
250
|
+
type: string;
|
|
251
|
+
content: string;
|
|
252
|
+
url: string;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const DOCUMENT_ATTACHMENT_REGEX =
|
|
256
|
+
/<document_attachment filename="([^"]+)" type="([^"]+)">([\s\S]*?)<\/document_attachment>/g;
|
|
257
|
+
|
|
258
|
+
const ATTACHMENT_LINK_AFTER_REGEX =
|
|
259
|
+
/<attachment_link>\s*([\s\S]*?)\s*<\/attachment_link>/;
|
|
260
|
+
|
|
261
|
+
export const parseDocumentAttachmentsFromMessage = (
|
|
262
|
+
text: string
|
|
263
|
+
): ParsedDocumentAttachment[] => {
|
|
264
|
+
if (!text) return [];
|
|
265
|
+
|
|
266
|
+
const attachments: ParsedDocumentAttachment[] = [];
|
|
267
|
+
const regex = new RegExp(DOCUMENT_ATTACHMENT_REGEX.source, 'g');
|
|
268
|
+
let match;
|
|
269
|
+
|
|
270
|
+
while ((match = regex.exec(text)) !== null) {
|
|
271
|
+
const [, filename, type, content] = match;
|
|
272
|
+
const afterTag = text.slice(match.index + match[0].length);
|
|
273
|
+
const linkMatch = afterTag.match(ATTACHMENT_LINK_AFTER_REGEX);
|
|
274
|
+
const rawUrl = linkMatch?.[1]?.trim() || '';
|
|
275
|
+
const url = /^https?:\/\//.test(rawUrl) ? rawUrl : '';
|
|
276
|
+
|
|
277
|
+
attachments.push({
|
|
278
|
+
filename,
|
|
279
|
+
type,
|
|
280
|
+
content: content.trim(),
|
|
281
|
+
url,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return attachments;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const extractAttachmentLinks = (content: string): string[] => {
|
|
289
|
+
return parseDocumentAttachmentsFromMessage(content).map(
|
|
290
|
+
attachment => attachment.url
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const extractAttachmentLink = (content: string): string | null => {
|
|
295
|
+
const match = content?.match(
|
|
296
|
+
/<attachment_link>\s*([\s\S]*?)\s*<\/attachment_link>/
|
|
297
|
+
);
|
|
298
|
+
const rawUrl = match?.[1]?.trim() || '';
|
|
299
|
+
return /^https?:\/\//.test(rawUrl) ? rawUrl : null;
|
|
300
|
+
};
|
|
301
|
+
|
|
240
302
|
export const stripDocumentAttachmentTags = (text: string): string => {
|
|
241
303
|
const documentAttachmentTagRegex = /<document_attachment filename="([^"]+)" type="([^"]+)">([\s\S]*?)<\/document_attachment>/g;
|
|
242
304
|
return text
|
|
@@ -245,6 +307,36 @@ export const stripDocumentAttachmentTags = (text: string): string => {
|
|
|
245
307
|
.replace(/<attachment_link>\s*[\s\S]*?\s*<\/attachment_link>/g, '');
|
|
246
308
|
};
|
|
247
309
|
|
|
310
|
+
export const getDocumentAttachmentAssetUrl = (attachment: {
|
|
311
|
+
content?: string | null;
|
|
312
|
+
url?: string | null;
|
|
313
|
+
}): string =>
|
|
314
|
+
attachment.url?.trim() ||
|
|
315
|
+
extractAttachmentLink(attachment.content || '') ||
|
|
316
|
+
'';
|
|
317
|
+
|
|
318
|
+
export const isAssetOnlyDocumentAttachment = (attachment: {
|
|
319
|
+
title?: string;
|
|
320
|
+
name?: string;
|
|
321
|
+
content?: string | null;
|
|
322
|
+
url?: string | null;
|
|
323
|
+
}): boolean => {
|
|
324
|
+
const filename = attachment.title || attachment.name || '';
|
|
325
|
+
const assetUrl = getDocumentAttachmentAssetUrl(attachment);
|
|
326
|
+
|
|
327
|
+
if (!assetUrl) return false;
|
|
328
|
+
|
|
329
|
+
if (isOfficeNativeFilename(filename)) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const strippedContent = attachment.content
|
|
334
|
+
? stripDocumentAttachmentTags(attachment.content).trim()
|
|
335
|
+
: '';
|
|
336
|
+
|
|
337
|
+
return !strippedContent;
|
|
338
|
+
};
|
|
339
|
+
|
|
248
340
|
export const stripOutputTags = (text: string): string => {
|
|
249
341
|
const outputTagRegex = /<output.*?<\/output>/gs;
|
|
250
342
|
|
|
@@ -271,10 +363,23 @@ export const stripReasoningTags = (text: string) => {
|
|
|
271
363
|
return strippedText;
|
|
272
364
|
};
|
|
273
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Strip HTML tags / decode entities without executing scripts or event handlers.
|
|
368
|
+
* Uses an inert DOMParser document (no resource loads / onerror), unlike live innerHTML.
|
|
369
|
+
*/
|
|
274
370
|
export const stripHTML = (text: string) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
371
|
+
if (typeof DOMParser !== 'undefined') {
|
|
372
|
+
try {
|
|
373
|
+
return (
|
|
374
|
+
new DOMParser().parseFromString(text, 'text/html').body.textContent ||
|
|
375
|
+
''
|
|
376
|
+
);
|
|
377
|
+
} catch {
|
|
378
|
+
// fall through
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Non-DOM / parse failure: strip tags only (does not decode entities)
|
|
382
|
+
return text.replace(/<[^>]*>/g, '');
|
|
278
383
|
};
|
|
279
384
|
|
|
280
385
|
/** Ensures all <a> tags in HTML open in a new tab (target="_blank" rel="noopener noreferrer"). */
|