@memori.ai/memori-react 8.15.1 → 8.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/components/Chat/Chat.css +2 -1
- package/dist/components/Chat/Chat.js +17 -17
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatBubble/ChatBubble.css +1 -1
- package/dist/components/ContentPreviewModal/ContentPreviewModal.css +114 -0
- package/dist/components/ContentPreviewModal/ContentPreviewModal.d.ts +14 -0
- package/dist/components/ContentPreviewModal/ContentPreviewModal.js +18 -0
- package/dist/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -0
- package/dist/components/ContentPreviewModal/index.d.ts +2 -0
- package/dist/components/ContentPreviewModal/index.js +9 -0
- package/dist/components/ContentPreviewModal/index.js.map +1 -0
- package/dist/components/FilePreview/FilePreview.css +1 -1
- package/dist/components/FilePreview/FilePreview.js +43 -13
- package/dist/components/FilePreview/FilePreview.js.map +1 -1
- package/dist/components/MediaWidget/DocumentCard.d.ts +3 -0
- package/dist/components/MediaWidget/DocumentCard.js +9 -0
- package/dist/components/MediaWidget/DocumentCard.js.map +1 -0
- package/dist/components/MediaWidget/MediaItemWidget.css +946 -19
- package/dist/components/MediaWidget/MediaItemWidget.d.ts +5 -36
- package/dist/components/MediaWidget/MediaItemWidget.js +295 -198
- package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/dist/components/MediaWidget/MediaItemWidget.types.d.ts +62 -0
- package/dist/components/MediaWidget/MediaItemWidget.types.js +3 -0
- package/dist/components/MediaWidget/MediaItemWidget.types.js.map +1 -0
- package/dist/components/MediaWidget/MediaItemWidget.utils.d.ts +23 -0
- package/dist/components/MediaWidget/MediaItemWidget.utils.js +162 -0
- package/dist/components/MediaWidget/MediaItemWidget.utils.js.map +1 -0
- package/dist/components/MediaWidget/MediaPreviewModal.d.ts +15 -0
- package/dist/components/MediaWidget/MediaPreviewModal.js +162 -0
- package/dist/components/MediaWidget/MediaPreviewModal.js.map +1 -0
- package/dist/components/MediaWidget/MediaWidget.js +1 -2
- package/dist/components/MediaWidget/MediaWidget.js.map +1 -1
- package/dist/components/Snippet/Snippet.css +64 -33
- package/dist/components/Snippet/Snippet.js +17 -4
- package/dist/components/Snippet/Snippet.js.map +1 -1
- package/dist/components/StartPanel/StartPanel.js +1 -2
- package/dist/components/StartPanel/StartPanel.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.css +0 -5
- package/dist/components/layouts/WebsiteAssistant.js +8 -8
- package/dist/components/layouts/WebsiteAssistant.js.map +1 -1
- package/dist/components/layouts/chat.css +1 -1
- package/dist/components/layouts/website-assistant.css +405 -197
- package/dist/helpers/constants.js +0 -7
- package/dist/helpers/constants.js.map +1 -1
- package/dist/helpers/utils.d.ts +1 -0
- package/dist/helpers/utils.js +3 -1
- package/dist/helpers/utils.js.map +1 -1
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.css +0 -2
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/esm/components/Chat/Chat.css +2 -1
- package/esm/components/Chat/Chat.js +17 -17
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatBubble/ChatBubble.css +1 -1
- package/esm/components/ContentPreviewModal/ContentPreviewModal.css +114 -0
- package/esm/components/ContentPreviewModal/ContentPreviewModal.d.ts +14 -0
- package/esm/components/ContentPreviewModal/ContentPreviewModal.js +15 -0
- package/esm/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -0
- package/esm/components/ContentPreviewModal/index.d.ts +2 -0
- package/esm/components/ContentPreviewModal/index.js +2 -0
- package/esm/components/ContentPreviewModal/index.js.map +1 -0
- package/esm/components/FilePreview/FilePreview.css +1 -1
- package/esm/components/FilePreview/FilePreview.js +44 -14
- package/esm/components/FilePreview/FilePreview.js.map +1 -1
- package/esm/components/MediaWidget/DocumentCard.d.ts +3 -0
- package/esm/components/MediaWidget/DocumentCard.js +5 -0
- package/esm/components/MediaWidget/DocumentCard.js.map +1 -0
- package/esm/components/MediaWidget/MediaItemWidget.css +946 -19
- package/esm/components/MediaWidget/MediaItemWidget.d.ts +5 -36
- package/esm/components/MediaWidget/MediaItemWidget.js +296 -197
- package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/esm/components/MediaWidget/MediaItemWidget.types.d.ts +62 -0
- package/esm/components/MediaWidget/MediaItemWidget.types.js +2 -0
- package/esm/components/MediaWidget/MediaItemWidget.types.js.map +1 -0
- package/esm/components/MediaWidget/MediaItemWidget.utils.d.ts +23 -0
- package/esm/components/MediaWidget/MediaItemWidget.utils.js +149 -0
- package/esm/components/MediaWidget/MediaItemWidget.utils.js.map +1 -0
- package/esm/components/MediaWidget/MediaPreviewModal.d.ts +15 -0
- package/esm/components/MediaWidget/MediaPreviewModal.js +157 -0
- package/esm/components/MediaWidget/MediaPreviewModal.js.map +1 -0
- package/esm/components/MediaWidget/MediaWidget.js +1 -2
- package/esm/components/MediaWidget/MediaWidget.js.map +1 -1
- package/esm/components/Snippet/Snippet.css +64 -33
- package/esm/components/Snippet/Snippet.js +18 -5
- package/esm/components/Snippet/Snippet.js.map +1 -1
- package/esm/components/StartPanel/StartPanel.js +1 -2
- package/esm/components/StartPanel/StartPanel.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.css +0 -5
- package/esm/components/layouts/WebsiteAssistant.js +8 -8
- package/esm/components/layouts/WebsiteAssistant.js.map +1 -1
- package/esm/components/layouts/chat.css +1 -1
- package/esm/components/layouts/website-assistant.css +405 -197
- package/esm/helpers/constants.js +0 -7
- package/esm/helpers/constants.js.map +1 -1
- package/esm/helpers/utils.d.ts +1 -0
- package/esm/helpers/utils.js +1 -0
- package/esm/helpers/utils.js.map +1 -1
- package/esm/index.js +43 -1
- package/esm/index.js.map +1 -1
- package/esm/styles.css +0 -2
- package/esm/version.d.ts +1 -0
- package/esm/version.js +2 -0
- package/esm/version.js.map +1 -0
- package/package.json +5 -3
- package/src/components/Chat/Chat.css +2 -1
- package/src/components/Chat/Chat.stories.tsx +124 -0
- package/src/components/Chat/Chat.tsx +72 -71
- package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +567 -1034
- package/src/components/ChatBubble/ChatBubble.css +1 -1
- package/src/components/ContentPreviewModal/ContentPreviewModal.css +114 -0
- package/src/components/ContentPreviewModal/ContentPreviewModal.tsx +69 -0
- package/src/components/ContentPreviewModal/index.ts +2 -0
- package/src/components/FilePreview/FilePreview.css +1 -1
- package/src/components/FilePreview/FilePreview.tsx +60 -37
- package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +15 -105
- package/src/components/MediaWidget/DocumentCard.test.tsx +45 -0
- package/src/components/MediaWidget/DocumentCard.tsx +19 -0
- package/src/components/MediaWidget/MediaItemWidget.css +946 -19
- package/src/components/MediaWidget/MediaItemWidget.test.tsx +89 -1
- package/src/components/MediaWidget/MediaItemWidget.tsx +734 -461
- package/src/components/MediaWidget/MediaItemWidget.types.ts +65 -0
- package/src/components/MediaWidget/MediaItemWidget.utils.test.ts +324 -0
- package/src/components/MediaWidget/MediaItemWidget.utils.ts +194 -0
- package/src/components/MediaWidget/MediaPreviewModal.test.tsx +271 -0
- package/src/components/MediaWidget/MediaPreviewModal.tsx +423 -0
- package/src/components/MediaWidget/MediaWidget.stories.tsx +193 -0
- package/src/components/MediaWidget/MediaWidget.tsx +2 -4
- package/src/components/MediaWidget/__snapshots__/DocumentCard.test.tsx.snap +24 -0
- package/src/components/MediaWidget/__snapshots__/MediaItemWidget.test.tsx.snap +162 -170
- package/src/components/MediaWidget/__snapshots__/MediaWidget.test.tsx.snap +21 -63
- package/src/components/Snippet/Snippet.css +64 -33
- package/src/components/Snippet/Snippet.tsx +30 -21
- package/src/components/Snippet/__snapshots__/Snippet.test.tsx.snap +314 -297
- package/src/components/StartPanel/StartPanel.tsx +0 -9
- package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +12 -636
- package/src/components/UploadButton/UploadButton.css +0 -5
- package/src/components/layouts/WebsiteAssistant.tsx +66 -62
- package/src/components/layouts/__snapshots__/Chat.test.tsx.snap +1 -53
- package/src/components/layouts/__snapshots__/FullPage.test.tsx.snap +2 -106
- package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +1 -53
- package/src/components/layouts/__snapshots__/Totem.test.tsx.snap +1 -53
- package/src/components/layouts/__snapshots__/WebsiteAssistant.test.tsx.snap +32 -33
- package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +1 -53
- package/src/components/layouts/chat.css +1 -1
- package/src/components/layouts/layouts.stories.tsx +68 -0
- package/src/components/layouts/website-assistant.css +405 -197
- package/src/helpers/constants.ts +0 -7
- package/src/helpers/utils.ts +4 -0
- package/src/index.test.tsx +8 -0
- package/src/index.tsx +51 -1
- package/src/styles.css +0 -2
- package/src/version.ts +2 -0
- package/src/components/AttachmentLinkModal/AttachmentLinkModal.css +0 -68
- package/src/components/AttachmentLinkModal/AttachmentLinkModal.stories.tsx +0 -32
- package/src/components/AttachmentLinkModal/AttachmentLinkModal.test.tsx +0 -10
- package/src/components/AttachmentLinkModal/AttachmentLinkModal.tsx +0 -131
- package/src/components/AttachmentLinkModal/__snapshots__/AttachmentLinkModal.test.tsx.snap +0 -9
- package/src/components/MediaWidget/LinkItemWidget.css +0 -46
- package/src/components/MediaWidget/LinkItemWidget.stories.tsx +0 -61
- package/src/components/MediaWidget/LinkItemWidget.test.tsx +0 -33
- package/src/components/MediaWidget/LinkItemWidget.tsx +0 -204
- package/src/components/MediaWidget/__snapshots__/LinkItemWidget.test.tsx.snap +0 -253
|
@@ -1,58 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
// MediaItemWidget.tsx — Main Media Item Widget file for rendering various types of media inside Memori
|
|
2
|
+
import type { Medium } from '@memori.ai/memori-api-client/dist/types';
|
|
3
|
+
import React, {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
memo,
|
|
9
|
+
useRef,
|
|
10
|
+
} from 'react';
|
|
3
11
|
import { getResourceUrl } from '../../helpers/media';
|
|
12
|
+
import {
|
|
13
|
+
withLinksOpenInNewTab,
|
|
14
|
+
stripDocumentAttachmentTags,
|
|
15
|
+
} from '../../helpers/utils';
|
|
4
16
|
import { getTranslation } from '../../helpers/translations';
|
|
5
17
|
import { prismSyntaxLangs } from '../../helpers/constants';
|
|
6
18
|
import ModelViewer from '../CustomGLBModelViewer/ModelViewer';
|
|
7
19
|
import Snippet from '../Snippet/Snippet';
|
|
8
20
|
import Card from '../ui/Card';
|
|
9
21
|
import Modal from '../ui/Modal';
|
|
10
|
-
import Button from '../ui/Button';
|
|
11
22
|
import File from '../icons/File';
|
|
12
|
-
import FilePdf from '../icons/FilePdf';
|
|
13
|
-
import FileExcel from '../icons/FileExcel';
|
|
14
|
-
import FileWord from '../icons/FileWord';
|
|
15
|
-
import Copy from '../icons/Copy';
|
|
16
23
|
import { Transition } from '@headlessui/react';
|
|
17
|
-
import { stripHTML } from '../../helpers/utils';
|
|
18
24
|
import cx from 'classnames';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
import Sound from '../icons/Sound';
|
|
26
|
+
import Link from '../icons/Link';
|
|
27
|
+
import { ellipsis } from 'ellipsed';
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
MediaItemWidgetProps as Props,
|
|
31
|
+
MediaItem,
|
|
32
|
+
RenderMediaItemProps,
|
|
33
|
+
RenderSnippetItemProps,
|
|
34
|
+
LinkPreviewInfo,
|
|
35
|
+
} from './MediaItemWidget.types';
|
|
36
|
+
import {
|
|
37
|
+
formatBytes,
|
|
38
|
+
getFileExtensionFromUrl,
|
|
39
|
+
getFileExtensionFromMime,
|
|
40
|
+
countLines,
|
|
41
|
+
shouldUseDarkFileCard,
|
|
42
|
+
fetchLinkPreview,
|
|
43
|
+
getContentSize,
|
|
44
|
+
normalizeUrl,
|
|
45
|
+
getImageDisplaySource,
|
|
46
|
+
FALLBACK_IMAGE_BASE64,
|
|
47
|
+
TEXT_FILE_EXTENSIONS,
|
|
48
|
+
IMAGE_MIME_TYPES,
|
|
49
|
+
} from './MediaItemWidget.utils';
|
|
50
|
+
import { DocumentCard } from './DocumentCard';
|
|
51
|
+
import { MediaPreviewModal } from './MediaPreviewModal';
|
|
52
|
+
|
|
53
|
+
export type {
|
|
54
|
+
LinkPreviewInfo,
|
|
55
|
+
ILinkPreviewInfo,
|
|
56
|
+
} from './MediaItemWidget.types';
|
|
57
|
+
export type { Props };
|
|
58
|
+
|
|
59
|
+
// List of code mime types from Prism's available languages
|
|
60
|
+
const CODE_MIME_TYPES = prismSyntaxLangs.map(l => l.mimeType);
|
|
61
|
+
|
|
62
|
+
/** Image MIME types that open in the preview modal on click (when mediumID + onClick are set) */
|
|
63
|
+
const IMAGE_MODAL_MIME_TYPES = [
|
|
64
|
+
'image/jpeg',
|
|
65
|
+
'image/png',
|
|
66
|
+
'image/jpg',
|
|
67
|
+
'image/gif',
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
function isImageMime(mimeType: string): boolean {
|
|
71
|
+
return (IMAGE_MODAL_MIME_TYPES as readonly string[]).includes(mimeType);
|
|
28
72
|
}
|
|
29
73
|
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
// RenderMediaItem — Renders the suitable content for a Medium (images, files, html, code, audio, video…)
|
|
75
|
+
export const RenderMediaItem = memo(function RenderMediaItem({
|
|
76
|
+
isChild: _isChild = false,
|
|
32
77
|
item,
|
|
33
78
|
sessionID,
|
|
34
79
|
tenantID,
|
|
35
80
|
preview = false,
|
|
36
81
|
baseURL,
|
|
37
82
|
apiURL,
|
|
38
|
-
onClick,
|
|
83
|
+
onClick: _onClick,
|
|
39
84
|
customMediaRenderer,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
tenantID?: string;
|
|
45
|
-
preview?: boolean;
|
|
46
|
-
baseURL?: string;
|
|
47
|
-
apiURL?: string;
|
|
48
|
-
onClick?: (mediumID: string) => void;
|
|
49
|
-
customMediaRenderer?: (mimeType: string) => JSX.Element | null;
|
|
50
|
-
}) => {
|
|
51
|
-
const [modalOpen, setModalOpen] = useState(false);
|
|
85
|
+
descriptionOneLine = false,
|
|
86
|
+
onLinkPreviewInfo,
|
|
87
|
+
}: RenderMediaItemProps): React.ReactElement | null {
|
|
88
|
+
// State for "copy to clipboard" notification for snippets
|
|
52
89
|
const [copyNotification, setCopyNotification] = useState(false);
|
|
90
|
+
// State for fallback image
|
|
53
91
|
const [imageError, setImageError] = useState(false);
|
|
54
|
-
|
|
55
|
-
const
|
|
92
|
+
// Link preview info (site title, description, image, etc)
|
|
93
|
+
const [link, setLink] = useState<
|
|
94
|
+
(LinkPreviewInfo & { urlKey: string }) | null
|
|
95
|
+
>(null);
|
|
96
|
+
// Persistent ref for onLinkPreviewInfo callback
|
|
97
|
+
const onLinkPreviewInfoRef = useRef(onLinkPreviewInfo);
|
|
98
|
+
onLinkPreviewInfoRef.current = onLinkPreviewInfo;
|
|
99
|
+
|
|
100
|
+
// Get URL with possible session/tenant/base/api
|
|
101
|
+
const resourceUrl = getResourceUrl({
|
|
56
102
|
resourceURI: item.url,
|
|
57
103
|
sessionID,
|
|
58
104
|
tenantID,
|
|
@@ -60,300 +106,487 @@ export const RenderMediaItem = ({
|
|
|
60
106
|
apiURL,
|
|
61
107
|
});
|
|
62
108
|
|
|
63
|
-
|
|
109
|
+
// Normalize URL (strip protocol, etc)
|
|
110
|
+
const normURL = normalizeUrl(item.url);
|
|
111
|
+
|
|
112
|
+
// Fetch link preview info for HTML links, only if relevant and not already loaded
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (
|
|
115
|
+
item.mimeType !== 'text/html' ||
|
|
116
|
+
!normURL ||
|
|
117
|
+
normURL === link?.urlKey ||
|
|
118
|
+
!baseURL
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let cancelled = false;
|
|
123
|
+
fetchLinkPreview(normURL, baseURL).then(siteInfo => {
|
|
124
|
+
if (cancelled) return;
|
|
125
|
+
setLink(
|
|
126
|
+
siteInfo
|
|
127
|
+
? ({ ...siteInfo, urlKey: normURL } as LinkPreviewInfo & {
|
|
128
|
+
urlKey: string;
|
|
129
|
+
})
|
|
130
|
+
: null
|
|
131
|
+
);
|
|
132
|
+
if (siteInfo && onLinkPreviewInfoRef.current) {
|
|
133
|
+
onLinkPreviewInfoRef.current(siteInfo);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return () => {
|
|
137
|
+
cancelled = true;
|
|
138
|
+
};
|
|
139
|
+
}, [item?.url, baseURL, item.mimeType, normURL, link?.urlKey]);
|
|
64
140
|
|
|
141
|
+
// Custom renderer for media type, overrides our logic
|
|
142
|
+
const customRenderer = customMediaRenderer?.(item.mimeType);
|
|
65
143
|
if (customRenderer) {
|
|
66
144
|
return customRenderer;
|
|
67
145
|
}
|
|
68
146
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.includes(item.mimeType);
|
|
147
|
+
// Media type detection flags
|
|
148
|
+
const isCodeSnippet = CODE_MIME_TYPES.includes(item.mimeType);
|
|
72
149
|
const isHTML = item.mimeType === 'text/html';
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return isImageRGB ? (
|
|
113
|
-
<picture className="memori-media-item--figure">
|
|
114
|
-
<div
|
|
115
|
-
className="memori-media-item--rgb-item"
|
|
116
|
-
style={{
|
|
117
|
-
backgroundColor: item.url,
|
|
118
|
-
}}
|
|
119
|
-
/>
|
|
120
|
-
{item.title && (
|
|
121
|
-
<figcaption className="memori-media-item--figure-caption">
|
|
122
|
-
{item.title}
|
|
123
|
-
</figcaption>
|
|
124
|
-
)}
|
|
125
|
-
</picture>
|
|
126
|
-
) : (
|
|
127
|
-
<picture className="memori-media-item--figure">
|
|
128
|
-
{!preview && imageSrc && (
|
|
129
|
-
<source
|
|
130
|
-
srcSet={imageSrc}
|
|
131
|
-
type={item.mimeType}
|
|
150
|
+
const isDocumentAttachment = item.properties?.isDocumentAttachment === true;
|
|
151
|
+
const isAttachedFile = item.properties?.isAttachedFile === true;
|
|
152
|
+
|
|
153
|
+
// Single source of truth for image display (resource URL, base64, or rgb/rgba)
|
|
154
|
+
const imageDisplay = getImageDisplaySource(item, resourceUrl);
|
|
155
|
+
const { src: imageSrc, isRgb: isImageRGB } = imageDisplay;
|
|
156
|
+
|
|
157
|
+
// Link preview fields (title, description, video, image)
|
|
158
|
+
const linkTitle =
|
|
159
|
+
item.title && item.title.length > 0 ? item.title : link?.title;
|
|
160
|
+
const linkDescription = link?.description;
|
|
161
|
+
const linkVideo = link?.video;
|
|
162
|
+
const linkImage = link?.image ?? link?.images?.[0];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render the actual content for a media item based on its MIME type
|
|
166
|
+
*/
|
|
167
|
+
const renderMediaContent = useCallback(
|
|
168
|
+
(medium: Medium & { type?: string }) => {
|
|
169
|
+
// Get resource url for this medium
|
|
170
|
+
const url = getResourceUrl({
|
|
171
|
+
resourceURI: medium.url,
|
|
172
|
+
sessionID,
|
|
173
|
+
tenantID,
|
|
174
|
+
baseURL,
|
|
175
|
+
apiURL,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
switch (medium.mimeType) {
|
|
179
|
+
// Render images (RGB images as colored swatches)
|
|
180
|
+
case 'image/jpeg':
|
|
181
|
+
case 'image/png':
|
|
182
|
+
case 'image/jpg':
|
|
183
|
+
case 'image/gif':
|
|
184
|
+
return isImageRGB ? (
|
|
185
|
+
<picture className="memori-media-item--figure">
|
|
186
|
+
<div
|
|
187
|
+
className="memori-media-item--rgb-item"
|
|
188
|
+
style={{ backgroundColor: medium.url }}
|
|
132
189
|
/>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
</picture>
|
|
191
|
+
) : (
|
|
192
|
+
<picture className="memori-media-item--figure">
|
|
193
|
+
{!preview && imageSrc && (
|
|
194
|
+
<source srcSet={imageSrc} type={medium.mimeType} />
|
|
195
|
+
)}
|
|
196
|
+
<img
|
|
197
|
+
alt={medium.title}
|
|
198
|
+
src={imageError || !imageSrc ? FALLBACK_IMAGE_BASE64 : imageSrc}
|
|
199
|
+
onError={() => setImageError(true)}
|
|
200
|
+
/>
|
|
201
|
+
</picture>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Render video types
|
|
205
|
+
case 'video/mp4':
|
|
206
|
+
case 'video/quicktime':
|
|
207
|
+
case 'video/avi':
|
|
208
|
+
case 'video/mpeg':
|
|
209
|
+
return (
|
|
210
|
+
<div className="memori-media-item--video-container">
|
|
211
|
+
<video
|
|
212
|
+
className="memori-media-item--video-player"
|
|
213
|
+
controls
|
|
214
|
+
src={url}
|
|
215
|
+
title={medium.title}
|
|
216
|
+
>
|
|
217
|
+
{/* Quicktime special fallback for .mp4 */}
|
|
218
|
+
{medium.mimeType === 'video/quicktime' && (
|
|
219
|
+
<source src={medium.url} type="video/mp4" />
|
|
220
|
+
)}
|
|
221
|
+
<source src={medium.url} type={medium.mimeType} />
|
|
222
|
+
Your browser does not support this video format.
|
|
223
|
+
</video>
|
|
224
|
+
{/* Play overlay icon (hidden by default) */}
|
|
225
|
+
<div className="memori-media-item--video-overlay hidden">
|
|
226
|
+
<svg
|
|
227
|
+
className="memori-media-item--play-icon"
|
|
228
|
+
viewBox="0 0 24 24"
|
|
229
|
+
fill="currentColor"
|
|
230
|
+
>
|
|
231
|
+
<path d="M8 5v14l11-7z" />
|
|
232
|
+
</svg>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Render audio types (shows a sound icon + audio controls)
|
|
238
|
+
case 'audio/mpeg3':
|
|
239
|
+
case 'audio/wav':
|
|
240
|
+
case 'audio/mpeg':
|
|
241
|
+
return (
|
|
242
|
+
<div className="memori-media-item--audio-container">
|
|
243
|
+
<div className="memori-media-item--audio-icon">
|
|
244
|
+
<Sound />
|
|
245
|
+
</div>
|
|
246
|
+
<audio
|
|
247
|
+
className="memori-media-item--audio-player"
|
|
248
|
+
controls
|
|
249
|
+
src={url}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Render 3D models (GLB only)
|
|
255
|
+
case 'model/gltf-binary':
|
|
256
|
+
return (
|
|
257
|
+
<div className="memori-media-item--model-container">
|
|
258
|
+
<div className="memori-media-item--model-viewer">
|
|
259
|
+
<ModelViewer
|
|
260
|
+
src={url}
|
|
261
|
+
alt=""
|
|
262
|
+
poster="data:image/png;base64,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"
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// HTML files are now handled as file cards (rendered above in the isFile check)
|
|
269
|
+
// This case is kept for backwards compatibility but should not be reached
|
|
270
|
+
case 'text/html':
|
|
271
|
+
// Fallback to file card - HTML files are handled in the file card section above
|
|
272
|
+
return (
|
|
273
|
+
<DocumentCard
|
|
274
|
+
title={medium.title || 'File'}
|
|
275
|
+
badge={getFileExtensionFromMime(medium.mimeType)}
|
|
276
|
+
meta={(() => {
|
|
277
|
+
const size = getContentSize(medium);
|
|
278
|
+
return size != null && size > 0 ? formatBytes(size) : null;
|
|
279
|
+
})()}
|
|
280
|
+
icon={<Link className="memori-media-item--document-icon-svg" />}
|
|
138
281
|
/>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Generic fallback: Render a document card for unknown types
|
|
285
|
+
default:
|
|
286
|
+
return (
|
|
287
|
+
<DocumentCard
|
|
288
|
+
title={medium.title || 'File'}
|
|
289
|
+
badge={getFileExtensionFromMime(medium.mimeType)}
|
|
290
|
+
meta={(() => {
|
|
291
|
+
const size = getContentSize(medium);
|
|
292
|
+
return size != null && size > 0 ? formatBytes(size) : null;
|
|
293
|
+
})()}
|
|
294
|
+
icon={<File className="memori-media-item--document-icon-svg" />}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
[
|
|
300
|
+
sessionID,
|
|
301
|
+
tenantID,
|
|
302
|
+
baseURL,
|
|
303
|
+
apiURL,
|
|
304
|
+
preview,
|
|
305
|
+
imageSrc,
|
|
306
|
+
imageError,
|
|
307
|
+
isImageRGB,
|
|
308
|
+
linkImage,
|
|
309
|
+
linkVideo,
|
|
310
|
+
]
|
|
311
|
+
);
|
|
157
312
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)}
|
|
172
|
-
<source src={item.url} type={item.mimeType} />
|
|
173
|
-
Your browser does not support this video format.
|
|
174
|
-
<br />
|
|
175
|
-
<a href={item.url} target="_blank" rel="noopener noreferrer">
|
|
176
|
-
Download the video
|
|
177
|
-
</a>
|
|
178
|
-
</video>
|
|
179
|
-
);
|
|
313
|
+
// Extension and file detection helpers
|
|
314
|
+
const fileExtensionFromUrl = getFileExtensionFromUrl(normURL || item.url);
|
|
315
|
+
const fileExtensionFromMime = getFileExtensionFromMime(item.mimeType);
|
|
316
|
+
const fileExtension = fileExtensionFromUrl || fileExtensionFromMime;
|
|
317
|
+
const isFile = shouldUseDarkFileCard(
|
|
318
|
+
item,
|
|
319
|
+
fileExtensionFromUrl,
|
|
320
|
+
item.mimeType
|
|
321
|
+
);
|
|
322
|
+
// Text file detection for line counting
|
|
323
|
+
const isTextFile = (TEXT_FILE_EXTENSIONS as readonly string[]).includes(
|
|
324
|
+
fileExtension || ''
|
|
325
|
+
);
|
|
180
326
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
327
|
+
// Derive line count and line label for text files
|
|
328
|
+
const lineCount =
|
|
329
|
+
isTextFile && item.content ? countLines(item.content) : null;
|
|
330
|
+
const lineText =
|
|
331
|
+
lineCount !== null
|
|
332
|
+
? lineCount === 1
|
|
333
|
+
? '1 line'
|
|
334
|
+
: `${lineCount} lines`
|
|
335
|
+
: null;
|
|
336
|
+
|
|
337
|
+
// File-like cards that are NOT code: render as clickable file cards
|
|
338
|
+
if (isFile && !isCodeSnippet) {
|
|
339
|
+
const contentSize = getContentSize(item);
|
|
340
|
+
const sizeText =
|
|
341
|
+
contentSize != null && contentSize > 0 ? formatBytes(contentSize) : null;
|
|
342
|
+
const displayName = item.title || linkTitle || 'File';
|
|
343
|
+
const metaParts = [lineText, sizeText].filter(Boolean);
|
|
344
|
+
const metaLine = metaParts.length > 0 ? metaParts.join(' · ') : null;
|
|
345
|
+
|
|
346
|
+
// Document attachments and attached files should open in modal, not as links
|
|
347
|
+
if ((isDocumentAttachment || isAttachedFile) && item.mediumID && _onClick) {
|
|
348
|
+
return (
|
|
349
|
+
<div
|
|
350
|
+
onClick={() => _onClick(item)}
|
|
351
|
+
className="memori-media-item--link memori-media-item--document-link"
|
|
352
|
+
style={{ cursor: 'pointer' }}
|
|
353
|
+
title={displayName}
|
|
354
|
+
role="button"
|
|
355
|
+
tabIndex={0}
|
|
356
|
+
onKeyDown={e => {
|
|
357
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
358
|
+
e.preventDefault();
|
|
359
|
+
_onClick(item);
|
|
360
|
+
}
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
<DocumentCard
|
|
364
|
+
title={displayName}
|
|
365
|
+
badge={
|
|
366
|
+
item.mimeType === 'text/html' && !!item.url
|
|
367
|
+
? 'Link'
|
|
368
|
+
: fileExtension
|
|
369
|
+
}
|
|
370
|
+
meta={metaLine}
|
|
371
|
+
icon={
|
|
372
|
+
item.mimeType === 'text/html' ? (
|
|
373
|
+
<Link className="memori-media-item--document-icon-svg" />
|
|
374
|
+
) : (
|
|
375
|
+
<File className="memori-media-item--document-icon-svg" />
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
187
382
|
|
|
188
|
-
|
|
383
|
+
// Build href: open in new tab (never modal). Use URL, or blob for content-only items.
|
|
384
|
+
const getFileCardHref = (): string => {
|
|
385
|
+
if (item.url) {
|
|
189
386
|
return (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
387
|
+
getResourceUrl({
|
|
388
|
+
resourceURI: item.url,
|
|
389
|
+
sessionID,
|
|
390
|
+
tenantID,
|
|
391
|
+
baseURL,
|
|
392
|
+
apiURL,
|
|
393
|
+
}) ||
|
|
394
|
+
item.url ||
|
|
395
|
+
'#'
|
|
195
396
|
);
|
|
397
|
+
}
|
|
398
|
+
if (isHTML && item.content) {
|
|
399
|
+
let htmlContent = item.content;
|
|
400
|
+
if (
|
|
401
|
+
item.properties?.isDocumentAttachment ||
|
|
402
|
+
htmlContent.includes('document_attachment') ||
|
|
403
|
+
htmlContent.includes('<document_attachment')
|
|
404
|
+
) {
|
|
405
|
+
if (htmlContent.includes('<') || htmlContent.includes('"')) {
|
|
406
|
+
const div = document.createElement('div');
|
|
407
|
+
div.innerHTML = htmlContent;
|
|
408
|
+
htmlContent = div.textContent || div.innerText || htmlContent;
|
|
409
|
+
}
|
|
410
|
+
htmlContent = stripDocumentAttachmentTags(htmlContent);
|
|
411
|
+
}
|
|
412
|
+
const blob = new Blob([htmlContent], { type: 'text/html' });
|
|
413
|
+
return URL.createObjectURL(blob);
|
|
414
|
+
}
|
|
415
|
+
if (item.content) {
|
|
416
|
+
const blob = new Blob([item.content], {
|
|
417
|
+
type: item.mimeType || 'text/plain',
|
|
418
|
+
});
|
|
419
|
+
return URL.createObjectURL(blob);
|
|
420
|
+
}
|
|
421
|
+
return '#';
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const hrefUrl = getFileCardHref();
|
|
196
425
|
|
|
197
|
-
default:
|
|
198
|
-
return <File className="memori-media-item--icon" />;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Handle code snippets with modal
|
|
203
|
-
if ((isCodeSnippet && item.content) || isHTML) {
|
|
204
426
|
return (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
title={
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
<Snippet medium={item} showCopyButton={true} />
|
|
228
|
-
</div>
|
|
229
|
-
</Modal>
|
|
230
|
-
</>
|
|
427
|
+
<a
|
|
428
|
+
href={hrefUrl}
|
|
429
|
+
target="_blank"
|
|
430
|
+
rel="noopener noreferrer"
|
|
431
|
+
className="memori-media-item--link memori-media-item--document-link"
|
|
432
|
+
title={displayName}
|
|
433
|
+
>
|
|
434
|
+
<DocumentCard
|
|
435
|
+
title={displayName}
|
|
436
|
+
badge={
|
|
437
|
+
item.mimeType === 'text/html' && !!item.url ? 'Link' : fileExtension
|
|
438
|
+
}
|
|
439
|
+
meta={metaLine}
|
|
440
|
+
icon={
|
|
441
|
+
item.mimeType === 'text/html' ? (
|
|
442
|
+
<Link className="memori-media-item--document-icon-svg" />
|
|
443
|
+
) : (
|
|
444
|
+
<File className="memori-media-item--document-icon-svg" />
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
/>
|
|
448
|
+
</a>
|
|
231
449
|
);
|
|
232
450
|
}
|
|
233
451
|
|
|
234
|
-
if
|
|
235
|
-
|
|
236
|
-
item?.type === 'document' &&
|
|
237
|
-
item.content &&
|
|
238
|
-
!['image/jpeg', 'image/png', 'image/jpg', 'image/gif'].includes(
|
|
239
|
-
item.mimeType
|
|
240
|
-
)
|
|
241
|
-
) {
|
|
452
|
+
// Inline previews for code snippets: open in new tab (blob URL if no resource URL)
|
|
453
|
+
if (isCodeSnippet && item.content && item.mediumID && _onClick) {
|
|
242
454
|
return (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
455
|
+
<div
|
|
456
|
+
className="memori-media-item--link"
|
|
457
|
+
style={{ cursor: 'pointer' }}
|
|
458
|
+
onClick={() => _onClick(item)}
|
|
459
|
+
title={item.title}
|
|
460
|
+
role="button"
|
|
461
|
+
tabIndex={0}
|
|
462
|
+
onKeyDown={e => {
|
|
463
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
248
464
|
e.preventDefault();
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
465
|
+
_onClick(item);
|
|
466
|
+
}
|
|
467
|
+
}}
|
|
468
|
+
>
|
|
469
|
+
<Card hoverable cover={renderMediaContent(item)} title={item.title} />
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
255
473
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
474
|
+
// HTML file with link info / preview or video/image: render card with link preview (image, video, description)
|
|
475
|
+
if (isHTML && (linkImage || linkVideo || linkDescription)) {
|
|
476
|
+
// Compute card cover image/video src
|
|
477
|
+
const coverSrc =
|
|
478
|
+
linkImage?.includes('data:image') === true
|
|
479
|
+
? undefined
|
|
480
|
+
: linkImage?.startsWith('https')
|
|
481
|
+
? linkImage
|
|
482
|
+
: linkImage
|
|
483
|
+
? `https://${linkImage.replace('http://', '')}`
|
|
484
|
+
: undefined;
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<a
|
|
488
|
+
href={item.url || '#'}
|
|
489
|
+
target="_blank"
|
|
490
|
+
rel="noopener noreferrer"
|
|
491
|
+
className="memori-media-item--link"
|
|
492
|
+
title={linkTitle}
|
|
493
|
+
>
|
|
494
|
+
<Card
|
|
495
|
+
hoverable
|
|
496
|
+
className={cx('memori-media-item--card', {
|
|
497
|
+
'memori-media-item--card-description-oneline': descriptionOneLine,
|
|
498
|
+
'memori-media-item--card-has-image': !!linkImage,
|
|
499
|
+
'memori-media-item--card-has-video': !!linkVideo,
|
|
500
|
+
})}
|
|
501
|
+
cover={
|
|
502
|
+
linkVideo ? (
|
|
503
|
+
<iframe
|
|
504
|
+
width="100%"
|
|
505
|
+
height="100%"
|
|
506
|
+
src={linkVideo}
|
|
507
|
+
title="Video player"
|
|
508
|
+
frameBorder="0"
|
|
509
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
510
|
+
allowFullScreen
|
|
511
|
+
/>
|
|
512
|
+
) : linkImage ? (
|
|
513
|
+
<img
|
|
514
|
+
className="memori-media-item--card-cover-img"
|
|
515
|
+
src={coverSrc}
|
|
516
|
+
alt={linkTitle}
|
|
517
|
+
/>
|
|
518
|
+
) : (
|
|
519
|
+
<div className="memori-media-item--card-cover-icon">
|
|
520
|
+
<Link className="memori-media-item--icon" />
|
|
521
|
+
</div>
|
|
522
|
+
)
|
|
286
523
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
dangerouslySetInnerHTML={{
|
|
292
|
-
__html: stripHTML(item.content || ''),
|
|
293
|
-
}}
|
|
294
|
-
/>
|
|
295
|
-
</div>
|
|
296
|
-
</Modal>
|
|
297
|
-
</>
|
|
524
|
+
title={linkTitle}
|
|
525
|
+
description={linkDescription}
|
|
526
|
+
/>
|
|
527
|
+
</a>
|
|
298
528
|
);
|
|
299
529
|
}
|
|
300
530
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
531
|
+
// Run ellipsed.js to clamp link card description, only when a linkDescription is present
|
|
532
|
+
useEffect(() => {
|
|
533
|
+
if (!linkDescription) return;
|
|
534
|
+
const t = setTimeout(() => {
|
|
535
|
+
ellipsis('.memori-media-item--card .memori-card--description', 3, {
|
|
536
|
+
responsive: true,
|
|
537
|
+
});
|
|
538
|
+
}, 300);
|
|
539
|
+
return () => clearTimeout(t);
|
|
540
|
+
}, [linkDescription, item.mediumID]);
|
|
541
|
+
|
|
542
|
+
// -------------------------------------------------------------------------
|
|
543
|
+
// Image link flow: images with mediumID open in preview modal on click
|
|
544
|
+
// -------------------------------------------------------------------------
|
|
545
|
+
if (isImageMime(item.mimeType)) {
|
|
546
|
+
if (isImageRGB) {
|
|
547
|
+
return (
|
|
307
548
|
<Card
|
|
308
549
|
hoverable
|
|
309
550
|
className="memori-media-item--card memori-media-item--image"
|
|
310
551
|
cover={renderMediaContent(item)}
|
|
311
552
|
/>
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
if (item.mediumID && _onClick) {
|
|
556
|
+
return (
|
|
557
|
+
<div
|
|
558
|
+
onClick={() => _onClick(item)}
|
|
559
|
+
className="memori-media-item--link memori-media-item--image-link"
|
|
560
|
+
style={{ cursor: 'pointer' }}
|
|
561
|
+
title={item.title}
|
|
562
|
+
role="button"
|
|
563
|
+
tabIndex={0}
|
|
564
|
+
onKeyDown={e => {
|
|
565
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
325
566
|
e.preventDefault();
|
|
567
|
+
_onClick(item);
|
|
326
568
|
}
|
|
327
569
|
}}
|
|
328
|
-
target="_blank"
|
|
329
|
-
rel="noopener noreferrer"
|
|
330
|
-
title={item.title}
|
|
331
570
|
>
|
|
332
571
|
<Card
|
|
333
572
|
hoverable
|
|
334
573
|
className="memori-media-item--card memori-media-item--image"
|
|
335
574
|
cover={renderMediaContent(item)}
|
|
336
575
|
/>
|
|
337
|
-
</
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
case 'application/msword':
|
|
341
|
-
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
|
342
|
-
case 'application/vnd.ms-excel':
|
|
343
|
-
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
|
|
344
|
-
case 'application/pdf':
|
|
345
|
-
return (
|
|
346
|
-
<a
|
|
347
|
-
className="memori-media-item--link"
|
|
348
|
-
href={url}
|
|
349
|
-
target="_blank"
|
|
350
|
-
rel="noopener noreferrer"
|
|
351
|
-
title={item.title}
|
|
352
|
-
>
|
|
353
|
-
<Card hoverable cover={renderMediaContent(item)} title={item.title} />
|
|
354
|
-
</a>
|
|
576
|
+
</div>
|
|
355
577
|
);
|
|
578
|
+
}
|
|
579
|
+
return (
|
|
580
|
+
<Card
|
|
581
|
+
hoverable
|
|
582
|
+
className="memori-media-item--card memori-media-item--image"
|
|
583
|
+
cover={renderMediaContent(item)}
|
|
584
|
+
/>
|
|
585
|
+
);
|
|
586
|
+
}
|
|
356
587
|
|
|
588
|
+
// Video, audio, 3D, and other types: open in new tab (never modal)
|
|
589
|
+
switch (item.mimeType) {
|
|
357
590
|
case 'video/mp4':
|
|
358
591
|
case 'video/quicktime':
|
|
359
592
|
case 'video/avi':
|
|
@@ -361,129 +594,148 @@ export const RenderMediaItem = ({
|
|
|
361
594
|
return (
|
|
362
595
|
<a
|
|
363
596
|
className="memori-media-item--link"
|
|
364
|
-
href={
|
|
365
|
-
onClick={e => {
|
|
366
|
-
if (isChild) {
|
|
367
|
-
e.preventDefault();
|
|
368
|
-
}
|
|
369
|
-
if (onClick) {
|
|
370
|
-
e.preventDefault();
|
|
371
|
-
onClick(item.mediumID);
|
|
372
|
-
}
|
|
373
|
-
}}
|
|
597
|
+
href={resourceUrl || '#'}
|
|
374
598
|
target="_blank"
|
|
375
599
|
rel="noopener noreferrer"
|
|
376
600
|
title={item.title}
|
|
377
601
|
>
|
|
378
|
-
|
|
602
|
+
{renderMediaContent(item)}
|
|
379
603
|
</a>
|
|
380
604
|
);
|
|
381
605
|
|
|
606
|
+
// Audio and 3D models: open URL in new tab when available
|
|
382
607
|
case 'audio/mpeg3':
|
|
383
608
|
case 'audio/wav':
|
|
384
609
|
case 'audio/mpeg':
|
|
385
|
-
return (
|
|
386
|
-
<a
|
|
387
|
-
className="memori-media-item--link"
|
|
388
|
-
href={url}
|
|
389
|
-
target="_blank"
|
|
390
|
-
rel="noopener noreferrer"
|
|
391
|
-
title={item.title}
|
|
392
|
-
>
|
|
393
|
-
<Card hoverable cover={renderMediaContent(item)} title={item.title} />
|
|
394
|
-
</a>
|
|
395
|
-
);
|
|
396
|
-
|
|
397
610
|
case 'model/gltf-binary':
|
|
398
|
-
|
|
611
|
+
if (resourceUrl) {
|
|
612
|
+
return (
|
|
613
|
+
<a
|
|
614
|
+
className="memori-media-item--link"
|
|
615
|
+
href={resourceUrl}
|
|
616
|
+
target="_blank"
|
|
617
|
+
rel="noopener noreferrer"
|
|
618
|
+
title={item.title}
|
|
619
|
+
>
|
|
620
|
+
{renderMediaContent(item)}
|
|
621
|
+
</a>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return renderMediaContent(item);
|
|
399
625
|
|
|
626
|
+
// All other files: open URL in new tab (never modal)
|
|
400
627
|
default:
|
|
401
628
|
return (
|
|
402
629
|
<a
|
|
403
630
|
className="memori-media-item--link"
|
|
404
|
-
href={
|
|
631
|
+
href={resourceUrl || '#'}
|
|
405
632
|
target="_blank"
|
|
406
633
|
rel="noopener noreferrer"
|
|
407
634
|
title={item.title}
|
|
408
635
|
>
|
|
409
|
-
|
|
636
|
+
{renderMediaContent(item)}
|
|
410
637
|
</a>
|
|
411
638
|
);
|
|
412
639
|
}
|
|
413
|
-
};
|
|
640
|
+
});
|
|
414
641
|
|
|
415
|
-
//
|
|
416
|
-
const
|
|
417
|
-
if (!content) return 0;
|
|
418
|
-
// Support all common newline conventions:
|
|
419
|
-
// - Unix: \n
|
|
420
|
-
// - Windows: \r\n
|
|
421
|
-
// - Classic Mac: \r
|
|
422
|
-
return content.split(/\r\n|\r|\n/).length;
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
export const RenderSnippetItem = ({
|
|
642
|
+
// RenderSnippetItem: Renders a single code snippet (opens in new tab via href)
|
|
643
|
+
export const RenderSnippetItem = memo(function RenderSnippetItem({
|
|
426
644
|
item,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}: {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
645
|
+
onClick: _onClick, // kept for API compatibility; links open in new tab, not modal
|
|
646
|
+
sessionID,
|
|
647
|
+
tenantID,
|
|
648
|
+
baseURL,
|
|
649
|
+
apiURL,
|
|
650
|
+
}: RenderSnippetItemProps): React.ReactElement {
|
|
651
|
+
void _onClick;
|
|
652
|
+
const resourceUrl = getResourceUrl({
|
|
653
|
+
resourceURI: item.url,
|
|
654
|
+
sessionID,
|
|
655
|
+
tenantID,
|
|
656
|
+
baseURL,
|
|
657
|
+
apiURL,
|
|
658
|
+
});
|
|
659
|
+
const hasUrl = !!(resourceUrl && resourceUrl !== '#');
|
|
660
|
+
|
|
661
|
+
// Count lines, chars, determine "short" snippet
|
|
440
662
|
const lineCount = countLines(item.content);
|
|
441
663
|
const contentLength = item.content?.length ?? 0;
|
|
442
|
-
// Treat very long single-line snippets as "long" so we show a preview,
|
|
443
|
-
// otherwise plain-text uploads with only '\r' (or no '\n') would render full.
|
|
444
664
|
const isShortSnippet = lineCount <= 5 && contentLength <= 200;
|
|
665
|
+
const lineText = lineCount === 1 ? '1 riga' : `${lineCount} righe`;
|
|
445
666
|
|
|
446
|
-
// For short snippets, show them directly without the clickable link
|
|
447
667
|
if (isShortSnippet) {
|
|
448
668
|
return (
|
|
449
669
|
<div className="memori-media-item--snippet-direct">
|
|
450
670
|
<Card className="memori-media-item--card memori-media-item--snippet">
|
|
451
|
-
<div className="memori-media-item--snippet-
|
|
452
|
-
<
|
|
671
|
+
<div className="memori-media-item--snippet-body">
|
|
672
|
+
<div className="memori-media-item--snippet-title">{item.title}</div>
|
|
673
|
+
<div className="memori-media-item--snippet-preview">
|
|
674
|
+
<Snippet showCopyButton preview={false} medium={item} />
|
|
675
|
+
</div>
|
|
676
|
+
<div className="memori-media-item--snippet-header">
|
|
677
|
+
<span className="memori-media-item--snippet-meta">
|
|
678
|
+
{lineText}
|
|
679
|
+
</span>
|
|
680
|
+
</div>
|
|
453
681
|
</div>
|
|
454
682
|
</Card>
|
|
455
683
|
</div>
|
|
456
684
|
);
|
|
457
685
|
}
|
|
458
686
|
|
|
459
|
-
//
|
|
687
|
+
// Long snippet: open in new tab (resource URL or blob URL from content)
|
|
688
|
+
const snippetHref = hasUrl
|
|
689
|
+
? resourceUrl
|
|
690
|
+
: item.content
|
|
691
|
+
? (() => {
|
|
692
|
+
const blob = new Blob([item.content], {
|
|
693
|
+
type: item.mimeType || 'text/plain',
|
|
694
|
+
});
|
|
695
|
+
return URL.createObjectURL(blob);
|
|
696
|
+
})()
|
|
697
|
+
: '#';
|
|
698
|
+
|
|
460
699
|
return (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
700
|
+
<div
|
|
701
|
+
onClick={() => {
|
|
702
|
+
if (item.mediumID && _onClick) {
|
|
703
|
+
_onClick(item);
|
|
704
|
+
}
|
|
705
|
+
}}
|
|
706
|
+
style={{ cursor: 'pointer' }}
|
|
707
|
+
role="button"
|
|
708
|
+
tabIndex={0}
|
|
709
|
+
onKeyDown={e => {
|
|
710
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
465
711
|
e.preventDefault();
|
|
466
|
-
if (
|
|
467
|
-
|
|
712
|
+
if (item.mediumID && _onClick) {
|
|
713
|
+
_onClick(item);
|
|
468
714
|
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
715
|
+
}
|
|
716
|
+
}}
|
|
717
|
+
className="memori-media-item--link"
|
|
718
|
+
title={item.title}
|
|
719
|
+
>
|
|
720
|
+
<Card
|
|
721
|
+
hoverable
|
|
722
|
+
className="memori-media-item--card memori-media-item--snippet"
|
|
472
723
|
>
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
// onClick={() => setModalOpen(true)}
|
|
476
|
-
className="memori-media-item--card memori-media-item--snippet"
|
|
477
|
-
>
|
|
724
|
+
<div className="memori-media-item--snippet-body">
|
|
725
|
+
<div className="memori-media-item--snippet-title">{item.title}</div>
|
|
478
726
|
<div className="memori-media-item--snippet-preview">
|
|
479
|
-
<Snippet showCopyButton={false} preview
|
|
727
|
+
<Snippet showCopyButton={false} preview medium={item} />
|
|
480
728
|
</div>
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
729
|
+
<div className="memori-media-item--snippet-header">
|
|
730
|
+
<span className="memori-media-item--snippet-meta">{lineText}</span>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
</Card>
|
|
734
|
+
</div>
|
|
484
735
|
);
|
|
485
|
-
};
|
|
736
|
+
});
|
|
486
737
|
|
|
738
|
+
// Main MediaItemWidget component: renders all media, overlays, and preview modals
|
|
487
739
|
const MediaItemWidget: React.FC<Props> = ({
|
|
488
740
|
items,
|
|
489
741
|
sessionID,
|
|
@@ -493,79 +745,128 @@ const MediaItemWidget: React.FC<Props> = ({
|
|
|
493
745
|
apiURL,
|
|
494
746
|
customMediaRenderer,
|
|
495
747
|
fromUser = false,
|
|
496
|
-
|
|
748
|
+
descriptionOneLine = false,
|
|
749
|
+
onLinkPreviewInfo,
|
|
750
|
+
}) => {
|
|
751
|
+
// Internal tracked media set (may be translated versions)
|
|
497
752
|
const [media, setMedia] = useState(items);
|
|
498
|
-
|
|
753
|
+
// Modal state (holds currently open medium)
|
|
754
|
+
const [openModalMedium, setOpenModalMedium] = useState<Medium | undefined>();
|
|
499
755
|
|
|
500
|
-
//
|
|
756
|
+
// Update component-local media when prop changes (reset modal to current items)
|
|
501
757
|
useEffect(() => {
|
|
502
758
|
setMedia(items);
|
|
503
759
|
}, [items]);
|
|
504
760
|
|
|
761
|
+
// Optional: Translate all media captions/titles if translateTo provided
|
|
505
762
|
const translateMediaCaptions = useCallback(async () => {
|
|
506
763
|
if (!translateTo) return;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
console.error(e);
|
|
517
|
-
return media;
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
return media;
|
|
764
|
+
const translated = await Promise.all(
|
|
765
|
+
(items ?? []).map(async m => {
|
|
766
|
+
if (!m.title) return m;
|
|
767
|
+
try {
|
|
768
|
+
const t = await getTranslation(m.title, translateTo);
|
|
769
|
+
return { ...m, title: t.text ?? m.title };
|
|
770
|
+
} catch (e) {
|
|
771
|
+
console.error(e);
|
|
772
|
+
return m;
|
|
521
773
|
}
|
|
522
774
|
})
|
|
523
775
|
);
|
|
524
|
-
|
|
525
|
-
setMedia(translatedMedia);
|
|
776
|
+
setMedia(translated);
|
|
526
777
|
}, [translateTo, items]);
|
|
778
|
+
|
|
527
779
|
useEffect(() => {
|
|
528
780
|
if (translateTo) translateMediaCaptions();
|
|
529
781
|
}, [translateTo, translateMediaCaptions]);
|
|
530
782
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
783
|
+
// Derive top-level "display" lists:
|
|
784
|
+
// 1. All non-code, non-executable media sorted by timestamp (displayed as document, images, video, etc)
|
|
785
|
+
const nonCodeDisplayMedia = useMemo(
|
|
786
|
+
() =>
|
|
787
|
+
media
|
|
788
|
+
.filter(
|
|
789
|
+
m =>
|
|
790
|
+
!m.properties?.executable && !CODE_MIME_TYPES.includes(m.mimeType)
|
|
791
|
+
)
|
|
792
|
+
.sort((a, b) => {
|
|
793
|
+
const at = a.creationTimestamp ?? 0;
|
|
794
|
+
const bt = b.creationTimestamp ?? 0;
|
|
795
|
+
return at > bt ? 1 : at < bt ? -1 : 0;
|
|
796
|
+
}),
|
|
797
|
+
[media]
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// 2. Only code snippets (unless marked as executable)
|
|
801
|
+
const codeSnippets = useMemo(
|
|
802
|
+
() =>
|
|
803
|
+
media.filter(
|
|
804
|
+
m => !m.properties?.executable && CODE_MIME_TYPES.includes(m.mimeType)
|
|
805
|
+
),
|
|
806
|
+
[media]
|
|
807
|
+
);
|
|
544
808
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
809
|
+
// 3. Only CSS code marked as executable (to inject as <style>)
|
|
810
|
+
const cssExecutableCode = useMemo(
|
|
811
|
+
() =>
|
|
812
|
+
media.filter(
|
|
813
|
+
m => m.mimeType === 'text/css' && !!m.properties?.executable
|
|
814
|
+
),
|
|
815
|
+
[media]
|
|
549
816
|
);
|
|
550
817
|
|
|
551
|
-
|
|
552
|
-
|
|
818
|
+
// How many images are present for determining layout
|
|
819
|
+
const imageCount = useMemo(
|
|
820
|
+
() =>
|
|
821
|
+
nonCodeDisplayMedia.filter(m =>
|
|
822
|
+
(IMAGE_MIME_TYPES as readonly string[]).includes(m.mimeType)
|
|
823
|
+
).length,
|
|
824
|
+
[nonCodeDisplayMedia]
|
|
553
825
|
);
|
|
554
826
|
|
|
827
|
+
// Media "card open"/preview modal: pass the clicked item so the correct one opens
|
|
828
|
+
// (avoids wrong image when multiple items share the same mediumID)
|
|
829
|
+
const handleMediaItemClick = useCallback((item: MediaItem) => {
|
|
830
|
+
setOpenModalMedium(item);
|
|
831
|
+
}, []);
|
|
832
|
+
|
|
833
|
+
// Modal for code snippets
|
|
834
|
+
const handleSnippetClick = useCallback((item: Medium & { type?: string }) => {
|
|
835
|
+
setOpenModalMedium(item);
|
|
836
|
+
}, []);
|
|
837
|
+
|
|
838
|
+
// Simple close modal action callback
|
|
839
|
+
const handleCloseModal = useCallback(() => {
|
|
840
|
+
setOpenModalMedium(undefined);
|
|
841
|
+
}, []);
|
|
842
|
+
|
|
843
|
+
// Navigate to another media in modal (by mediumID) — used by modal carousel/nav
|
|
844
|
+
const handleModalNavigate = useCallback(
|
|
845
|
+
(mediumID: string) => {
|
|
846
|
+
setOpenModalMedium(media.find(m => m.mediumID === mediumID));
|
|
847
|
+
},
|
|
848
|
+
[media]
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
// Render transitions and the main grid layouts for media
|
|
555
852
|
return (
|
|
556
853
|
<Transition appear show as="div" className="memori-media-items">
|
|
557
|
-
{
|
|
854
|
+
{/* Main media grid: non-code media (images, files, html, video, etc) */}
|
|
855
|
+
{nonCodeDisplayMedia.length > 0 && (
|
|
558
856
|
<div
|
|
559
857
|
className={cx('memori-media-items--grid memori-chat-scroll-item', {
|
|
560
858
|
'memori-media-items--user': fromUser,
|
|
561
859
|
'memori-media-items--agent': !fromUser,
|
|
860
|
+
'memori-media-items--single': imageCount === 1,
|
|
861
|
+
'memori-media-items--few': imageCount >= 2 && imageCount <= 4,
|
|
862
|
+
'memori-media-items--many': imageCount >= 5,
|
|
562
863
|
})}
|
|
563
864
|
>
|
|
564
|
-
{nonCodeDisplayMedia.map((item
|
|
865
|
+
{nonCodeDisplayMedia.map((item, index) => (
|
|
565
866
|
<Transition.Child
|
|
867
|
+
key={`media-${index}-${item.mediumID ?? item.url ?? 'n'}`}
|
|
566
868
|
as="div"
|
|
567
869
|
className="memori-media-item"
|
|
568
|
-
key={item.url + '&index=' + index}
|
|
569
870
|
enter={`ease-out duration-500 delay-${index * 100}`}
|
|
570
871
|
enterFrom="opacity-0 scale-95"
|
|
571
872
|
enterTo="opacity-1 scale-100"
|
|
@@ -579,11 +880,7 @@ const MediaItemWidget: React.FC<Props> = ({
|
|
|
579
880
|
tenantID={tenantID}
|
|
580
881
|
baseURL={baseURL}
|
|
581
882
|
apiURL={apiURL}
|
|
582
|
-
onClick={
|
|
583
|
-
setOpenModalMedium(
|
|
584
|
-
nonCodeDisplayMedia.find(m => m.mediumID === mediumID)
|
|
585
|
-
);
|
|
586
|
-
}}
|
|
883
|
+
onClick={handleMediaItemClick}
|
|
587
884
|
item={{
|
|
588
885
|
...item,
|
|
589
886
|
title: item.title,
|
|
@@ -592,23 +889,27 @@ const MediaItemWidget: React.FC<Props> = ({
|
|
|
592
889
|
type: 'document',
|
|
593
890
|
}}
|
|
594
891
|
customMediaRenderer={customMediaRenderer}
|
|
892
|
+
descriptionOneLine={descriptionOneLine}
|
|
893
|
+
onLinkPreviewInfo={onLinkPreviewInfo}
|
|
595
894
|
/>
|
|
596
895
|
</Transition.Child>
|
|
597
896
|
))}
|
|
598
897
|
</div>
|
|
599
898
|
)}
|
|
600
|
-
|
|
899
|
+
|
|
900
|
+
{/* Grid of pure code snippets, shown as cards or compact snippets */}
|
|
901
|
+
{codeSnippets.length > 0 && (
|
|
601
902
|
<div
|
|
602
903
|
className={cx('memori-media-items--grid memori-chat-scroll-item', {
|
|
603
904
|
'memori-media-items--user': fromUser,
|
|
604
905
|
'memori-media-items--agent': !fromUser,
|
|
605
906
|
})}
|
|
606
907
|
>
|
|
607
|
-
{codeSnippets.map((item
|
|
908
|
+
{codeSnippets.map((item, index) => (
|
|
608
909
|
<Transition.Child
|
|
910
|
+
key={`snippet-${index}-${item.mediumID ?? item.url ?? 'n'}`}
|
|
609
911
|
as="div"
|
|
610
912
|
className="memori-media-item"
|
|
611
|
-
key={item.mediumID + '&index=' + index}
|
|
612
913
|
enter={`ease-out duration-500 delay-${index * 100}`}
|
|
613
914
|
enterFrom="opacity-0 scale-95"
|
|
614
915
|
enterTo="opacity-1 scale-100"
|
|
@@ -621,12 +922,7 @@ const MediaItemWidget: React.FC<Props> = ({
|
|
|
621
922
|
tenantID={tenantID}
|
|
622
923
|
baseURL={baseURL}
|
|
623
924
|
apiURL={apiURL}
|
|
624
|
-
onClick={
|
|
625
|
-
const foundMedium = codeSnippets.find(
|
|
626
|
-
m => m.mediumID === mediumID
|
|
627
|
-
);
|
|
628
|
-
setOpenModalMedium(foundMedium);
|
|
629
|
-
}}
|
|
925
|
+
onClick={handleSnippetClick}
|
|
630
926
|
item={{
|
|
631
927
|
...item,
|
|
632
928
|
title: item.title,
|
|
@@ -639,55 +935,32 @@ const MediaItemWidget: React.FC<Props> = ({
|
|
|
639
935
|
))}
|
|
640
936
|
</div>
|
|
641
937
|
)}
|
|
642
|
-
|
|
938
|
+
|
|
939
|
+
{/* CSS executables: Inject as <style> (only shared to DOM, not shown visually) */}
|
|
940
|
+
{cssExecutableCode.map(m => (
|
|
643
941
|
<style
|
|
644
|
-
key={
|
|
645
|
-
dangerouslySetInnerHTML={{ __html:
|
|
646
|
-
|
|
942
|
+
key={m.mediumID}
|
|
943
|
+
dangerouslySetInnerHTML={{ __html: m.content || '' }}
|
|
944
|
+
/>
|
|
647
945
|
))}
|
|
648
946
|
|
|
947
|
+
{/* Modal preview: shows when openModalMedium set */}
|
|
649
948
|
{openModalMedium && (
|
|
650
|
-
<
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
style={{
|
|
663
|
-
minHeight: '100%',
|
|
664
|
-
background: 'none',
|
|
665
|
-
}}
|
|
666
|
-
className="memori-media-item-preview--content"
|
|
667
|
-
>
|
|
668
|
-
<Snippet preview={false} medium={openModalMedium} />
|
|
669
|
-
</div>
|
|
670
|
-
) : (
|
|
671
|
-
<RenderMediaItem
|
|
672
|
-
isChild
|
|
673
|
-
sessionID={sessionID}
|
|
674
|
-
tenantID={tenantID}
|
|
675
|
-
baseURL={baseURL}
|
|
676
|
-
apiURL={apiURL}
|
|
677
|
-
item={{
|
|
678
|
-
...openModalMedium,
|
|
679
|
-
title: openModalMedium.title,
|
|
680
|
-
url: openModalMedium.url,
|
|
681
|
-
content: openModalMedium.content,
|
|
682
|
-
type: 'document',
|
|
683
|
-
}}
|
|
684
|
-
customMediaRenderer={customMediaRenderer}
|
|
685
|
-
/>
|
|
686
|
-
)}
|
|
687
|
-
</Modal>
|
|
949
|
+
<MediaPreviewModal
|
|
950
|
+
medium={openModalMedium}
|
|
951
|
+
onClose={handleCloseModal}
|
|
952
|
+
sessionID={sessionID}
|
|
953
|
+
tenantID={tenantID}
|
|
954
|
+
baseURL={baseURL}
|
|
955
|
+
apiURL={apiURL}
|
|
956
|
+
customMediaRenderer={customMediaRenderer}
|
|
957
|
+
descriptionOneLine={descriptionOneLine}
|
|
958
|
+
onLinkPreviewInfo={onLinkPreviewInfo}
|
|
959
|
+
onMediumClick={handleModalNavigate}
|
|
960
|
+
/>
|
|
688
961
|
)}
|
|
689
962
|
</Transition>
|
|
690
963
|
);
|
|
691
964
|
};
|
|
692
965
|
|
|
693
|
-
export default memo(MediaItemWidget);
|
|
966
|
+
export default memo(MediaItemWidget);
|