@vertesia/ui 0.73.0 → 0.76.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/lib/esm/core/components/Center.js +1 -1
- package/lib/esm/core/components/Center.js.map +1 -1
- package/lib/esm/core/components/Overlay.js +57 -0
- package/lib/esm/core/components/Overlay.js.map +1 -0
- package/lib/esm/core/components/SidePanel.js +6 -6
- package/lib/esm/core/components/SidePanel.js.map +1 -1
- package/lib/esm/core/components/index.js +1 -0
- package/lib/esm/core/components/index.js.map +1 -1
- package/lib/esm/core/components/shadcn/tabs.js +43 -5
- package/lib/esm/core/components/shadcn/tabs.js.map +1 -1
- package/lib/esm/features/agent/PayloadBuilder.js +9 -2
- package/lib/esm/features/agent/PayloadBuilder.js.map +1 -1
- package/lib/esm/features/facets/DocumentsFacetsNav.js +4 -2
- package/lib/esm/features/facets/DocumentsFacetsNav.js.map +1 -1
- package/lib/esm/features/facets/VTypeFacet.js +2 -1
- package/lib/esm/features/facets/VTypeFacet.js.map +1 -1
- package/lib/esm/features/magic-pdf/DownloadPopover.js +17 -2
- package/lib/esm/features/magic-pdf/DownloadPopover.js.map +1 -1
- package/lib/esm/features/magic-pdf/MagicPdfView.js +26 -3
- package/lib/esm/features/magic-pdf/MagicPdfView.js.map +1 -1
- package/lib/esm/features/magic-pdf/PageSlider.js +21 -8
- package/lib/esm/features/magic-pdf/PageSlider.js.map +1 -1
- package/lib/esm/features/magic-pdf/PdfPageProvider.js +55 -0
- package/lib/esm/features/magic-pdf/PdfPageProvider.js.map +1 -1
- package/lib/esm/features/magic-pdf/TextPageView.js +20 -2
- package/lib/esm/features/magic-pdf/TextPageView.js.map +1 -1
- package/lib/esm/features/store/collections/CreateCollection.js +1 -1
- package/lib/esm/features/store/collections/CreateCollection.js.map +1 -1
- package/lib/esm/features/store/objects/DocumentPreviewPanel.js +2 -4
- package/lib/esm/features/store/objects/DocumentPreviewPanel.js.map +1 -1
- package/lib/esm/features/store/objects/DocumentSearchResults.js +19 -21
- package/lib/esm/features/store/objects/DocumentSearchResults.js.map +1 -1
- package/lib/esm/features/store/objects/components/ContentOverview.js +2 -4
- package/lib/esm/features/store/objects/components/ContentOverview.js.map +1 -1
- package/lib/esm/features/store/objects/components/VectorSearchWidget.js +51 -46
- package/lib/esm/features/store/objects/components/VectorSearchWidget.js.map +1 -1
- package/lib/esm/features/store/objects/layout/documentLayout.js +1 -1
- package/lib/esm/features/store/objects/layout/documentLayout.js.map +1 -1
- package/lib/esm/features/store/objects/search/DocumentSearchContext.js +50 -34
- package/lib/esm/features/store/objects/search/DocumentSearchContext.js.map +1 -1
- package/lib/esm/features/store/objects/search/DocumentSearchProvider.js +1 -3
- package/lib/esm/features/store/objects/search/DocumentSearchProvider.js.map +1 -1
- package/lib/esm/features/store/objects/upload/useSmartFileUploadProcessing.js +4 -11
- package/lib/esm/features/store/objects/upload/useSmartFileUploadProcessing.js.map +1 -1
- package/lib/esm/features/user/UserInfo.js +2 -2
- package/lib/esm/features/user/UserInfo.js.map +1 -1
- package/lib/esm/session/UserSessionProvider.js +6 -3
- package/lib/esm/session/UserSessionProvider.js.map +1 -1
- package/lib/esm/session/auth/composable.js +3 -3
- package/lib/esm/session/auth/composable.js.map +1 -1
- package/lib/esm/session/auth/firebase.js +7 -0
- package/lib/esm/session/auth/firebase.js.map +1 -1
- package/lib/esm/session/auth/useAuthState.js +0 -3
- package/lib/esm/session/auth/useAuthState.js.map +1 -1
- package/lib/esm/shell/apps/StandaloneApp.js +1 -1
- package/lib/esm/shell/apps/StandaloneApp.js.map +1 -1
- package/lib/esm/shell/login/EnterpriseSigninButton.js +3 -0
- package/lib/esm/shell/login/EnterpriseSigninButton.js.map +1 -1
- package/lib/esm/shell/login/InviteAcceptModal.js +1 -1
- package/lib/esm/shell/login/InviteAcceptModal.js.map +1 -1
- package/lib/esm/widgets/form/ManagedObject.js +1 -1
- package/lib/esm/widgets/index.js +1 -0
- package/lib/esm/widgets/index.js.map +1 -1
- package/lib/esm/widgets/markdown/MarkdownRenderer.js +24 -0
- package/lib/esm/widgets/markdown/MarkdownRenderer.js.map +1 -0
- package/lib/esm/widgets/markdown/index.js +2 -0
- package/lib/esm/widgets/markdown/index.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/core/components/Overlay.d.ts +25 -0
- package/lib/types/core/components/Overlay.d.ts.map +1 -0
- package/lib/types/core/components/SidePanel.d.ts +3 -1
- package/lib/types/core/components/SidePanel.d.ts.map +1 -1
- package/lib/types/core/components/index.d.ts +1 -0
- package/lib/types/core/components/index.d.ts.map +1 -1
- package/lib/types/core/components/shadcn/tabs.d.ts.map +1 -1
- package/lib/types/env/index.d.ts +2 -2
- package/lib/types/env/index.d.ts.map +1 -1
- package/lib/types/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/types/features/facets/DocumentsFacetsNav.d.ts.map +1 -1
- package/lib/types/features/facets/VTypeFacet.d.ts.map +1 -1
- package/lib/types/features/magic-pdf/DownloadPopover.d.ts.map +1 -1
- package/lib/types/features/magic-pdf/PageSlider.d.ts +2 -1
- package/lib/types/features/magic-pdf/PageSlider.d.ts.map +1 -1
- package/lib/types/features/magic-pdf/PdfPageProvider.d.ts +10 -0
- package/lib/types/features/magic-pdf/PdfPageProvider.d.ts.map +1 -1
- package/lib/types/features/magic-pdf/TextPageView.d.ts.map +1 -1
- package/lib/types/features/magic-pdf/types.d.ts +1 -1
- package/lib/types/features/magic-pdf/types.d.ts.map +1 -1
- package/lib/types/features/store/objects/DocumentPreviewPanel.d.ts.map +1 -1
- package/lib/types/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
- package/lib/types/features/store/objects/components/ContentOverview.d.ts.map +1 -1
- package/lib/types/features/store/objects/components/VectorSearchWidget.d.ts +4 -3
- package/lib/types/features/store/objects/components/VectorSearchWidget.d.ts.map +1 -1
- package/lib/types/features/store/objects/search/DocumentSearchContext.d.ts +5 -2
- package/lib/types/features/store/objects/search/DocumentSearchContext.d.ts.map +1 -1
- package/lib/types/features/store/objects/search/DocumentSearchProvider.d.ts +2 -4
- package/lib/types/features/store/objects/search/DocumentSearchProvider.d.ts.map +1 -1
- package/lib/types/features/store/objects/upload/useSmartFileUploadProcessing.d.ts.map +1 -1
- package/lib/types/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/types/session/auth/composable.d.ts +1 -1
- package/lib/types/session/auth/composable.d.ts.map +1 -1
- package/lib/types/session/auth/firebase.d.ts.map +1 -1
- package/lib/types/session/auth/useAuthState.d.ts.map +1 -1
- package/lib/types/shell/login/EnterpriseSigninButton.d.ts.map +1 -1
- package/lib/types/widgets/index.d.ts +1 -0
- package/lib/types/widgets/index.d.ts.map +1 -1
- package/lib/types/widgets/markdown/MarkdownRenderer.d.ts +9 -0
- package/lib/types/widgets/markdown/MarkdownRenderer.d.ts.map +1 -0
- package/lib/types/widgets/markdown/index.d.ts +2 -0
- package/lib/types/widgets/markdown/index.d.ts.map +1 -0
- package/lib/vertesia-ui-core.js +1 -1
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +1 -1
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +1 -1
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +1 -1
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +6 -4
- package/src/core/components/Center.tsx +1 -1
- package/src/core/components/Overlay.tsx +129 -0
- package/src/core/components/SidePanel.tsx +38 -33
- package/src/core/components/index.ts +1 -0
- package/src/core/components/shadcn/tabs.tsx +48 -5
- package/src/env/index.ts +1 -1
- package/src/features/agent/PayloadBuilder.tsx +8 -2
- package/src/features/facets/DocumentsFacetsNav.tsx +4 -2
- package/src/features/facets/VTypeFacet.tsx +2 -1
- package/src/features/magic-pdf/DownloadPopover.tsx +38 -5
- package/src/features/magic-pdf/MagicPdfView.tsx +31 -5
- package/src/features/magic-pdf/PageSlider.tsx +44 -14
- package/src/features/magic-pdf/PdfPageProvider.tsx +81 -0
- package/src/features/magic-pdf/TextPageView.tsx +29 -2
- package/src/features/magic-pdf/types.ts +1 -1
- package/src/features/store/collections/CreateCollection.tsx +1 -1
- package/src/features/store/objects/DocumentPreviewPanel.tsx +2 -4
- package/src/features/store/objects/DocumentSearchResults.tsx +24 -26
- package/src/features/store/objects/components/ContentOverview.tsx +3 -6
- package/src/features/store/objects/components/VectorSearchWidget.tsx +88 -60
- package/src/features/store/objects/layout/documentLayout.tsx +1 -1
- package/src/features/store/objects/search/DocumentSearchContext.ts +57 -36
- package/src/features/store/objects/search/DocumentSearchProvider.tsx +2 -6
- package/src/features/store/objects/upload/useSmartFileUploadProcessing.ts +6 -12
- package/src/features/user/UserInfo.tsx +2 -2
- package/src/session/UserSessionProvider.tsx +5 -3
- package/src/session/auth/composable.ts +3 -3
- package/src/session/auth/firebase.ts +8 -0
- package/src/session/auth/useAuthState.ts +0 -4
- package/src/shell/apps/StandaloneApp.tsx +1 -1
- package/src/shell/login/EnterpriseSigninButton.tsx +3 -0
- package/src/shell/login/InviteAcceptModal.tsx +1 -1
- package/src/widgets/form/ManagedObject.ts +1 -1
- package/src/widgets/index.ts +1 -0
- package/src/widgets/markdown/MarkdownRenderer.tsx +45 -0
- package/src/widgets/markdown/index.ts +1 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DocumentMetadata } from "@vertesia/common";
|
|
1
2
|
import { Center } from "@vertesia/ui/core";
|
|
2
3
|
import clsx from "clsx";
|
|
3
4
|
import { AtSignIcon, ChevronsDown, ChevronsUp, ImageIcon, InfoIcon } from "lucide-react";
|
|
@@ -6,7 +7,8 @@ import { usePdfPagesInfo } from "./PdfPageProvider";
|
|
|
6
7
|
|
|
7
8
|
enum ImageType {
|
|
8
9
|
default,
|
|
9
|
-
|
|
10
|
+
original,
|
|
11
|
+
instrumented,
|
|
10
12
|
annotated,
|
|
11
13
|
}
|
|
12
14
|
|
|
@@ -14,11 +16,25 @@ interface PageSliderProps {
|
|
|
14
16
|
currentPage: number;
|
|
15
17
|
onChange: (pageNumber: number) => void;
|
|
16
18
|
className?: string;
|
|
19
|
+
object: any; // ContentObject type
|
|
17
20
|
}
|
|
18
|
-
export function PageSlider({ className, currentPage, onChange }: PageSliderProps) {
|
|
19
|
-
const
|
|
21
|
+
export function PageSlider({ className, currentPage, onChange, object }: PageSliderProps) {
|
|
22
|
+
const getProcessorType = (): string => {
|
|
23
|
+
if (object.metadata?.type === "document") {
|
|
24
|
+
const docMetadata = object.metadata as DocumentMetadata;
|
|
25
|
+
return docMetadata.content_processor?.type || "xml";
|
|
26
|
+
}
|
|
27
|
+
return "xml"; // default
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getDefaultImageType = (): ImageType => {
|
|
31
|
+
const processorType = getProcessorType();
|
|
32
|
+
return processorType === "markdown" ? ImageType.original : ImageType.default;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const [imageType, setImageType] = useState<ImageType>(getDefaultImageType());
|
|
20
36
|
const ref = useRef<HTMLDivElement>(null);
|
|
21
|
-
const { urls, annotatedUrls, instrumentedUrls } = usePdfPagesInfo();
|
|
37
|
+
const { urls, originalUrls, annotatedUrls, instrumentedUrls } = usePdfPagesInfo();
|
|
22
38
|
const goPrev = () => {
|
|
23
39
|
if (currentPage > 1) {
|
|
24
40
|
onChange(currentPage - 1);
|
|
@@ -46,7 +62,8 @@ export function PageSlider({ className, currentPage, onChange }: PageSliderProps
|
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
const actualUrls = imageType === ImageType.instrumented ? instrumentedUrls :
|
|
49
|
-
(imageType === ImageType.annotated ? annotatedUrls :
|
|
65
|
+
(imageType === ImageType.annotated ? annotatedUrls :
|
|
66
|
+
(imageType === ImageType.original ? originalUrls : urls));
|
|
50
67
|
|
|
51
68
|
return (
|
|
52
69
|
<div ref={ref} className={clsx('flex flex-col items-stretch gap-y-2', className)}>
|
|
@@ -56,15 +73,28 @@ export function PageSlider({ className, currentPage, onChange }: PageSliderProps
|
|
|
56
73
|
<ChevronsUp className='w-5 h-5' />
|
|
57
74
|
</button>
|
|
58
75
|
<div className="absolute right-3 flex gap-x-1">
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
{getProcessorType() === "markdown" ? (
|
|
77
|
+
<>
|
|
78
|
+
<button className={getRadioButtonClass(ImageType.original, imageType)}
|
|
79
|
+
onClick={() => setImageType(ImageType.original)}
|
|
80
|
+
><ImageIcon className="w-5 h-5 mt-1" /></button>
|
|
81
|
+
<button className={getRadioButtonClass(ImageType.instrumented, imageType)}
|
|
82
|
+
onClick={() => setImageType(ImageType.instrumented)}
|
|
83
|
+
><InfoIcon className="w-5 h-5 mt-1" /></button>
|
|
84
|
+
</>
|
|
85
|
+
) : (
|
|
86
|
+
<>
|
|
87
|
+
<button className={getRadioButtonClass(ImageType.default, imageType)}
|
|
88
|
+
onClick={() => setImageType(ImageType.default)}
|
|
89
|
+
><ImageIcon className="w-5 h-5 mt-1" /></button>
|
|
90
|
+
<button className={getRadioButtonClass(ImageType.instrumented, imageType)}
|
|
91
|
+
onClick={() => setImageType(ImageType.instrumented)}
|
|
92
|
+
><InfoIcon className="w-5 h-5 mt-1" /></button>
|
|
93
|
+
<button className={getRadioButtonClass(ImageType.annotated, imageType)}
|
|
94
|
+
onClick={() => setImageType(ImageType.annotated)}
|
|
95
|
+
><AtSignIcon className="w-5 h-5 mt-1" /></button>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
68
98
|
</div>
|
|
69
99
|
</div>
|
|
70
100
|
<div className='flex flex-col items-center gap-2 flex-1 overflow-y-auto px-2'>
|
|
@@ -13,9 +13,11 @@ const ADVANCED_PROCESSING_PREFIX = "magic-pdf";
|
|
|
13
13
|
interface PdfPagesInfo {
|
|
14
14
|
count: number;
|
|
15
15
|
urls: string[];
|
|
16
|
+
originalUrls: string[];
|
|
16
17
|
annotatedUrls: string[];
|
|
17
18
|
instrumentedUrls: string[];
|
|
18
19
|
layoutProvider: PageLayoutProvider;
|
|
20
|
+
markdownProvider: PageMarkdownProvider;
|
|
19
21
|
xml: string;
|
|
20
22
|
xmlPages: string[];
|
|
21
23
|
}
|
|
@@ -54,6 +56,40 @@ class PageLayoutProvider {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
class PageMarkdownProvider {
|
|
60
|
+
markdownUrls: string[] = [];
|
|
61
|
+
cache: string[];
|
|
62
|
+
constructor(public totalPages: number) {
|
|
63
|
+
this.cache = new Array<string>(totalPages);
|
|
64
|
+
}
|
|
65
|
+
async loadUrls(vertesia: VertesiaClient, objectId: string) {
|
|
66
|
+
const markdownPromises: Promise<GetFileUrlResponse>[] = [];
|
|
67
|
+
for (let i = 0; i < this.totalPages; i++) {
|
|
68
|
+
markdownPromises.push(getMarkdownUrlForPage(vertesia, objectId, i + 1));
|
|
69
|
+
}
|
|
70
|
+
const markdownUrls = await Promise.all(markdownPromises);
|
|
71
|
+
this.markdownUrls = markdownUrls.map((r) => r.url);
|
|
72
|
+
}
|
|
73
|
+
async getPageMarkdown(page: number) {
|
|
74
|
+
const index = page - 1;
|
|
75
|
+
let content = this.cache[index];
|
|
76
|
+
if (content === undefined) {
|
|
77
|
+
const url = this.markdownUrls[index];
|
|
78
|
+
content = await fetch(url, { method: "GET" }).then((r) => {
|
|
79
|
+
if (r.ok) {
|
|
80
|
+
return r.text();
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"Failed to fetch markdown: " + r.statusText,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this.cache[index] = content;
|
|
88
|
+
}
|
|
89
|
+
return content;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
const PdfPageContext = createContext<PdfPagesInfo | undefined>(undefined);
|
|
58
94
|
|
|
59
95
|
interface PdfPageProviderProps {
|
|
@@ -103,10 +139,22 @@ function getPageInstrumentedImagePath(
|
|
|
103
139
|
return `${getBasePath(objectId)}/pages/page-${pageNumber}.instrumented${ext}`;
|
|
104
140
|
}
|
|
105
141
|
|
|
142
|
+
function getPageOriginalImagePath(
|
|
143
|
+
objectId: string,
|
|
144
|
+
pageNumber: number,
|
|
145
|
+
ext = ".jpg",
|
|
146
|
+
) {
|
|
147
|
+
return `${getBasePath(objectId)}/pages/page-${pageNumber}.original${ext}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
106
150
|
function getLayoutJsonPath(objectId: string, pageNumber: number) {
|
|
107
151
|
return `${getBasePath(objectId)}/pages/page-${pageNumber}.layout.json`;
|
|
108
152
|
}
|
|
109
153
|
|
|
154
|
+
function getMarkdownPath(objectId: string, pageNumber: number) {
|
|
155
|
+
return `${getBasePath(objectId)}/pages/page-${pageNumber}.md`;
|
|
156
|
+
}
|
|
157
|
+
|
|
110
158
|
export function getResourceUrl(
|
|
111
159
|
vertesia: VertesiaClient,
|
|
112
160
|
objectId: string,
|
|
@@ -147,6 +195,16 @@ function getInstrumentedImageUrlForPage(
|
|
|
147
195
|
);
|
|
148
196
|
}
|
|
149
197
|
|
|
198
|
+
function getOriginalImageUrlForPage(
|
|
199
|
+
vertesia: VertesiaClient,
|
|
200
|
+
objectId: string,
|
|
201
|
+
pageNumber: number,
|
|
202
|
+
): Promise<GetFileUrlResponse> {
|
|
203
|
+
return vertesia.files.getDownloadUrl(
|
|
204
|
+
getPageOriginalImagePath(objectId, pageNumber),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
150
208
|
function getLayoutUrlForPage(
|
|
151
209
|
vertesia: VertesiaClient,
|
|
152
210
|
objectId: string,
|
|
@@ -157,6 +215,16 @@ function getLayoutUrlForPage(
|
|
|
157
215
|
);
|
|
158
216
|
}
|
|
159
217
|
|
|
218
|
+
function getMarkdownUrlForPage(
|
|
219
|
+
vertesia: VertesiaClient,
|
|
220
|
+
objectId: string,
|
|
221
|
+
pageNumber: number,
|
|
222
|
+
): Promise<GetFileUrlResponse> {
|
|
223
|
+
return vertesia.files.getDownloadUrl(
|
|
224
|
+
getMarkdownPath(objectId, pageNumber),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
160
228
|
async function getPdfPagesInfo(
|
|
161
229
|
vertesia: VertesiaClient,
|
|
162
230
|
object: ContentObject,
|
|
@@ -186,17 +254,30 @@ async function getPdfPagesInfo(
|
|
|
186
254
|
instrumentedImageUrlPromises,
|
|
187
255
|
);
|
|
188
256
|
|
|
257
|
+
const originalImageUrlPromises: Promise<GetFileUrlResponse>[] = [];
|
|
258
|
+
for (let i = 0; i < page_count; i++) {
|
|
259
|
+
originalImageUrlPromises.push(
|
|
260
|
+
getOriginalImageUrlForPage(vertesia, object.id, i + 1),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const originalImageUrls = await Promise.all(originalImageUrlPromises);
|
|
264
|
+
|
|
189
265
|
const layoutProvider = new PageLayoutProvider(page_count);
|
|
190
266
|
await layoutProvider.loadUrls(vertesia, object.id);
|
|
191
267
|
|
|
268
|
+
const markdownProvider = new PageMarkdownProvider(page_count);
|
|
269
|
+
await markdownProvider.loadUrls(vertesia, object.id);
|
|
270
|
+
|
|
192
271
|
const xml = object.text ? cleanXml(object.text) : "";
|
|
193
272
|
|
|
194
273
|
return {
|
|
195
274
|
count: page_count,
|
|
196
275
|
urls: imageUrls.map((r) => r.url),
|
|
276
|
+
originalUrls: originalImageUrls.map((r) => r.url),
|
|
197
277
|
annotatedUrls: annotatedImageUrls.map((r) => r.url),
|
|
198
278
|
instrumentedUrls: instrumentedImageUrls.map((r) => r.url),
|
|
199
279
|
layoutProvider,
|
|
280
|
+
markdownProvider,
|
|
200
281
|
xml,
|
|
201
282
|
xmlPages: object.text ? extractXmlPages(xml) : [],
|
|
202
283
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JSONCode, Theme, XMLViewer } from '@vertesia/ui/widgets';
|
|
1
|
+
import { JSONCode, Theme, XMLViewer, MarkdownRenderer } from '@vertesia/ui/widgets';
|
|
2
2
|
import { useEffect, useLayoutEffect, useState } from "react";
|
|
3
3
|
import { usePdfPagesInfo } from "./PdfPageProvider";
|
|
4
4
|
import { ViewType } from "./types";
|
|
@@ -14,12 +14,20 @@ const darkTheme: Theme = {
|
|
|
14
14
|
cdataColor: "#33CC66",
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
interface TextPageViewProps {
|
|
18
19
|
pageNumber: number;
|
|
19
20
|
viewType: ViewType;
|
|
20
21
|
}
|
|
21
22
|
export function TextPageView({ viewType, pageNumber }: TextPageViewProps) {
|
|
22
|
-
|
|
23
|
+
switch (viewType) {
|
|
24
|
+
case "json":
|
|
25
|
+
return <JsonPageLayoutView pageNumber={pageNumber} />;
|
|
26
|
+
case "markdown":
|
|
27
|
+
return <MarkdownPageView pageNumber={pageNumber} />;
|
|
28
|
+
default:
|
|
29
|
+
return <XmlPageView pageNumber={pageNumber} />;
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
interface XmlPageViewProps {
|
|
@@ -62,3 +70,22 @@ function JsonPageLayoutView({ pageNumber }: JsonPageLayoutViewProps) {
|
|
|
62
70
|
content && <JSONCode className="w-full" data={content} />
|
|
63
71
|
)
|
|
64
72
|
}
|
|
73
|
+
|
|
74
|
+
interface MarkdownPageViewProps {
|
|
75
|
+
pageNumber: number;
|
|
76
|
+
}
|
|
77
|
+
function MarkdownPageView({ pageNumber }: MarkdownPageViewProps) {
|
|
78
|
+
const [content, setContent] = useState<string>();
|
|
79
|
+
const { markdownProvider } = usePdfPagesInfo();
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
markdownProvider.getPageMarkdown(pageNumber).then(setContent).catch((err: any) => {
|
|
82
|
+
console.error(err);
|
|
83
|
+
setContent(undefined);
|
|
84
|
+
});
|
|
85
|
+
}, [pageNumber]);
|
|
86
|
+
return (
|
|
87
|
+
<div className="px-4 py-2 prose prose-sm max-w-none dark:prose-invert">
|
|
88
|
+
{content ? <MarkdownRenderer>{content}</MarkdownRenderer> : <div>No markdown content available</div>}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type ViewType = "xml" | "json";
|
|
1
|
+
export type ViewType = "xml" | "json" | "markdown";
|
|
@@ -74,7 +74,7 @@ export function CreateCollectionForm({ onClose, redirect = true, onAddToCollecti
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
return (
|
|
77
|
-
<form>
|
|
77
|
+
<form onSubmit={(e) => e.preventDefault()}>
|
|
78
78
|
<VModalBody>
|
|
79
79
|
<FormItem label="Name" required>
|
|
80
80
|
<Input type="text" value={payload.name || ""} onChange={(value) => setPayloadProp("name", value)} />
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
import { Button, Spinner, useToast } from "@vertesia/ui/core";
|
|
6
6
|
import { useNavigate } from "@vertesia/ui/router";
|
|
7
7
|
import { useUserSession } from "@vertesia/ui/session";
|
|
8
|
-
import { JSONDisplay } from "@vertesia/ui/widgets";
|
|
8
|
+
import { JSONDisplay, MarkdownRenderer } from "@vertesia/ui/widgets";
|
|
9
9
|
import {
|
|
10
10
|
ChevronRight,
|
|
11
11
|
Download,
|
|
@@ -16,8 +16,6 @@ import {
|
|
|
16
16
|
X,
|
|
17
17
|
} from "lucide-react";
|
|
18
18
|
import { useEffect, useState } from "react";
|
|
19
|
-
import Markdown from "react-markdown";
|
|
20
|
-
import remarkGfm from "remark-gfm";
|
|
21
19
|
|
|
22
20
|
interface DocumentPreviewPanelProps {
|
|
23
21
|
objectId: string | null;
|
|
@@ -233,7 +231,7 @@ export function DocumentPreviewPanel({
|
|
|
233
231
|
<div className="shadow rounded-md p-4 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
|
234
232
|
{seemsMarkdown ? (
|
|
235
233
|
<div className="prose prose-sm max-w-none prose-p:my-2 prose-pre:bg-gray-800 prose-pre:my-2 prose-headings:text-indigo-700 dark:prose-invert dark:prose-headings:text-indigo-300">
|
|
236
|
-
<
|
|
234
|
+
<MarkdownRenderer>{text}</MarkdownRenderer>
|
|
237
235
|
</div>
|
|
238
236
|
) : (
|
|
239
237
|
<pre className="text-wrap whitespace-pre-wrap dark:text-gray-200">
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { ColumnLayout, ContentObject, ContentObjectItem, ComplexSearchQuery } from '@vertesia/common';
|
|
3
|
+
import {
|
|
4
|
+
|
|
5
|
+
Button, Divider, ErrorBox, SidePanel, Spinner, useIntersectionObserver, useToast,
|
|
6
|
+
FilterProvider, FilterBtn, FilterBar, FilterClear, Filter as BaseFilter
|
|
7
|
+
} from '@vertesia/ui/core';
|
|
5
8
|
import { useNavigate } from "@vertesia/ui/router";
|
|
6
9
|
import { TypeRegistry, useUserSession } from '@vertesia/ui/session';
|
|
7
10
|
import { Download, RefreshCw, Eye } from 'lucide-react';
|
|
8
|
-
import { FilterProvider, FilterBtn, FilterBar, FilterClear, Filter as BaseFilter } from '@vertesia/ui/core';
|
|
9
11
|
import { useDocumentFilterGroups, useDocumentFilterHandler } from "../../facets/DocumentsFacetsNav";
|
|
10
12
|
import { VectorSearchWidget } from './components/VectorSearchWidget';
|
|
11
|
-
|
|
12
13
|
import { ContentDispositionButton } from './components/ContentDispositionButton';
|
|
13
14
|
import { DocumentTable } from './DocumentTable';
|
|
14
15
|
import { useDocumentSearch, useWatchDocumentSearchFacets, useWatchDocumentSearchResult } from './search/DocumentSearchContext';
|
|
@@ -90,7 +91,6 @@ export function DocumentSearchResults({ layout, onUpload, allowFilter = true, al
|
|
|
90
91
|
const [selectedObject, setSelectedObject] = useState<ContentObjectItem | null>(null);
|
|
91
92
|
const { typeRegistry } = useUserSession();
|
|
92
93
|
const { search, isLoading, error, objects } = useWatchDocumentSearchResult();
|
|
93
|
-
const [vQuery, setVQuery] = useState<VectorSearchQuery | undefined>(undefined);
|
|
94
94
|
const [actualLayout, setActualLayout] = useState<ColumnLayout[]>(
|
|
95
95
|
typeRegistry ? layout || getTableLayout(typeRegistry, search.query.type) : defaultLayout,
|
|
96
96
|
);
|
|
@@ -111,37 +111,35 @@ export function DocumentSearchResults({ layout, onUpload, allowFilter = true, al
|
|
|
111
111
|
}
|
|
112
112
|
}, { deps: [isReady, objects.length] });
|
|
113
113
|
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
search.search().then(() => setIsReady(true));
|
|
116
|
-
}, []);
|
|
117
114
|
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (vQuery) {
|
|
128
|
-
search.query.vector = vQuery;
|
|
129
|
-
if (!actualLayout.find((c) => c.name === "Vector Similarity")) {
|
|
115
|
+
// Handler for vector search widget
|
|
116
|
+
const handleVectorSearch = (query?: ComplexSearchQuery) => {
|
|
117
|
+
if (query && query.vector) {
|
|
118
|
+
search.query.vector = query.vector;
|
|
119
|
+
search.query.full_text = query.full_text;
|
|
120
|
+
search.query.weights = query.weights;
|
|
121
|
+
search.query.score_aggregation = query.score_aggregation;
|
|
122
|
+
search.query.dynamic_scaling = query.dynamic_scaling;
|
|
123
|
+
if (!actualLayout.find((c) => c.name === "Search Score")) {
|
|
130
124
|
const layout = [
|
|
131
125
|
...actualLayout,
|
|
132
126
|
{
|
|
133
|
-
name: "
|
|
127
|
+
name: "Search Score",
|
|
134
128
|
field: "score",
|
|
135
129
|
} satisfies ColumnLayout,
|
|
136
130
|
];
|
|
137
131
|
setActualLayout(layout);
|
|
138
132
|
}
|
|
139
133
|
search.search().then(() => setIsReady(true));
|
|
134
|
+
} else if (query && query.full_text) {
|
|
135
|
+
search.query.full_text = query.full_text;
|
|
136
|
+
search.search().then(() => setIsReady(true));
|
|
140
137
|
} else {
|
|
141
138
|
delete search.query.vector;
|
|
139
|
+
delete search.query.full_text;
|
|
142
140
|
search.search().then(() => setIsReady(true));
|
|
143
141
|
}
|
|
144
|
-
}
|
|
142
|
+
};
|
|
145
143
|
|
|
146
144
|
const facets = useWatchDocumentSearchFacets();
|
|
147
145
|
const facetSearch = useDocumentSearch();
|
|
@@ -202,7 +200,7 @@ export function DocumentSearchResults({ layout, onUpload, allowFilter = true, al
|
|
|
202
200
|
<div className="flex flex-row gap-4 items-center justify-between w-full">
|
|
203
201
|
<div className="flex gap-2 items-center w-2/3">
|
|
204
202
|
{
|
|
205
|
-
allowSearch && <VectorSearchWidget onChange={
|
|
203
|
+
allowSearch && <VectorSearchWidget onChange={handleVectorSearch} isLoading={isLoading} refresh={refreshTrigger} className="w-full" />
|
|
206
204
|
}
|
|
207
205
|
<FilterBtn />
|
|
208
206
|
</div>
|
|
@@ -223,7 +221,7 @@ export function DocumentSearchResults({ layout, onUpload, allowFilter = true, al
|
|
|
223
221
|
<div className="flex flex-row gap-4 items-center justify-between w-full">
|
|
224
222
|
<div className="flex gap-2 items-center w-2/3">
|
|
225
223
|
{
|
|
226
|
-
allowSearch && <VectorSearchWidget onChange={
|
|
224
|
+
allowSearch && <VectorSearchWidget onChange={handleVectorSearch} isLoading={isLoading} refresh={refreshTrigger} />
|
|
227
225
|
}
|
|
228
226
|
</div>
|
|
229
227
|
<div className="flex gap-1 items-center">
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import Markdown from "react-markdown";
|
|
3
|
-
import remarkGfm from "remark-gfm";
|
|
4
2
|
|
|
5
3
|
import { useUserSession } from "@vertesia/ui/session";
|
|
6
4
|
import { Button, Spinner, useToast } from "@vertesia/ui/core";
|
|
7
|
-
import { JSONDisplay } from "@vertesia/ui/widgets";
|
|
5
|
+
import { JSONDisplay, MarkdownRenderer } from "@vertesia/ui/widgets";
|
|
8
6
|
import { ContentObject, ImageRenditionFormat } from "@vertesia/common";
|
|
9
7
|
import { Copy, Download, SquarePen } from "lucide-react";
|
|
10
8
|
import { PropertiesEditorModal } from "./PropertiesEditorModal";
|
|
@@ -314,8 +312,7 @@ export function ContentOverview({
|
|
|
314
312
|
<div className="border shadow-xs rounded-xs max-w-7xl">
|
|
315
313
|
{seemsMarkdown ? (
|
|
316
314
|
<div className="vprose prose-sm p-1">
|
|
317
|
-
<
|
|
318
|
-
remarkPlugins={[remarkGfm]}
|
|
315
|
+
<MarkdownRenderer
|
|
319
316
|
components={{
|
|
320
317
|
a: ({ node, ...props }: { node?: any; href?: string; children?: React.ReactNode }) => {
|
|
321
318
|
const href = props.href || "";
|
|
@@ -378,7 +375,7 @@ export function ContentOverview({
|
|
|
378
375
|
}}
|
|
379
376
|
>
|
|
380
377
|
{text}
|
|
381
|
-
</
|
|
378
|
+
</MarkdownRenderer>
|
|
382
379
|
</div>
|
|
383
380
|
) : (
|
|
384
381
|
<pre className="text-wrap bg-muted text-muted p-2">
|
|
@@ -1,37 +1,61 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { ProjectConfiguration, SupportedEmbeddingTypes,
|
|
4
|
-
import { Button, Input, useToast } from '@vertesia/ui/core';
|
|
3
|
+
import { ProjectConfiguration, ComplexSearchQuery, SupportedEmbeddingTypes, SearchTypes } from '@vertesia/common';
|
|
4
|
+
import { Button, Input, useToast, Modal, ModalTitle, ModalBody, ModalFooter, Checkbox, NumberInput } from '@vertesia/ui/core';
|
|
5
5
|
import { useUserSession } from '@vertesia/ui/session';
|
|
6
|
+
import { Settings } from 'lucide-react';
|
|
6
7
|
|
|
7
8
|
interface VectorSearchWidgetProps {
|
|
8
|
-
onChange: (query?:
|
|
9
|
+
onChange: (query?: ComplexSearchQuery) => void;
|
|
9
10
|
className?: string;
|
|
10
11
|
status?: boolean;
|
|
11
12
|
isLoading?: boolean;
|
|
12
|
-
refresh: number
|
|
13
|
+
refresh: number;
|
|
14
|
+
searchTypes?: (keyof typeof SearchTypes)[];
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
const allTypes = Object.values(SearchTypes);
|
|
18
|
+
const embeddingTypes = Object.values(SupportedEmbeddingTypes);
|
|
19
|
+
|
|
20
|
+
export function VectorSearchWidget({ onChange, isLoading, refresh, searchTypes }: VectorSearchWidgetProps) {
|
|
15
21
|
const { client, project } = useUserSession();
|
|
16
22
|
const toast = useToast();
|
|
17
23
|
|
|
18
24
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
|
19
|
-
const [searchSketch, setSearchSketch] = useState<string | undefined>(undefined);
|
|
20
25
|
const [config, setConfig] = useState<ProjectConfiguration | undefined>(undefined);
|
|
21
26
|
const isReady = !!project && (!!config?.embeddings.text || !!config?.embeddings.image);
|
|
22
27
|
const [status, setStatus] = useState<string | undefined>(undefined);
|
|
23
28
|
|
|
29
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
30
|
+
// Default to all types, or use prop if provided
|
|
31
|
+
const [selectedTypes, setSelectedTypes] = useState<(keyof typeof SearchTypes)[]>(searchTypes || allTypes);
|
|
32
|
+
const [limit, setLimit] = useState<number>(100);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (searchTypes) setSelectedTypes(searchTypes);
|
|
35
|
+
}, [searchTypes]);
|
|
36
|
+
|
|
37
|
+
// Always derive embeddingSearchTypes and full_text from selectedTypes
|
|
38
|
+
const embeddingSearchTypes: Record<string, boolean> = {};
|
|
39
|
+
let fullTextEnabled = false;
|
|
40
|
+
let vectorSearchEnabled = false;
|
|
41
|
+
selectedTypes.forEach(type => {
|
|
42
|
+
if (type === SearchTypes.full_text) {
|
|
43
|
+
fullTextEnabled = true;
|
|
44
|
+
} else {
|
|
45
|
+
vectorSearchEnabled = true;
|
|
46
|
+
}
|
|
47
|
+
if (embeddingTypes.includes(type as SupportedEmbeddingTypes)) {
|
|
48
|
+
embeddingSearchTypes[type] = true;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
24
52
|
useEffect(() => {
|
|
25
53
|
setSearchText(undefined);
|
|
26
|
-
setSearchSketch(undefined);
|
|
27
54
|
setStatus(undefined);
|
|
28
55
|
}, [refresh]);
|
|
29
56
|
|
|
30
57
|
useEffect(() => {
|
|
31
|
-
if (!project)
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
58
|
+
if (!project) return;
|
|
35
59
|
client.projects.retrieve(project.id).then((project) => {
|
|
36
60
|
setConfig(project.configuration);
|
|
37
61
|
})
|
|
@@ -49,67 +73,71 @@ export function VectorSearchWidget({ onChange, isLoading, refresh }: VectorSearc
|
|
|
49
73
|
}
|
|
50
74
|
}, [searchText]);
|
|
51
75
|
|
|
52
|
-
const
|
|
53
|
-
if (!isReady || !searchText
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const query: VectorSearchQuery = {
|
|
64
|
-
values: response.values,
|
|
65
|
-
type: SupportedEmbeddingTypes.text
|
|
76
|
+
const fireSearch = () => {
|
|
77
|
+
if (!isReady || !searchText) return;
|
|
78
|
+
const query: ComplexSearchQuery = {
|
|
79
|
+
vector: vectorSearchEnabled ? {
|
|
80
|
+
text: searchText,
|
|
81
|
+
config: embeddingSearchTypes,
|
|
82
|
+
} : undefined,
|
|
83
|
+
full_text: fullTextEnabled ? searchText : undefined,
|
|
84
|
+
limit: limit
|
|
66
85
|
};
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
onChange(query);
|
|
87
|
+
setStatus("Searching...");
|
|
69
88
|
};
|
|
70
89
|
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
90
|
+
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
91
|
+
if (e.key === "Enter") {
|
|
92
|
+
fireSearch();
|
|
74
93
|
}
|
|
75
|
-
|
|
76
|
-
setStatus('Generating image embeddings...');
|
|
77
|
-
const response = await client.environments.embeddings(config.embeddings.image?.environment, {
|
|
78
|
-
model: config.embeddings.image?.model,
|
|
79
|
-
image: searchSketch,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const query: VectorSearchQuery = {
|
|
83
|
-
values: response.values,
|
|
84
|
-
type: SupportedEmbeddingTypes.image
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
return query;
|
|
88
94
|
};
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
onChange(query);
|
|
99
|
-
setStatus("Searching...");
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
104
|
-
if (e.key === "Enter") {
|
|
105
|
-
fireSearch("text");
|
|
96
|
+
// Modal state for search type selection
|
|
97
|
+
const handleCheckboxChange = (type: keyof typeof SearchTypes) => (checked: boolean) => {
|
|
98
|
+
if (checked) {
|
|
99
|
+
setSelectedTypes(prev => Array.from(new Set([...prev, type])));
|
|
100
|
+
} else {
|
|
101
|
+
setSelectedTypes(prev => prev.filter(t => t !== type));
|
|
106
102
|
}
|
|
107
103
|
};
|
|
108
104
|
|
|
109
105
|
return (
|
|
110
106
|
<div className="flex gap-1 items-center w-1/2">
|
|
111
107
|
<Input placeholder="Type what you are looking for, or select a filter" value={searchText} onChange={setSearchText} onKeyDown={handleKeyPress} />
|
|
112
|
-
<Button variant="
|
|
108
|
+
<Button variant="ghost" onClick={() => setShowSettings(true)} alt="Semantic search settings" className="ml-1"><Settings size={18} /></Button>
|
|
109
|
+
<Modal isOpen={showSettings} onClose={() => setShowSettings(false)}>
|
|
110
|
+
<ModalTitle>Search Types</ModalTitle>
|
|
111
|
+
<ModalBody>
|
|
112
|
+
<div className="flex flex-col gap-2">
|
|
113
|
+
<label className="flex items-center gap-2">
|
|
114
|
+
<Checkbox
|
|
115
|
+
checked={selectedTypes.includes(SearchTypes.full_text)}
|
|
116
|
+
onCheckedChange={handleCheckboxChange(SearchTypes.full_text)}
|
|
117
|
+
/>
|
|
118
|
+
<span>Full Text</span>
|
|
119
|
+
</label>
|
|
120
|
+
<div className="font-semibold mt-2 mb-1">Embeddings</div>
|
|
121
|
+
{embeddingTypes.map(type => (
|
|
122
|
+
<label key={type} className="flex items-center gap-2">
|
|
123
|
+
<Checkbox
|
|
124
|
+
checked={selectedTypes.includes(type)}
|
|
125
|
+
onCheckedChange={handleCheckboxChange(type)}
|
|
126
|
+
/>
|
|
127
|
+
<span>{type.charAt(0).toUpperCase() + type.slice(1)}</span>
|
|
128
|
+
</label>
|
|
129
|
+
))}
|
|
130
|
+
<div className="mt-3">
|
|
131
|
+
<span className="mr-2">Limit</span>
|
|
132
|
+
<NumberInput type="number" min={1} value={limit} onChange={v => setLimit(Number(v) || 1)} style={{ width: 80 }} />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</ModalBody>
|
|
136
|
+
<ModalFooter>
|
|
137
|
+
<Button variant="outline" onClick={() => setShowSettings(false)}>Close</Button>
|
|
138
|
+
</ModalFooter>
|
|
139
|
+
</Modal>
|
|
140
|
+
<Button variant="secondary" isLoading={isLoading} onClick={fireSearch} isDisabled={!isReady} alt="Semantic search">Search</Button>
|
|
113
141
|
</div>
|
|
114
142
|
);
|
|
115
143
|
}
|
|
@@ -28,7 +28,7 @@ export function DocumentTableView({ objects, selection, isLoading, onRowClick, c
|
|
|
28
28
|
))}
|
|
29
29
|
</tr>
|
|
30
30
|
</thead>
|
|
31
|
-
<TBody isLoading={isLoading} columns={columns.length}>
|
|
31
|
+
<TBody isLoading={isLoading} columns={columns.length + 1}>
|
|
32
32
|
{
|
|
33
33
|
objects?.map((obj: ContentObjectItem) => {
|
|
34
34
|
return (
|