@agent-native/creative-context 0.3.0 → 0.5.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/README.md +51 -9
- package/agent-native.package.json +4 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-context-memberships.d.ts +11 -0
- package/dist/actions/list-context-memberships.d.ts.map +1 -0
- package/dist/actions/list-context-memberships.js +47 -0
- package/dist/actions/list-context-memberships.js.map +1 -0
- package/dist/actions/list-creative-contexts.d.ts +12 -0
- package/dist/actions/list-creative-contexts.d.ts.map +1 -0
- package/dist/actions/list-creative-contexts.js +28 -0
- package/dist/actions/list-creative-contexts.js.map +1 -0
- package/dist/actions/manage-context-membership.d.ts +64 -0
- package/dist/actions/manage-context-membership.d.ts.map +1 -0
- package/dist/actions/manage-context-membership.js +70 -0
- package/dist/actions/manage-context-membership.js.map +1 -0
- package/dist/actions/manage-context-pack.d.ts +3 -0
- package/dist/actions/manage-context-pack.js +6 -0
- package/dist/actions/manage-context-pack.js.map +1 -1
- package/dist/actions/manage-context-source.js +4 -11
- package/dist/actions/manage-context-source.js.map +1 -1
- package/dist/actions/manage-creative-context.d.ts +28 -0
- package/dist/actions/manage-creative-context.d.ts.map +1 -0
- package/dist/actions/manage-creative-context.js +65 -0
- package/dist/actions/manage-creative-context.js.map +1 -0
- package/dist/actions/process-context-purge.d.ts +16 -1
- package/dist/actions/process-context-purge.js +2 -1
- package/dist/actions/process-context-purge.js.map +1 -1
- package/dist/actions/search-creative-context.d.ts +1 -0
- package/dist/actions/search-creative-context.js +2 -1
- package/dist/actions/search-creative-context.js.map +1 -1
- package/dist/client/CreativeContextChip.d.ts +5 -1
- package/dist/client/CreativeContextChip.d.ts.map +1 -1
- package/dist/client/CreativeContextChip.js +36 -7
- package/dist/client/CreativeContextChip.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +406 -89
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/CreativeContextShareTab.d.ts +56 -0
- package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
- package/dist/client/CreativeContextShareTab.js +213 -0
- package/dist/client/CreativeContextShareTab.js.map +1 -0
- package/dist/client/actions.d.ts +210 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +318 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/application-state.d.ts +2 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +25 -2
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +77 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/types.d.ts +2 -2
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1592 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +95 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +80 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/server/generation-artifact-access.js +2 -0
- package/dist/server/generation-artifact-access.js.map +1 -1
- package/dist/server/generation-context.d.ts +12 -4
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +129 -7
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +16 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/isolated-a2a.d.ts +3 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -1
- package/dist/server/isolated-a2a.js +3 -1
- package/dist/server/isolated-a2a.js.map +1 -1
- package/dist/server/media.d.ts +1 -1
- package/dist/server/media.d.ts.map +1 -1
- package/dist/server/media.js +24 -9
- package/dist/server/media.js.map +1 -1
- package/dist/server/native-resource-capture.d.ts +78 -0
- package/dist/server/native-resource-capture.d.ts.map +1 -0
- package/dist/server/native-resource-capture.js +94 -0
- package/dist/server/native-resource-capture.js.map +1 -0
- package/dist/server/public-serialization.js +1 -1
- package/dist/server/public-serialization.js.map +1 -1
- package/dist/server/retrieval.d.ts +1 -0
- package/dist/server/retrieval.d.ts.map +1 -1
- package/dist/server/retrieval.js +2 -0
- package/dist/server/retrieval.js.map +1 -1
- package/dist/server/safe-native-preview.d.ts +15 -0
- package/dist/server/safe-native-preview.d.ts.map +1 -0
- package/dist/server/safe-native-preview.js +149 -0
- package/dist/server/safe-native-preview.js.map +1 -0
- package/dist/store/brand.d.ts.map +1 -1
- package/dist/store/brand.js +2 -16
- package/dist/store/brand.js.map +1 -1
- package/dist/store/content.d.ts +1 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +84 -20
- package/dist/store/content.js.map +1 -1
- package/dist/store/contexts.d.ts +216 -0
- package/dist/store/contexts.d.ts.map +1 -0
- package/dist/store/contexts.js +1341 -0
- package/dist/store/contexts.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/packs.d.ts +3 -0
- package/dist/store/packs.d.ts.map +1 -1
- package/dist/store/packs.js +126 -9
- package/dist/store/packs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +103 -6
- package/dist/store/purge.js.map +1 -1
- package/dist/store/sources.d.ts.map +1 -1
- package/dist/store/sources.js +2 -15
- package/dist/store/sources.js.map +1 -1
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +3 -1
- package/dist/store/suggestions.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +13 -5
- package/package.json +3 -3
- package/src/actions/index.ts +8 -0
- package/src/actions/job-public-serialization.spec.ts +21 -0
- package/src/actions/list-context-memberships.ts +56 -0
- package/src/actions/list-creative-contexts.spec.ts +38 -0
- package/src/actions/list-creative-contexts.ts +32 -0
- package/src/actions/manage-context-membership.ts +80 -0
- package/src/actions/manage-context-pack.ts +6 -0
- package/src/actions/manage-context-source.ts +8 -11
- package/src/actions/manage-creative-context.ts +73 -0
- package/src/actions/process-context-purge.ts +4 -1
- package/src/actions/search-creative-context.ts +2 -1
- package/src/client/CreativeContextChip.spec.ts +32 -0
- package/src/client/CreativeContextChip.tsx +83 -14
- package/src/client/CreativeContextPanel.tsx +1947 -885
- package/src/client/CreativeContextShareTab.spec.ts +70 -0
- package/src/client/CreativeContextShareTab.tsx +657 -0
- package/src/client/actions.spec.ts +177 -0
- package/src/client/actions.ts +566 -0
- package/src/client/application-state.spec.ts +5 -0
- package/src/client/application-state.ts +30 -2
- package/src/client/index.ts +33 -0
- package/src/client/messages.ts +95 -0
- package/src/connectors/types.ts +6 -8
- package/src/eval/library-flow.acceptance.spec.tsx +1 -0
- package/src/schema/index.ts +116 -0
- package/src/schema/migrations.ts +80 -0
- package/src/server/generation-artifact-access.ts +1 -0
- package/src/server/generation-context-a2a.spec.ts +133 -0
- package/src/server/generation-context.ts +153 -7
- package/src/server/index.spec.ts +15 -0
- package/src/server/index.ts +16 -0
- package/src/server/isolated-a2a.ts +4 -1
- package/src/server/media.spec.ts +30 -2
- package/src/server/media.ts +31 -12
- package/src/server/native-resource-capture.spec.ts +121 -0
- package/src/server/native-resource-capture.ts +198 -0
- package/src/server/public-serialization.spec.ts +1 -0
- package/src/server/public-serialization.ts +1 -1
- package/src/server/retrieval.ts +3 -0
- package/src/server/safe-native-preview.spec.ts +29 -0
- package/src/server/safe-native-preview.ts +200 -0
- package/src/store/access.integration.spec.ts +1116 -0
- package/src/store/brand.ts +2 -21
- package/src/store/content.ts +217 -17
- package/src/store/contexts.ts +1826 -0
- package/src/store/index.ts +1 -0
- package/src/store/packs.ts +279 -24
- package/src/store/purge.ts +139 -6
- package/src/store/sources.ts +2 -18
- package/src/store/suggestions.ts +3 -1
- package/src/types.ts +95 -0
|
@@ -28,13 +28,23 @@ import {
|
|
|
28
28
|
SelectItem,
|
|
29
29
|
SelectTrigger,
|
|
30
30
|
SelectValue,
|
|
31
|
+
Sheet,
|
|
32
|
+
SheetContent,
|
|
33
|
+
SheetDescription,
|
|
34
|
+
SheetHeader,
|
|
35
|
+
SheetTitle,
|
|
31
36
|
Skeleton,
|
|
37
|
+
Tabs,
|
|
38
|
+
TabsContent,
|
|
39
|
+
TabsList,
|
|
40
|
+
TabsTrigger,
|
|
32
41
|
Textarea,
|
|
33
42
|
} from "@agent-native/toolkit/ui";
|
|
34
43
|
import {
|
|
35
44
|
IconAlertTriangle,
|
|
36
45
|
IconArrowUpRight,
|
|
37
46
|
IconBooks,
|
|
47
|
+
IconChartBar,
|
|
38
48
|
IconCheck,
|
|
39
49
|
IconDots,
|
|
40
50
|
IconFileImport,
|
|
@@ -42,7 +52,9 @@ import {
|
|
|
42
52
|
IconLayout,
|
|
43
53
|
IconPalette,
|
|
44
54
|
IconPhoto,
|
|
55
|
+
IconPlayerPlay,
|
|
45
56
|
IconPin,
|
|
57
|
+
IconPlus,
|
|
46
58
|
IconRefresh,
|
|
47
59
|
IconSearch,
|
|
48
60
|
IconSlideshow,
|
|
@@ -70,6 +82,7 @@ import type {
|
|
|
70
82
|
} from "../types.js";
|
|
71
83
|
import {
|
|
72
84
|
useCreativeContextBrandProfile,
|
|
85
|
+
useCreativeContexts,
|
|
73
86
|
useCanonicalLogoCandidates,
|
|
74
87
|
useCreativeContextConnections,
|
|
75
88
|
useCreativeContextGooglePickerSession,
|
|
@@ -77,6 +90,9 @@ import {
|
|
|
77
90
|
useCreativeContextImportStatus,
|
|
78
91
|
useCreativeContextPack,
|
|
79
92
|
useCreativeContextPacks,
|
|
93
|
+
useContextMemberships,
|
|
94
|
+
useManageCreativeContext,
|
|
95
|
+
useManageContextMembership,
|
|
80
96
|
useCreativeContextSearch,
|
|
81
97
|
useCreativeContextSuggestions,
|
|
82
98
|
useCreativeContextSources,
|
|
@@ -89,6 +105,9 @@ import {
|
|
|
89
105
|
useRefreshCreativeContextSource,
|
|
90
106
|
useReviewCreativeContextItems,
|
|
91
107
|
useStartCreativeContextImport,
|
|
108
|
+
parseCreativeContexts,
|
|
109
|
+
parseContextMemberships,
|
|
110
|
+
type CreativeContextSafePreview,
|
|
92
111
|
type CreativeContextConnectionProvider,
|
|
93
112
|
type CreativeContextRootRecommendation,
|
|
94
113
|
type CreativeContextRecommendationProvider,
|
|
@@ -789,6 +808,394 @@ function ItemCuration({
|
|
|
789
808
|
);
|
|
790
809
|
}
|
|
791
810
|
|
|
811
|
+
type LibraryView = "items" | "sources" | "approvals" | "settings";
|
|
812
|
+
|
|
813
|
+
interface SafePreviewManifest {
|
|
814
|
+
title: string;
|
|
815
|
+
kind: string;
|
|
816
|
+
itemId: string;
|
|
817
|
+
itemVersionId: string;
|
|
818
|
+
preview: CreativeContextSafePreview | null;
|
|
819
|
+
media?: {
|
|
820
|
+
kind: string;
|
|
821
|
+
mimeType: string | null;
|
|
822
|
+
url: string;
|
|
823
|
+
} | null;
|
|
824
|
+
gallery?: Array<{
|
|
825
|
+
kind: string;
|
|
826
|
+
mimeType: string | null;
|
|
827
|
+
url: string;
|
|
828
|
+
}>;
|
|
829
|
+
posterUrl?: string;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function StructuredPreview({
|
|
833
|
+
preview,
|
|
834
|
+
compact = false,
|
|
835
|
+
}: {
|
|
836
|
+
preview: CreativeContextSafePreview | null;
|
|
837
|
+
compact?: boolean;
|
|
838
|
+
}) {
|
|
839
|
+
if (!preview) {
|
|
840
|
+
return (
|
|
841
|
+
<div className="flex h-full min-h-28 items-center justify-center bg-muted text-muted-foreground">
|
|
842
|
+
<IconFileText className="size-5" />
|
|
843
|
+
</div>
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (preview.type === "slides") {
|
|
847
|
+
const visibleSlides = compact ? preview.slides.slice(0, 3) : preview.slides;
|
|
848
|
+
return (
|
|
849
|
+
<div className="grid h-full grid-cols-3 gap-1.5 bg-muted/50 p-2">
|
|
850
|
+
{visibleSlides.map((slide) => (
|
|
851
|
+
<div
|
|
852
|
+
key={slide.index}
|
|
853
|
+
className="min-w-0 rounded border border-border/70 bg-background p-1.5"
|
|
854
|
+
>
|
|
855
|
+
<span className="text-[10px] font-medium text-muted-foreground">
|
|
856
|
+
{slide.index}
|
|
857
|
+
</span>
|
|
858
|
+
<p className="mt-1 line-clamp-2 text-[11px] font-medium leading-tight">
|
|
859
|
+
{slide.title}
|
|
860
|
+
</p>
|
|
861
|
+
{!compact && slide.excerpt ? (
|
|
862
|
+
<p className="mt-1 line-clamp-5 text-[10px] leading-snug text-muted-foreground">
|
|
863
|
+
{slide.excerpt}
|
|
864
|
+
</p>
|
|
865
|
+
) : null}
|
|
866
|
+
</div>
|
|
867
|
+
))}
|
|
868
|
+
{!visibleSlides.length ? (
|
|
869
|
+
<div className="col-span-3 flex items-center justify-center text-xs text-muted-foreground">
|
|
870
|
+
{preview.slideCount} slides
|
|
871
|
+
</div>
|
|
872
|
+
) : null}
|
|
873
|
+
</div>
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
if (preview.type === "slide") {
|
|
877
|
+
return (
|
|
878
|
+
<div className="flex h-full flex-col justify-between bg-muted/50 p-4">
|
|
879
|
+
<span className="text-xs text-muted-foreground">
|
|
880
|
+
Slide {preview.index}
|
|
881
|
+
</span>
|
|
882
|
+
<p className="line-clamp-3 text-sm font-semibold">{preview.title}</p>
|
|
883
|
+
{preview.excerpt ? (
|
|
884
|
+
<p className="line-clamp-5 text-xs leading-relaxed text-muted-foreground">
|
|
885
|
+
{preview.excerpt}
|
|
886
|
+
</p>
|
|
887
|
+
) : null}
|
|
888
|
+
</div>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
if (preview.type === "design" || preview.type === "design-frame") {
|
|
892
|
+
const frames =
|
|
893
|
+
preview.type === "design"
|
|
894
|
+
? preview.frames
|
|
895
|
+
: [
|
|
896
|
+
{
|
|
897
|
+
title: preview.title,
|
|
898
|
+
fileType: preview.fileType,
|
|
899
|
+
excerpt: preview.excerpt,
|
|
900
|
+
},
|
|
901
|
+
];
|
|
902
|
+
const visibleFrames = compact ? frames.slice(0, 4) : frames;
|
|
903
|
+
return (
|
|
904
|
+
<div className="grid h-full grid-cols-2 gap-1.5 bg-muted/50 p-2">
|
|
905
|
+
{visibleFrames.map((frame, index) => (
|
|
906
|
+
<div
|
|
907
|
+
key={`${frame.title}-${index}`}
|
|
908
|
+
className="min-w-0 rounded border border-border/70 bg-background p-2"
|
|
909
|
+
>
|
|
910
|
+
<div className="flex items-center gap-1 text-muted-foreground">
|
|
911
|
+
<IconLayout className="size-3 shrink-0" />
|
|
912
|
+
<span className="truncate text-[10px] uppercase tracking-wide">
|
|
913
|
+
{frame.fileType}
|
|
914
|
+
</span>
|
|
915
|
+
</div>
|
|
916
|
+
<p className="mt-2 line-clamp-2 text-xs font-medium leading-tight">
|
|
917
|
+
{frame.title}
|
|
918
|
+
</p>
|
|
919
|
+
{!compact && frame.excerpt ? (
|
|
920
|
+
<p className="mt-1 line-clamp-4 text-[10px] leading-snug text-muted-foreground">
|
|
921
|
+
{frame.excerpt}
|
|
922
|
+
</p>
|
|
923
|
+
) : null}
|
|
924
|
+
</div>
|
|
925
|
+
))}
|
|
926
|
+
</div>
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (preview.type === "document") {
|
|
930
|
+
const visibleBlocks = preview.blocks.slice(0, compact ? 7 : 40);
|
|
931
|
+
return (
|
|
932
|
+
<article className="h-full overflow-auto bg-background p-4">
|
|
933
|
+
{visibleBlocks.length ? (
|
|
934
|
+
<div className="space-y-2">
|
|
935
|
+
{visibleBlocks.map((block, index) => {
|
|
936
|
+
if (block.kind === "heading") {
|
|
937
|
+
return (
|
|
938
|
+
<p
|
|
939
|
+
key={`${block.kind}-${index}`}
|
|
940
|
+
className={
|
|
941
|
+
(block.level ?? 2) <= 2
|
|
942
|
+
? "text-sm font-semibold"
|
|
943
|
+
: "text-xs font-medium"
|
|
944
|
+
}
|
|
945
|
+
>
|
|
946
|
+
{block.text}
|
|
947
|
+
</p>
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
if (block.kind === "bullet") {
|
|
951
|
+
return (
|
|
952
|
+
<p
|
|
953
|
+
key={`${block.kind}-${index}`}
|
|
954
|
+
className="flex gap-2 text-xs leading-relaxed text-muted-foreground before:content-['•']"
|
|
955
|
+
>
|
|
956
|
+
{block.text}
|
|
957
|
+
</p>
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (block.kind === "quote") {
|
|
961
|
+
return (
|
|
962
|
+
<blockquote
|
|
963
|
+
key={`${block.kind}-${index}`}
|
|
964
|
+
className="border-s-2 border-border ps-3 text-xs italic leading-relaxed text-muted-foreground"
|
|
965
|
+
>
|
|
966
|
+
{block.text}
|
|
967
|
+
</blockquote>
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (block.kind === "code") {
|
|
971
|
+
return (
|
|
972
|
+
<pre
|
|
973
|
+
key={`${block.kind}-${index}`}
|
|
974
|
+
className="overflow-hidden rounded bg-muted p-2 font-mono text-[10px] leading-relaxed"
|
|
975
|
+
>
|
|
976
|
+
{block.text}
|
|
977
|
+
</pre>
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return (
|
|
981
|
+
<p
|
|
982
|
+
key={`${block.kind}-${index}`}
|
|
983
|
+
className="text-xs leading-relaxed text-muted-foreground"
|
|
984
|
+
>
|
|
985
|
+
{block.text}
|
|
986
|
+
</p>
|
|
987
|
+
);
|
|
988
|
+
})}
|
|
989
|
+
</div>
|
|
990
|
+
) : preview.excerpt ? (
|
|
991
|
+
<p className="mt-3 line-clamp-6 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
|
|
992
|
+
{preview.excerpt}
|
|
993
|
+
</p>
|
|
994
|
+
) : null}
|
|
995
|
+
</article>
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
if (preview.type === "asset") {
|
|
999
|
+
return (
|
|
1000
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 bg-muted text-muted-foreground">
|
|
1001
|
+
{preview.mediaType === "video" ? (
|
|
1002
|
+
<IconPlayerPlay className="size-6" />
|
|
1003
|
+
) : (
|
|
1004
|
+
<IconPhoto className="size-6" />
|
|
1005
|
+
)}
|
|
1006
|
+
{!compact && preview.width && preview.height ? (
|
|
1007
|
+
<span className="text-xs">
|
|
1008
|
+
{preview.width} × {preview.height}
|
|
1009
|
+
</span>
|
|
1010
|
+
) : null}
|
|
1011
|
+
</div>
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return (
|
|
1015
|
+
<div className="grid h-full grid-cols-2 gap-2 bg-muted/50 p-3">
|
|
1016
|
+
{preview.panels.slice(0, compact ? 4 : 24).map((panel) => (
|
|
1017
|
+
<div
|
|
1018
|
+
key={panel.id}
|
|
1019
|
+
className="rounded border border-border/70 bg-background p-2"
|
|
1020
|
+
>
|
|
1021
|
+
<div className="flex items-center gap-1 text-muted-foreground">
|
|
1022
|
+
<IconChartBar className="size-3" />
|
|
1023
|
+
<span className="truncate text-[10px] capitalize">
|
|
1024
|
+
{panel.visualization}
|
|
1025
|
+
</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
<p className="mt-2 line-clamp-2 text-xs font-medium">{panel.title}</p>
|
|
1028
|
+
</div>
|
|
1029
|
+
))}
|
|
1030
|
+
{!preview.panels.length ? (
|
|
1031
|
+
<div className="col-span-2 flex items-center justify-center text-xs text-muted-foreground">
|
|
1032
|
+
Synthetic dashboard preview
|
|
1033
|
+
</div>
|
|
1034
|
+
) : null}
|
|
1035
|
+
</div>
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function ContextPreviewVisual({
|
|
1040
|
+
manifest,
|
|
1041
|
+
compact = false,
|
|
1042
|
+
}: {
|
|
1043
|
+
manifest: SafePreviewManifest;
|
|
1044
|
+
compact?: boolean;
|
|
1045
|
+
}) {
|
|
1046
|
+
if (manifest.media?.mimeType?.startsWith("video/")) {
|
|
1047
|
+
return (
|
|
1048
|
+
<video
|
|
1049
|
+
controls={!compact}
|
|
1050
|
+
muted={compact}
|
|
1051
|
+
playsInline
|
|
1052
|
+
preload="metadata"
|
|
1053
|
+
src={manifest.media.url}
|
|
1054
|
+
poster={manifest.posterUrl}
|
|
1055
|
+
className="h-full w-full bg-black object-contain"
|
|
1056
|
+
/>
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (manifest.media) {
|
|
1060
|
+
return (
|
|
1061
|
+
<img
|
|
1062
|
+
src={manifest.media.url}
|
|
1063
|
+
alt=""
|
|
1064
|
+
className="h-full w-full object-contain"
|
|
1065
|
+
/>
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
return <StructuredPreview preview={manifest.preview} compact={compact} />;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function ContextPreviewSheet({
|
|
1072
|
+
manifest,
|
|
1073
|
+
onOpenChange,
|
|
1074
|
+
}: {
|
|
1075
|
+
manifest: SafePreviewManifest | null;
|
|
1076
|
+
onOpenChange: (open: boolean) => void;
|
|
1077
|
+
}) {
|
|
1078
|
+
const [selectedMediaUrl, setSelectedMediaUrl] = useState<string | null>(null);
|
|
1079
|
+
useEffect(() => setSelectedMediaUrl(null), [manifest?.itemVersionId]);
|
|
1080
|
+
const selectedMedia =
|
|
1081
|
+
manifest?.gallery?.find((medium) => medium.url === selectedMediaUrl) ??
|
|
1082
|
+
manifest?.media ??
|
|
1083
|
+
null;
|
|
1084
|
+
return (
|
|
1085
|
+
<Sheet open={Boolean(manifest)} onOpenChange={onOpenChange}>
|
|
1086
|
+
<SheetContent side="right" className="w-full overflow-y-auto sm:max-w-xl">
|
|
1087
|
+
<SheetHeader>
|
|
1088
|
+
<SheetTitle>{manifest?.title ?? "Context preview"}</SheetTitle>
|
|
1089
|
+
<SheetDescription>{manifest?.kind ?? ""}</SheetDescription>
|
|
1090
|
+
</SheetHeader>
|
|
1091
|
+
{manifest && (selectedMedia || manifest.preview) ? (
|
|
1092
|
+
<div className="mt-5 min-h-56 overflow-hidden rounded-md border border-border">
|
|
1093
|
+
<ContextPreviewVisual
|
|
1094
|
+
manifest={{ ...manifest, media: selectedMedia }}
|
|
1095
|
+
/>
|
|
1096
|
+
</div>
|
|
1097
|
+
) : (
|
|
1098
|
+
<div className="mt-5 flex min-h-44 items-center justify-center rounded-md border border-dashed border-border text-sm text-muted-foreground">
|
|
1099
|
+
No safe preview is available for this item.
|
|
1100
|
+
</div>
|
|
1101
|
+
)}
|
|
1102
|
+
{manifest?.gallery && manifest.gallery.length > 1 ? (
|
|
1103
|
+
<div className="mt-3 grid grid-cols-3 gap-2 sm:grid-cols-4">
|
|
1104
|
+
{manifest.gallery.map((medium, index) => (
|
|
1105
|
+
<button
|
|
1106
|
+
key={`${medium.url}-${index}`}
|
|
1107
|
+
type="button"
|
|
1108
|
+
className={`aspect-video overflow-hidden rounded border bg-muted transition-colors ${
|
|
1109
|
+
medium.url === (selectedMedia?.url ?? manifest.media?.url)
|
|
1110
|
+
? "border-foreground"
|
|
1111
|
+
: "border-border hover:border-foreground/50"
|
|
1112
|
+
}`}
|
|
1113
|
+
onClick={() => setSelectedMediaUrl(medium.url)}
|
|
1114
|
+
>
|
|
1115
|
+
<img
|
|
1116
|
+
src={medium.url}
|
|
1117
|
+
alt={`Preview ${index + 1}`}
|
|
1118
|
+
loading="lazy"
|
|
1119
|
+
className="h-full w-full object-contain"
|
|
1120
|
+
/>
|
|
1121
|
+
</button>
|
|
1122
|
+
))}
|
|
1123
|
+
</div>
|
|
1124
|
+
) : null}
|
|
1125
|
+
</SheetContent>
|
|
1126
|
+
</Sheet>
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function ContextRail({
|
|
1131
|
+
contexts,
|
|
1132
|
+
selectedContextId,
|
|
1133
|
+
disabled,
|
|
1134
|
+
canCreate,
|
|
1135
|
+
onSelect,
|
|
1136
|
+
onCreate,
|
|
1137
|
+
}: {
|
|
1138
|
+
contexts: Array<{
|
|
1139
|
+
id: string;
|
|
1140
|
+
name: string;
|
|
1141
|
+
description?: string | null;
|
|
1142
|
+
memberCount: number;
|
|
1143
|
+
}>;
|
|
1144
|
+
selectedContextId: string | null | undefined;
|
|
1145
|
+
disabled: boolean;
|
|
1146
|
+
canCreate: boolean;
|
|
1147
|
+
onSelect: (contextId: string) => void;
|
|
1148
|
+
onCreate: () => void;
|
|
1149
|
+
}) {
|
|
1150
|
+
return (
|
|
1151
|
+
<aside className="border-b border-border/70 pb-5">
|
|
1152
|
+
<div className="flex items-center justify-between gap-3">
|
|
1153
|
+
<div>
|
|
1154
|
+
<h2 className="text-sm font-semibold">Contexts</h2>
|
|
1155
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
1156
|
+
Choose the reusable context that should guide this work.
|
|
1157
|
+
</p>
|
|
1158
|
+
</div>
|
|
1159
|
+
<Badge variant="outline">{contexts.length}</Badge>
|
|
1160
|
+
</div>
|
|
1161
|
+
<div className="mt-3 flex gap-2 overflow-x-auto pb-1">
|
|
1162
|
+
{contexts.map((context) => (
|
|
1163
|
+
<button
|
|
1164
|
+
key={context.id}
|
|
1165
|
+
type="button"
|
|
1166
|
+
disabled={disabled}
|
|
1167
|
+
aria-pressed={selectedContextId === context.id}
|
|
1168
|
+
onClick={() => onSelect(context.id)}
|
|
1169
|
+
className={`min-w-40 rounded-md border px-3 py-2 text-start transition-colors disabled:opacity-60 ${selectedContextId === context.id ? "border-foreground/25 bg-accent text-foreground" : "border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground"}`}
|
|
1170
|
+
>
|
|
1171
|
+
<span className="block truncate text-sm font-medium">
|
|
1172
|
+
{context.name}
|
|
1173
|
+
</span>
|
|
1174
|
+
<span className="mt-0.5 block truncate text-xs">
|
|
1175
|
+
{context.description || `${context.memberCount} resources`}
|
|
1176
|
+
</span>
|
|
1177
|
+
</button>
|
|
1178
|
+
))}
|
|
1179
|
+
<Button
|
|
1180
|
+
type="button"
|
|
1181
|
+
variant="outline"
|
|
1182
|
+
className="min-w-40 justify-start"
|
|
1183
|
+
disabled={disabled || !canCreate}
|
|
1184
|
+
onClick={onCreate}
|
|
1185
|
+
>
|
|
1186
|
+
<IconPlus /> New context
|
|
1187
|
+
</Button>
|
|
1188
|
+
{!contexts.length ? (
|
|
1189
|
+
<p className="py-2 text-sm text-muted-foreground">
|
|
1190
|
+
Create a context from a resource’s Share tab to start organizing the
|
|
1191
|
+
Library.
|
|
1192
|
+
</p>
|
|
1193
|
+
) : null}
|
|
1194
|
+
</div>
|
|
1195
|
+
</aside>
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
792
1199
|
export function CreativeContextPanel({
|
|
793
1200
|
scope = "user",
|
|
794
1201
|
canManageOrg = false,
|
|
@@ -800,6 +1207,7 @@ export function CreativeContextPanel({
|
|
|
800
1207
|
const { data: org } = useOrg();
|
|
801
1208
|
const [libraryScope, setLibraryScope] = useState<AgentPageScope>(scope);
|
|
802
1209
|
const sourcesQuery = useCreativeContextSources({ limit: 100 });
|
|
1210
|
+
const contextsQuery = useCreativeContexts();
|
|
803
1211
|
const packsQuery = useCreativeContextPacks();
|
|
804
1212
|
const brandProfileQuery = useCreativeContextBrandProfile();
|
|
805
1213
|
const suggestionsQuery = useCreativeContextSuggestions();
|
|
@@ -819,6 +1227,9 @@ export function CreativeContextPanel({
|
|
|
819
1227
|
const manageLayoutTemplate = useManageLayoutTemplate();
|
|
820
1228
|
const contextState = useCreativeContextState();
|
|
821
1229
|
const [query, setQuery] = useState("");
|
|
1230
|
+
const [libraryView, setLibraryView] = useState<LibraryView>("items");
|
|
1231
|
+
const [previewManifest, setPreviewManifest] =
|
|
1232
|
+
useState<SafePreviewManifest | null>(null);
|
|
822
1233
|
const [savingState, setSavingState] = useState(false);
|
|
823
1234
|
const [stateError, setStateError] = useState<string | null>(null);
|
|
824
1235
|
const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
|
|
@@ -887,6 +1298,13 @@ export function CreativeContextPanel({
|
|
|
887
1298
|
);
|
|
888
1299
|
const [reviewedItems, setReviewedItems] = useState<ContextReviewItem[]>([]);
|
|
889
1300
|
const [reviewError, setReviewError] = useState<string | null>(null);
|
|
1301
|
+
const [membershipUpdateCandidate, setMembershipUpdateCandidate] = useState<{
|
|
1302
|
+
id: string;
|
|
1303
|
+
title: string;
|
|
1304
|
+
} | null>(null);
|
|
1305
|
+
const [updatingMembershipId, setUpdatingMembershipId] = useState<
|
|
1306
|
+
string | null
|
|
1307
|
+
>(null);
|
|
890
1308
|
const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
|
|
891
1309
|
const [suggestionError, setSuggestionError] = useState<string | null>(null);
|
|
892
1310
|
const previewQuery = usePreviewCreativeContextImport(previewSourceId);
|
|
@@ -945,6 +1363,42 @@ export function CreativeContextPanel({
|
|
|
945
1363
|
),
|
|
946
1364
|
[libraryScope, packsQuery.data?.packs],
|
|
947
1365
|
);
|
|
1366
|
+
const contexts = useMemo(
|
|
1367
|
+
() => parseCreativeContexts(contextsQuery.data),
|
|
1368
|
+
[contextsQuery.data],
|
|
1369
|
+
);
|
|
1370
|
+
const selectedLibraryContextId =
|
|
1371
|
+
contextState.state.selectedContextId ?? contexts[0]?.id ?? null;
|
|
1372
|
+
const contextMembershipsQuery = useContextMemberships(
|
|
1373
|
+
selectedLibraryContextId ? { contextId: selectedLibraryContextId } : null,
|
|
1374
|
+
);
|
|
1375
|
+
const manageContext = useManageCreativeContext();
|
|
1376
|
+
const manageContextMembership = useManageContextMembership();
|
|
1377
|
+
const contextMemberships = parseContextMemberships(
|
|
1378
|
+
contextMembershipsQuery.data,
|
|
1379
|
+
);
|
|
1380
|
+
const publishedContextMemberships = contextMemberships.filter(
|
|
1381
|
+
(membership) => membership.publishedItem,
|
|
1382
|
+
);
|
|
1383
|
+
const pendingContextMemberships = contextMemberships.filter(
|
|
1384
|
+
(membership) => membership.pendingSubmission,
|
|
1385
|
+
);
|
|
1386
|
+
const selectedLibraryContext = contexts.find(
|
|
1387
|
+
(context) => context.id === selectedLibraryContextId,
|
|
1388
|
+
);
|
|
1389
|
+
const [contextSettingsName, setContextSettingsName] = useState("");
|
|
1390
|
+
const [contextSettingsDescription, setContextSettingsDescription] =
|
|
1391
|
+
useState("");
|
|
1392
|
+
const [contextSettingsPolicy, setContextSettingsPolicy] = useState<
|
|
1393
|
+
"open" | "review" | "admins-only"
|
|
1394
|
+
>("open");
|
|
1395
|
+
const [newContextName, setNewContextName] = useState("");
|
|
1396
|
+
const [newContextPolicy, setNewContextPolicy] = useState<
|
|
1397
|
+
"open" | "review" | "admins-only"
|
|
1398
|
+
>("open");
|
|
1399
|
+
const [contextSettingsError, setContextSettingsError] = useState<
|
|
1400
|
+
string | null
|
|
1401
|
+
>(null);
|
|
948
1402
|
const activePack = packs.find(
|
|
949
1403
|
(pack) => pack.id === contextState.state.currentPackId,
|
|
950
1404
|
);
|
|
@@ -960,6 +1414,13 @@ export function CreativeContextPanel({
|
|
|
960
1414
|
const brandVoicePreview =
|
|
961
1415
|
brandProposal?.voiceDescriptors?.join(" · ") ?? brandProposal?.voiceLine;
|
|
962
1416
|
const canManageScope = libraryScope === "user" || canManageOrg;
|
|
1417
|
+
const canCreateContext =
|
|
1418
|
+
canManageScope && contexts.some((context) => context.access.canAdmin);
|
|
1419
|
+
const activeAppId = contextsQuery.data?.appId;
|
|
1420
|
+
const appDefaultContextId = contextsQuery.data?.appDefaultContextId ?? null;
|
|
1421
|
+
const canSetAppDefault = Boolean(
|
|
1422
|
+
activeAppId && selectedLibraryContext?.access.canAdmin && canManageScope,
|
|
1423
|
+
);
|
|
963
1424
|
const proposalCapabilities = suggestionsQuery.data?.capabilities;
|
|
964
1425
|
const logoCandidates = proposalCapabilities?.canonicalLogo
|
|
965
1426
|
? (logoCandidatesQuery.data?.candidates ?? [])
|
|
@@ -970,6 +1431,13 @@ export function CreativeContextPanel({
|
|
|
970
1431
|
(suggestion.status === "proposed" || suggestion.status === "promoted"),
|
|
971
1432
|
);
|
|
972
1433
|
|
|
1434
|
+
useEffect(() => {
|
|
1435
|
+
setContextSettingsName(selectedLibraryContext?.name ?? "");
|
|
1436
|
+
setContextSettingsDescription(selectedLibraryContext?.description ?? "");
|
|
1437
|
+
setContextSettingsPolicy(selectedLibraryContext?.approvalPolicy ?? "open");
|
|
1438
|
+
setContextSettingsError(null);
|
|
1439
|
+
}, [selectedLibraryContext]);
|
|
1440
|
+
|
|
973
1441
|
useEffect(() => {
|
|
974
1442
|
if (!previewSourceId || !previewQuery.data) return;
|
|
975
1443
|
const defaults = previewQuery.data.smartDefaultExternalIds ?? [];
|
|
@@ -1020,8 +1488,18 @@ export function CreativeContextPanel({
|
|
|
1020
1488
|
try {
|
|
1021
1489
|
await contextState.setState(
|
|
1022
1490
|
mode === "off"
|
|
1023
|
-
? {
|
|
1024
|
-
|
|
1491
|
+
? {
|
|
1492
|
+
contextMode: "off",
|
|
1493
|
+
selectedContextId: null,
|
|
1494
|
+
currentPackId: null,
|
|
1495
|
+
pinnedPackId: null,
|
|
1496
|
+
}
|
|
1497
|
+
: {
|
|
1498
|
+
...contextState.state,
|
|
1499
|
+
contextMode: "auto",
|
|
1500
|
+
selectedContextId: null,
|
|
1501
|
+
pinnedPackId: null,
|
|
1502
|
+
},
|
|
1025
1503
|
);
|
|
1026
1504
|
} catch {
|
|
1027
1505
|
setStateError(t("creativeContext.stateSaveFailed"));
|
|
@@ -1037,6 +1515,7 @@ export function CreativeContextPanel({
|
|
|
1037
1515
|
await contextState.setState({
|
|
1038
1516
|
...contextState.state,
|
|
1039
1517
|
contextMode: "auto",
|
|
1518
|
+
selectedContextId: null,
|
|
1040
1519
|
pinnedPackId: packId,
|
|
1041
1520
|
});
|
|
1042
1521
|
} catch {
|
|
@@ -1046,6 +1525,60 @@ export function CreativeContextPanel({
|
|
|
1046
1525
|
}
|
|
1047
1526
|
}
|
|
1048
1527
|
|
|
1528
|
+
async function selectContext(contextId: string) {
|
|
1529
|
+
setSavingState(true);
|
|
1530
|
+
setStateError(null);
|
|
1531
|
+
try {
|
|
1532
|
+
await contextState.setState({
|
|
1533
|
+
...contextState.state,
|
|
1534
|
+
contextMode: "auto",
|
|
1535
|
+
selectedContextId: contextId,
|
|
1536
|
+
pinnedPackId: null,
|
|
1537
|
+
});
|
|
1538
|
+
} catch {
|
|
1539
|
+
setStateError(t("creativeContext.stateSaveFailed"));
|
|
1540
|
+
} finally {
|
|
1541
|
+
setSavingState(false);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function reviewContextMembership(
|
|
1546
|
+
membershipId: string,
|
|
1547
|
+
operation: "approve" | "request-changes",
|
|
1548
|
+
) {
|
|
1549
|
+
if (!selectedLibraryContextId) return;
|
|
1550
|
+
try {
|
|
1551
|
+
await manageContextMembership.mutateAsync({
|
|
1552
|
+
operation,
|
|
1553
|
+
contextId: selectedLibraryContextId,
|
|
1554
|
+
membershipId,
|
|
1555
|
+
});
|
|
1556
|
+
await contextMembershipsQuery.refetch();
|
|
1557
|
+
} catch {
|
|
1558
|
+
setReviewError(t("creativeContext.saveFailed"));
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function submitLatestContextMembershipUpdate() {
|
|
1563
|
+
if (!selectedLibraryContextId || !membershipUpdateCandidate) return;
|
|
1564
|
+
setReviewError(null);
|
|
1565
|
+
setUpdatingMembershipId(membershipUpdateCandidate.id);
|
|
1566
|
+
try {
|
|
1567
|
+
await manageContextMembership.mutateAsync({
|
|
1568
|
+
operation: "submit-latest",
|
|
1569
|
+
contextId: selectedLibraryContextId,
|
|
1570
|
+
membershipId: membershipUpdateCandidate.id,
|
|
1571
|
+
confirmBroaderPublication: true,
|
|
1572
|
+
});
|
|
1573
|
+
setMembershipUpdateCandidate(null);
|
|
1574
|
+
await contextMembershipsQuery.refetch();
|
|
1575
|
+
} catch {
|
|
1576
|
+
setReviewError(t("creativeContext.submitUpdateFailed"));
|
|
1577
|
+
} finally {
|
|
1578
|
+
setUpdatingMembershipId(null);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1049
1582
|
function refresh(sourceId: string) {
|
|
1050
1583
|
setRefreshMessage(null);
|
|
1051
1584
|
refreshSource.mutate(
|
|
@@ -1412,16 +1945,84 @@ export function CreativeContextPanel({
|
|
|
1412
1945
|
}
|
|
1413
1946
|
}
|
|
1414
1947
|
|
|
1948
|
+
async function saveContextSettings() {
|
|
1949
|
+
if (!selectedLibraryContext?.access.canAdmin || !contextSettingsName.trim())
|
|
1950
|
+
return;
|
|
1951
|
+
setContextSettingsError(null);
|
|
1952
|
+
try {
|
|
1953
|
+
await manageContext.mutateAsync({
|
|
1954
|
+
operation: "update",
|
|
1955
|
+
contextId: selectedLibraryContext.id,
|
|
1956
|
+
patch: {
|
|
1957
|
+
name: contextSettingsName.trim(),
|
|
1958
|
+
description: contextSettingsDescription.trim() || null,
|
|
1959
|
+
approvalPolicy: contextSettingsPolicy,
|
|
1960
|
+
},
|
|
1961
|
+
});
|
|
1962
|
+
await contextsQuery.refetch();
|
|
1963
|
+
} catch {
|
|
1964
|
+
setContextSettingsError("Could not update this context.");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
async function createSpecialtyContext() {
|
|
1969
|
+
if (!canCreateContext || !newContextName.trim()) return;
|
|
1970
|
+
setContextSettingsError(null);
|
|
1971
|
+
try {
|
|
1972
|
+
const result = await manageContext.mutateAsync({
|
|
1973
|
+
operation: "create",
|
|
1974
|
+
name: newContextName.trim(),
|
|
1975
|
+
kind: "specialty",
|
|
1976
|
+
approvalPolicy: newContextPolicy,
|
|
1977
|
+
});
|
|
1978
|
+
setNewContextName("");
|
|
1979
|
+
setNewContextPolicy("open");
|
|
1980
|
+
await contextsQuery.refetch();
|
|
1981
|
+
if (result.context?.id) await selectContext(result.context.id);
|
|
1982
|
+
} catch {
|
|
1983
|
+
setContextSettingsError("Could not create this context.");
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
async function setAppDefaultContext() {
|
|
1988
|
+
if (!activeAppId || !selectedLibraryContext || !canSetAppDefault) return;
|
|
1989
|
+
setContextSettingsError(null);
|
|
1990
|
+
try {
|
|
1991
|
+
await manageContext.mutateAsync({
|
|
1992
|
+
operation: "set-app-default",
|
|
1993
|
+
contextId: selectedLibraryContext.id,
|
|
1994
|
+
appId: activeAppId,
|
|
1995
|
+
});
|
|
1996
|
+
await contextsQuery.refetch();
|
|
1997
|
+
} catch {
|
|
1998
|
+
setContextSettingsError(
|
|
1999
|
+
"Could not update the automatic context for this app.",
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1415
2004
|
async function search(event: FormEvent<HTMLFormElement>) {
|
|
1416
2005
|
event.preventDefault();
|
|
1417
2006
|
const searchText = query.trim();
|
|
1418
|
-
if (
|
|
2007
|
+
if (
|
|
2008
|
+
!searchText ||
|
|
2009
|
+
(!sources.length &&
|
|
2010
|
+
!selectedLibraryContextId &&
|
|
2011
|
+
!contextState.state.pinnedPackId)
|
|
2012
|
+
)
|
|
2013
|
+
return;
|
|
1419
2014
|
setSearchError(null);
|
|
1420
2015
|
try {
|
|
1421
2016
|
const result = await searchContext.mutateAsync({
|
|
1422
2017
|
query: searchText,
|
|
1423
|
-
sourceIds:
|
|
2018
|
+
sourceIds:
|
|
2019
|
+
selectedLibraryContextId || contextState.state.pinnedPackId
|
|
2020
|
+
? undefined
|
|
2021
|
+
: sources.map((source) => source.id),
|
|
1424
2022
|
packId: contextState.state.pinnedPackId ?? undefined,
|
|
2023
|
+
contextId: contextState.state.pinnedPackId
|
|
2024
|
+
? undefined
|
|
2025
|
+
: (selectedLibraryContextId ?? undefined),
|
|
1425
2026
|
limit: 20,
|
|
1426
2027
|
snapshot: true,
|
|
1427
2028
|
});
|
|
@@ -1437,8 +2038,12 @@ export function CreativeContextPanel({
|
|
|
1437
2038
|
}
|
|
1438
2039
|
|
|
1439
2040
|
const loading =
|
|
1440
|
-
sourcesQuery.isLoading ||
|
|
1441
|
-
|
|
2041
|
+
sourcesQuery.isLoading ||
|
|
2042
|
+
packsQuery.isLoading ||
|
|
2043
|
+
contextsQuery.isLoading ||
|
|
2044
|
+
contextState.isLoading;
|
|
2045
|
+
const unavailable =
|
|
2046
|
+
sourcesQuery.error || packsQuery.error || contextsQuery.error;
|
|
1442
2047
|
|
|
1443
2048
|
return (
|
|
1444
2049
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-7 p-6 lg:p-10">
|
|
@@ -1456,7 +2061,11 @@ export function CreativeContextPanel({
|
|
|
1456
2061
|
(org?.orgId ? (
|
|
1457
2062
|
<ScopeControl scope={libraryScope} onChange={setLibraryScope} />
|
|
1458
2063
|
) : null)}
|
|
1459
|
-
<CreativeContextChip
|
|
2064
|
+
<CreativeContextChip
|
|
2065
|
+
state={contextState.state}
|
|
2066
|
+
packs={packs}
|
|
2067
|
+
contexts={contexts}
|
|
2068
|
+
/>
|
|
1460
2069
|
</div>
|
|
1461
2070
|
</header>
|
|
1462
2071
|
|
|
@@ -1473,92 +2082,469 @@ export function CreativeContextPanel({
|
|
|
1473
2082
|
</div>
|
|
1474
2083
|
) : (
|
|
1475
2084
|
<>
|
|
1476
|
-
<
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
{logoCandidates.length || proposedLayouts.length ? (
|
|
1534
|
-
<section className="border-t border-border/70 pt-6">
|
|
1535
|
-
<div className="flex items-center gap-2">
|
|
1536
|
-
<IconSparkles className="size-5 text-muted-foreground" />
|
|
1537
|
-
<h2 className="text-lg font-semibold">
|
|
1538
|
-
{t("creativeContext.suggestions")}
|
|
1539
|
-
</h2>
|
|
1540
|
-
</div>
|
|
1541
|
-
{logoCandidates.length ? (
|
|
1542
|
-
<div className="mt-4">
|
|
1543
|
-
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
|
1544
|
-
<IconPhoto className="size-4 text-muted-foreground" />
|
|
1545
|
-
{t("creativeContext.logo")}
|
|
1546
|
-
</h3>
|
|
1547
|
-
<div className="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
1548
|
-
{logoCandidates.map((candidate) => (
|
|
1549
|
-
<article
|
|
1550
|
-
key={candidate.mediaId}
|
|
1551
|
-
className="overflow-hidden rounded-md border border-border"
|
|
2085
|
+
<ContextRail
|
|
2086
|
+
contexts={contexts}
|
|
2087
|
+
selectedContextId={contextState.state.selectedContextId}
|
|
2088
|
+
disabled={savingState}
|
|
2089
|
+
canCreate={canCreateContext}
|
|
2090
|
+
onSelect={(contextId) => void selectContext(contextId)}
|
|
2091
|
+
onCreate={() => setLibraryView("settings")}
|
|
2092
|
+
/>
|
|
2093
|
+
<Tabs
|
|
2094
|
+
value={libraryView}
|
|
2095
|
+
onValueChange={(value) => setLibraryView(value as LibraryView)}
|
|
2096
|
+
>
|
|
2097
|
+
<TabsList className="w-full justify-start overflow-x-auto">
|
|
2098
|
+
<TabsTrigger value="items">Items</TabsTrigger>
|
|
2099
|
+
<TabsTrigger value="sources">Sources</TabsTrigger>
|
|
2100
|
+
<TabsTrigger value="approvals">Approvals</TabsTrigger>
|
|
2101
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
2102
|
+
</TabsList>
|
|
2103
|
+
<TabsContent value="items">
|
|
2104
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
2105
|
+
{publishedContextMemberships.map((membership) => {
|
|
2106
|
+
const item = membership.publishedItem!;
|
|
2107
|
+
const imageMedium = item.media.find((medium) =>
|
|
2108
|
+
medium.mimeType?.startsWith("image/"),
|
|
2109
|
+
);
|
|
2110
|
+
const playbackMedium = item.media.find((medium) =>
|
|
2111
|
+
medium.mimeType?.startsWith("video/"),
|
|
2112
|
+
);
|
|
2113
|
+
const medium = imageMedium ?? playbackMedium ?? item.media[0];
|
|
2114
|
+
const sheetMedium = playbackMedium ?? medium;
|
|
2115
|
+
const updateAvailable =
|
|
2116
|
+
membership.nativeUpdateStatus?.state === "update-available";
|
|
2117
|
+
return (
|
|
2118
|
+
<article
|
|
2119
|
+
key={membership.id}
|
|
2120
|
+
className="overflow-hidden rounded-md border border-border"
|
|
2121
|
+
>
|
|
2122
|
+
<button
|
|
2123
|
+
type="button"
|
|
2124
|
+
onClick={() =>
|
|
2125
|
+
setPreviewManifest({
|
|
2126
|
+
title: item.title,
|
|
2127
|
+
kind: item.kind,
|
|
2128
|
+
itemId: item.id,
|
|
2129
|
+
itemVersionId: item.itemVersionId,
|
|
2130
|
+
preview: item.preview,
|
|
2131
|
+
media: sheetMedium ?? null,
|
|
2132
|
+
gallery: item.media.filter((candidate) =>
|
|
2133
|
+
candidate.mimeType?.startsWith("image/"),
|
|
2134
|
+
),
|
|
2135
|
+
posterUrl:
|
|
2136
|
+
playbackMedium && imageMedium
|
|
2137
|
+
? imageMedium.url
|
|
2138
|
+
: undefined,
|
|
2139
|
+
})
|
|
2140
|
+
}
|
|
2141
|
+
className="block w-full text-start transition-colors hover:bg-accent/40"
|
|
1552
2142
|
>
|
|
1553
|
-
<
|
|
1554
|
-
<
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2143
|
+
<span className="block aspect-video overflow-hidden">
|
|
2144
|
+
<ContextPreviewVisual
|
|
2145
|
+
compact
|
|
2146
|
+
manifest={{
|
|
2147
|
+
title: item.title,
|
|
2148
|
+
kind: item.kind,
|
|
2149
|
+
itemId: item.id,
|
|
2150
|
+
itemVersionId: item.itemVersionId,
|
|
2151
|
+
preview: item.preview,
|
|
2152
|
+
media: medium ?? null,
|
|
2153
|
+
gallery: item.media,
|
|
2154
|
+
}}
|
|
1558
2155
|
/>
|
|
1559
|
-
</
|
|
1560
|
-
<
|
|
1561
|
-
<
|
|
2156
|
+
</span>
|
|
2157
|
+
<span className="block p-3">
|
|
2158
|
+
<span className="block truncate text-sm font-medium">
|
|
2159
|
+
{item.title}
|
|
2160
|
+
</span>
|
|
2161
|
+
<span className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
2162
|
+
<span>{item.kind}</span>
|
|
2163
|
+
<span>·</span>
|
|
2164
|
+
<span className="capitalize">
|
|
2165
|
+
{membership.rank}
|
|
2166
|
+
</span>
|
|
2167
|
+
<Badge variant="secondary">Published</Badge>
|
|
2168
|
+
{updateAvailable ? (
|
|
2169
|
+
<Badge variant="outline">
|
|
2170
|
+
{t("creativeContext.updateAvailable")}
|
|
2171
|
+
</Badge>
|
|
2172
|
+
) : null}
|
|
2173
|
+
</span>
|
|
2174
|
+
<span className="mt-1 block truncate font-mono text-[10px] text-muted-foreground">
|
|
2175
|
+
Version {item.itemVersionId.slice(0, 12)}
|
|
2176
|
+
</span>
|
|
2177
|
+
</span>
|
|
2178
|
+
</button>
|
|
2179
|
+
{updateAvailable &&
|
|
2180
|
+
selectedLibraryContext?.access.canSubmit ? (
|
|
2181
|
+
<div className="border-t border-border/70 p-2">
|
|
2182
|
+
<Button
|
|
2183
|
+
type="button"
|
|
2184
|
+
variant="outline"
|
|
2185
|
+
size="sm"
|
|
2186
|
+
className="w-full"
|
|
2187
|
+
disabled={updatingMembershipId === membership.id}
|
|
2188
|
+
onClick={() =>
|
|
2189
|
+
setMembershipUpdateCandidate({
|
|
2190
|
+
id: membership.id,
|
|
2191
|
+
title: item.title,
|
|
2192
|
+
})
|
|
2193
|
+
}
|
|
2194
|
+
>
|
|
2195
|
+
<IconRefresh />
|
|
2196
|
+
{updatingMembershipId === membership.id
|
|
2197
|
+
? t("creativeContext.submittingUpdate")
|
|
2198
|
+
: t("creativeContext.submitUpdate")}
|
|
2199
|
+
</Button>
|
|
2200
|
+
</div>
|
|
2201
|
+
) : null}
|
|
2202
|
+
</article>
|
|
2203
|
+
);
|
|
2204
|
+
})}
|
|
2205
|
+
</div>
|
|
2206
|
+
{!publishedContextMemberships.length ? (
|
|
2207
|
+
<p className="py-4 text-sm text-muted-foreground">
|
|
2208
|
+
Approved context items appear here after publication.
|
|
2209
|
+
</p>
|
|
2210
|
+
) : null}
|
|
2211
|
+
{reviewError ? (
|
|
2212
|
+
<p className="mt-3 text-sm text-destructive">{reviewError}</p>
|
|
2213
|
+
) : null}
|
|
2214
|
+
</TabsContent>
|
|
2215
|
+
<TabsContent value="approvals">
|
|
2216
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
2217
|
+
{pendingContextMemberships.map((membership) => {
|
|
2218
|
+
const submission = membership.pendingSubmission!;
|
|
2219
|
+
const item = submission.proposedItem;
|
|
2220
|
+
const medium = item?.media[0];
|
|
2221
|
+
return (
|
|
2222
|
+
<article
|
|
2223
|
+
key={membership.id}
|
|
2224
|
+
className="overflow-hidden rounded-md border border-border"
|
|
2225
|
+
>
|
|
2226
|
+
{item ? (
|
|
2227
|
+
<button
|
|
2228
|
+
type="button"
|
|
2229
|
+
className="block aspect-video w-full overflow-hidden text-start"
|
|
2230
|
+
onClick={() =>
|
|
2231
|
+
setPreviewManifest({
|
|
2232
|
+
title: item.title,
|
|
2233
|
+
kind: item.kind,
|
|
2234
|
+
itemId: item.id,
|
|
2235
|
+
itemVersionId: item.itemVersionId,
|
|
2236
|
+
preview: item.preview,
|
|
2237
|
+
media: medium ?? null,
|
|
2238
|
+
gallery: item.media.filter((candidate) =>
|
|
2239
|
+
candidate.mimeType?.startsWith("image/"),
|
|
2240
|
+
),
|
|
2241
|
+
})
|
|
2242
|
+
}
|
|
2243
|
+
>
|
|
2244
|
+
<ContextPreviewVisual
|
|
2245
|
+
compact
|
|
2246
|
+
manifest={{
|
|
2247
|
+
title: item.title,
|
|
2248
|
+
kind: item.kind,
|
|
2249
|
+
itemId: item.id,
|
|
2250
|
+
itemVersionId: item.itemVersionId,
|
|
2251
|
+
preview: item.preview,
|
|
2252
|
+
media: medium ?? null,
|
|
2253
|
+
}}
|
|
2254
|
+
/>
|
|
2255
|
+
</button>
|
|
2256
|
+
) : null}
|
|
2257
|
+
<div className="p-3">
|
|
2258
|
+
<p className="truncate text-sm font-medium">
|
|
2259
|
+
{item?.title ?? "Pending context submission"}
|
|
2260
|
+
</p>
|
|
2261
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2262
|
+
Submitted by {submission.submittedBy}
|
|
2263
|
+
</p>
|
|
2264
|
+
{submission.note ? (
|
|
2265
|
+
<p className="mt-2 line-clamp-3 text-xs text-muted-foreground">
|
|
2266
|
+
{submission.note}
|
|
2267
|
+
</p>
|
|
2268
|
+
) : null}
|
|
2269
|
+
{selectedLibraryContext?.access.canReview ? (
|
|
2270
|
+
<div className="mt-3 flex gap-2">
|
|
2271
|
+
<Button
|
|
2272
|
+
size="sm"
|
|
2273
|
+
onClick={() =>
|
|
2274
|
+
void reviewContextMembership(
|
|
2275
|
+
membership.id,
|
|
2276
|
+
"approve",
|
|
2277
|
+
)
|
|
2278
|
+
}
|
|
2279
|
+
>
|
|
2280
|
+
Approve
|
|
2281
|
+
</Button>
|
|
2282
|
+
<Button
|
|
2283
|
+
size="sm"
|
|
2284
|
+
variant="outline"
|
|
2285
|
+
onClick={() =>
|
|
2286
|
+
void reviewContextMembership(
|
|
2287
|
+
membership.id,
|
|
2288
|
+
"request-changes",
|
|
2289
|
+
)
|
|
2290
|
+
}
|
|
2291
|
+
>
|
|
2292
|
+
Request changes
|
|
2293
|
+
</Button>
|
|
2294
|
+
</div>
|
|
2295
|
+
) : (
|
|
2296
|
+
<Badge variant="outline" className="mt-3">
|
|
2297
|
+
Awaiting review
|
|
2298
|
+
</Badge>
|
|
2299
|
+
)}
|
|
2300
|
+
</div>
|
|
2301
|
+
</article>
|
|
2302
|
+
);
|
|
2303
|
+
})}
|
|
2304
|
+
</div>
|
|
2305
|
+
{!pendingContextMemberships.length ? (
|
|
2306
|
+
<p className="py-4 text-sm text-muted-foreground">
|
|
2307
|
+
No context submissions need review.
|
|
2308
|
+
</p>
|
|
2309
|
+
) : null}
|
|
2310
|
+
</TabsContent>
|
|
2311
|
+
<TabsContent value="sources">
|
|
2312
|
+
<p className="text-sm text-muted-foreground">
|
|
2313
|
+
Sources and their review queues are managed below.
|
|
2314
|
+
</p>
|
|
2315
|
+
</TabsContent>
|
|
2316
|
+
<TabsContent value="settings">
|
|
2317
|
+
<div className="grid gap-5 lg:grid-cols-2">
|
|
2318
|
+
<section className="space-y-3 rounded-md border border-border p-4">
|
|
2319
|
+
<div>
|
|
2320
|
+
<h3 className="text-sm font-semibold">Context settings</h3>
|
|
2321
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
2322
|
+
Automatic selection uses Default plus at most one matching
|
|
2323
|
+
specialty. Exact packs remain available under advanced
|
|
2324
|
+
provenance.
|
|
2325
|
+
</p>
|
|
2326
|
+
</div>
|
|
2327
|
+
<Input
|
|
2328
|
+
value={contextSettingsName}
|
|
2329
|
+
disabled={!selectedLibraryContext?.access.canAdmin}
|
|
2330
|
+
onChange={(event) =>
|
|
2331
|
+
setContextSettingsName(event.target.value)
|
|
2332
|
+
}
|
|
2333
|
+
placeholder="Context name"
|
|
2334
|
+
/>
|
|
2335
|
+
<Textarea
|
|
2336
|
+
value={contextSettingsDescription}
|
|
2337
|
+
disabled={!selectedLibraryContext?.access.canAdmin}
|
|
2338
|
+
onChange={(event) =>
|
|
2339
|
+
setContextSettingsDescription(event.target.value)
|
|
2340
|
+
}
|
|
2341
|
+
placeholder="When should agents use this context?"
|
|
2342
|
+
rows={3}
|
|
2343
|
+
/>
|
|
2344
|
+
<Select
|
|
2345
|
+
value={contextSettingsPolicy}
|
|
2346
|
+
disabled={!selectedLibraryContext?.access.canAdmin}
|
|
2347
|
+
onValueChange={(value) =>
|
|
2348
|
+
setContextSettingsPolicy(
|
|
2349
|
+
value as "open" | "review" | "admins-only",
|
|
2350
|
+
)
|
|
2351
|
+
}
|
|
2352
|
+
>
|
|
2353
|
+
<SelectTrigger>
|
|
2354
|
+
<SelectValue />
|
|
2355
|
+
</SelectTrigger>
|
|
2356
|
+
<SelectContent>
|
|
2357
|
+
<SelectItem value="open">Open publishing</SelectItem>
|
|
2358
|
+
<SelectItem value="review">Require review</SelectItem>
|
|
2359
|
+
<SelectItem value="admins-only">Admins only</SelectItem>
|
|
2360
|
+
</SelectContent>
|
|
2361
|
+
</Select>
|
|
2362
|
+
<Button
|
|
2363
|
+
type="button"
|
|
2364
|
+
size="sm"
|
|
2365
|
+
disabled={
|
|
2366
|
+
!selectedLibraryContext?.access.canAdmin ||
|
|
2367
|
+
!contextSettingsName.trim() ||
|
|
2368
|
+
manageContext.isPending
|
|
2369
|
+
}
|
|
2370
|
+
onClick={() => void saveContextSettings()}
|
|
2371
|
+
>
|
|
2372
|
+
Save settings
|
|
2373
|
+
</Button>
|
|
2374
|
+
{activeAppId ? (
|
|
2375
|
+
<div className="border-t border-border/70 pt-3">
|
|
2376
|
+
<p className="text-xs text-muted-foreground">
|
|
2377
|
+
Automatic generations use Default plus this context for
|
|
2378
|
+
{` ${activeAppId}`} when no context is chosen
|
|
2379
|
+
explicitly.
|
|
2380
|
+
</p>
|
|
2381
|
+
<Button
|
|
2382
|
+
type="button"
|
|
2383
|
+
size="sm"
|
|
2384
|
+
variant="outline"
|
|
2385
|
+
className="mt-3"
|
|
2386
|
+
disabled={
|
|
2387
|
+
!canSetAppDefault ||
|
|
2388
|
+
appDefaultContextId === selectedLibraryContext?.id ||
|
|
2389
|
+
manageContext.isPending
|
|
2390
|
+
}
|
|
2391
|
+
onClick={() => void setAppDefaultContext()}
|
|
2392
|
+
>
|
|
2393
|
+
{appDefaultContextId === selectedLibraryContext?.id
|
|
2394
|
+
? `Automatic for ${activeAppId}`
|
|
2395
|
+
: `Use automatically for ${activeAppId}`}
|
|
2396
|
+
</Button>
|
|
2397
|
+
</div>
|
|
2398
|
+
) : null}
|
|
2399
|
+
</section>
|
|
2400
|
+
<section className="space-y-3 rounded-md border border-dashed border-border p-4">
|
|
2401
|
+
<div>
|
|
2402
|
+
<h3 className="text-sm font-semibold">New specialty</h3>
|
|
2403
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
2404
|
+
Create a focused style such as Marketing, Product, or
|
|
2405
|
+
Sales. Contexts start open unless you choose review.
|
|
2406
|
+
</p>
|
|
2407
|
+
</div>
|
|
2408
|
+
<Input
|
|
2409
|
+
value={newContextName}
|
|
2410
|
+
disabled={!canCreateContext}
|
|
2411
|
+
onChange={(event) => setNewContextName(event.target.value)}
|
|
2412
|
+
placeholder="Marketing"
|
|
2413
|
+
/>
|
|
2414
|
+
<Select
|
|
2415
|
+
value={newContextPolicy}
|
|
2416
|
+
disabled={!canCreateContext}
|
|
2417
|
+
onValueChange={(value) =>
|
|
2418
|
+
setNewContextPolicy(
|
|
2419
|
+
value as "open" | "review" | "admins-only",
|
|
2420
|
+
)
|
|
2421
|
+
}
|
|
2422
|
+
>
|
|
2423
|
+
<SelectTrigger>
|
|
2424
|
+
<SelectValue />
|
|
2425
|
+
</SelectTrigger>
|
|
2426
|
+
<SelectContent>
|
|
2427
|
+
<SelectItem value="open">Open publishing</SelectItem>
|
|
2428
|
+
<SelectItem value="review">Require review</SelectItem>
|
|
2429
|
+
<SelectItem value="admins-only">Admins only</SelectItem>
|
|
2430
|
+
</SelectContent>
|
|
2431
|
+
</Select>
|
|
2432
|
+
<Button
|
|
2433
|
+
type="button"
|
|
2434
|
+
size="sm"
|
|
2435
|
+
variant="outline"
|
|
2436
|
+
disabled={
|
|
2437
|
+
!canCreateContext ||
|
|
2438
|
+
!newContextName.trim() ||
|
|
2439
|
+
manageContext.isPending
|
|
2440
|
+
}
|
|
2441
|
+
onClick={() => void createSpecialtyContext()}
|
|
2442
|
+
>
|
|
2443
|
+
<IconPlus /> Create context
|
|
2444
|
+
</Button>
|
|
2445
|
+
</section>
|
|
2446
|
+
</div>
|
|
2447
|
+
{contextSettingsError ? (
|
|
2448
|
+
<p className="mt-3 text-xs text-destructive">
|
|
2449
|
+
{contextSettingsError}
|
|
2450
|
+
</p>
|
|
2451
|
+
) : null}
|
|
2452
|
+
</TabsContent>
|
|
2453
|
+
</Tabs>
|
|
2454
|
+
{libraryView === "settings" ? (
|
|
2455
|
+
<section className="space-y-3">
|
|
2456
|
+
<div>
|
|
2457
|
+
<h2 className="text-sm font-semibold">
|
|
2458
|
+
{t("creativeContext.modeLabel")}
|
|
2459
|
+
</h2>
|
|
2460
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
2461
|
+
{activePack
|
|
2462
|
+
? `${t("creativeContext.activePack")}: ${activePack.name}`
|
|
2463
|
+
: t("creativeContext.noActivePack")}
|
|
2464
|
+
</p>
|
|
2465
|
+
</div>
|
|
2466
|
+
<div
|
|
2467
|
+
className="flex flex-col gap-2 sm:flex-row"
|
|
2468
|
+
role="radiogroup"
|
|
2469
|
+
>
|
|
2470
|
+
<ContextModeButton
|
|
2471
|
+
mode="auto"
|
|
2472
|
+
activeMode={contextState.state.contextMode}
|
|
2473
|
+
label={t("creativeContext.automatic")}
|
|
2474
|
+
description={t("creativeContext.automaticDescription")}
|
|
2475
|
+
disabled={savingState}
|
|
2476
|
+
onSelect={(mode) => void changeMode(mode)}
|
|
2477
|
+
/>
|
|
2478
|
+
<ContextModeButton
|
|
2479
|
+
mode="off"
|
|
2480
|
+
activeMode={contextState.state.contextMode}
|
|
2481
|
+
label={t("creativeContext.off")}
|
|
2482
|
+
description={t("creativeContext.offDescription")}
|
|
2483
|
+
disabled={savingState}
|
|
2484
|
+
onSelect={(mode) => void changeMode(mode)}
|
|
2485
|
+
/>
|
|
2486
|
+
</div>
|
|
2487
|
+
{stateError ? (
|
|
2488
|
+
<p className="text-xs text-destructive">{stateError}</p>
|
|
2489
|
+
) : null}
|
|
2490
|
+
</section>
|
|
2491
|
+
) : null}
|
|
2492
|
+
|
|
2493
|
+
{libraryView === "settings" &&
|
|
2494
|
+
brandProfileQuery.data?.profile &&
|
|
2495
|
+
brandProfileQuery.data.dna ? (
|
|
2496
|
+
<section className="border-t border-border/70 pt-6">
|
|
2497
|
+
<div className="rounded-md border border-border p-4">
|
|
2498
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
2499
|
+
<div>
|
|
2500
|
+
<h2 className="text-sm font-semibold">
|
|
2501
|
+
{t("creativeContext.publishedBrandContext")}
|
|
2502
|
+
</h2>
|
|
2503
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2504
|
+
{brandProfileQuery.data.profile.name}
|
|
2505
|
+
</p>
|
|
2506
|
+
</div>
|
|
2507
|
+
<Badge variant="secondary">
|
|
2508
|
+
v{brandProfileQuery.data.dna.versionNumber}
|
|
2509
|
+
</Badge>
|
|
2510
|
+
</div>
|
|
2511
|
+
<p className="mt-3 text-sm text-muted-foreground">
|
|
2512
|
+
{brandProfileQuery.data.dna.payload.summary}
|
|
2513
|
+
</p>
|
|
2514
|
+
</div>
|
|
2515
|
+
</section>
|
|
2516
|
+
) : null}
|
|
2517
|
+
|
|
2518
|
+
{libraryView === "approvals" &&
|
|
2519
|
+
(logoCandidates.length || proposedLayouts.length) ? (
|
|
2520
|
+
<section className="border-t border-border/70 pt-6">
|
|
2521
|
+
<div className="flex items-center gap-2">
|
|
2522
|
+
<IconSparkles className="size-5 text-muted-foreground" />
|
|
2523
|
+
<h2 className="text-lg font-semibold">
|
|
2524
|
+
{t("creativeContext.suggestions")}
|
|
2525
|
+
</h2>
|
|
2526
|
+
</div>
|
|
2527
|
+
{logoCandidates.length ? (
|
|
2528
|
+
<div className="mt-4">
|
|
2529
|
+
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
|
2530
|
+
<IconPhoto className="size-4 text-muted-foreground" />
|
|
2531
|
+
{t("creativeContext.logo")}
|
|
2532
|
+
</h3>
|
|
2533
|
+
<div className="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
2534
|
+
{logoCandidates.map((candidate) => (
|
|
2535
|
+
<article
|
|
2536
|
+
key={candidate.mediaId}
|
|
2537
|
+
className="overflow-hidden rounded-md border border-border"
|
|
2538
|
+
>
|
|
2539
|
+
<div className="flex h-28 items-center justify-center bg-muted/40 p-3">
|
|
2540
|
+
<img
|
|
2541
|
+
src={candidate.thumbnailUrl}
|
|
2542
|
+
alt={candidate.title}
|
|
2543
|
+
className="max-h-full max-w-full object-contain"
|
|
2544
|
+
/>
|
|
2545
|
+
</div>
|
|
2546
|
+
<div className="flex items-center justify-between gap-2 p-3">
|
|
2547
|
+
<p className="min-w-0 truncate text-sm font-medium">
|
|
1562
2548
|
{candidate.title}
|
|
1563
2549
|
</p>
|
|
1564
2550
|
<Button
|
|
@@ -1663,851 +2649,927 @@ export function CreativeContextPanel({
|
|
|
1663
2649
|
</section>
|
|
1664
2650
|
) : null}
|
|
1665
2651
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
<
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2652
|
+
{libraryView === "sources" ? (
|
|
2653
|
+
<>
|
|
2654
|
+
<section className="border-t border-border/70 pt-6">
|
|
2655
|
+
<div>
|
|
2656
|
+
<h2 className="text-lg font-semibold">
|
|
2657
|
+
{t("creativeContext.addSource")}
|
|
2658
|
+
</h2>
|
|
2659
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
2660
|
+
{t("creativeContext.sourcesDescription")}
|
|
2661
|
+
</p>
|
|
2662
|
+
</div>
|
|
2663
|
+
<div className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-5">
|
|
2664
|
+
{CONNECTORS.map((connector) => {
|
|
2665
|
+
const Icon = connector.icon;
|
|
2666
|
+
return (
|
|
2667
|
+
<button
|
|
2668
|
+
key={connector.kind}
|
|
2669
|
+
type="button"
|
|
2670
|
+
disabled={!canManageScope}
|
|
2671
|
+
onClick={() => {
|
|
2672
|
+
setConnectorKind(connector.kind);
|
|
2673
|
+
setSourceName(connector.label);
|
|
2674
|
+
setSourceReference("");
|
|
2675
|
+
setUploadedFiles([]);
|
|
2676
|
+
setPickerRecommendations([]);
|
|
2677
|
+
seenRecommendationIdsRef.current.clear();
|
|
2678
|
+
setSelectedConnectionId("");
|
|
2679
|
+
setSelectedRecommendationIds(new Set());
|
|
2680
|
+
setSetupError(null);
|
|
2681
|
+
}}
|
|
2682
|
+
className="flex min-h-24 flex-col items-start justify-between rounded-md border border-border p-3 text-start transition-colors hover:bg-accent/50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
2683
|
+
>
|
|
2684
|
+
<Icon className="size-5 text-muted-foreground" />
|
|
2685
|
+
<span className="text-sm font-medium">
|
|
2686
|
+
{connector.label}
|
|
2687
|
+
</span>
|
|
2688
|
+
</button>
|
|
2689
|
+
);
|
|
2690
|
+
})}
|
|
2691
|
+
</div>
|
|
2692
|
+
{selectedConnector ? (
|
|
2693
|
+
<form
|
|
2694
|
+
className="mt-4 rounded-md border border-border p-4"
|
|
2695
|
+
onSubmit={(event) => void previewImport(event)}
|
|
1695
2696
|
>
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
<
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2697
|
+
{connectionProvider ? (
|
|
2698
|
+
<div className="mb-3">
|
|
2699
|
+
{connectionsQuery.isLoading ? (
|
|
2700
|
+
<Skeleton className="h-9 w-full" />
|
|
2701
|
+
) : connectionsQuery.data?.needsSetup ? (
|
|
2702
|
+
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground">
|
|
2703
|
+
<span>{t("creativeContext.setupConnection")}</span>
|
|
2704
|
+
<Button
|
|
2705
|
+
asChild
|
|
2706
|
+
type="button"
|
|
2707
|
+
variant="outline"
|
|
2708
|
+
size="sm"
|
|
2709
|
+
>
|
|
2710
|
+
<a
|
|
2711
|
+
href={
|
|
2712
|
+
connectionsQuery.data.connectPath ||
|
|
2713
|
+
connectionsQuery.data.connectionsPath ||
|
|
2714
|
+
connectionsHref
|
|
2715
|
+
}
|
|
2716
|
+
>
|
|
2717
|
+
{t("creativeContext.connectProvider")}
|
|
2718
|
+
<IconArrowUpRight />
|
|
2719
|
+
</a>
|
|
2720
|
+
</Button>
|
|
2721
|
+
</div>
|
|
2722
|
+
) : connectionsQuery.data?.needsPicker ? (
|
|
2723
|
+
<label className="block space-y-1.5 text-xs font-medium">
|
|
2724
|
+
<span>{t("creativeContext.chooseConnection")}</span>
|
|
2725
|
+
<Select
|
|
2726
|
+
value={selectedConnectionId}
|
|
2727
|
+
onValueChange={setSelectedConnectionId}
|
|
2728
|
+
>
|
|
2729
|
+
<SelectTrigger>
|
|
2730
|
+
<SelectValue
|
|
2731
|
+
placeholder={t(
|
|
2732
|
+
"creativeContext.chooseConnection",
|
|
2733
|
+
)}
|
|
2734
|
+
/>
|
|
2735
|
+
</SelectTrigger>
|
|
2736
|
+
<SelectContent>
|
|
2737
|
+
{connectionsQuery.data.connections.map(
|
|
2738
|
+
(connection) => (
|
|
2739
|
+
<SelectItem
|
|
2740
|
+
key={connection.connectionId}
|
|
2741
|
+
value={connection.connectionId}
|
|
2742
|
+
>
|
|
2743
|
+
{connection.label}
|
|
2744
|
+
</SelectItem>
|
|
2745
|
+
),
|
|
2746
|
+
)}
|
|
2747
|
+
</SelectContent>
|
|
2748
|
+
</Select>
|
|
2749
|
+
</label>
|
|
2750
|
+
) : null}
|
|
1733
2751
|
</div>
|
|
1734
|
-
) : connectionsQuery.data?.needsPicker ? (
|
|
1735
|
-
<label className="block space-y-1.5 text-xs font-medium">
|
|
1736
|
-
<span>{t("creativeContext.chooseConnection")}</span>
|
|
1737
|
-
<Select
|
|
1738
|
-
value={selectedConnectionId}
|
|
1739
|
-
onValueChange={setSelectedConnectionId}
|
|
1740
|
-
>
|
|
1741
|
-
<SelectTrigger>
|
|
1742
|
-
<SelectValue
|
|
1743
|
-
placeholder={t(
|
|
1744
|
-
"creativeContext.chooseConnection",
|
|
1745
|
-
)}
|
|
1746
|
-
/>
|
|
1747
|
-
</SelectTrigger>
|
|
1748
|
-
<SelectContent>
|
|
1749
|
-
{connectionsQuery.data.connections.map(
|
|
1750
|
-
(connection) => (
|
|
1751
|
-
<SelectItem
|
|
1752
|
-
key={connection.connectionId}
|
|
1753
|
-
value={connection.connectionId}
|
|
1754
|
-
>
|
|
1755
|
-
{connection.label}
|
|
1756
|
-
</SelectItem>
|
|
1757
|
-
),
|
|
1758
|
-
)}
|
|
1759
|
-
</SelectContent>
|
|
1760
|
-
</Select>
|
|
1761
|
-
</label>
|
|
1762
2752
|
) : null}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2753
|
+
{recommendationProvider && selectedConnectionId ? (
|
|
2754
|
+
<div className="mb-3 rounded-md border border-border p-3">
|
|
2755
|
+
{connectorKind === "google-slides" ? (
|
|
2756
|
+
<div className="mb-3 flex flex-wrap items-center justify-between gap-3 border-b border-border/60 pb-3">
|
|
2757
|
+
<p className="max-w-lg text-xs text-muted-foreground">
|
|
2758
|
+
{t("creativeContext.googlePickerDescription")}
|
|
2759
|
+
</p>
|
|
2760
|
+
<Button
|
|
2761
|
+
type="button"
|
|
2762
|
+
variant="outline"
|
|
2763
|
+
size="sm"
|
|
2764
|
+
disabled={openingGooglePicker}
|
|
2765
|
+
onClick={() => void chooseGoogleSlides()}
|
|
2766
|
+
>
|
|
2767
|
+
<IconSlideshow />
|
|
2768
|
+
{openingGooglePicker
|
|
2769
|
+
? t("creativeContext.loading")
|
|
2770
|
+
: t("creativeContext.choosePresentations")}
|
|
2771
|
+
</Button>
|
|
2772
|
+
</div>
|
|
2773
|
+
) : null}
|
|
2774
|
+
{recommendationsQuery.isLoading &&
|
|
2775
|
+
!availableRecommendations.length ? (
|
|
2776
|
+
<Skeleton className="h-16 w-full" />
|
|
2777
|
+
) : availableRecommendations.length ? (
|
|
2778
|
+
<div className="space-y-2">
|
|
2779
|
+
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
|
2780
|
+
<span>
|
|
2781
|
+
{t("creativeContext.discoveredItems", {
|
|
2782
|
+
count: formatNumber(
|
|
2783
|
+
availableRecommendations.length,
|
|
2784
|
+
),
|
|
2785
|
+
})}
|
|
2786
|
+
</span>
|
|
2787
|
+
<span>
|
|
2788
|
+
{t("creativeContext.selectedItems", {
|
|
2789
|
+
count: formatNumber(
|
|
2790
|
+
selectedRecommendationIds.size,
|
|
2791
|
+
),
|
|
2792
|
+
})}
|
|
2793
|
+
</span>
|
|
2794
|
+
</div>
|
|
2795
|
+
<div className="max-h-52 divide-y divide-border/60 overflow-y-auto">
|
|
2796
|
+
{availableRecommendations.map(
|
|
2797
|
+
(recommendation) => (
|
|
2798
|
+
<label
|
|
2799
|
+
key={recommendation.externalId}
|
|
2800
|
+
className="flex cursor-pointer items-start gap-3 py-2"
|
|
2801
|
+
>
|
|
2802
|
+
<Checkbox
|
|
2803
|
+
className="mt-0.5"
|
|
2804
|
+
checked={selectedRecommendationIds.has(
|
|
2805
|
+
recommendation.externalId,
|
|
2806
|
+
)}
|
|
2807
|
+
onCheckedChange={(checked) =>
|
|
2808
|
+
setSelectedRecommendationIds(
|
|
2809
|
+
(current) => {
|
|
2810
|
+
const next = new Set(current);
|
|
2811
|
+
if (checked) {
|
|
2812
|
+
next.add(
|
|
2813
|
+
recommendation.externalId,
|
|
2814
|
+
);
|
|
2815
|
+
} else {
|
|
2816
|
+
next.delete(
|
|
2817
|
+
recommendation.externalId,
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
return next;
|
|
2821
|
+
},
|
|
2822
|
+
)
|
|
2823
|
+
}
|
|
2824
|
+
/>
|
|
2825
|
+
<span className="min-w-0">
|
|
2826
|
+
<span className="block truncate text-sm">
|
|
2827
|
+
{recommendation.title}
|
|
2828
|
+
</span>
|
|
2829
|
+
<span className="block text-xs text-muted-foreground">
|
|
2830
|
+
{recommendation.containerRef ??
|
|
2831
|
+
recommendation.kind}
|
|
2832
|
+
</span>
|
|
2833
|
+
</span>
|
|
2834
|
+
</label>
|
|
2835
|
+
),
|
|
2836
|
+
)}
|
|
2837
|
+
</div>
|
|
2838
|
+
</div>
|
|
2839
|
+
) : (
|
|
2840
|
+
<p className="text-xs text-muted-foreground">
|
|
2841
|
+
{recommendationsQuery.data?.unavailableReason ??
|
|
2842
|
+
t("creativeContext.unavailable")}
|
|
2843
|
+
</p>
|
|
2844
|
+
)}
|
|
1784
2845
|
</div>
|
|
1785
2846
|
) : null}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
<span>
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2847
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
2848
|
+
<label className="space-y-1.5 text-xs font-medium">
|
|
2849
|
+
<span>{t("creativeContext.sourceName")}</span>
|
|
2850
|
+
<Input
|
|
2851
|
+
value={sourceName}
|
|
2852
|
+
onChange={(event) =>
|
|
2853
|
+
setSourceName(event.target.value)
|
|
2854
|
+
}
|
|
2855
|
+
required
|
|
2856
|
+
/>
|
|
2857
|
+
</label>
|
|
2858
|
+
{selectedConnector.kind === "upload" ? (
|
|
2859
|
+
<div className="space-y-1.5 text-xs font-medium">
|
|
2860
|
+
<span>{t("creativeContext.sourceReference")}</span>
|
|
2861
|
+
<input
|
|
2862
|
+
ref={fileInputRef}
|
|
2863
|
+
type="file"
|
|
2864
|
+
multiple
|
|
2865
|
+
accept=".pptx,.docx,.pdf,.png,.jpg,.jpeg,.webp,.gif,.svg,application/pdf,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg,image/webp,image/gif,image/svg+xml"
|
|
2866
|
+
className="hidden"
|
|
2867
|
+
onChange={chooseFiles}
|
|
2868
|
+
/>
|
|
2869
|
+
<input
|
|
2870
|
+
ref={folderInputRef}
|
|
2871
|
+
type="file"
|
|
2872
|
+
multiple
|
|
2873
|
+
className="hidden"
|
|
2874
|
+
onChange={chooseFiles}
|
|
2875
|
+
{...({ webkitdirectory: "", directory: "" } as {
|
|
2876
|
+
webkitdirectory: string;
|
|
2877
|
+
directory: string;
|
|
1804
2878
|
})}
|
|
1805
|
-
|
|
2879
|
+
/>
|
|
2880
|
+
<div
|
|
2881
|
+
onDragOver={(event) => event.preventDefault()}
|
|
2882
|
+
onDrop={dropFiles}
|
|
2883
|
+
className="rounded-md border border-dashed border-border p-4"
|
|
2884
|
+
>
|
|
2885
|
+
<p className="text-xs text-muted-foreground">
|
|
2886
|
+
{t("creativeContext.dropFiles")}
|
|
2887
|
+
</p>
|
|
2888
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
2889
|
+
<Button
|
|
2890
|
+
type="button"
|
|
2891
|
+
variant="outline"
|
|
2892
|
+
size="sm"
|
|
2893
|
+
onClick={() => fileInputRef.current?.click()}
|
|
2894
|
+
>
|
|
2895
|
+
{t("creativeContext.chooseFiles")}
|
|
2896
|
+
</Button>
|
|
2897
|
+
<Button
|
|
2898
|
+
type="button"
|
|
2899
|
+
variant="outline"
|
|
2900
|
+
size="sm"
|
|
2901
|
+
onClick={() => folderInputRef.current?.click()}
|
|
2902
|
+
>
|
|
2903
|
+
{t("creativeContext.chooseFolder")}
|
|
2904
|
+
</Button>
|
|
2905
|
+
</div>
|
|
2906
|
+
{uploadedFiles.length ? (
|
|
2907
|
+
<ul className="mt-3 space-y-1 text-xs text-muted-foreground">
|
|
2908
|
+
{uploadedFiles.map((file) => (
|
|
2909
|
+
<li key={file.id} className="truncate">
|
|
2910
|
+
{file.fileName}
|
|
2911
|
+
</li>
|
|
2912
|
+
))}
|
|
2913
|
+
</ul>
|
|
2914
|
+
) : null}
|
|
2915
|
+
</div>
|
|
1806
2916
|
</div>
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2917
|
+
) : (
|
|
2918
|
+
<label className="space-y-1.5 text-xs font-medium">
|
|
2919
|
+
<span>{t("creativeContext.sourceReference")}</span>
|
|
2920
|
+
{selectedConnector.kind === "website" ||
|
|
2921
|
+
selectedConnector.kind === "figma" ||
|
|
2922
|
+
selectedConnector.kind === "notion" ? (
|
|
2923
|
+
<Textarea
|
|
2924
|
+
value={sourceReference}
|
|
2925
|
+
onChange={(event) =>
|
|
2926
|
+
setSourceReference(event.target.value)
|
|
2927
|
+
}
|
|
2928
|
+
placeholder={
|
|
2929
|
+
selectedConnector.referencePlaceholder
|
|
2930
|
+
}
|
|
2931
|
+
required={selectedConnector.referenceRequired}
|
|
2932
|
+
rows={3}
|
|
2933
|
+
/>
|
|
2934
|
+
) : (
|
|
2935
|
+
<Input
|
|
2936
|
+
value={sourceReference}
|
|
2937
|
+
onChange={(event) =>
|
|
2938
|
+
setSourceReference(event.target.value)
|
|
2939
|
+
}
|
|
2940
|
+
placeholder={
|
|
2941
|
+
selectedConnector.referencePlaceholder
|
|
2942
|
+
}
|
|
2943
|
+
required={selectedConnector.referenceRequired}
|
|
2944
|
+
/>
|
|
2945
|
+
)}
|
|
2946
|
+
</label>
|
|
2947
|
+
)}
|
|
2948
|
+
</div>
|
|
2949
|
+
<div className="mt-3 flex justify-end gap-2">
|
|
2950
|
+
<Button
|
|
2951
|
+
type="button"
|
|
2952
|
+
variant="ghost"
|
|
2953
|
+
onClick={() => setConnectorKind(null)}
|
|
2954
|
+
>
|
|
2955
|
+
{t("creativeContext.cancel")}
|
|
2956
|
+
</Button>
|
|
2957
|
+
<Button
|
|
2958
|
+
type="submit"
|
|
2959
|
+
disabled={
|
|
2960
|
+
manageSource.isPending ||
|
|
2961
|
+
uploadResource.isPending ||
|
|
2962
|
+
(selectedConnector.kind === "upload" &&
|
|
2963
|
+
!uploadedFiles.length) ||
|
|
2964
|
+
(Boolean(connectionProvider) &&
|
|
2965
|
+
!selectedConnectionId) ||
|
|
2966
|
+
(selectedConnector.referenceRequired &&
|
|
2967
|
+
!sourceReference.trim()) ||
|
|
2968
|
+
(Boolean(recommendationProvider) &&
|
|
2969
|
+
!sourceReference.trim() &&
|
|
2970
|
+
!selectedRecommendationIds.size)
|
|
2971
|
+
}
|
|
2972
|
+
>
|
|
2973
|
+
<IconFileImport />
|
|
2974
|
+
{t("creativeContext.preview")}
|
|
2975
|
+
</Button>
|
|
2976
|
+
</div>
|
|
2977
|
+
</form>
|
|
2978
|
+
) : null}
|
|
2979
|
+
|
|
2980
|
+
{previewSourceId ? (
|
|
2981
|
+
<div className="mt-4 rounded-md border border-border p-4">
|
|
2982
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
2983
|
+
<div>
|
|
2984
|
+
<h3 className="text-sm font-semibold">
|
|
2985
|
+
{previewSourceName}
|
|
2986
|
+
</h3>
|
|
2987
|
+
{previewQuery.isLoading ? (
|
|
2988
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2989
|
+
{t("creativeContext.loading")}
|
|
2990
|
+
</p>
|
|
2991
|
+
) : (
|
|
2992
|
+
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
2993
|
+
<span>
|
|
2994
|
+
{t("creativeContext.discoveredItems", {
|
|
2995
|
+
count: formatNumber(
|
|
2996
|
+
previewQuery.data?.total ??
|
|
2997
|
+
previewQuery.data?.items.length ??
|
|
2998
|
+
0,
|
|
2999
|
+
),
|
|
3000
|
+
})}
|
|
3001
|
+
</span>
|
|
3002
|
+
<span>
|
|
3003
|
+
{t("creativeContext.selectedItems", {
|
|
3004
|
+
count: formatNumber(
|
|
3005
|
+
selectedPreviewItemIds.size,
|
|
3006
|
+
),
|
|
3007
|
+
})}
|
|
3008
|
+
</span>
|
|
3009
|
+
</div>
|
|
3010
|
+
)}
|
|
3011
|
+
</div>
|
|
3012
|
+
<Button
|
|
3013
|
+
type="button"
|
|
3014
|
+
disabled={
|
|
3015
|
+
previewQuery.isLoading ||
|
|
3016
|
+
!previewQuery.data?.items.length ||
|
|
3017
|
+
!selectedPreviewItemIds.size ||
|
|
3018
|
+
startImport.isPending ||
|
|
3019
|
+
importJob?.status === "queued" ||
|
|
3020
|
+
importJob?.status === "running"
|
|
3021
|
+
}
|
|
3022
|
+
onClick={() => void beginImport()}
|
|
3023
|
+
>
|
|
3024
|
+
<IconFileImport />
|
|
3025
|
+
{startImport.isPending
|
|
3026
|
+
? t("creativeContext.importing")
|
|
3027
|
+
: t("creativeContext.startImport")}
|
|
3028
|
+
</Button>
|
|
3029
|
+
</div>
|
|
3030
|
+
{previewQuery.data?.items.length ? (
|
|
3031
|
+
<div className="mt-3">
|
|
3032
|
+
<div className="flex items-center gap-2 border-y border-border/60 py-2">
|
|
3033
|
+
<Button
|
|
3034
|
+
type="button"
|
|
3035
|
+
variant="ghost"
|
|
3036
|
+
size="sm"
|
|
3037
|
+
onClick={() =>
|
|
3038
|
+
setSelectedPreviewItemIds(
|
|
3039
|
+
new Set(
|
|
3040
|
+
previewQuery.data?.items.map(
|
|
3041
|
+
(item) => item.externalId,
|
|
3042
|
+
) ?? [],
|
|
3043
|
+
),
|
|
3044
|
+
)
|
|
3045
|
+
}
|
|
3046
|
+
>
|
|
3047
|
+
{t("creativeContext.selectAll")}
|
|
3048
|
+
</Button>
|
|
3049
|
+
<Button
|
|
3050
|
+
type="button"
|
|
3051
|
+
variant="ghost"
|
|
3052
|
+
size="sm"
|
|
3053
|
+
onClick={() => setSelectedPreviewItemIds(new Set())}
|
|
3054
|
+
>
|
|
3055
|
+
{t("creativeContext.clearSelection")}
|
|
3056
|
+
</Button>
|
|
3057
|
+
</div>
|
|
3058
|
+
<div className="max-h-80 divide-y divide-border/60 overflow-y-auto">
|
|
3059
|
+
{previewQuery.data.items.map((item) => (
|
|
3060
|
+
<label
|
|
3061
|
+
key={item.externalId}
|
|
3062
|
+
className="flex cursor-pointer items-start gap-3 py-2"
|
|
3063
|
+
>
|
|
3064
|
+
<Checkbox
|
|
3065
|
+
className="mt-0.5"
|
|
3066
|
+
checked={selectedPreviewItemIds.has(
|
|
3067
|
+
item.externalId,
|
|
3068
|
+
)}
|
|
3069
|
+
onCheckedChange={(checked) =>
|
|
3070
|
+
setSelectedPreviewItemIds((current) => {
|
|
3071
|
+
const next = new Set(current);
|
|
3072
|
+
if (checked) next.add(item.externalId);
|
|
3073
|
+
else next.delete(item.externalId);
|
|
3074
|
+
return next;
|
|
3075
|
+
})
|
|
1828
3076
|
}
|
|
1829
3077
|
/>
|
|
1830
3078
|
<span className="min-w-0">
|
|
1831
3079
|
<span className="block truncate text-sm">
|
|
1832
|
-
{
|
|
3080
|
+
{item.title}
|
|
1833
3081
|
</span>
|
|
1834
3082
|
<span className="block text-xs text-muted-foreground">
|
|
1835
|
-
{
|
|
1836
|
-
recommendation.kind}
|
|
3083
|
+
{item.kind}
|
|
1837
3084
|
</span>
|
|
1838
3085
|
</span>
|
|
1839
3086
|
</label>
|
|
1840
3087
|
))}
|
|
1841
3088
|
</div>
|
|
1842
3089
|
</div>
|
|
3090
|
+
) : null}
|
|
3091
|
+
</div>
|
|
3092
|
+
) : null}
|
|
3093
|
+
|
|
3094
|
+
{importJob ? (
|
|
3095
|
+
<div className="mt-4 rounded-md border border-border p-4">
|
|
3096
|
+
<div className="flex items-center gap-2">
|
|
3097
|
+
{importJob.status === "completed" ? (
|
|
3098
|
+
<IconCheck className="size-5 text-emerald-600" />
|
|
3099
|
+
) : importJob.status === "failed" ? (
|
|
3100
|
+
<IconAlertTriangle className="size-5 text-destructive" />
|
|
3101
|
+
) : (
|
|
3102
|
+
<IconRefresh className="size-5 animate-spin text-muted-foreground" />
|
|
3103
|
+
)}
|
|
3104
|
+
<h3 className="text-sm font-semibold">
|
|
3105
|
+
{importJob.status === "completed"
|
|
3106
|
+
? t("creativeContext.importComplete")
|
|
3107
|
+
: importJob.status === "failed"
|
|
3108
|
+
? t("creativeContext.importFailed")
|
|
3109
|
+
: t("creativeContext.importing")}
|
|
3110
|
+
</h3>
|
|
3111
|
+
</div>
|
|
3112
|
+
{importJob.status === "completed" && brandProposal ? (
|
|
3113
|
+
<div className="mt-4 rounded-md bg-muted/50 p-4">
|
|
3114
|
+
<div className="flex items-start gap-3">
|
|
3115
|
+
<IconSparkles className="mt-0.5 size-5 text-muted-foreground" />
|
|
3116
|
+
<div className="min-w-0 flex-1">
|
|
3117
|
+
<h4 className="text-sm font-semibold">
|
|
3118
|
+
{t("creativeContext.brandDnaTitle")}
|
|
3119
|
+
</h4>
|
|
3120
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
3121
|
+
{brandProposal.summary}
|
|
3122
|
+
</p>
|
|
3123
|
+
{brandProposal.colors.length ? (
|
|
3124
|
+
<div className="mt-4">
|
|
3125
|
+
<p className="text-xs font-medium">
|
|
3126
|
+
{t("creativeContext.colors")}
|
|
3127
|
+
</p>
|
|
3128
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
3129
|
+
{brandProposal.colors.map((color) => (
|
|
3130
|
+
<div
|
|
3131
|
+
key={color}
|
|
3132
|
+
className="flex items-center gap-1.5 rounded border border-border bg-background px-2 py-1 text-xs"
|
|
3133
|
+
>
|
|
3134
|
+
<span
|
|
3135
|
+
className="size-4 rounded-sm border border-black/10"
|
|
3136
|
+
style={{ backgroundColor: color }}
|
|
3137
|
+
/>
|
|
3138
|
+
{color}
|
|
3139
|
+
</div>
|
|
3140
|
+
))}
|
|
3141
|
+
</div>
|
|
3142
|
+
</div>
|
|
3143
|
+
) : null}
|
|
3144
|
+
{brandProposal.fonts.length ? (
|
|
3145
|
+
<div className="mt-4">
|
|
3146
|
+
<p className="text-xs font-medium">
|
|
3147
|
+
{t("creativeContext.fonts")}
|
|
3148
|
+
</p>
|
|
3149
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
3150
|
+
{brandProposal.fonts.map((font) => (
|
|
3151
|
+
<Badge key={font} variant="outline">
|
|
3152
|
+
{font}
|
|
3153
|
+
</Badge>
|
|
3154
|
+
))}
|
|
3155
|
+
</div>
|
|
3156
|
+
</div>
|
|
3157
|
+
) : null}
|
|
3158
|
+
{brandLayoutThumbnails.length ? (
|
|
3159
|
+
<div className="mt-4">
|
|
3160
|
+
<p className="text-xs font-medium">
|
|
3161
|
+
{t("creativeContext.layouts")}
|
|
3162
|
+
</p>
|
|
3163
|
+
<div className="mt-2 grid grid-cols-3 gap-2">
|
|
3164
|
+
{brandLayoutThumbnails.map((thumbnail) => (
|
|
3165
|
+
<AccessScopedThumbnail
|
|
3166
|
+
key={thumbnail.itemVersionId}
|
|
3167
|
+
itemId={thumbnail.itemId}
|
|
3168
|
+
itemVersionId={thumbnail.itemVersionId}
|
|
3169
|
+
className="aspect-video w-full rounded border border-border object-cover"
|
|
3170
|
+
/>
|
|
3171
|
+
))}
|
|
3172
|
+
</div>
|
|
3173
|
+
</div>
|
|
3174
|
+
) : null}
|
|
3175
|
+
{brandVoicePreview ? (
|
|
3176
|
+
<div className="mt-4">
|
|
3177
|
+
<p className="text-xs font-medium">
|
|
3178
|
+
{t("creativeContext.voice")}
|
|
3179
|
+
</p>
|
|
3180
|
+
<blockquote className="mt-1 border-s-2 border-border ps-3 text-sm text-muted-foreground">
|
|
3181
|
+
{brandVoicePreview}
|
|
3182
|
+
</blockquote>
|
|
3183
|
+
</div>
|
|
3184
|
+
) : null}
|
|
3185
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
3186
|
+
<Button
|
|
3187
|
+
type="button"
|
|
3188
|
+
disabled={publishBrandDna.isPending}
|
|
3189
|
+
onClick={() => void publishProposal()}
|
|
3190
|
+
>
|
|
3191
|
+
<IconSparkles />
|
|
3192
|
+
{publishBrandDna.isPending
|
|
3193
|
+
? t("creativeContext.applyingBrandContext")
|
|
3194
|
+
: t("creativeContext.applyBrandContext")}
|
|
3195
|
+
</Button>
|
|
3196
|
+
<Button asChild type="button" variant="outline">
|
|
3197
|
+
<a href="/agent">
|
|
3198
|
+
{t("creativeContext.generateWithContext")}
|
|
3199
|
+
<IconArrowUpRight />
|
|
3200
|
+
</a>
|
|
3201
|
+
</Button>
|
|
3202
|
+
</div>
|
|
3203
|
+
{publishedMessage ? (
|
|
3204
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
3205
|
+
{publishedMessage}
|
|
3206
|
+
</p>
|
|
3207
|
+
) : null}
|
|
3208
|
+
</div>
|
|
3209
|
+
</div>
|
|
3210
|
+
</div>
|
|
3211
|
+
) : importJob.error ? (
|
|
3212
|
+
<p className="mt-2 text-sm text-destructive">
|
|
3213
|
+
{importJob.error}
|
|
3214
|
+
</p>
|
|
1843
3215
|
) : (
|
|
1844
|
-
<p className="text-xs text-muted-foreground">
|
|
1845
|
-
{
|
|
1846
|
-
|
|
3216
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
3217
|
+
{formatNumber(importJob.progressCurrent)} /{" "}
|
|
3218
|
+
{formatNumber(importJob.progressTotal ?? 0)}
|
|
1847
3219
|
</p>
|
|
1848
3220
|
)}
|
|
1849
3221
|
</div>
|
|
1850
3222
|
) : null}
|
|
1851
|
-
|
|
1852
|
-
<
|
|
1853
|
-
<span>{t("creativeContext.sourceName")}</span>
|
|
1854
|
-
<Input
|
|
1855
|
-
value={sourceName}
|
|
1856
|
-
onChange={(event) => setSourceName(event.target.value)}
|
|
1857
|
-
required
|
|
1858
|
-
/>
|
|
1859
|
-
</label>
|
|
1860
|
-
{selectedConnector.kind === "upload" ? (
|
|
1861
|
-
<div className="space-y-1.5 text-xs font-medium">
|
|
1862
|
-
<span>{t("creativeContext.sourceReference")}</span>
|
|
1863
|
-
<input
|
|
1864
|
-
ref={fileInputRef}
|
|
1865
|
-
type="file"
|
|
1866
|
-
multiple
|
|
1867
|
-
accept=".pptx,.docx,.pdf,.png,.jpg,.jpeg,.webp,.gif,.svg,application/pdf,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg,image/webp,image/gif,image/svg+xml"
|
|
1868
|
-
className="hidden"
|
|
1869
|
-
onChange={chooseFiles}
|
|
1870
|
-
/>
|
|
1871
|
-
<input
|
|
1872
|
-
ref={folderInputRef}
|
|
1873
|
-
type="file"
|
|
1874
|
-
multiple
|
|
1875
|
-
className="hidden"
|
|
1876
|
-
onChange={chooseFiles}
|
|
1877
|
-
{...({ webkitdirectory: "", directory: "" } as {
|
|
1878
|
-
webkitdirectory: string;
|
|
1879
|
-
directory: string;
|
|
1880
|
-
})}
|
|
1881
|
-
/>
|
|
1882
|
-
<div
|
|
1883
|
-
onDragOver={(event) => event.preventDefault()}
|
|
1884
|
-
onDrop={dropFiles}
|
|
1885
|
-
className="rounded-md border border-dashed border-border p-4"
|
|
1886
|
-
>
|
|
1887
|
-
<p className="text-xs text-muted-foreground">
|
|
1888
|
-
{t("creativeContext.dropFiles")}
|
|
1889
|
-
</p>
|
|
1890
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
1891
|
-
<Button
|
|
1892
|
-
type="button"
|
|
1893
|
-
variant="outline"
|
|
1894
|
-
size="sm"
|
|
1895
|
-
onClick={() => fileInputRef.current?.click()}
|
|
1896
|
-
>
|
|
1897
|
-
{t("creativeContext.chooseFiles")}
|
|
1898
|
-
</Button>
|
|
1899
|
-
<Button
|
|
1900
|
-
type="button"
|
|
1901
|
-
variant="outline"
|
|
1902
|
-
size="sm"
|
|
1903
|
-
onClick={() => folderInputRef.current?.click()}
|
|
1904
|
-
>
|
|
1905
|
-
{t("creativeContext.chooseFolder")}
|
|
1906
|
-
</Button>
|
|
1907
|
-
</div>
|
|
1908
|
-
{uploadedFiles.length ? (
|
|
1909
|
-
<ul className="mt-3 space-y-1 text-xs text-muted-foreground">
|
|
1910
|
-
{uploadedFiles.map((file) => (
|
|
1911
|
-
<li key={file.id} className="truncate">
|
|
1912
|
-
{file.fileName}
|
|
1913
|
-
</li>
|
|
1914
|
-
))}
|
|
1915
|
-
</ul>
|
|
1916
|
-
) : null}
|
|
1917
|
-
</div>
|
|
1918
|
-
</div>
|
|
1919
|
-
) : (
|
|
1920
|
-
<label className="space-y-1.5 text-xs font-medium">
|
|
1921
|
-
<span>{t("creativeContext.sourceReference")}</span>
|
|
1922
|
-
{selectedConnector.kind === "website" ||
|
|
1923
|
-
selectedConnector.kind === "figma" ||
|
|
1924
|
-
selectedConnector.kind === "notion" ? (
|
|
1925
|
-
<Textarea
|
|
1926
|
-
value={sourceReference}
|
|
1927
|
-
onChange={(event) =>
|
|
1928
|
-
setSourceReference(event.target.value)
|
|
1929
|
-
}
|
|
1930
|
-
placeholder={selectedConnector.referencePlaceholder}
|
|
1931
|
-
required={selectedConnector.referenceRequired}
|
|
1932
|
-
rows={3}
|
|
1933
|
-
/>
|
|
1934
|
-
) : (
|
|
1935
|
-
<Input
|
|
1936
|
-
value={sourceReference}
|
|
1937
|
-
onChange={(event) =>
|
|
1938
|
-
setSourceReference(event.target.value)
|
|
1939
|
-
}
|
|
1940
|
-
placeholder={selectedConnector.referencePlaceholder}
|
|
1941
|
-
required={selectedConnector.referenceRequired}
|
|
1942
|
-
/>
|
|
1943
|
-
)}
|
|
1944
|
-
</label>
|
|
1945
|
-
)}
|
|
1946
|
-
</div>
|
|
1947
|
-
<div className="mt-3 flex justify-end gap-2">
|
|
1948
|
-
<Button
|
|
1949
|
-
type="button"
|
|
1950
|
-
variant="ghost"
|
|
1951
|
-
onClick={() => setConnectorKind(null)}
|
|
1952
|
-
>
|
|
1953
|
-
{t("creativeContext.cancel")}
|
|
1954
|
-
</Button>
|
|
1955
|
-
<Button
|
|
1956
|
-
type="submit"
|
|
1957
|
-
disabled={
|
|
1958
|
-
manageSource.isPending ||
|
|
1959
|
-
uploadResource.isPending ||
|
|
1960
|
-
(selectedConnector.kind === "upload" &&
|
|
1961
|
-
!uploadedFiles.length) ||
|
|
1962
|
-
(Boolean(connectionProvider) && !selectedConnectionId) ||
|
|
1963
|
-
(selectedConnector.referenceRequired &&
|
|
1964
|
-
!sourceReference.trim()) ||
|
|
1965
|
-
(Boolean(recommendationProvider) &&
|
|
1966
|
-
!sourceReference.trim() &&
|
|
1967
|
-
!selectedRecommendationIds.size)
|
|
1968
|
-
}
|
|
1969
|
-
>
|
|
1970
|
-
<IconFileImport />
|
|
1971
|
-
{t("creativeContext.preview")}
|
|
1972
|
-
</Button>
|
|
1973
|
-
</div>
|
|
1974
|
-
</form>
|
|
1975
|
-
) : null}
|
|
1976
|
-
|
|
1977
|
-
{previewSourceId ? (
|
|
1978
|
-
<div className="mt-4 rounded-md border border-border p-4">
|
|
1979
|
-
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1980
|
-
<div>
|
|
3223
|
+
{promotionPreview ? (
|
|
3224
|
+
<div className="mt-4 rounded-md border border-border p-4">
|
|
1981
3225
|
<h3 className="text-sm font-semibold">
|
|
1982
|
-
{
|
|
3226
|
+
{t("creativeContext.promoteToOrganization")}
|
|
1983
3227
|
</h3>
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
0,
|
|
1996
|
-
),
|
|
1997
|
-
})}
|
|
1998
|
-
</span>
|
|
1999
|
-
<span>
|
|
2000
|
-
{t("creativeContext.selectedItems", {
|
|
2001
|
-
count: formatNumber(selectedPreviewItemIds.size),
|
|
2002
|
-
})}
|
|
2003
|
-
</span>
|
|
3228
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
3229
|
+
{t("creativeContext.promotionDescription")}
|
|
3230
|
+
</p>
|
|
3231
|
+
<dl className="mt-3 grid gap-2 text-xs sm:grid-cols-3">
|
|
3232
|
+
<div>
|
|
3233
|
+
<dt className="text-muted-foreground">
|
|
3234
|
+
{t("creativeContext.sourceReference")}
|
|
3235
|
+
</dt>
|
|
3236
|
+
<dd className="mt-0.5 truncate font-medium">
|
|
3237
|
+
{promotionPreview.containerRef}
|
|
3238
|
+
</dd>
|
|
2004
3239
|
</div>
|
|
2005
|
-
|
|
3240
|
+
<div>
|
|
3241
|
+
<dt className="text-muted-foreground">
|
|
3242
|
+
{t("creativeContext.itemsLabel", { count: "" })}
|
|
3243
|
+
</dt>
|
|
3244
|
+
<dd className="mt-0.5 font-medium">
|
|
3245
|
+
{formatNumber(promotionPreview.itemCount)}
|
|
3246
|
+
</dd>
|
|
3247
|
+
</div>
|
|
3248
|
+
<div>
|
|
3249
|
+
<dt className="text-muted-foreground">
|
|
3250
|
+
{t("creativeContext.restrictedItems", { count: "" })}
|
|
3251
|
+
</dt>
|
|
3252
|
+
<dd className="mt-0.5 font-medium">
|
|
3253
|
+
{formatNumber(promotionPreview.restrictedItemCount)}
|
|
3254
|
+
</dd>
|
|
3255
|
+
</div>
|
|
3256
|
+
</dl>
|
|
3257
|
+
<Button
|
|
3258
|
+
type="button"
|
|
3259
|
+
className="mt-4"
|
|
3260
|
+
disabled={manageSource.isPending}
|
|
3261
|
+
onClick={() => void confirmPromotion()}
|
|
3262
|
+
>
|
|
3263
|
+
{t("creativeContext.promoteToOrganization")}
|
|
3264
|
+
</Button>
|
|
2006
3265
|
</div>
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
</Button>
|
|
2024
|
-
</div>
|
|
2025
|
-
{previewQuery.data?.items.length ? (
|
|
3266
|
+
) : null}
|
|
3267
|
+
{promotionMessage ? (
|
|
3268
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
3269
|
+
{promotionMessage}
|
|
3270
|
+
</p>
|
|
3271
|
+
) : null}
|
|
3272
|
+
{setupError ? (
|
|
3273
|
+
<p className="mt-2 text-sm text-destructive">{setupError}</p>
|
|
3274
|
+
) : null}
|
|
3275
|
+
</section>
|
|
3276
|
+
|
|
3277
|
+
<section className="border-t border-border/70 pt-6">
|
|
3278
|
+
<h2 className="text-lg font-semibold">
|
|
3279
|
+
{t("creativeContext.sourcesTitle")}
|
|
3280
|
+
</h2>
|
|
3281
|
+
{sources.length ? (
|
|
2026
3282
|
<div className="mt-3">
|
|
2027
|
-
|
|
2028
|
-
<
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
new Set(
|
|
2035
|
-
previewQuery.data?.items.map(
|
|
2036
|
-
(item) => item.externalId,
|
|
2037
|
-
) ?? [],
|
|
2038
|
-
),
|
|
2039
|
-
)
|
|
3283
|
+
{sources.map((source) => (
|
|
3284
|
+
<SourceRow
|
|
3285
|
+
key={source.id}
|
|
3286
|
+
source={source}
|
|
3287
|
+
refreshing={
|
|
3288
|
+
refreshSource.isPending &&
|
|
3289
|
+
refreshSource.variables?.sourceId === source.id
|
|
2040
3290
|
}
|
|
2041
|
-
|
|
2042
|
-
{
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
))}
|
|
2083
|
-
</div>
|
|
3291
|
+
canReview={canManageScope}
|
|
3292
|
+
canPromote={
|
|
3293
|
+
libraryScope === "user" &&
|
|
3294
|
+
Boolean(org?.orgId) &&
|
|
3295
|
+
canManageOrg &&
|
|
3296
|
+
source.visibility === "private"
|
|
3297
|
+
}
|
|
3298
|
+
onRefresh={refresh}
|
|
3299
|
+
onReview={(selected) =>
|
|
3300
|
+
void openItemCuration(selected, "restricted")
|
|
3301
|
+
}
|
|
3302
|
+
onCurate={(selected) => void openItemCuration(selected)}
|
|
3303
|
+
onPromote={(selected) =>
|
|
3304
|
+
void previewSourcePromotion(selected)
|
|
3305
|
+
}
|
|
3306
|
+
onPause={(selected) => void pauseSource(selected)}
|
|
3307
|
+
onRestore={(selected) => void restoreSource(selected)}
|
|
3308
|
+
onDelete={setDeleteSource}
|
|
3309
|
+
/>
|
|
3310
|
+
))}
|
|
3311
|
+
</div>
|
|
3312
|
+
) : (
|
|
3313
|
+
<div className="mt-4 rounded-md border border-dashed border-border p-6 text-center">
|
|
3314
|
+
<IconBooks className="mx-auto size-7 text-muted-foreground" />
|
|
3315
|
+
<h3 className="mt-3 text-sm font-semibold">
|
|
3316
|
+
{t("creativeContext.noSourcesTitle")}
|
|
3317
|
+
</h3>
|
|
3318
|
+
<p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
|
|
3319
|
+
{t("creativeContext.noSourcesDescription")}
|
|
3320
|
+
</p>
|
|
3321
|
+
<Button
|
|
3322
|
+
asChild
|
|
3323
|
+
variant="outline"
|
|
3324
|
+
size="sm"
|
|
3325
|
+
className="mt-4"
|
|
3326
|
+
>
|
|
3327
|
+
<a href={connectionsHref}>
|
|
3328
|
+
{t("creativeContext.connectSources")}
|
|
3329
|
+
<IconArrowUpRight />
|
|
3330
|
+
</a>
|
|
3331
|
+
</Button>
|
|
2084
3332
|
</div>
|
|
3333
|
+
)}
|
|
3334
|
+
{reviewSource ? (
|
|
3335
|
+
<ItemCuration
|
|
3336
|
+
source={reviewSource}
|
|
3337
|
+
items={reviewedItems}
|
|
3338
|
+
busy={reviewItems.isPending}
|
|
3339
|
+
onReview={(operation, itemId) =>
|
|
3340
|
+
void reviewContextItem(operation, itemId)
|
|
3341
|
+
}
|
|
3342
|
+
onClose={() => {
|
|
3343
|
+
setReviewSource(null);
|
|
3344
|
+
setReviewedItems([]);
|
|
3345
|
+
}}
|
|
3346
|
+
/>
|
|
2085
3347
|
) : null}
|
|
2086
|
-
|
|
2087
|
-
|
|
3348
|
+
{reviewError ? (
|
|
3349
|
+
<p className="mt-2 text-sm text-destructive">{reviewError}</p>
|
|
3350
|
+
) : null}
|
|
3351
|
+
{refreshMessage ? (
|
|
3352
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
3353
|
+
{refreshMessage}
|
|
3354
|
+
</p>
|
|
3355
|
+
) : null}
|
|
3356
|
+
{lifecycleMessage ? (
|
|
3357
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
3358
|
+
{lifecycleMessage}
|
|
3359
|
+
</p>
|
|
3360
|
+
) : null}
|
|
3361
|
+
</section>
|
|
3362
|
+
</>
|
|
3363
|
+
) : null}
|
|
2088
3364
|
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
)
|
|
2097
|
-
<
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
3365
|
+
{libraryView === "settings" ? (
|
|
3366
|
+
<section className="border-t border-border/70 pt-6">
|
|
3367
|
+
<h2 className="text-lg font-semibold">
|
|
3368
|
+
{t("creativeContext.packsTitle")}
|
|
3369
|
+
</h2>
|
|
3370
|
+
{packs.length ? (
|
|
3371
|
+
<div className="mt-3">
|
|
3372
|
+
{packs.map((pack) => (
|
|
3373
|
+
<PackRow
|
|
3374
|
+
key={pack.id}
|
|
3375
|
+
pack={pack}
|
|
3376
|
+
pinned={contextState.state.pinnedPackId === pack.id}
|
|
3377
|
+
disabled={savingState}
|
|
3378
|
+
onPin={(packId) => void changePinnedPack(packId)}
|
|
3379
|
+
onDetails={setSelectedPackId}
|
|
3380
|
+
/>
|
|
3381
|
+
))}
|
|
2106
3382
|
</div>
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
3383
|
+
) : (
|
|
3384
|
+
<p className="mt-3 rounded-md border border-dashed border-border p-5 text-sm text-muted-foreground">
|
|
3385
|
+
{t("creativeContext.noPacks")}
|
|
3386
|
+
</p>
|
|
3387
|
+
)}
|
|
3388
|
+
{selectedPackId ? (
|
|
3389
|
+
<div className="mt-4 rounded-md border border-border p-4">
|
|
3390
|
+
<div className="flex items-start justify-between gap-3">
|
|
3391
|
+
<div>
|
|
3392
|
+
<h3 className="text-sm font-semibold">
|
|
3393
|
+
{packQuery.data?.pack?.name ??
|
|
3394
|
+
t("creativeContext.packDetails")}
|
|
3395
|
+
</h3>
|
|
3396
|
+
{packQuery.data?.pack?.derivedFromPackId ? (
|
|
3397
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
3398
|
+
{t("creativeContext.influence")}:{" "}
|
|
3399
|
+
{packQuery.data.pack.derivedFromPackId}
|
|
2117
3400
|
</p>
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
</div>
|
|
2138
|
-
) : null}
|
|
2139
|
-
{brandProposal.fonts.length ? (
|
|
2140
|
-
<div className="mt-4">
|
|
2141
|
-
<p className="text-xs font-medium">
|
|
2142
|
-
{t("creativeContext.fonts")}
|
|
2143
|
-
</p>
|
|
2144
|
-
<div className="mt-2 flex flex-wrap gap-2">
|
|
2145
|
-
{brandProposal.fonts.map((font) => (
|
|
2146
|
-
<Badge key={font} variant="outline">
|
|
2147
|
-
{font}
|
|
2148
|
-
</Badge>
|
|
2149
|
-
))}
|
|
2150
|
-
</div>
|
|
2151
|
-
</div>
|
|
2152
|
-
) : null}
|
|
2153
|
-
{brandLayoutThumbnails.length ? (
|
|
2154
|
-
<div className="mt-4">
|
|
2155
|
-
<p className="text-xs font-medium">
|
|
2156
|
-
{t("creativeContext.layouts")}
|
|
2157
|
-
</p>
|
|
2158
|
-
<div className="mt-2 grid grid-cols-3 gap-2">
|
|
2159
|
-
{brandLayoutThumbnails.map((thumbnail) => (
|
|
2160
|
-
<AccessScopedThumbnail
|
|
2161
|
-
key={thumbnail.itemVersionId}
|
|
2162
|
-
itemId={thumbnail.itemId}
|
|
2163
|
-
itemVersionId={thumbnail.itemVersionId}
|
|
2164
|
-
className="aspect-video w-full rounded border border-border object-cover"
|
|
2165
|
-
/>
|
|
2166
|
-
))}
|
|
2167
|
-
</div>
|
|
2168
|
-
</div>
|
|
2169
|
-
) : null}
|
|
2170
|
-
{brandVoicePreview ? (
|
|
2171
|
-
<div className="mt-4">
|
|
2172
|
-
<p className="text-xs font-medium">
|
|
2173
|
-
{t("creativeContext.voice")}
|
|
3401
|
+
) : null}
|
|
3402
|
+
</div>
|
|
3403
|
+
<Button
|
|
3404
|
+
type="button"
|
|
3405
|
+
variant="ghost"
|
|
3406
|
+
size="sm"
|
|
3407
|
+
onClick={() => setSelectedPackId(null)}
|
|
3408
|
+
>
|
|
3409
|
+
{t("creativeContext.cancel")}
|
|
3410
|
+
</Button>
|
|
3411
|
+
</div>
|
|
3412
|
+
{packQuery.data?.pack?.members.length ? (
|
|
3413
|
+
<div className="mt-3 divide-y divide-border/60">
|
|
3414
|
+
{packQuery.data.pack.members.map((member) => (
|
|
3415
|
+
<div key={member.id} className="py-2 text-xs">
|
|
3416
|
+
<p className="font-medium">{member.itemId}</p>
|
|
3417
|
+
{member.reason ? (
|
|
3418
|
+
<p className="mt-0.5 text-muted-foreground">
|
|
3419
|
+
{t("creativeContext.influence")}: {member.reason}
|
|
2174
3420
|
</p>
|
|
2175
|
-
|
|
2176
|
-
{brandVoicePreview}
|
|
2177
|
-
</blockquote>
|
|
2178
|
-
</div>
|
|
2179
|
-
) : null}
|
|
2180
|
-
<div className="mt-4 flex flex-wrap gap-2">
|
|
2181
|
-
<Button
|
|
2182
|
-
type="button"
|
|
2183
|
-
disabled={publishBrandDna.isPending}
|
|
2184
|
-
onClick={() => void publishProposal()}
|
|
2185
|
-
>
|
|
2186
|
-
<IconSparkles />
|
|
2187
|
-
{publishBrandDna.isPending
|
|
2188
|
-
? t("creativeContext.applyingBrandContext")
|
|
2189
|
-
: t("creativeContext.applyBrandContext")}
|
|
2190
|
-
</Button>
|
|
2191
|
-
<Button asChild type="button" variant="outline">
|
|
2192
|
-
<a href="/agent">
|
|
2193
|
-
{t("creativeContext.generateWithContext")}
|
|
2194
|
-
<IconArrowUpRight />
|
|
2195
|
-
</a>
|
|
2196
|
-
</Button>
|
|
3421
|
+
) : null}
|
|
2197
3422
|
</div>
|
|
2198
|
-
|
|
2199
|
-
<p className="mt-2 text-xs text-muted-foreground">
|
|
2200
|
-
{publishedMessage}
|
|
2201
|
-
</p>
|
|
2202
|
-
) : null}
|
|
2203
|
-
</div>
|
|
3423
|
+
))}
|
|
2204
3424
|
</div>
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
) : null}
|
|
2218
|
-
{promotionPreview ? (
|
|
2219
|
-
<div className="mt-4 rounded-md border border-border p-4">
|
|
2220
|
-
<h3 className="text-sm font-semibold">
|
|
2221
|
-
{t("creativeContext.promoteToOrganization")}
|
|
2222
|
-
</h3>
|
|
3425
|
+
) : null}
|
|
3426
|
+
</div>
|
|
3427
|
+
) : null}
|
|
3428
|
+
</section>
|
|
3429
|
+
) : null}
|
|
3430
|
+
|
|
3431
|
+
{libraryView === "items" ? (
|
|
3432
|
+
<section className="border-t border-border/70 pt-6">
|
|
3433
|
+
<div>
|
|
3434
|
+
<h2 className="text-lg font-semibold">
|
|
3435
|
+
{t("creativeContext.searchTitle")}
|
|
3436
|
+
</h2>
|
|
2223
3437
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
2224
|
-
{t("creativeContext.
|
|
3438
|
+
{t("creativeContext.searchDescription")}
|
|
2225
3439
|
</p>
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
</div>
|
|
2243
|
-
<div>
|
|
2244
|
-
<dt className="text-muted-foreground">
|
|
2245
|
-
{t("creativeContext.restrictedItems", { count: "" })}
|
|
2246
|
-
</dt>
|
|
2247
|
-
<dd className="mt-0.5 font-medium">
|
|
2248
|
-
{formatNumber(promotionPreview.restrictedItemCount)}
|
|
2249
|
-
</dd>
|
|
2250
|
-
</div>
|
|
2251
|
-
</dl>
|
|
3440
|
+
</div>
|
|
3441
|
+
<form
|
|
3442
|
+
className="mt-4 flex gap-2"
|
|
3443
|
+
onSubmit={(event) => void search(event)}
|
|
3444
|
+
>
|
|
3445
|
+
<div className="relative min-w-0 flex-1">
|
|
3446
|
+
<IconSearch className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
3447
|
+
<Input
|
|
3448
|
+
type="search"
|
|
3449
|
+
value={query}
|
|
3450
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
3451
|
+
placeholder={t("creativeContext.searchPlaceholder")}
|
|
3452
|
+
aria-label={t("creativeContext.searchPlaceholder")}
|
|
3453
|
+
className="ps-9"
|
|
3454
|
+
/>
|
|
3455
|
+
</div>
|
|
2252
3456
|
<Button
|
|
2253
|
-
type="
|
|
2254
|
-
|
|
2255
|
-
disabled={
|
|
2256
|
-
|
|
3457
|
+
type="submit"
|
|
3458
|
+
variant="outline"
|
|
3459
|
+
disabled={
|
|
3460
|
+
!query.trim() ||
|
|
3461
|
+
(!sources.length &&
|
|
3462
|
+
!selectedLibraryContextId &&
|
|
3463
|
+
!contextState.state.pinnedPackId) ||
|
|
3464
|
+
searchContext.isPending
|
|
3465
|
+
}
|
|
2257
3466
|
>
|
|
2258
|
-
|
|
3467
|
+
<IconSearch />
|
|
3468
|
+
{t("creativeContext.searchTitle")}
|
|
2259
3469
|
</Button>
|
|
2260
|
-
</
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
) : null}
|
|
2267
|
-
{setupError ? (
|
|
2268
|
-
<p className="mt-2 text-sm text-destructive">{setupError}</p>
|
|
2269
|
-
) : null}
|
|
2270
|
-
</section>
|
|
2271
|
-
|
|
2272
|
-
<section className="border-t border-border/70 pt-6">
|
|
2273
|
-
<h2 className="text-lg font-semibold">
|
|
2274
|
-
{t("creativeContext.sourcesTitle")}
|
|
2275
|
-
</h2>
|
|
2276
|
-
{sources.length ? (
|
|
2277
|
-
<div className="mt-3">
|
|
2278
|
-
{sources.map((source) => (
|
|
2279
|
-
<SourceRow
|
|
2280
|
-
key={source.id}
|
|
2281
|
-
source={source}
|
|
2282
|
-
refreshing={
|
|
2283
|
-
refreshSource.isPending &&
|
|
2284
|
-
refreshSource.variables?.sourceId === source.id
|
|
2285
|
-
}
|
|
2286
|
-
canReview={canManageScope}
|
|
2287
|
-
canPromote={
|
|
2288
|
-
libraryScope === "user" &&
|
|
2289
|
-
Boolean(org?.orgId) &&
|
|
2290
|
-
canManageOrg &&
|
|
2291
|
-
source.visibility === "private"
|
|
2292
|
-
}
|
|
2293
|
-
onRefresh={refresh}
|
|
2294
|
-
onReview={(selected) =>
|
|
2295
|
-
void openItemCuration(selected, "restricted")
|
|
2296
|
-
}
|
|
2297
|
-
onCurate={(selected) => void openItemCuration(selected)}
|
|
2298
|
-
onPromote={(selected) =>
|
|
2299
|
-
void previewSourcePromotion(selected)
|
|
2300
|
-
}
|
|
2301
|
-
onPause={(selected) => void pauseSource(selected)}
|
|
2302
|
-
onRestore={(selected) => void restoreSource(selected)}
|
|
2303
|
-
onDelete={setDeleteSource}
|
|
2304
|
-
/>
|
|
2305
|
-
))}
|
|
2306
|
-
</div>
|
|
2307
|
-
) : (
|
|
2308
|
-
<div className="mt-4 rounded-md border border-dashed border-border p-6 text-center">
|
|
2309
|
-
<IconBooks className="mx-auto size-7 text-muted-foreground" />
|
|
2310
|
-
<h3 className="mt-3 text-sm font-semibold">
|
|
2311
|
-
{t("creativeContext.noSourcesTitle")}
|
|
2312
|
-
</h3>
|
|
2313
|
-
<p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
|
|
2314
|
-
{t("creativeContext.noSourcesDescription")}
|
|
3470
|
+
</form>
|
|
3471
|
+
{searchError ? (
|
|
3472
|
+
<p className="mt-4 text-sm text-destructive">{searchError}</p>
|
|
3473
|
+
) : !searchContext.data && !searchContext.isPending ? (
|
|
3474
|
+
<p className="mt-4 text-sm text-muted-foreground">
|
|
3475
|
+
{t("creativeContext.searchPrompt")}
|
|
2315
3476
|
</p>
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
</a>
|
|
2321
|
-
</Button>
|
|
2322
|
-
</div>
|
|
2323
|
-
)}
|
|
2324
|
-
{reviewSource ? (
|
|
2325
|
-
<ItemCuration
|
|
2326
|
-
source={reviewSource}
|
|
2327
|
-
items={reviewedItems}
|
|
2328
|
-
busy={reviewItems.isPending}
|
|
2329
|
-
onReview={(operation, itemId) =>
|
|
2330
|
-
void reviewContextItem(operation, itemId)
|
|
2331
|
-
}
|
|
2332
|
-
onClose={() => {
|
|
2333
|
-
setReviewSource(null);
|
|
2334
|
-
setReviewedItems([]);
|
|
2335
|
-
}}
|
|
2336
|
-
/>
|
|
2337
|
-
) : null}
|
|
2338
|
-
{reviewError ? (
|
|
2339
|
-
<p className="mt-2 text-sm text-destructive">{reviewError}</p>
|
|
2340
|
-
) : null}
|
|
2341
|
-
{refreshMessage ? (
|
|
2342
|
-
<p className="mt-2 text-xs text-muted-foreground">
|
|
2343
|
-
{refreshMessage}
|
|
2344
|
-
</p>
|
|
2345
|
-
) : null}
|
|
2346
|
-
{lifecycleMessage ? (
|
|
2347
|
-
<p className="mt-2 text-xs text-muted-foreground">
|
|
2348
|
-
{lifecycleMessage}
|
|
2349
|
-
</p>
|
|
2350
|
-
) : null}
|
|
2351
|
-
</section>
|
|
2352
|
-
|
|
2353
|
-
<section className="border-t border-border/70 pt-6">
|
|
2354
|
-
<h2 className="text-lg font-semibold">
|
|
2355
|
-
{t("creativeContext.packsTitle")}
|
|
2356
|
-
</h2>
|
|
2357
|
-
{packs.length ? (
|
|
2358
|
-
<div className="mt-3">
|
|
2359
|
-
{packs.map((pack) => (
|
|
2360
|
-
<PackRow
|
|
2361
|
-
key={pack.id}
|
|
2362
|
-
pack={pack}
|
|
2363
|
-
pinned={contextState.state.pinnedPackId === pack.id}
|
|
2364
|
-
disabled={savingState}
|
|
2365
|
-
onPin={(packId) => void changePinnedPack(packId)}
|
|
2366
|
-
onDetails={setSelectedPackId}
|
|
2367
|
-
/>
|
|
2368
|
-
))}
|
|
2369
|
-
</div>
|
|
2370
|
-
) : (
|
|
2371
|
-
<p className="mt-3 rounded-md border border-dashed border-border p-5 text-sm text-muted-foreground">
|
|
2372
|
-
{t("creativeContext.noPacks")}
|
|
2373
|
-
</p>
|
|
2374
|
-
)}
|
|
2375
|
-
{selectedPackId ? (
|
|
2376
|
-
<div className="mt-4 rounded-md border border-border p-4">
|
|
2377
|
-
<div className="flex items-start justify-between gap-3">
|
|
2378
|
-
<div>
|
|
2379
|
-
<h3 className="text-sm font-semibold">
|
|
2380
|
-
{packQuery.data?.pack?.name ??
|
|
2381
|
-
t("creativeContext.packDetails")}
|
|
2382
|
-
</h3>
|
|
2383
|
-
{packQuery.data?.pack?.derivedFromPackId ? (
|
|
2384
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
2385
|
-
{t("creativeContext.influence")}:{" "}
|
|
2386
|
-
{packQuery.data.pack.derivedFromPackId}
|
|
2387
|
-
</p>
|
|
2388
|
-
) : null}
|
|
2389
|
-
</div>
|
|
2390
|
-
<Button
|
|
2391
|
-
type="button"
|
|
2392
|
-
variant="ghost"
|
|
2393
|
-
size="sm"
|
|
2394
|
-
onClick={() => setSelectedPackId(null)}
|
|
2395
|
-
>
|
|
2396
|
-
{t("creativeContext.cancel")}
|
|
2397
|
-
</Button>
|
|
3477
|
+
) : searchContext.isPending ? (
|
|
3478
|
+
<div className="mt-4 space-y-2">
|
|
3479
|
+
<Skeleton className="h-20 w-full" />
|
|
3480
|
+
<Skeleton className="h-20 w-full" />
|
|
2398
3481
|
</div>
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
3482
|
+
) : searchContext.data?.results.length ? (
|
|
3483
|
+
<div className="mt-4 space-y-2">
|
|
3484
|
+
<p className="text-xs text-muted-foreground">
|
|
3485
|
+
{t("creativeContext.resultsLabel", {
|
|
3486
|
+
count: formatNumber(searchContext.data.results.length),
|
|
3487
|
+
})}
|
|
3488
|
+
</p>
|
|
3489
|
+
{searchContext.data.results.map((result) => (
|
|
3490
|
+
<article
|
|
3491
|
+
key={`${result.itemVersionId}:${result.chunkId ?? "item"}`}
|
|
3492
|
+
className="rounded-md border border-border/70 p-3"
|
|
3493
|
+
>
|
|
3494
|
+
<div className="flex items-start justify-between gap-3">
|
|
3495
|
+
<div className="min-w-0">
|
|
3496
|
+
<h3 className="truncate text-sm font-medium">
|
|
3497
|
+
{result.title}
|
|
3498
|
+
</h3>
|
|
3499
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
3500
|
+
{result.sourceName} · {result.kind}
|
|
2407
3501
|
</p>
|
|
3502
|
+
</div>
|
|
3503
|
+
{result.canonicalUrl ? (
|
|
3504
|
+
<Button asChild variant="ghost" size="icon">
|
|
3505
|
+
<a
|
|
3506
|
+
href={result.canonicalUrl}
|
|
3507
|
+
target="_blank"
|
|
3508
|
+
rel="noreferrer"
|
|
3509
|
+
aria-label={result.title}
|
|
3510
|
+
>
|
|
3511
|
+
<IconArrowUpRight />
|
|
3512
|
+
</a>
|
|
3513
|
+
</Button>
|
|
2408
3514
|
) : null}
|
|
2409
3515
|
</div>
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
<h2 className="text-lg font-semibold">
|
|
2420
|
-
{t("creativeContext.searchTitle")}
|
|
2421
|
-
</h2>
|
|
2422
|
-
<p className="mt-1 text-sm text-muted-foreground">
|
|
2423
|
-
{t("creativeContext.searchDescription")}
|
|
2424
|
-
</p>
|
|
2425
|
-
</div>
|
|
2426
|
-
<form
|
|
2427
|
-
className="mt-4 flex gap-2"
|
|
2428
|
-
onSubmit={(event) => void search(event)}
|
|
2429
|
-
>
|
|
2430
|
-
<div className="relative min-w-0 flex-1">
|
|
2431
|
-
<IconSearch className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
2432
|
-
<Input
|
|
2433
|
-
type="search"
|
|
2434
|
-
value={query}
|
|
2435
|
-
onChange={(event) => setQuery(event.target.value)}
|
|
2436
|
-
placeholder={t("creativeContext.searchPlaceholder")}
|
|
2437
|
-
aria-label={t("creativeContext.searchPlaceholder")}
|
|
2438
|
-
className="ps-9"
|
|
2439
|
-
/>
|
|
2440
|
-
</div>
|
|
2441
|
-
<Button
|
|
2442
|
-
type="submit"
|
|
2443
|
-
variant="outline"
|
|
2444
|
-
disabled={
|
|
2445
|
-
!query.trim() || !sources.length || searchContext.isPending
|
|
2446
|
-
}
|
|
2447
|
-
>
|
|
2448
|
-
<IconSearch />
|
|
2449
|
-
{t("creativeContext.searchTitle")}
|
|
2450
|
-
</Button>
|
|
2451
|
-
</form>
|
|
2452
|
-
{searchError ? (
|
|
2453
|
-
<p className="mt-4 text-sm text-destructive">{searchError}</p>
|
|
2454
|
-
) : !searchContext.data && !searchContext.isPending ? (
|
|
2455
|
-
<p className="mt-4 text-sm text-muted-foreground">
|
|
2456
|
-
{t("creativeContext.searchPrompt")}
|
|
2457
|
-
</p>
|
|
2458
|
-
) : searchContext.isPending ? (
|
|
2459
|
-
<div className="mt-4 space-y-2">
|
|
2460
|
-
<Skeleton className="h-20 w-full" />
|
|
2461
|
-
<Skeleton className="h-20 w-full" />
|
|
2462
|
-
</div>
|
|
2463
|
-
) : searchContext.data?.results.length ? (
|
|
2464
|
-
<div className="mt-4 space-y-2">
|
|
2465
|
-
<p className="text-xs text-muted-foreground">
|
|
2466
|
-
{t("creativeContext.resultsLabel", {
|
|
2467
|
-
count: formatNumber(searchContext.data.results.length),
|
|
2468
|
-
})}
|
|
3516
|
+
<p className="mt-2 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
|
|
3517
|
+
{result.excerpt}
|
|
3518
|
+
</p>
|
|
3519
|
+
</article>
|
|
3520
|
+
))}
|
|
3521
|
+
</div>
|
|
3522
|
+
) : (
|
|
3523
|
+
<p className="mt-4 text-sm text-muted-foreground">
|
|
3524
|
+
{t("creativeContext.noResults")}
|
|
2469
3525
|
</p>
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
className="rounded-md border border-border/70 p-3"
|
|
2474
|
-
>
|
|
2475
|
-
<div className="flex items-start justify-between gap-3">
|
|
2476
|
-
<div className="min-w-0">
|
|
2477
|
-
<h3 className="truncate text-sm font-medium">
|
|
2478
|
-
{result.title}
|
|
2479
|
-
</h3>
|
|
2480
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2481
|
-
{result.sourceName} · {result.kind}
|
|
2482
|
-
</p>
|
|
2483
|
-
</div>
|
|
2484
|
-
{result.canonicalUrl ? (
|
|
2485
|
-
<Button asChild variant="ghost" size="icon">
|
|
2486
|
-
<a
|
|
2487
|
-
href={result.canonicalUrl}
|
|
2488
|
-
target="_blank"
|
|
2489
|
-
rel="noreferrer"
|
|
2490
|
-
aria-label={result.title}
|
|
2491
|
-
>
|
|
2492
|
-
<IconArrowUpRight />
|
|
2493
|
-
</a>
|
|
2494
|
-
</Button>
|
|
2495
|
-
) : null}
|
|
2496
|
-
</div>
|
|
2497
|
-
<p className="mt-2 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
|
|
2498
|
-
{result.excerpt}
|
|
2499
|
-
</p>
|
|
2500
|
-
</article>
|
|
2501
|
-
))}
|
|
2502
|
-
</div>
|
|
2503
|
-
) : (
|
|
2504
|
-
<p className="mt-4 text-sm text-muted-foreground">
|
|
2505
|
-
{t("creativeContext.noResults")}
|
|
2506
|
-
</p>
|
|
2507
|
-
)}
|
|
2508
|
-
</section>
|
|
3526
|
+
)}
|
|
3527
|
+
</section>
|
|
3528
|
+
) : null}
|
|
2509
3529
|
</>
|
|
2510
3530
|
)}
|
|
3531
|
+
<ContextPreviewSheet
|
|
3532
|
+
manifest={previewManifest}
|
|
3533
|
+
onOpenChange={(open) => {
|
|
3534
|
+
if (!open) setPreviewManifest(null);
|
|
3535
|
+
}}
|
|
3536
|
+
/>
|
|
3537
|
+
<AlertDialog
|
|
3538
|
+
open={Boolean(membershipUpdateCandidate)}
|
|
3539
|
+
onOpenChange={(open) => {
|
|
3540
|
+
if (!open && !updatingMembershipId)
|
|
3541
|
+
setMembershipUpdateCandidate(null);
|
|
3542
|
+
}}
|
|
3543
|
+
>
|
|
3544
|
+
<AlertDialogContent>
|
|
3545
|
+
<AlertDialogHeader>
|
|
3546
|
+
<AlertDialogTitle>
|
|
3547
|
+
{t("creativeContext.submitUpdateTitle")}
|
|
3548
|
+
</AlertDialogTitle>
|
|
3549
|
+
<AlertDialogDescription>
|
|
3550
|
+
{t("creativeContext.submitUpdateDescription", {
|
|
3551
|
+
name: membershipUpdateCandidate?.title ?? "",
|
|
3552
|
+
})}
|
|
3553
|
+
</AlertDialogDescription>
|
|
3554
|
+
</AlertDialogHeader>
|
|
3555
|
+
<AlertDialogFooter>
|
|
3556
|
+
<AlertDialogCancel disabled={Boolean(updatingMembershipId)}>
|
|
3557
|
+
{t("creativeContext.cancel")}
|
|
3558
|
+
</AlertDialogCancel>
|
|
3559
|
+
<AlertDialogAction
|
|
3560
|
+
disabled={Boolean(updatingMembershipId)}
|
|
3561
|
+
onClick={(event) => {
|
|
3562
|
+
event.preventDefault();
|
|
3563
|
+
void submitLatestContextMembershipUpdate();
|
|
3564
|
+
}}
|
|
3565
|
+
>
|
|
3566
|
+
{updatingMembershipId
|
|
3567
|
+
? t("creativeContext.submittingUpdate")
|
|
3568
|
+
: t("creativeContext.submitUpdate")}
|
|
3569
|
+
</AlertDialogAction>
|
|
3570
|
+
</AlertDialogFooter>
|
|
3571
|
+
</AlertDialogContent>
|
|
3572
|
+
</AlertDialog>
|
|
2511
3573
|
<AlertDialog
|
|
2512
3574
|
open={Boolean(deleteSource)}
|
|
2513
3575
|
onOpenChange={(open) => {
|