@vertesia/ui 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/lib/esm/features/facets/RunsFacetsNav.js +15 -5
  2. package/lib/esm/features/facets/RunsFacetsNav.js.map +1 -1
  3. package/lib/esm/features/magic-pdf/DownloadPopover.js +17 -2
  4. package/lib/esm/features/magic-pdf/DownloadPopover.js.map +1 -1
  5. package/lib/esm/features/magic-pdf/MagicPdfView.js +26 -3
  6. package/lib/esm/features/magic-pdf/MagicPdfView.js.map +1 -1
  7. package/lib/esm/features/magic-pdf/PageSlider.js +21 -8
  8. package/lib/esm/features/magic-pdf/PageSlider.js.map +1 -1
  9. package/lib/esm/features/magic-pdf/PdfPageProvider.js +55 -0
  10. package/lib/esm/features/magic-pdf/PdfPageProvider.js.map +1 -1
  11. package/lib/esm/features/magic-pdf/TextPageView.js +21 -1
  12. package/lib/esm/features/magic-pdf/TextPageView.js.map +1 -1
  13. package/lib/esm/features/store/objects/layout/documentLayout.js +1 -1
  14. package/lib/esm/features/store/objects/layout/documentLayout.js.map +1 -1
  15. package/lib/esm/shell/apps/AppInstallationProvider.js +13 -0
  16. package/lib/esm/shell/apps/AppInstallationProvider.js.map +1 -0
  17. package/lib/esm/shell/apps/AppProjectSelector.js +25 -0
  18. package/lib/esm/shell/apps/AppProjectSelector.js.map +1 -0
  19. package/lib/esm/shell/apps/StandaloneApp.js +60 -0
  20. package/lib/esm/shell/apps/StandaloneApp.js.map +1 -0
  21. package/lib/esm/shell/apps/index.js +3 -1
  22. package/lib/esm/shell/apps/index.js.map +1 -1
  23. package/lib/esm/shell/login/InviteAcceptModal.js +1 -1
  24. package/lib/esm/shell/login/InviteAcceptModal.js.map +1 -1
  25. package/lib/tsconfig.tsbuildinfo +1 -1
  26. package/lib/types/features/facets/RunsFacetsNav.d.ts +1 -0
  27. package/lib/types/features/facets/RunsFacetsNav.d.ts.map +1 -1
  28. package/lib/types/features/facets/VFacetsNav.d.ts +1 -1
  29. package/lib/types/features/facets/VFacetsNav.d.ts.map +1 -1
  30. package/lib/types/features/magic-pdf/DownloadPopover.d.ts.map +1 -1
  31. package/lib/types/features/magic-pdf/PageSlider.d.ts +2 -1
  32. package/lib/types/features/magic-pdf/PageSlider.d.ts.map +1 -1
  33. package/lib/types/features/magic-pdf/PdfPageProvider.d.ts +10 -0
  34. package/lib/types/features/magic-pdf/PdfPageProvider.d.ts.map +1 -1
  35. package/lib/types/features/magic-pdf/TextPageView.d.ts.map +1 -1
  36. package/lib/types/features/magic-pdf/types.d.ts +1 -1
  37. package/lib/types/features/magic-pdf/types.d.ts.map +1 -1
  38. package/lib/types/shell/apps/AppInstallationProvider.d.ts +12 -0
  39. package/lib/types/shell/apps/AppInstallationProvider.d.ts.map +1 -0
  40. package/lib/types/shell/apps/AppProjectSelector.d.ts +12 -0
  41. package/lib/types/shell/apps/AppProjectSelector.d.ts.map +1 -0
  42. package/lib/types/shell/apps/StandaloneApp.d.ts +23 -0
  43. package/lib/types/shell/apps/StandaloneApp.d.ts.map +1 -0
  44. package/lib/types/shell/apps/index.d.ts +3 -1
  45. package/lib/types/shell/apps/index.d.ts.map +1 -1
  46. package/lib/vertesia-ui-features.js +1 -1
  47. package/lib/vertesia-ui-features.js.map +1 -1
  48. package/lib/vertesia-ui-shell.js +1 -1
  49. package/lib/vertesia-ui-shell.js.map +1 -1
  50. package/package.json +7 -7
  51. package/src/features/facets/RunsFacetsNav.tsx +17 -5
  52. package/src/features/facets/VFacetsNav.tsx +1 -1
  53. package/src/features/magic-pdf/DownloadPopover.tsx +38 -5
  54. package/src/features/magic-pdf/MagicPdfView.tsx +31 -5
  55. package/src/features/magic-pdf/PageSlider.tsx +44 -14
  56. package/src/features/magic-pdf/PdfPageProvider.tsx +81 -0
  57. package/src/features/magic-pdf/TextPageView.tsx +29 -1
  58. package/src/features/magic-pdf/types.ts +1 -1
  59. package/src/features/store/objects/layout/documentLayout.tsx +1 -1
  60. package/src/shell/apps/AppInstallationProvider.tsx +22 -0
  61. package/src/shell/apps/AppProjectSelector.tsx +47 -0
  62. package/src/shell/apps/StandaloneApp.tsx +108 -0
  63. package/src/shell/apps/index.ts +3 -1
  64. package/src/shell/login/InviteAcceptModal.tsx +1 -1
  65. package/lib/esm/shell/apps/CheckAppAccess.js +0 -23
  66. package/lib/esm/shell/apps/CheckAppAccess.js.map +0 -1
  67. package/lib/types/shell/apps/CheckAppAccess.d.ts +0 -6
  68. package/lib/types/shell/apps/CheckAppAccess.d.ts.map +0 -1
  69. package/src/shell/apps/CheckAppAccess.tsx +0 -25
@@ -13,6 +13,7 @@ interface RunsFacetsNavProps {
13
13
  environments?: any[];
14
14
  models?: any[];
15
15
  statuses?: any[];
16
+ tags?: any[];
16
17
  finish_reason?: any[];
17
18
  created_by?: any[];
18
19
  };
@@ -35,16 +36,25 @@ export function useRunsFilterGroups(facets: RunsFacetsNavProps['facets']): Filte
35
36
  if (facets.environments) {
36
37
  const environmentFilterGroup = VEnvironmentFacet({
37
38
  buckets: facets.environments || [],
38
- name: 'Environments'
39
+ name: 'environments',
39
40
  });
40
41
  customFilterGroups.push(environmentFilterGroup);
41
42
  }
42
43
 
44
+ // Add tags filter as stringList type (allows custom input)
45
+ const tagsFilterGroup = {
46
+ name: 'tags',
47
+ placeholder: 'Tags',
48
+ type: 'stringList' as const,
49
+ multiple: true
50
+ };
51
+ customFilterGroups.push(tagsFilterGroup);
52
+
43
53
  if (facets.models) {
44
54
  const modelFilterGroup = VStringFacet({
45
55
  search: null as any, // This will be provided by the search context
46
56
  buckets: facets.models || [],
47
- name: 'Model'
57
+ name: 'model'
48
58
  });
49
59
  customFilterGroups.push(modelFilterGroup);
50
60
  }
@@ -53,7 +63,7 @@ export function useRunsFilterGroups(facets: RunsFacetsNavProps['facets']): Filte
53
63
  const statusFilterGroup = VStringFacet({
54
64
  search: null as any, // This will be provided by the search context
55
65
  buckets: facets.statuses || [],
56
- name: 'Status'
66
+ name: 'status'
57
67
  });
58
68
  customFilterGroups.push(statusFilterGroup);
59
69
  }
@@ -105,11 +115,13 @@ export function useRunsFilterGroups(facets: RunsFacetsNavProps['facets']): Filte
105
115
  export function useRunsFilterHandler(search: SearchInterface) {
106
116
  return (newFilters: BaseFilter[]) => {
107
117
  if (newFilters.length === 0) {
108
- search.clearFilters();
118
+ // Clear filters without applying defaults - user wants to remove all filters
119
+ search.clearFilters(true, false);
109
120
  return;
110
121
  }
111
122
 
112
- search.clearFilters(false);
123
+ // Clear all filters first without defaults, then apply new ones
124
+ search.clearFilters(false, false);
113
125
 
114
126
  newFilters.forEach(filter => {
115
127
  if (filter.value && filter.value.length > 0) {
@@ -4,7 +4,7 @@ import { useState } from 'react';
4
4
  export interface SearchInterface {
5
5
  getFilterValue(name: string): any;
6
6
  setFilterValue(name: string, value: any): void;
7
- clearFilters(autoSearch?: boolean): void;
7
+ clearFilters(autoSearch?: boolean, applyDefaults?: boolean): void;
8
8
  search(): Promise<boolean | undefined>;
9
9
  readonly isRunning: boolean;
10
10
  query: Record<string, any>;
@@ -1,4 +1,4 @@
1
- import { ContentObject } from "@vertesia/common";
1
+ import { ContentObject, DocumentMetadata } from "@vertesia/common";
2
2
  import { useUserSession } from "@vertesia/ui/session";
3
3
  import { Popover } from "@vertesia/ui/widgets";
4
4
  import { CloudDownload } from "lucide-react";
@@ -12,18 +12,51 @@ export function DownloadPopover({ object }: DownloadPopoverProps) {
12
12
  const onDownload = (name: string) => {
13
13
  getResourceUrl(client, object.id, name).then(url => window.open(url, '_blank'));
14
14
  }
15
+
16
+ const getProcessorType = (): string => {
17
+ if (object.metadata?.type === "document") {
18
+ const docMetadata = object.metadata as DocumentMetadata;
19
+ return docMetadata.content_processor?.type || "xml";
20
+ }
21
+ return "xml"; // default
22
+ };
23
+
24
+ const processorType = getProcessorType();
25
+
26
+ const renderDownloadOptions = () => {
27
+ if (processorType === "markdown") {
28
+ return (
29
+ <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("document.md")}>
30
+ document.md
31
+ </button>
32
+ );
33
+ }
34
+
35
+ // Default XML processor options
36
+ return (
37
+ <>
38
+ <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("annotated.pdf")}>
39
+ annotated.pdf
40
+ </button>
41
+ <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("document.xml")}>
42
+ document.xml
43
+ </button>
44
+ <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("analyzed-pages.json")}>
45
+ analyzed-pages.json
46
+ </button>
47
+ </>
48
+ );
49
+ };
50
+
15
51
  return (
16
52
  <div className="absolute bottom-[58px] right-[20px] w-[36px] h-[36px] cursor-pointer text-indigo-400 border-indigo-400 hover:border-indigo-500 hover:text-indigo-500 border-2 rounded-full shadow-xs flex items-center justify-center">
17
53
  <Popover strategy='absolute' placement='top-end' zIndex={100} offset={20}>
18
54
  <Popover.Trigger click>
19
55
  <CloudDownload className='size-6' />
20
-
21
56
  </Popover.Trigger>
22
57
  <Popover.Content>
23
58
  <div className="rounded-md shadow-md border border-gray-100 bg-white dark:bg-slate-50 dark:border-slate-100 min-w-[200px] flex flex-col divide-y">
24
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("annotated.pdf")}>annotated.pdf</button>
25
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("document.xml")}>document.xml</button>
26
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("analyzed-pages.json")}>analyzed-pages.json</button>
59
+ {renderDownloadOptions()}
27
60
  </div>
28
61
  </Popover.Content>
29
62
  </Popover>
@@ -1,4 +1,4 @@
1
- import { ContentObject } from "@vertesia/common";
1
+ import { ContentObject, DocumentMetadata } from "@vertesia/common";
2
2
  import { ErrorBox, useFetch } from "@vertesia/ui/core";
3
3
  import { useUserSession } from "@vertesia/ui/session";
4
4
  import { X } from "lucide-react";
@@ -39,8 +39,27 @@ interface _MagicPdfViewProps {
39
39
  onClose?: () => void;
40
40
  }
41
41
  function MagicPdfViewImpl({ object, onClose }: _MagicPdfViewProps) {
42
- const [viewType, setViewType] = useState<ViewType>("xml");
42
+ const getInitialViewType = (): ViewType => {
43
+ if (object.metadata?.type === "document") {
44
+ const docMetadata = object.metadata as DocumentMetadata;
45
+ const processorType = docMetadata.content_processor?.type;
46
+ if (processorType === "markdown") return "markdown";
47
+ if (processorType === "xml") return "xml";
48
+ }
49
+ return "xml"; // default
50
+ };
51
+
52
+ const getProcessorType = (): string => {
53
+ if (object.metadata?.type === "document") {
54
+ const docMetadata = object.metadata as DocumentMetadata;
55
+ return docMetadata.content_processor?.type || "xml";
56
+ }
57
+ return "xml"; // default
58
+ };
59
+
60
+ const [viewType, setViewType] = useState<ViewType>(getInitialViewType());
43
61
  const [pageNumber, setPageNumber] = useState(1);
62
+ const processorType = getProcessorType();
44
63
  const handler = useRef<HTMLDivElement>(null);
45
64
  const left = useRef<HTMLDivElement>(null);
46
65
  const right = useRef<HTMLDivElement>(null);
@@ -51,14 +70,14 @@ function MagicPdfViewImpl({ object, onClose }: _MagicPdfViewProps) {
51
70
  return (
52
71
  <>
53
72
  <div ref={left} className={`absolute top-0 left-0 bottom-0 w-[50%] bg-gray-100 dark:bg-slate-800 flex items-stretch justify-stretch py-2`}>
54
- <PageSlider className="flex-1" currentPage={pageNumber} onChange={setPageNumber} />
73
+ <PageSlider className="flex-1" currentPage={pageNumber} onChange={setPageNumber} object={object} />
55
74
  <div ref={handler} className='w-[2px] p-[2px] m-0 bg-slate-300 cursor-ew-resize'></div>
56
75
  </div>
57
76
  <div ref={right} className={`absolute top-0 left-[50%] right-0 bottom-0 flex items-stretch justify-stretch overflow-auto p-2`}>
58
77
  <TextPageView pageNumber={pageNumber} viewType={viewType} />
59
78
  </div>
60
79
  <DownloadPopover object={object} />
61
- <ContentSwitcher type={viewType} onSwitch={setViewType} />
80
+ {processorType === "xml" && <ContentSwitcher type={viewType} onSwitch={setViewType} />}
62
81
  {!!onClose &&
63
82
  <div className="absolute top-6 right-7 w-9 h-9 cursor-pointer text-red-400 border-red-400 hover:border-red-500 hover:text-red-500 border-2 rounded-full shadow-xs flex items-center justify-center"
64
83
  onClick={onClose}>
@@ -80,6 +99,8 @@ function ContentSwitcher({ type = "xml", onSwitch }: ContentSwitcherProps) {
80
99
  if (type === "xml") {
81
100
  onSwitch("json");
82
101
  } else if (type === "json") {
102
+ onSwitch("markdown");
103
+ } else if (type === "markdown") {
83
104
  onSwitch("xml");
84
105
  }
85
106
  }
@@ -87,7 +108,8 @@ function ContentSwitcher({ type = "xml", onSwitch }: ContentSwitcherProps) {
87
108
  <div className="absolute bottom-[16px] right-[20px] w-[36px] h-[36px] cursor-pointer text-indigo-400 border-indigo-400 hover:border-indigo-500 hover:text-indigo-500 border-2 rounded-full shadow-xs flex items-center justify-center"
88
109
  onClick={_onSwitch}>
89
110
  {type === "xml" && JSON}
90
- {type === "json" && XML}
111
+ {type === "json" && MARKDOWN}
112
+ {type === "markdown" && XML}
91
113
  </div>
92
114
  )
93
115
 
@@ -100,3 +122,7 @@ const JSON = <svg width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://w
100
122
  const XML = <svg width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
101
123
  <path d="M4.708 5.578L2.061 8.224l2.647 2.646-.708.708-3-3V7.87l3-3 .708.708zm7-.708L11 5.578l2.647 2.646L11 10.87l.708.708 3-3V7.87l-3-3zM4.908 13l.894.448 5-10L9.908 3l-5 10z" />
102
124
  </svg>
125
+
126
+ const MARKDOWN = <svg width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
127
+ <path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7.5L5.5 9l-1 1.5H3V5h1.5l1 2 2-2H9v6zm2.99.5L9.5 8H11V5h1v3h1.5l-2.51 3.5z"/>
128
+ </svg>
@@ -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
- instrumented,
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 [imageType, setImageType] = useState<ImageType>(ImageType.default);
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 : urls);
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
- <button className={getRadioButtonClass(ImageType.default, imageType)}
60
- onClick={() => setImageType(ImageType.default)}
61
- ><ImageIcon className="w-5 h-5 mt-1" /></button>
62
- <button className={getRadioButtonClass(ImageType.instrumented, imageType)}
63
- onClick={() => setImageType(ImageType.instrumented)}
64
- ><InfoIcon className="w-5 h-5 mt-1" /></button>
65
- <button className={getRadioButtonClass(ImageType.annotated, imageType)}
66
- onClick={() => setImageType(ImageType.annotated)}
67
- ><AtSignIcon className="w-5 h-5 mt-1" /></button>
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,5 +1,7 @@
1
1
  import { JSONCode, Theme, XMLViewer } from '@vertesia/ui/widgets';
2
2
  import { useEffect, useLayoutEffect, useState } from "react";
3
+ import Markdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
3
5
  import { usePdfPagesInfo } from "./PdfPageProvider";
4
6
  import { ViewType } from "./types";
5
7
 
@@ -19,7 +21,14 @@ interface TextPageViewProps {
19
21
  viewType: ViewType;
20
22
  }
21
23
  export function TextPageView({ viewType, pageNumber }: TextPageViewProps) {
22
- return viewType === "json" ? <JsonPageLayoutView pageNumber={pageNumber} /> : <XmlPageView pageNumber={pageNumber} />
24
+ switch (viewType) {
25
+ case "json":
26
+ return <JsonPageLayoutView pageNumber={pageNumber} />;
27
+ case "markdown":
28
+ return <MarkdownPageView pageNumber={pageNumber} />;
29
+ default:
30
+ return <XmlPageView pageNumber={pageNumber} />;
31
+ }
23
32
  }
24
33
 
25
34
  interface XmlPageViewProps {
@@ -62,3 +71,22 @@ function JsonPageLayoutView({ pageNumber }: JsonPageLayoutViewProps) {
62
71
  content && <JSONCode className="w-full" data={content} />
63
72
  )
64
73
  }
74
+
75
+ interface MarkdownPageViewProps {
76
+ pageNumber: number;
77
+ }
78
+ function MarkdownPageView({ pageNumber }: MarkdownPageViewProps) {
79
+ const [content, setContent] = useState<string>();
80
+ const { markdownProvider } = usePdfPagesInfo();
81
+ useEffect(() => {
82
+ markdownProvider.getPageMarkdown(pageNumber).then(setContent).catch((err: any) => {
83
+ console.error(err);
84
+ setContent(undefined);
85
+ });
86
+ }, [pageNumber]);
87
+ return (
88
+ <div className="px-4 py-2 prose prose-sm max-w-none dark:prose-invert">
89
+ {content ? <Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown> : <div>No markdown content available</div>}
90
+ </div>
91
+ )
92
+ }
@@ -1 +1 @@
1
- export type ViewType = "xml" | "json";
1
+ export type ViewType = "xml" | "json" | "markdown";
@@ -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 (
@@ -0,0 +1,22 @@
1
+ import { AppInstallationWithManifest } from "@vertesia/common";
2
+ import { createContext, ReactNode, useContext } from "react";
3
+
4
+
5
+ export const AppInstallationContext = createContext<AppInstallationWithManifest | null>(null);
6
+
7
+ export function AppInstallationProvider({ installation, children }: { installation: AppInstallationWithManifest, children: ReactNode }) {
8
+ return (
9
+ <AppInstallationContext.Provider value={installation}>
10
+ {children}
11
+ </AppInstallationContext.Provider>
12
+ )
13
+ }
14
+
15
+ /**
16
+ * Get the current app installation obejct when called in an app context otheriwse returns null
17
+ */
18
+ export function useAppInstallation() {
19
+ return useContext(AppInstallationContext);
20
+ }
21
+
22
+
@@ -0,0 +1,47 @@
1
+ import { ProjectRef, RequireAtLeastOne } from "@vertesia/common";
2
+ import { SelectBox, useFetch } from "@vertesia/ui/core";
3
+ import { useUserSession } from "@vertesia/ui/session";
4
+ import { useState } from "react";
5
+
6
+ interface AppProjectSelectorProps {
7
+ app: RequireAtLeastOne<{ id?: string, name?: string }, 'id' | 'name'>;
8
+ onChange: (value: ProjectRef) => void;
9
+ placeholder?: string;
10
+ }
11
+ export function AppProjectSelector({ app, onChange, placeholder }: AppProjectSelectorProps) {
12
+ const { client, project } = useUserSession();
13
+ const { data: projects, error } = useFetch(() => {
14
+ return client.apps.getAppInstallationProjects(app);
15
+ }, [app.id, app.name])
16
+
17
+ if (error) {
18
+ return <span className='text-red-600'>Error: failed to fetch projects: {error.message}</span>
19
+ }
20
+
21
+ return <SelectProject placeholder={placeholder} initialValue={project?.id} projects={projects || []} onChange={onChange} />
22
+ }
23
+
24
+ interface SelectProjectProps {
25
+ initialValue?: string
26
+ projects: ProjectRef[]
27
+ onChange: (value: ProjectRef) => void
28
+ placeholder?: string;
29
+ }
30
+ function SelectProject({ initialValue, projects, onChange, placeholder = "Select Project" }: Readonly<SelectProjectProps>) {
31
+ const [value, setValue] = useState<ProjectRef | undefined>(() => {
32
+ return initialValue ? projects.find(p => p.id === initialValue) : undefined
33
+ });
34
+ const _onChange = (value: ProjectRef) => {
35
+ setValue(value)
36
+ onChange(value)
37
+ }
38
+ return (
39
+ <SelectBox
40
+ by="id"
41
+ value={value}
42
+ options={projects}
43
+ optionLabel={(option) => option.name}
44
+ placeholder={placeholder}
45
+ onChange={_onChange} />
46
+ )
47
+ }