@vertesia/ui 1.0.0-dev.20260225.024852Z → 1.0.0-dev.20260305.083323Z

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 (121) hide show
  1. package/lib/esm/core/components/shadcn/selectBox.js +1 -1
  2. package/lib/esm/core/components/shadcn/theme/ThemeSwitcher.js +3 -4
  3. package/lib/esm/core/components/shadcn/theme/ThemeSwitcher.js.map +1 -1
  4. package/lib/esm/core/components/shadcn/tooltip.js +1 -1
  5. package/lib/esm/core/components/shadcn/tooltip.js.map +1 -1
  6. package/lib/esm/env/index.js +3 -0
  7. package/lib/esm/env/index.js.map +1 -1
  8. package/lib/esm/features/agent/chat/ModernAgentConversation.js +3 -3
  9. package/lib/esm/features/agent/chat/ModernAgentConversation.js.map +1 -1
  10. package/lib/esm/features/agent/chat/ModernAgentOutput/utils.js +8 -1
  11. package/lib/esm/features/agent/chat/ModernAgentOutput/utils.js.map +1 -1
  12. package/lib/esm/features/facets/CollectionsFacetsNav.js +2 -2
  13. package/lib/esm/features/facets/CollectionsFacetsNav.js.map +1 -1
  14. package/lib/esm/features/facets/DocumentsFacetsNav.js +2 -2
  15. package/lib/esm/features/facets/DocumentsFacetsNav.js.map +1 -1
  16. package/lib/esm/features/facets/utils/TypeFacet.js +2 -2
  17. package/lib/esm/features/facets/utils/TypeFacet.js.map +1 -1
  18. package/lib/esm/features/store/collections/BrowseCollectionView.js +3 -1
  19. package/lib/esm/features/store/collections/BrowseCollectionView.js.map +1 -1
  20. package/lib/esm/features/store/collections/CreateCollection.js +13 -13
  21. package/lib/esm/features/store/collections/CreateCollection.js.map +1 -1
  22. package/lib/esm/features/store/objects/DocumentSearchResults.js +2 -1
  23. package/lib/esm/features/store/objects/DocumentSearchResults.js.map +1 -1
  24. package/lib/esm/features/store/objects/upload/DocumentUploadModal.js +3 -1
  25. package/lib/esm/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
  26. package/lib/esm/features/store/types/ContentObjectTypesSearch.js +4 -3
  27. package/lib/esm/features/store/types/ContentObjectTypesSearch.js.map +1 -1
  28. package/lib/esm/features/store/types/SelectContentType.js +9 -9
  29. package/lib/esm/features/store/types/SelectContentType.js.map +1 -1
  30. package/lib/esm/features/store/types/SelectContentTypeModal.js +2 -2
  31. package/lib/esm/features/store/types/SelectContentTypeModal.js.map +1 -1
  32. package/lib/esm/features/store/types/TypeRegistry.js.map +1 -0
  33. package/lib/esm/features/store/types/TypeRegistryProvider.js +53 -0
  34. package/lib/esm/features/store/types/TypeRegistryProvider.js.map +1 -0
  35. package/lib/esm/features/store/types/index.js +2 -0
  36. package/lib/esm/features/store/types/index.js.map +1 -1
  37. package/lib/esm/session/UserSession.js +9 -27
  38. package/lib/esm/session/UserSession.js.map +1 -1
  39. package/lib/esm/session/index.js +0 -1
  40. package/lib/esm/session/index.js.map +1 -1
  41. package/lib/esm/shell/SplashScreen.js +2 -2
  42. package/lib/esm/shell/SplashScreen.js.map +1 -1
  43. package/lib/esm/shell/VertesiaShell.js +2 -2
  44. package/lib/esm/shell/VertesiaShell.js.map +1 -1
  45. package/lib/esm/shell/login/TerminalLogin.js +1 -1
  46. package/lib/esm/shell/login/TerminalLogin.js.map +1 -1
  47. package/lib/tsconfig.tsbuildinfo +1 -1
  48. package/lib/types/core/components/shadcn/theme/ThemeSwitcher.d.ts +6 -1
  49. package/lib/types/core/components/shadcn/theme/ThemeSwitcher.d.ts.map +1 -1
  50. package/lib/types/env/index.d.ts +2 -0
  51. package/lib/types/env/index.d.ts.map +1 -1
  52. package/lib/types/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  53. package/lib/types/features/agent/chat/ModernAgentOutput/utils.d.ts.map +1 -1
  54. package/lib/types/features/facets/CollectionsFacetsNav.d.ts.map +1 -1
  55. package/lib/types/features/facets/utils/TypeFacet.d.ts.map +1 -1
  56. package/lib/types/features/facets/utils/VTypeFacet.d.ts +1 -1
  57. package/lib/types/features/facets/utils/VTypeFacet.d.ts.map +1 -1
  58. package/lib/types/features/store/collections/BrowseCollectionView.d.ts.map +1 -1
  59. package/lib/types/features/store/collections/CreateCollection.d.ts.map +1 -1
  60. package/lib/types/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
  61. package/lib/types/features/store/objects/upload/DocumentUploadModal.d.ts.map +1 -1
  62. package/lib/types/features/store/types/ContentObjectTypesSearch.d.ts.map +1 -1
  63. package/lib/types/features/store/types/TypeRegistry.d.ts.map +1 -0
  64. package/lib/types/features/store/types/TypeRegistryProvider.d.ts +15 -0
  65. package/lib/types/features/store/types/TypeRegistryProvider.d.ts.map +1 -0
  66. package/lib/types/features/store/types/index.d.ts +2 -0
  67. package/lib/types/features/store/types/index.d.ts.map +1 -1
  68. package/lib/types/session/UserSession.d.ts +0 -4
  69. package/lib/types/session/UserSession.d.ts.map +1 -1
  70. package/lib/types/session/index.d.ts +0 -1
  71. package/lib/types/session/index.d.ts.map +1 -1
  72. package/lib/types/shell/VertesiaShell.d.ts.map +1 -1
  73. package/lib/types/shell/login/TerminalLogin.d.ts.map +1 -1
  74. package/lib/vertesia-ui-core.js +1 -1
  75. package/lib/vertesia-ui-core.js.map +1 -1
  76. package/lib/vertesia-ui-env.js +1 -1
  77. package/lib/vertesia-ui-env.js.map +1 -1
  78. package/lib/vertesia-ui-features.js +1 -1
  79. package/lib/vertesia-ui-features.js.map +1 -1
  80. package/lib/vertesia-ui-session.js +1 -1
  81. package/lib/vertesia-ui-session.js.map +1 -1
  82. package/lib/vertesia-ui-shell.js +1 -1
  83. package/lib/vertesia-ui-shell.js.map +1 -1
  84. package/llms.txt +124 -4
  85. package/package.json +29 -7
  86. package/src/core/components/shadcn/selectBox.tsx +1 -1
  87. package/src/core/components/shadcn/theme/ThemeSwitcher.tsx +13 -8
  88. package/src/core/components/shadcn/tooltip.tsx +1 -1
  89. package/src/css/base.css +31 -0
  90. package/src/css/color.css +136 -0
  91. package/src/css/custom-tooltips.css +64 -0
  92. package/src/css/index.css +27 -0
  93. package/src/css/theme.css +86 -0
  94. package/src/css/typography.css +88 -0
  95. package/src/css/utilities.css +72 -0
  96. package/src/env/index.ts +5 -0
  97. package/src/features/agent/chat/ModernAgentConversation.tsx +84 -91
  98. package/src/features/agent/chat/ModernAgentOutput/utils.ts +7 -1
  99. package/src/features/facets/CollectionsFacetsNav.tsx +2 -2
  100. package/src/features/facets/DocumentsFacetsNav.tsx +2 -2
  101. package/src/features/facets/utils/TypeFacet.tsx +2 -2
  102. package/src/features/facets/utils/VTypeFacet.tsx +1 -1
  103. package/src/features/store/collections/BrowseCollectionView.tsx +5 -2
  104. package/src/features/store/collections/CreateCollection.tsx +36 -34
  105. package/src/features/store/objects/DocumentSearchResults.tsx +4 -2
  106. package/src/features/store/objects/upload/DocumentUploadModal.tsx +3 -1
  107. package/src/features/store/types/ContentObjectTypesSearch.tsx +4 -3
  108. package/src/features/store/types/SelectContentType.tsx +9 -9
  109. package/src/features/store/types/SelectContentTypeModal.tsx +2 -2
  110. package/src/features/store/types/TypeRegistryProvider.tsx +73 -0
  111. package/src/features/store/types/index.ts +2 -0
  112. package/src/session/UserSession.ts +9 -28
  113. package/src/session/index.ts +0 -1
  114. package/src/shell/SplashScreen.tsx +4 -2
  115. package/src/shell/VertesiaShell.tsx +10 -8
  116. package/src/shell/login/TerminalLogin.tsx +1 -2
  117. package/lib/esm/session/TypeRegistry.js.map +0 -1
  118. package/lib/types/session/TypeRegistry.d.ts.map +0 -1
  119. /package/lib/esm/{session → features/store/types}/TypeRegistry.js +0 -0
  120. /package/lib/types/{session → features/store/types}/TypeRegistry.d.ts +0 -0
  121. /package/src/{session → features/store/types}/TypeRegistry.ts +0 -0
@@ -497,91 +497,85 @@ function StartWorkflowView({
497
497
  }
498
498
 
499
499
  return (
500
- <div
501
- className={`flex flex-col h-full bg-white dark:bg-gray-900 overflow-hidden border-0 relative ${isDragOver ? 'ring-2 ring-blue-400 ring-inset' : ''}`}
502
- onDragEnter={handleDragEnter}
503
- onDragOver={handleDragOver}
504
- onDragLeave={handleDragLeave}
505
- onDrop={handleDrop}
506
- >
507
- {/* Drag overlay for full-panel file drop */}
508
- {isDragOver && (
509
- <div className="absolute inset-0 flex items-center justify-center bg-blue-100/80 dark:bg-blue-900/40 z-50 pointer-events-none rounded-lg">
510
- <div className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2 text-lg">
511
- <UploadIcon className="size-6" />
512
- Drop files to stage for upload
513
- </div>
514
- </div>
515
- )}
516
-
517
- {/* Hidden file input */}
518
- <input
519
- ref={fileInputRef}
520
- type="file"
521
- multiple
522
- accept={acceptedFileTypes}
523
- onChange={handleFileInputChange}
524
- className="hidden"
525
- />
526
-
527
- {/* Header */}
528
- <div className="flex items-center justify-between py-2 px-3 border-b border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900">
529
- <div className="flex items-center space-x-2">
530
- <div className="p-1">
531
- <Cpu className="size-3.5 text-muted" />
532
- </div>
533
- <span className="font-medium text-sm text-gray-700 dark:text-gray-300">
534
- {title}
535
- </span>
536
- </div>
537
-
538
- {/* Close button if needed */}
539
- {onClose && !isModal && (
540
- <Button
541
- size="xs"
542
- variant="ghost"
543
- onClick={onClose}
544
- title="Close"
545
- className="text-slate-500 hover:text-slate-700"
546
- >
547
- <XIcon className="size-4" />
548
- </Button>
549
- )}
550
- </div>
551
-
552
- {/* Empty conversation area with instructions */}
500
+ <div className="flex flex-col h-full bg-background items-center">
553
501
  <div
554
- className={`flex-1 overflow-y-auto px-4 py-6 bg-white dark:bg-gray-900 flex flex-col ${
555
- fullWidth ? 'items-start justify-start' : 'items-center justify-center'
556
- }`}
502
+ className={cn(
503
+ "flex flex-col h-full w-full overflow-hidden border-0 relative",
504
+ fullWidth ? "" : "max-w-4xl"
505
+ )}
506
+ onDragEnter={handleDragEnter}
507
+ onDragOver={handleDragOver}
508
+ onDragLeave={handleDragLeave}
509
+ onDrop={handleDrop}
557
510
  >
558
- {initialMessage && (
559
- <div
560
- className={`px-4 py-3 mb-4 bg-blue-50/80 dark:bg-blue-900/30 border-l-2 border-blue-400 dark:border-blue-500 text-blue-700 dark:text-blue-300 ${
561
- fullWidth ? 'w-full' : 'max-w-md'
562
- }`}
563
- >
564
- {initialMessage}
511
+ {/* Drag overlay for full-panel file drop */}
512
+ {isDragOver && (
513
+ <div className="absolute inset-0 flex items-center justify-center bg-info-background z-50 pointer-events-none rounded-lg">
514
+ <div className="text-info font-medium flex items-center gap-2 text-lg">
515
+ <UploadIcon className="size-6" />
516
+ Drop files to stage for upload
517
+ </div>
565
518
  </div>
566
519
  )}
567
520
 
568
- <div
569
- className={`bg-white dark:bg-slate-800 p-4 border-l-2 border-blue-400 dark:border-blue-500 ${
570
- fullWidth ? 'w-full' : 'max-w-md'
571
- }`}
572
- >
573
- <div className="text-base text-slate-600 dark:text-slate-300 font-medium">
574
- Enter a message to start a conversation
521
+ {/* Hidden file input */}
522
+ <input
523
+ ref={fileInputRef}
524
+ type="file"
525
+ multiple
526
+ accept={acceptedFileTypes}
527
+ onChange={handleFileInputChange}
528
+ className="hidden"
529
+ />
530
+
531
+ {/* Header */}
532
+ <div className="flex items-center justify-between py-2 px-3 border-b border-border bg-background">
533
+ <div className="flex items-center space-x-2">
534
+ <div className="p-1">
535
+ <Cpu className="size-3.5 text-muted" />
536
+ </div>
537
+ <span className="font-medium text-sm text-foreground">
538
+ {title}
539
+ </span>
575
540
  </div>
576
- <div className="mt-3 text-sm text-slate-500 dark:text-slate-400">
577
- Type your question below and press Enter or click {startButtonText}{" "}
578
- to begin
541
+
542
+ {/* Close button if needed */}
543
+ {onClose && !isModal && (
544
+ <Button
545
+ size="xs"
546
+ variant="ghost"
547
+ onClick={onClose}
548
+ title="Close"
549
+ className="text-muted hover:text-foreground"
550
+ >
551
+ <XIcon className="size-4" />
552
+ </Button>
553
+ )}
554
+ </div>
555
+
556
+ {/* Empty conversation area with instructions */}
557
+ <div className="flex-1 overflow-y-auto bg-background flex flex-col items-center justify-end">
558
+ <div className="w-full px-4 py-6">
559
+ {initialMessage && (
560
+ <div className="px-4 py-3 mb-4 bg-info-background border-l-2 border-info text-info">
561
+ {initialMessage}
562
+ </div>
563
+ )}
564
+
565
+ <div className="bg-card p-4 border-l-2 border-info">
566
+ <div className="text-base text-foreground font-medium">
567
+ Enter a message to start a conversation
568
+ </div>
569
+ <div className="mt-3 text-sm text-muted">
570
+ Type your question below and press Enter or click {startButtonText}{" "}
571
+ to begin
572
+ </div>
573
+ </div>
579
574
  </div>
580
575
  </div>
581
- </div>
582
576
 
583
- {/* Input Area */}
584
- <div className="py-3 px-3 border-t border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900">
577
+ {/* Input Area */}
578
+ <div className="py-3 px-3 border-t border-border bg-background">
585
579
  {/* Staged files display */}
586
580
  {stagedFiles.length > 0 && (
587
581
  <div className="flex flex-wrap gap-2 mb-3">
@@ -619,23 +613,21 @@ function StartWorkflowView({
619
613
  </div>
620
614
 
621
615
  <div className="flex items-end gap-2">
622
- <div className="flex-1">
623
- <textarea
624
- ref={inputRef}
625
- value={inputValue}
626
- onChange={(e) => setInputValue(e.target.value)}
627
- onKeyDown={handleKeyDown}
628
- placeholder={placeholder}
629
- disabled={isSending}
630
- rows={2}
631
- className="w-full py-2.5 px-3 text-sm border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-white focus:border-gray-300 dark:focus:border-gray-600 focus:outline-none focus:ring-1 focus:ring-gray-300 dark:focus:ring-gray-600 rounded-md resize-none overflow-hidden"
632
- style={{ minHeight: '60px', maxHeight: '200px' }}
633
- />
634
- </div>
616
+ <textarea
617
+ ref={inputRef}
618
+ value={inputValue}
619
+ onChange={(e) => setInputValue(e.target.value)}
620
+ onKeyDown={handleKeyDown}
621
+ placeholder={placeholder}
622
+ disabled={isSending}
623
+ rows={2}
624
+ className="flex-1 py-2.5 px-3 text-sm border border-border bg-background text-foreground focus:border-ring focus:outline-none focus:ring-1 focus:ring-ring rounded-md resize-none overflow-hidden"
625
+ style={{ minHeight: '60px', maxHeight: '200px' }}
626
+ />
635
627
  <Button
636
628
  onClick={startWorkflowWithMessage}
637
629
  disabled={!inputValue.trim() || isSending}
638
- className="px-3 py-2.5 bg-gray-800 dark:bg-gray-700 hover:bg-gray-700 dark:hover:bg-gray-600 text-white text-xs rounded-md transition-colors"
630
+ className="px-3 py-2.5 text-xs rounded-md transition-colors"
639
631
  >
640
632
  {isSending ? (
641
633
  <Spinner size="sm" className="mr-1.5" />
@@ -645,13 +637,14 @@ function StartWorkflowView({
645
637
  {startButtonText}
646
638
  </Button>
647
639
  </div>
648
- <div className="text-xs text-gray-400 dark:text-gray-500 mt-2 text-center">
640
+ <div className="text-xs text-muted mt-2 text-center">
649
641
  {stagedFiles.length > 0
650
642
  ? `${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''} staged - will upload when conversation starts`
651
643
  : 'Enter to send • Shift+Enter for new line'}
652
644
  </div>
653
645
  </div>
654
646
  </div>
647
+ </div>
655
648
  );
656
649
  }
657
650
 
@@ -363,7 +363,13 @@ export function groupMessagesWithStreaming(
363
363
  for (const message of messages) {
364
364
  const activityGroupId = getActivityGroupId(message);
365
365
 
366
- if (activityGroupId) {
366
+ // Certain message types have dedicated renderers (AskUserWidget for
367
+ // REQUEST_INPUT, BatchProgressPanel for BATCH_PROGRESS) and must stay
368
+ // standalone so MessageItem / AllMessagesMixed can route them correctly.
369
+ if (message.type === AgentMessageType.REQUEST_INPUT
370
+ || message.type === AgentMessageType.BATCH_PROGRESS) {
371
+ standaloneMessages.push(message);
372
+ } else if (activityGroupId) {
367
373
  if (!activityGroups.has(activityGroupId)) {
368
374
  activityGroups.set(activityGroupId, {
369
375
  messages: [],
@@ -1,7 +1,7 @@
1
1
  import { Filter as BaseFilter, FilterProvider, FilterBtn, FilterBar, FilterClear, FilterGroup } from '@vertesia/ui/core';
2
2
  import { useState } from 'react';
3
+ import { useTypeRegistry } from '../store/types/TypeRegistryProvider.js';
3
4
  import { SearchInterface } from './utils/SearchInterface';
4
- import { useUserSession } from '@vertesia/ui/session';
5
5
 
6
6
  interface CollectionsFacetsNavProps {
7
7
  facets: {
@@ -14,7 +14,7 @@ interface CollectionsFacetsNavProps {
14
14
  // Hook to create filter groups for collections
15
15
  export function useCollectionsFilterGroups(facets: CollectionsFacetsNavProps['facets']): FilterGroup[] {
16
16
  void facets;
17
- const { typeRegistry } = useUserSession();
17
+ const { registry: typeRegistry } = useTypeRegistry();
18
18
 
19
19
  const customFilterGroups: FilterGroup[] = [];
20
20
 
@@ -1,6 +1,6 @@
1
1
  import { Filter as BaseFilter, FilterProvider, FilterBtn, FilterBar, FilterClear, FilterGroup } from '@vertesia/ui/core';
2
- import { useUserSession } from '@vertesia/ui/session';
3
2
  import { useState } from 'react';
3
+ import { useTypeRegistry } from '../store/types/TypeRegistryProvider.js';
4
4
  import { VStringFacet } from './utils/VStringFacet';
5
5
  import { VTypeFacet } from './utils/VTypeFacet';
6
6
  import { SearchInterface } from './utils/SearchInterface';
@@ -18,7 +18,7 @@ interface DocumentsFacetsNavProps {
18
18
 
19
19
  // Hook to create filter groups for documents
20
20
  export function useDocumentFilterGroups(facets: DocumentsFacetsNavProps['facets']): FilterGroup[] {
21
- const { typeRegistry } = useUserSession();
21
+ const { registry: typeRegistry } = useTypeRegistry();
22
22
  const customFilterGroups: FilterGroup[] = [];
23
23
 
24
24
  customFilterGroups.push({
@@ -1,7 +1,7 @@
1
- import { useUserSession } from "@vertesia/ui/session";
2
1
  import { FacetBucket } from "@vertesia/common";
3
2
  import { SelectBox } from "@vertesia/ui/core";
4
3
  import { useEffect, useState } from "react";
4
+ import { useTypeRegistry } from "../../store/types/TypeRegistryProvider.js";
5
5
 
6
6
  interface TypeFacetBucket {
7
7
  name: string;
@@ -21,7 +21,7 @@ interface TypeFacetProps {
21
21
  }
22
22
  export function TypeFacet({ search, buckets, placeholder = "Filter by Type", className }: TypeFacetProps) {
23
23
  const [options, setOptions] = useState<TypeFacetBucket[]>([]);
24
- const { typeRegistry } = useUserSession();
24
+ const { registry: typeRegistry } = useTypeRegistry();
25
25
  const filterValue = search.getFilterValue("type") as string;
26
26
  const onChange = (option: FacetBucket | undefined) => {
27
27
  search.setFilterValue("type", option?._id);
@@ -1,6 +1,6 @@
1
1
  import { FacetBucket } from "@vertesia/common";
2
2
  import { FilterGroup } from "@vertesia/ui/core";
3
- import { TypeRegistry } from "@vertesia/ui/session";
3
+ import { TypeRegistry } from "../../store/types/TypeRegistry.js";
4
4
 
5
5
  interface VTypeFacetProps {
6
6
  buckets: FacetBucket[];
@@ -1,6 +1,8 @@
1
1
  import { Collection, getContentTypeRefId } from "@vertesia/common";
2
2
  import { useToast } from "@vertesia/ui/core";
3
- import { TypeRegistry, useUserSession } from "@vertesia/ui/session";
3
+ import { useUserSession } from "@vertesia/ui/session";
4
+ import { TypeRegistry } from "../types/TypeRegistry.js";
5
+ import { useTypeRegistry } from "../types/TypeRegistryProvider.js";
4
6
  import { DocumentSearchResults, DocumentSearchResultsWithDropZone } from "../objects/DocumentSearchResults";
5
7
 
6
8
 
@@ -9,7 +11,8 @@ interface BrowseCollectionViewProps {
9
11
  }
10
12
  export function BrowseCollectionView({ collection }: BrowseCollectionViewProps) {
11
13
  const toast = useToast();
12
- const { client, typeRegistry } = useUserSession();
14
+ const { client } = useUserSession();
15
+ const { registry: typeRegistry } = useTypeRegistry();
13
16
 
14
17
  const onUploadDone = async (objectIds: string[]) => {
15
18
  if (objectIds.length > 0) {
@@ -74,55 +74,57 @@ export function CreateCollectionForm({ onClose, redirect = true, onAddToCollecti
74
74
  };
75
75
 
76
76
  return (
77
- <form onSubmit={(e) => e.preventDefault()}>
77
+ <>
78
78
  <ModalBody>
79
- <FormItem label="Name" required>
80
- <Input type="text" value={payload.name || ""} onChange={(value) => setPayloadProp("name", value)} />
81
- </FormItem>
82
- <FormItem label="Description" className="mt-2">
83
- <Textarea
84
- value={payload.description || ""}
85
- onChange={(ev) => setPayloadProp("description", ev.target.value)}
86
- />
87
- </FormItem>
88
- <FormItem label="Dynamic Collection" className="mt-2" direction="row" description="Dynamically fetch content for the collection based on a query. If not enabled, then content must be added by users or agents.">
89
- <Switch value={payload.dynamic || false} onChange={(value) => setPayloadProp("dynamic", value)} />
90
- </FormItem>
91
- { !payload.dynamic &&
92
- <FormItem label="Allowed Content Types" className="mt-4" description="Optionally select which content types can be added to the collection. If not set, then all content types are allowed.">
79
+ <form onSubmit={(e) => e.preventDefault()}>
80
+ <FormItem label="Name" required>
81
+ <Input type="text" value={payload.name || ""} onChange={(value) => setPayloadProp("name", value)} />
82
+ </FormItem>
83
+ <FormItem label="Description" className="mt-2">
84
+ <Textarea
85
+ value={payload.description || ""}
86
+ onChange={(ev) => setPayloadProp("description", ev.target.value)}
87
+ />
88
+ </FormItem>
89
+ <FormItem label="Dynamic Collection" className="mt-2" direction="row" description="Dynamically fetch content for the collection based on a query. If not enabled, then content must be added by users or agents.">
90
+ <Switch value={payload.dynamic || false} onChange={(value) => setPayloadProp("dynamic", value)} />
91
+ </FormItem>
92
+ {!payload.dynamic &&
93
+ <FormItem label="Allowed Content Types" className="mt-4" description="Optionally select which content types can be added to the collection. If not set, then all content types are allowed.">
94
+ <SelectContentType
95
+ defaultValue={payload.allowed_types || null}
96
+ onChange={(v) => {
97
+ if (Array.isArray(v)) {
98
+ setPayloadProp("allowed_types", v.map(type => type.id));
99
+ } else {
100
+ setPayloadProp("allowed_types", v ? [v.id] : []);
101
+ }
102
+ }}
103
+ isClearable multiple
104
+ />
105
+ </FormItem>
106
+ }
107
+ <FormItem label="Type" className="mt-2" description="Optionally select a content type to assign custom properties and data to the collection.">
93
108
  <SelectContentType
94
- defaultValue={payload.allowed_types || null}
109
+ defaultValue={payload.type || null}
95
110
  onChange={(v) => {
96
111
  if (Array.isArray(v)) {
97
- setPayloadProp("allowed_types", v.map(type => type.id));
112
+ setPayloadProp("type", v.length > 0 ? v[0].id : null);
98
113
  } else {
99
- setPayloadProp("allowed_types", v ? [v.id] : []);
114
+ setPayloadProp("type", v?.id || null);
100
115
  }
101
116
  }}
102
- isClearable multiple
117
+ isClearable
103
118
  />
104
119
  </FormItem>
105
- }
106
- <FormItem label="Type" className="mt-2" description="Optionally select a content type to assign custom properties and data to the collection.">
107
- <SelectContentType
108
- defaultValue={payload.type || null}
109
- onChange={(v) => {
110
- if (Array.isArray(v)) {
111
- setPayloadProp("type", v.length > 0 ? v[0].id : null);
112
- } else {
113
- setPayloadProp("type", v?.id || null);
114
- }
115
- }}
116
- isClearable
117
- />
118
- </FormItem>
120
+ </form>
119
121
  </ModalBody>
120
122
  <ModalFooter>
121
123
  <Button isDisabled={isProcessing} onClick={onCreate}>
122
124
  Create Collection
123
125
  </Button>
124
126
  </ModalFooter>
125
- </form >
127
+ </>
126
128
  );
127
129
  }
128
130
 
@@ -9,7 +9,9 @@ import {
9
9
  SidePanel, Spinner, useIntersectionObserver, useToast
10
10
  } from '@vertesia/ui/core';
11
11
  import { useNavigate } from "@vertesia/ui/router";
12
- import { TypeRegistry, useUserSession } from '@vertesia/ui/session';
12
+ import { useUserSession } from '@vertesia/ui/session';
13
+ import { TypeRegistry } from '../types/TypeRegistry.js';
14
+ import { useTypeRegistry } from '../types/TypeRegistryProvider.js';
13
15
  import { Download, ExternalLink, RefreshCw } from 'lucide-react';
14
16
  import { useEffect, useRef, useState } from "react";
15
17
  import { useDocumentFilterGroups, useDocumentFilterHandler } from "../../facets/DocumentsFacetsNav";
@@ -95,7 +97,7 @@ export function DocumentSearchResults({ layout, onUpload, allowFilter = true, al
95
97
  const searchContext = useDocumentSearch();
96
98
  const [isReady, setIsReady] = useState(false);
97
99
  const [selectedObject, setSelectedObject] = useState<ContentObjectItem | null>(null);
98
- const { typeRegistry } = useUserSession();
100
+ const { registry: typeRegistry } = useTypeRegistry();
99
101
  const { search, isLoading, error, objects, hasMore } = useWatchDocumentSearchResult();
100
102
  const [actualLayout, setActualLayout] = useState<ColumnLayout[]>(
101
103
  typeRegistry ? layout || getTableLayout(typeRegistry, search.query.type) : defaultLayout,
@@ -1,6 +1,7 @@
1
1
  import { Collection, ContentObjectTypeItem, DynamicCollection } from "@vertesia/common";
2
2
  import { Button, MessageBox, Modal, ModalBody, ModalFooter, ModalTitle, SelectBox, Spinner, useToast, VTooltip } from "@vertesia/ui/core";
3
3
  import { useUserSession } from "@vertesia/ui/session";
4
+ import { useTypeRegistry } from "../../types/TypeRegistryProvider.js";
4
5
  import { DropZone, UploadSummary } from '@vertesia/ui/widgets';
5
6
  import { AlertCircleIcon, CheckCircleIcon, FileIcon, FolderIcon, Info, UploadIcon, XCircleIcon } from "lucide-react";
6
7
  import { ReactNode, useEffect, useMemo, useState } from "react";
@@ -69,7 +70,8 @@ export function DocumentUploadModal({
69
70
  showTypeSelectionOnly = false,
70
71
  allowFolders = true,
71
72
  }: DocumentUploadModalProps) {
72
- const { client, typeRegistry } = useUserSession();
73
+ const { client } = useUserSession();
74
+ const { registry: typeRegistry } = useTypeRegistry();
73
75
  const toast = useToast();
74
76
  const [files, setFiles] = useState<File[]>([]);
75
77
  const [processedFiles, setProcessedFiles] = useState<FileWithMetadata[]>([]);
@@ -5,6 +5,7 @@ import { useWatchSearchResult } from "./search/ObjectTypeSearchContext";
5
5
 
6
6
  import { EmptyCollection, ErrorBox, Input, SelectBox, useDebounce, useIntersectionObserver, useToast } from "@vertesia/ui/core";
7
7
  import { useUserSession } from "@vertesia/ui/session";
8
+ import { useTypeRegistry } from "./TypeRegistryProvider.js";
8
9
 
9
10
  import { CreateOrUpdateTypeModal, CreateOrUpdateTypePayload } from "./CreateOrUpdateTypeModal";
10
11
 
@@ -14,8 +15,8 @@ interface ContentObjectTypesSearchProps {
14
15
  isDirty?: boolean;
15
16
  }
16
17
  export function ContentObjectTypesSearch({ isDirty = false }: ContentObjectTypesSearchProps) {
17
- const session = useUserSession();
18
- const { store } = session;
18
+ const { store } = useUserSession();
19
+ const { reload: reloadTypes } = useTypeRegistry();
19
20
 
20
21
  const toast = useToast();
21
22
 
@@ -80,7 +81,7 @@ export function ContentObjectTypesSearch({ isDirty = false }: ContentObjectTypes
80
81
  title: "Type created",
81
82
  duration: 2000
82
83
  });
83
- session.reloadTypes();
84
+ reloadTypes();
84
85
  search.search().then(() => setIsReady(true));
85
86
  }).catch(err => {
86
87
  toast({
@@ -1,7 +1,7 @@
1
1
  import { ContentObjectTypeItem } from "@vertesia/common";
2
2
  import { SelectBox } from "@vertesia/ui/core";
3
- import { useUserSession } from "@vertesia/ui/session";
4
3
  import { useEffect, useState } from "react";
4
+ import { useTypeRegistry } from "./TypeRegistryProvider.js";
5
5
 
6
6
  const optionLabel = (t: ContentObjectTypeItem | null) => {
7
7
  if (t === null) return 'None';
@@ -9,7 +9,7 @@ const optionLabel = (t: ContentObjectTypeItem | null) => {
9
9
  return (
10
10
  <div>
11
11
  <div className="text-sm">{t.name}</div>
12
- <div className="text-xs text-gray-500">{t.description}</div>
12
+ <div className="text-xs text-muted truncate">{t.description}</div>
13
13
  </div>
14
14
  );
15
15
  };
@@ -22,7 +22,7 @@ interface SelectContentTypeProps {
22
22
  multiple?: boolean;
23
23
  }
24
24
  export function SelectContentType({ className, defaultValue, onChange, isClearable, multiple}: SelectContentTypeProps) {
25
- const session = useUserSession();
25
+ const { registry: typeRegistry } = useTypeRegistry();
26
26
  const [isMounted, setIsMounted] = useState(false);
27
27
  const [selectedType, setSelectedType] = useState<ContentObjectTypeItem | undefined>();
28
28
  const [selectedTypes, setSelectedTypes] = useState<ContentObjectTypeItem[]>([])
@@ -30,18 +30,18 @@ export function SelectContentType({ className, defaultValue, onChange, isClearab
30
30
  useEffect(() => {
31
31
  if (!isMounted) {
32
32
  setIsMounted(true);
33
- if (session.typeRegistry && defaultValue) {
33
+ if (typeRegistry && defaultValue) {
34
34
  if (multiple && Array.isArray(defaultValue)) {
35
- const types = session.typeRegistry.types.filter(t => defaultValue.includes(t.id));
35
+ const types = typeRegistry.types.filter(t => defaultValue.includes(t.id));
36
36
  setSelectedTypes(types);
37
37
  }
38
- const type = session.typeRegistry.types.find(t => t.id === defaultValue);
38
+ const type = typeRegistry.types.find(t => t.id === defaultValue);
39
39
  if (type) {
40
40
  setSelectedType(type);
41
41
  }
42
42
  }
43
43
  }
44
- }, [session.typeRegistry, defaultValue, multiple])
44
+ }, [typeRegistry, defaultValue, multiple])
45
45
 
46
46
  const _onChange = (option: ContentObjectTypeItem | null) => {
47
47
  setSelectedType(option || undefined);
@@ -57,7 +57,7 @@ export function SelectContentType({ className, defaultValue, onChange, isClearab
57
57
  return (
58
58
  <div className='flex flex-col gap-4 content-between'>
59
59
  <SelectBox<ContentObjectTypeItem>
60
- options={session.typeRegistry?.types || []}
60
+ options={typeRegistry?.types || []}
61
61
  value={selectedTypes}
62
62
  onChange={_onChangeMultiple}
63
63
  placeholder="Choose Content Types..."
@@ -74,7 +74,7 @@ export function SelectContentType({ className, defaultValue, onChange, isClearab
74
74
  return (
75
75
  <div className='flex flex-col gap-4 content-between'>
76
76
  <SelectBox<ContentObjectTypeItem>
77
- options={session.typeRegistry?.types || []}
77
+ options={typeRegistry?.types || []}
78
78
  value={selectedType}
79
79
  onChange={_onChange}
80
80
  placeholder="Choose a Content Type..."
@@ -9,7 +9,7 @@ import {
9
9
  VTooltip,
10
10
  } from "@vertesia/ui/core";
11
11
  import { ContentObjectTypeItem } from "@vertesia/common";
12
- import { useUserSession } from "@vertesia/ui/session";
12
+ import { useTypeRegistry } from "./TypeRegistryProvider.js";
13
13
  import { CheckCircleIcon, Info } from "lucide-react";
14
14
 
15
15
  /**
@@ -49,7 +49,7 @@ export function SelectContentTypeModal({
49
49
  initialSelectedType = null,
50
50
  allowNone = true,
51
51
  }: SelectContentTypeModalProps) {
52
- const { typeRegistry } = useUserSession();
52
+ const { registry: typeRegistry } = useTypeRegistry();
53
53
  const [selectedType, setSelectedType] = useState<ContentObjectTypeItem | null>(initialSelectedType);
54
54
 
55
55
  // Get available types from the registry
@@ -0,0 +1,73 @@
1
+ import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
2
+
3
+ import { useUserSession } from '@vertesia/ui/session';
4
+ import { TypeRegistry } from './TypeRegistry.js';
5
+
6
+ interface TypeRegistryContextValue {
7
+ registry: TypeRegistry | undefined;
8
+ isLoading: boolean;
9
+ load: () => Promise<void>;
10
+ reload: () => Promise<void>;
11
+ }
12
+
13
+ const TypeRegistryContext = createContext<TypeRegistryContextValue>({
14
+ registry: undefined,
15
+ isLoading: false,
16
+ load: () => Promise.resolve(),
17
+ reload: () => Promise.resolve(),
18
+ });
19
+
20
+ interface TypeRegistryProviderProps {
21
+ children: ReactNode;
22
+ }
23
+
24
+ export function TypeRegistryProvider({ children }: TypeRegistryProviderProps) {
25
+ const { store, project } = useUserSession();
26
+ const [registry, setRegistry] = useState<TypeRegistry | undefined>();
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const fetchRef = useRef(false);
29
+
30
+ const load = useCallback(async () => {
31
+ if (!project || fetchRef.current) return;
32
+ fetchRef.current = true;
33
+ setIsLoading(true);
34
+ try {
35
+ const types = await store.types.catalog.list({ layout: true });
36
+ setRegistry(new TypeRegistry(types));
37
+ } catch (err) {
38
+ console.error('Failed to fetch types', err);
39
+ } finally {
40
+ setIsLoading(false);
41
+ fetchRef.current = false;
42
+ }
43
+ }, [store, project]);
44
+
45
+ const reload = useCallback(async () => {
46
+ fetchRef.current = false;
47
+ return load();
48
+ }, [load]);
49
+
50
+ // Reset when project changes
51
+ useEffect(() => {
52
+ setRegistry(undefined);
53
+ fetchRef.current = false;
54
+ }, [project]);
55
+
56
+ return (
57
+ <TypeRegistryContext.Provider value={{ registry, isLoading, load, reload }}>
58
+ {children}
59
+ </TypeRegistryContext.Provider>
60
+ );
61
+ }
62
+
63
+ export function useTypeRegistry() {
64
+ const ctx = useContext(TypeRegistryContext);
65
+
66
+ useEffect(() => {
67
+ if (!ctx.registry && !ctx.isLoading) {
68
+ ctx.load();
69
+ }
70
+ }, [ctx.registry, ctx.isLoading]);
71
+
72
+ return ctx;
73
+ }
@@ -6,3 +6,5 @@ export * from "./search";
6
6
  export * from "./SelectContentType";
7
7
  export * from "./SelectContentTypeModal";
8
8
  export * from "./TableLayoutEditor";
9
+ export * from "./TypeRegistry";
10
+ export * from "./TypeRegistryProvider";