@agent-native/core 0.77.24 → 0.78.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/template-analytics.md +5 -0
  4. package/corpus/core/docs/content/tracking.md +45 -0
  5. package/corpus/core/package.json +3 -1
  6. package/corpus/core/src/agent/types.ts +22 -0
  7. package/corpus/core/src/client/AgentPanel.tsx +34 -18
  8. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  9. package/corpus/core/src/client/agent-chat.ts +173 -0
  10. package/corpus/core/src/client/analytics.ts +141 -5
  11. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  12. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  13. package/corpus/core/src/client/composer/types.ts +22 -0
  14. package/corpus/core/src/client/index.ts +14 -0
  15. package/corpus/core/src/client/session-replay.ts +800 -0
  16. package/corpus/core/src/db/schema.ts +12 -0
  17. package/corpus/core/src/deploy/build.ts +75 -0
  18. package/corpus/core/src/private-blob/index.ts +18 -0
  19. package/corpus/core/src/private-blob/registry.ts +241 -0
  20. package/corpus/core/src/private-blob/types.ts +55 -0
  21. package/corpus/core/src/scripts/utils.ts +34 -6
  22. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  23. package/corpus/core/src/server/email.ts +5 -0
  24. package/corpus/core/src/server/index.ts +1 -0
  25. package/corpus/core/src/styles/agent-native.css +24 -0
  26. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
  27. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
  28. package/corpus/templates/analytics/AGENTS.md +32 -1
  29. package/corpus/templates/analytics/DEVELOPING.md +9 -1
  30. package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
  31. package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
  32. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
  33. package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
  34. package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
  35. package/corpus/templates/analytics/actions/navigate.ts +15 -5
  36. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  37. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
  38. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
  39. package/corpus/templates/analytics/actions/view-screen.ts +50 -2
  40. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
  41. package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
  44. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
  45. package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
  48. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
  53. package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
  54. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
  55. package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
  56. package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
  60. package/corpus/templates/analytics/package.json +5 -2
  61. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
  62. package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
  63. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
  64. package/corpus/templates/analytics/server/db/index.ts +12 -0
  65. package/corpus/templates/analytics/server/db/schema.ts +131 -0
  66. package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
  67. package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
  68. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
  69. package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
  73. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
  74. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
  75. package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
  76. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  77. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
  78. package/corpus/templates/analytics/server/plugins/db.ts +200 -0
  79. package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
  80. package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
  81. package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
  82. package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
  83. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
  84. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
  86. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
  87. package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
  88. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  89. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  90. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  91. package/corpus/templates/assets/AGENTS.md +18 -3
  92. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  93. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  94. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  95. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  96. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  97. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  98. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  99. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  100. package/corpus/templates/assets/actions/navigate.ts +1 -1
  101. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  102. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  103. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  104. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  105. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  106. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  107. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  108. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  109. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  110. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  111. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  112. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  113. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  114. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  115. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  116. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  117. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  118. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  119. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  120. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  121. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  122. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  123. package/corpus/templates/assets/server/db/index.ts +1 -1
  124. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  125. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  126. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  127. package/corpus/templates/assets/shared/api.ts +6 -3
  128. package/dist/agent/types.d.ts +21 -0
  129. package/dist/agent/types.d.ts.map +1 -1
  130. package/dist/agent/types.js.map +1 -1
  131. package/dist/client/AgentPanel.d.ts +6 -2
  132. package/dist/client/AgentPanel.d.ts.map +1 -1
  133. package/dist/client/AgentPanel.js +10 -4
  134. package/dist/client/AgentPanel.js.map +1 -1
  135. package/dist/client/AssistantChat.d.ts +6 -0
  136. package/dist/client/AssistantChat.d.ts.map +1 -1
  137. package/dist/client/AssistantChat.js +15 -3
  138. package/dist/client/AssistantChat.js.map +1 -1
  139. package/dist/client/agent-chat.d.ts +32 -0
  140. package/dist/client/agent-chat.d.ts.map +1 -1
  141. package/dist/client/agent-chat.js +113 -0
  142. package/dist/client/agent-chat.js.map +1 -1
  143. package/dist/client/analytics.d.ts +22 -3
  144. package/dist/client/analytics.d.ts.map +1 -1
  145. package/dist/client/analytics.js +96 -4
  146. package/dist/client/analytics.js.map +1 -1
  147. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  148. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  149. package/dist/client/composer/TiptapComposer.js +228 -56
  150. package/dist/client/composer/TiptapComposer.js.map +1 -1
  151. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  152. package/dist/client/composer/extensions/MentionReference.js +3 -0
  153. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  154. package/dist/client/composer/types.d.ts +21 -0
  155. package/dist/client/composer/types.d.ts.map +1 -1
  156. package/dist/client/composer/types.js.map +1 -1
  157. package/dist/client/index.d.ts +2 -2
  158. package/dist/client/index.d.ts.map +1 -1
  159. package/dist/client/index.js +2 -2
  160. package/dist/client/index.js.map +1 -1
  161. package/dist/client/session-replay.d.ts +41 -0
  162. package/dist/client/session-replay.d.ts.map +1 -0
  163. package/dist/client/session-replay.js +595 -0
  164. package/dist/client/session-replay.js.map +1 -0
  165. package/dist/collab/awareness.d.ts +2 -2
  166. package/dist/collab/awareness.d.ts.map +1 -1
  167. package/dist/collab/routes.d.ts +2 -2
  168. package/dist/db/schema.d.ts +3 -1
  169. package/dist/db/schema.d.ts.map +1 -1
  170. package/dist/db/schema.js +6 -2
  171. package/dist/db/schema.js.map +1 -1
  172. package/dist/deploy/build.d.ts +8 -0
  173. package/dist/deploy/build.d.ts.map +1 -1
  174. package/dist/deploy/build.js +66 -0
  175. package/dist/deploy/build.js.map +1 -1
  176. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +8 -8
  179. package/dist/private-blob/index.d.ts +3 -0
  180. package/dist/private-blob/index.d.ts.map +1 -0
  181. package/dist/private-blob/index.js +2 -0
  182. package/dist/private-blob/index.js.map +1 -0
  183. package/dist/private-blob/registry.d.ts +10 -0
  184. package/dist/private-blob/registry.d.ts.map +1 -0
  185. package/dist/private-blob/registry.js +152 -0
  186. package/dist/private-blob/registry.js.map +1 -0
  187. package/dist/private-blob/types.d.ts +51 -0
  188. package/dist/private-blob/types.d.ts.map +1 -0
  189. package/dist/private-blob/types.js +2 -0
  190. package/dist/private-blob/types.js.map +1 -0
  191. package/dist/resources/handlers.d.ts +3 -3
  192. package/dist/scripts/utils.d.ts +4 -4
  193. package/dist/scripts/utils.d.ts.map +1 -1
  194. package/dist/scripts/utils.js +30 -6
  195. package/dist/scripts/utils.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +5 -0
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  200. package/dist/server/email.d.ts +2 -0
  201. package/dist/server/email.d.ts.map +1 -1
  202. package/dist/server/email.js +3 -0
  203. package/dist/server/email.js.map +1 -1
  204. package/dist/server/index.d.ts +1 -1
  205. package/dist/server/index.d.ts.map +1 -1
  206. package/dist/server/index.js.map +1 -1
  207. package/dist/server/transcribe-voice.d.ts +1 -1
  208. package/dist/styles/agent-native.css +24 -0
  209. package/docs/content/template-analytics.md +5 -0
  210. package/docs/content/tracking.md +45 -0
  211. package/package.json +3 -1
  212. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +0 -209
@@ -1,42 +1,63 @@
1
1
  import {
2
- agentNativePath,
3
2
  appPath,
4
3
  getBrowserTabId,
5
4
  getEmbedAuthToken,
6
5
  isEmbedMcpChatBridgeActive,
7
6
  isEmbedAuthActive,
7
+ insertAgentComposerReference,
8
+ readClientAppState,
8
9
  sendMcpAppHostMessage,
9
10
  updateMcpAppModelContext,
10
11
  useActionMutation,
11
12
  useActionQuery,
13
+ useAgentChatGenerating,
12
14
  useT,
15
+ writeClientAppState,
13
16
  } from "@agent-native/core/client";
14
17
  import {
15
18
  createEmbeddedAppBridge,
16
19
  type EmbeddedAppBridge,
17
- } from "@agent-native/core/embedding/bridge";
20
+ } from "@agent-native/core/embedding";
18
21
  import {
19
22
  IconArrowUpRight,
20
23
  IconCheck,
24
+ IconChevronDown,
21
25
  IconChevronLeft,
22
26
  IconChevronRight,
23
27
  IconClipboard,
28
+ IconLibraryPhoto,
24
29
  IconPhotoPlus,
30
+ IconSearch,
25
31
  IconX,
26
32
  } from "@tabler/icons-react";
33
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
27
34
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
28
- import { Link, useSearchParams } from "react-router";
35
+ import {
36
+ Link,
37
+ useSearchParams,
38
+ useLocation,
39
+ useNavigate,
40
+ useParams,
41
+ } from "react-router";
29
42
  import { toast } from "sonner";
30
43
 
44
+ import { CreateLibraryDialog } from "@/components/library/CreateLibraryDialog";
45
+ import { LibraryPresetGrid } from "@/components/library/LibraryPresetGrid";
46
+ import { Badge } from "@/components/ui/badge";
31
47
  import { Button } from "@/components/ui/button";
32
48
  import {
33
49
  Dialog,
34
- DialogClose,
35
50
  DialogContent,
36
51
  DialogDescription,
37
52
  DialogTitle,
53
+ DialogClose,
38
54
  } from "@/components/ui/dialog";
39
55
  import { Input } from "@/components/ui/input";
56
+ import {
57
+ Popover,
58
+ PopoverContent,
59
+ PopoverTrigger,
60
+ } from "@/components/ui/popover";
40
61
  import {
41
62
  Select,
42
63
  SelectContent,
@@ -54,10 +75,27 @@ import {
54
75
  TooltipProvider,
55
76
  TooltipTrigger,
56
77
  } from "@/components/ui/tooltip";
78
+ import {
79
+ sortLibrariesByUsage,
80
+ type ImageLibrarySummary,
81
+ } from "@/lib/libraries";
82
+ import { buildPickerChatHandoffPrompt } from "@/lib/picker-chat-handoff";
57
83
  import { cn } from "@/lib/utils";
58
84
 
59
- import type { ImageQualityTier, StyleStrength } from "../../shared/api";
60
- import { DEFAULT_LIBRARY_PRESETS } from "../../shared/library-presets";
85
+ import type {
86
+ AssetVariantState,
87
+ ImageQualityTier,
88
+ StyleStrength,
89
+ } from "../../shared/api";
90
+ import {
91
+ DEFAULT_LIBRARY_PRESETS,
92
+ LibraryPreset,
93
+ } from "../../shared/library-presets";
94
+ import {
95
+ BrandKitDetailRoute,
96
+ LiveCandidatesStage,
97
+ type VariantSlot,
98
+ } from "./brand-kits.$id";
61
99
 
62
100
  type AssetTab = "all" | "generated" | "references";
63
101
 
@@ -84,6 +122,7 @@ type Asset = {
84
122
  id: string;
85
123
  libraryId: string;
86
124
  role?: string | null;
125
+ status?: string | null;
87
126
  title?: string | null;
88
127
  description?: string | null;
89
128
  altText?: string | null;
@@ -98,6 +137,7 @@ type Asset = {
98
137
  downloadUrl?: string;
99
138
  embedUrl?: string;
100
139
  embedPath?: string;
140
+ libraryTitle?: string | null;
101
141
  lineage?: {
102
142
  label?: string | null;
103
143
  kind?: string | null;
@@ -125,6 +165,7 @@ type GenerationPreset = {
125
165
  mediaType?: string | null;
126
166
  aspectRatio?: string | null;
127
167
  imageSize?: string | null;
168
+ model?: string | null;
128
169
  };
129
170
 
130
171
  type HostConfig = {
@@ -516,7 +557,7 @@ function blobToBase64(blob: Blob): Promise<string> {
516
557
  });
517
558
  }
518
559
 
519
- const MCP_IMAGE_CONTENT_MAX_BYTES = 4 * 1024 * 1024;
560
+ const MCP_IMAGE_CONTENT_MAX_BYTES = 2.5 * 1024 * 1024;
520
561
 
521
562
  function base64ByteLength(data: string) {
522
563
  const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
@@ -716,7 +757,1178 @@ function AssetOverlayImage({ asset }: { asset: Asset }) {
716
757
  );
717
758
  }
718
759
 
719
- export default function AssetPicker() {
760
+ function EmptyLibraryStarter({ onCreateBlank }: { onCreateBlank: () => void }) {
761
+ const t = useT();
762
+ const navigate = useNavigate();
763
+ const { data: presetData } = useActionQuery("list-library-presets", {});
764
+ const createFromPreset = useActionMutation("create-library-from-preset");
765
+ const [creatingPresetId, setCreatingPresetId] = useState<string | null>(null);
766
+ const presets = ((presetData as any)?.presets ?? []) as LibraryPreset[];
767
+
768
+ function createPresetLibrary(presetId: string) {
769
+ setCreatingPresetId(presetId);
770
+ createFromPreset.mutate(
771
+ { presetId } as any,
772
+ {
773
+ onSuccess: (library: any) => {
774
+ setCreatingPresetId(null);
775
+ navigate(`/library/${library.id}`);
776
+ },
777
+ onError: (error: Error) => {
778
+ setCreatingPresetId(null);
779
+ toast.error(error.message || t("brandKits.presetCreateFailed"));
780
+ },
781
+ } as any,
782
+ );
783
+ }
784
+
785
+ return (
786
+ <div className="mx-auto flex min-h-full max-w-5xl flex-col justify-center px-6 py-10">
787
+ <div className="mx-auto max-w-2xl text-center">
788
+ <IconLibraryPhoto className="mx-auto h-10 w-10 text-muted-foreground" />
789
+ <h2 className="mt-4 text-xl font-semibold tracking-tight">
790
+ {t("library.buildYourFirstKit")}
791
+ </h2>
792
+ <p className="mt-2 text-sm text-muted-foreground">
793
+ {t("library.buildYourFirstKitDescription")}
794
+ </p>
795
+ </div>
796
+ <div className="mt-6">
797
+ <LibraryPresetGrid
798
+ presets={presets}
799
+ creatingId={creatingPresetId}
800
+ onCreate={createPresetLibrary}
801
+ />
802
+ </div>
803
+ <div className="mt-5 flex justify-center">
804
+ <Button variant="outline" onClick={onCreateBlank}>
805
+ {t("library.createBlankKit")}
806
+ </Button>
807
+ </div>
808
+ </div>
809
+ );
810
+ }
811
+
812
+ function LibraryShellHeader({
813
+ selectedLibraryId = null,
814
+ libraries,
815
+ isLoading,
816
+ onCreateKit,
817
+ }: {
818
+ selectedLibraryId?: string | null;
819
+ libraries: ImageLibrarySummary[];
820
+ isLoading?: boolean;
821
+ onCreateKit: () => void;
822
+ }) {
823
+ const t = useT();
824
+ const selectedLibrary = selectedLibraryId
825
+ ? libraries.find((library) => library.id === selectedLibraryId)
826
+ : null;
827
+ const title =
828
+ selectedLibrary?.title ??
829
+ (selectedLibraryId ? t("library.library") : t("library.allAssets"));
830
+ const visibility = (selectedLibrary as any)?.visibility;
831
+
832
+ return (
833
+ <header className="border-b border-border bg-background px-4 py-3 md:px-6">
834
+ <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
835
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
836
+ <LibraryKitSelector
837
+ selectedLibraryId={selectedLibraryId}
838
+ libraries={libraries}
839
+ isLoading={isLoading}
840
+ triggerLabel={title}
841
+ triggerStyle="title"
842
+ onCreateKit={onCreateKit}
843
+ />
844
+ {visibility ? (
845
+ <Badge variant="outline" className="h-6 rounded-full px-2 text-xs">
846
+ {visibility}
847
+ </Badge>
848
+ ) : null}
849
+ </div>
850
+ <div className="flex shrink-0 flex-wrap items-center gap-2 self-start sm:self-auto sm:flex-nowrap">
851
+ {selectedLibraryId ? (
852
+ <div
853
+ id="assets-library-detail-primary-actions"
854
+ className="contents"
855
+ aria-label={t("library.primaryKitActions")}
856
+ />
857
+ ) : null}
858
+ <Button
859
+ size="sm"
860
+ className="h-8 shrink-0 gap-1.5"
861
+ onClick={onCreateKit}
862
+ >
863
+ <IconPhotoPlus className="h-4 w-4" />
864
+ {t("library.newKit")}
865
+ </Button>
866
+ {selectedLibraryId ? (
867
+ <div
868
+ id="assets-library-detail-more-actions"
869
+ className="contents"
870
+ aria-label={t("library.kitActions")}
871
+ />
872
+ ) : null}
873
+ </div>
874
+ </div>
875
+ </header>
876
+ );
877
+ }
878
+
879
+ function LibraryKitSelector({
880
+ selectedLibraryId = null,
881
+ libraries,
882
+ isLoading,
883
+ triggerLabel,
884
+ triggerStyle = "button",
885
+ onCreateKit,
886
+ }: {
887
+ selectedLibraryId?: string | null;
888
+ libraries: ImageLibrarySummary[];
889
+ isLoading?: boolean;
890
+ triggerLabel?: string;
891
+ triggerStyle?: "button" | "title";
892
+ onCreateKit: () => void;
893
+ }) {
894
+ const t = useT();
895
+ const navigate = useNavigate();
896
+ const [open, setOpen] = useState(false);
897
+ const [query, setQuery] = useState("");
898
+ const selectedLibrary = selectedLibraryId
899
+ ? libraries.find((library) => library.id === selectedLibraryId)
900
+ : null;
901
+ const filteredLibraries = useMemo(() => {
902
+ const items = sortLibrariesByUsage(libraries.filter(Boolean));
903
+ const q = query.trim().toLowerCase();
904
+ if (!q) return items;
905
+ return items.filter((library) =>
906
+ [library.title, library.description]
907
+ .filter(Boolean)
908
+ .join(" ")
909
+ .toLowerCase()
910
+ .includes(q),
911
+ );
912
+ }, [libraries, query]);
913
+
914
+ function selectLibrary(libraryId: string | null) {
915
+ setOpen(false);
916
+ navigate(libraryId ? `/library/${libraryId}` : "/library");
917
+ }
918
+ const titleTrigger = triggerStyle === "title";
919
+
920
+ return (
921
+ <Popover
922
+ open={open}
923
+ onOpenChange={(next) => {
924
+ setOpen(next);
925
+ if (!next) setQuery("");
926
+ }}
927
+ >
928
+ <PopoverTrigger asChild>
929
+ {titleTrigger ? (
930
+ <button
931
+ type="button"
932
+ className="-ml-1.5 inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left text-xl font-semibold leading-tight tracking-tight transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
933
+ >
934
+ <span className="block min-w-0 max-w-[min(48rem,calc(100vw-7rem))] truncate sm:max-w-none">
935
+ {triggerLabel ?? selectedLibrary?.title ?? t("library.allAssets")}
936
+ </span>
937
+ <IconChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
938
+ </button>
939
+ ) : (
940
+ <Button
941
+ variant="outline"
942
+ size="sm"
943
+ className="h-8 max-w-[18rem] gap-1.5 px-2.5"
944
+ >
945
+ <IconLibraryPhoto className="h-4 w-4 shrink-0 text-muted-foreground" />
946
+ <span className="min-w-0 truncate">
947
+ {triggerLabel ?? selectedLibrary?.title ?? t("library.allAssets")}
948
+ </span>
949
+ <IconChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
950
+ </Button>
951
+ )}
952
+ </PopoverTrigger>
953
+ <PopoverContent
954
+ align="start"
955
+ className="w-[min(25rem,calc(100vw-2rem))] p-2"
956
+ >
957
+ <div className="flex h-9 items-center gap-2 rounded-md border border-border bg-background px-2.5">
958
+ <IconSearch className="h-4 w-4 shrink-0 text-muted-foreground" />
959
+ <input
960
+ value={query}
961
+ onChange={(event) => setQuery(event.target.value)}
962
+ placeholder={t("library.searchKits")}
963
+ className="h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
964
+ />
965
+ </div>
966
+ <div className="mt-2 max-h-72 overflow-y-auto">
967
+ <button
968
+ type="button"
969
+ onClick={() => selectLibrary(null)}
970
+ className={cn(
971
+ "flex w-full min-w-0 items-center gap-3 rounded-md px-2 py-2 text-left transition hover:bg-accent",
972
+ !selectedLibraryId && "bg-accent",
973
+ )}
974
+ >
975
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground">
976
+ <IconLibraryPhoto className="h-4 w-4" />
977
+ </span>
978
+ <span className="min-w-0 flex-1">
979
+ <span className="block truncate text-sm font-medium">
980
+ {t("library.allAssets")}
981
+ </span>
982
+ <span className="block truncate text-xs text-muted-foreground">
983
+ {t("library.allAssetsDescription")}
984
+ </span>
985
+ </span>
986
+ {!selectedLibraryId ? <IconCheck className="h-4 w-4" /> : null}
987
+ </button>
988
+
989
+ {isLoading ? (
990
+ <div className="mt-2 grid gap-1.5">
991
+ {Array.from({ length: 3 }).map((_, index) => (
992
+ <Skeleton key={index} className="h-12 rounded-md" />
993
+ ))}
994
+ </div>
995
+ ) : filteredLibraries.length ? (
996
+ <div className="mt-1 grid gap-1">
997
+ {filteredLibraries.map((library) => {
998
+ const selected = selectedLibraryId === library.id;
999
+ return (
1000
+ <button
1001
+ key={library.id}
1002
+ type="button"
1003
+ onClick={() => selectLibrary(library.id)}
1004
+ className={cn(
1005
+ "flex w-full min-w-0 items-center gap-3 rounded-md px-2 py-2 text-left transition hover:bg-accent",
1006
+ selected && "bg-accent",
1007
+ )}
1008
+ >
1009
+ <span className="min-w-0 flex-1">
1010
+ <span className="block truncate text-sm font-medium">
1011
+ {library.title}
1012
+ </span>
1013
+ <span className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5">
1014
+ <Badge variant="secondary" className="px-1.5 py-0">
1015
+ {t("library.referenceCount", {
1016
+ count: library.referenceCount ?? 0,
1017
+ })}
1018
+ </Badge>
1019
+ <Badge variant="outline" className="px-1.5 py-0">
1020
+ {t("library.assetCount", {
1021
+ count: library.generatedCount ?? 0,
1022
+ })}
1023
+ </Badge>
1024
+ </span>
1025
+ </span>
1026
+ {selected ? <IconCheck className="h-4 w-4" /> : null}
1027
+ </button>
1028
+ );
1029
+ })}
1030
+ </div>
1031
+ ) : (
1032
+ <div className="px-2 py-6 text-center text-sm text-muted-foreground">
1033
+ {t("library.noKitsMatch")}
1034
+ </div>
1035
+ )}
1036
+ </div>
1037
+ <div className="mt-2 border-t border-border pt-2">
1038
+ <Button
1039
+ variant="ghost"
1040
+ size="sm"
1041
+ className="h-8 w-full justify-start gap-2"
1042
+ onClick={() => {
1043
+ setOpen(false);
1044
+ onCreateKit();
1045
+ }}
1046
+ >
1047
+ <IconPhotoPlus className="h-4 w-4" />
1048
+ {t("library.newKit")}
1049
+ </Button>
1050
+ </div>
1051
+ </PopoverContent>
1052
+ </Popover>
1053
+ );
1054
+ }
1055
+
1056
+ function AllAssetsBrowser() {
1057
+ const t = useT();
1058
+ const navigate = useNavigate();
1059
+ const [query, setQuery] = useState("");
1060
+ const [assetTab, setAssetTab] = useState<AssetTab>("all");
1061
+ const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
1062
+ const [standaloneSelection, setStandaloneSelection] = useState<ReturnType<
1063
+ typeof assetPayload
1064
+ > | null>(null);
1065
+ const [standaloneCopyOk, setStandaloneCopyOk] = useState(false);
1066
+
1067
+ const { data: assetData, isLoading } = useActionQuery("list-assets", {
1068
+ query: query.trim() || undefined,
1069
+ } as any) as {
1070
+ data?: { assets?: Asset[] };
1071
+ isLoading: boolean;
1072
+ };
1073
+
1074
+ const allAssets = assetData?.assets ?? [];
1075
+ const assets = useMemo(
1076
+ () => allAssets.filter((asset) => assetMatchesTab(asset, assetTab)),
1077
+ [allAssets, assetTab],
1078
+ );
1079
+ const visibleAssetCount = assets.length;
1080
+ const assetCountLabel = isLoading
1081
+ ? t("library.loading")
1082
+ : query.trim() || assetTab !== "all"
1083
+ ? t("library.shownCount", { count: visibleAssetCount })
1084
+ : t("library.assetCount", { count: allAssets.length });
1085
+ const standaloneSelectionText = useMemo(
1086
+ () =>
1087
+ standaloneSelection
1088
+ ? selectedAssetClipboardText(standaloneSelection)
1089
+ : "",
1090
+ [standaloneSelection],
1091
+ );
1092
+ const standaloneSelectionUrl =
1093
+ standaloneSelection?.url ??
1094
+ standaloneSelection?.downloadUrl ??
1095
+ standaloneSelection?.previewUrl;
1096
+
1097
+ const copyStandaloneSelection = useCallback(
1098
+ async (payload: ReturnType<typeof assetPayload>) => {
1099
+ const text = selectedAssetClipboardText(payload);
1100
+ try {
1101
+ await navigator.clipboard.writeText(text);
1102
+ setStandaloneCopyOk(true);
1103
+ toast.success(t("library.selectionCopied"));
1104
+ } catch {
1105
+ setStandaloneCopyOk(false);
1106
+ toast.info(t("library.selectionReady"));
1107
+ }
1108
+ },
1109
+ [],
1110
+ );
1111
+
1112
+ function chooseAsset(asset: Asset) {
1113
+ const payload = assetPayload(asset, "image");
1114
+ setStandaloneSelection(payload);
1115
+ setStandaloneCopyOk(false);
1116
+ void copyStandaloneSelection(payload);
1117
+ }
1118
+
1119
+ return (
1120
+ <div className="flex min-w-0 flex-col">
1121
+ <div className="border-b border-border px-4 py-3 md:px-6">
1122
+ <div className="flex w-full min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
1123
+ <div className="flex min-w-0 items-center gap-2">
1124
+ <Tabs
1125
+ value={assetTab}
1126
+ onValueChange={(value) => setAssetTab(value as AssetTab)}
1127
+ >
1128
+ <TabsList className="h-9">
1129
+ <TabsTrigger value="all">{t("library.tabsAll")}</TabsTrigger>
1130
+ <TabsTrigger value="generated">
1131
+ {t("library.generated")}
1132
+ </TabsTrigger>
1133
+ <TabsTrigger value="references">
1134
+ {t("library.references")}
1135
+ </TabsTrigger>
1136
+ </TabsList>
1137
+ </Tabs>
1138
+ <Badge
1139
+ variant="secondary"
1140
+ className="h-6 max-w-full rounded-full px-2 text-xs"
1141
+ >
1142
+ {assetCountLabel}
1143
+ </Badge>
1144
+ </div>
1145
+ <div className="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-border/70 bg-background px-3 focus-within:ring-1 focus-within:ring-ring sm:max-w-sm">
1146
+ <IconSearch className="h-4 w-4 shrink-0 text-muted-foreground" />
1147
+ <input
1148
+ type="search"
1149
+ value={query}
1150
+ onInput={(event) => setQuery(event.currentTarget.value)}
1151
+ onChange={(event) => setQuery(event.target.value)}
1152
+ placeholder={t("library.searchAssets")}
1153
+ className="h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
1154
+ />
1155
+ </div>
1156
+ </div>
1157
+ </div>
1158
+
1159
+ {standaloneSelection && (
1160
+ <section className="shrink-0 overflow-hidden border-b border-border bg-muted/30 px-4 py-3 md:px-6">
1161
+ <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
1162
+ <div className="min-w-0 flex-1">
1163
+ <div className="truncate text-sm font-medium">
1164
+ {selectedAssetLabel(standaloneSelection)}
1165
+ </div>
1166
+ {standaloneSelectionUrl && (
1167
+ <div className="truncate text-xs text-muted-foreground">
1168
+ {standaloneSelectionUrl}
1169
+ </div>
1170
+ )}
1171
+ </div>
1172
+ <div className="flex w-full min-w-0 flex-wrap gap-2 sm:w-auto sm:flex-nowrap sm:justify-end">
1173
+ <Button
1174
+ variant="outline"
1175
+ size="sm"
1176
+ className="h-8 shrink-0 gap-1.5"
1177
+ onClick={() => copyStandaloneSelection(standaloneSelection)}
1178
+ >
1179
+ {standaloneCopyOk ? (
1180
+ <IconCheck className="h-3.5 w-3.5" />
1181
+ ) : (
1182
+ <IconClipboard className="h-3.5 w-3.5" />
1183
+ )}
1184
+ {standaloneCopyOk ? t("library.copied") : t("library.copy")}
1185
+ </Button>
1186
+ <Button
1187
+ asChild
1188
+ variant="ghost"
1189
+ size="sm"
1190
+ className="h-8 shrink-0 gap-1.5"
1191
+ >
1192
+ <Link
1193
+ to={`/asset/${encodeURIComponent(
1194
+ standaloneSelection.assetId,
1195
+ )}`}
1196
+ >
1197
+ <IconArrowUpRight className="h-3.5 w-3.5" />
1198
+ {t("library.open")}
1199
+ </Link>
1200
+ </Button>
1201
+ <Button
1202
+ variant="ghost"
1203
+ size="icon"
1204
+ title={t("library.close")}
1205
+ aria-label={t("library.close")}
1206
+ className="h-8 w-8 shrink-0"
1207
+ onClick={() => {
1208
+ setStandaloneSelection(null);
1209
+ setStandaloneCopyOk(false);
1210
+ }}
1211
+ >
1212
+ <IconX className="h-3.5 w-3.5" />
1213
+ </Button>
1214
+ </div>
1215
+ </div>
1216
+ <details className="mt-2">
1217
+ <summary className="cursor-pointer select-none text-xs text-muted-foreground hover:text-foreground">
1218
+ {t("library.showPasteText")}
1219
+ </summary>
1220
+ <Textarea
1221
+ readOnly
1222
+ value={standaloneSelectionText}
1223
+ className="mt-2 h-24 max-w-full resize-none border-border/70 bg-background font-mono text-[11px] leading-relaxed"
1224
+ onFocus={(event) => event.currentTarget.select()}
1225
+ />
1226
+ </details>
1227
+ </section>
1228
+ )}
1229
+
1230
+ <main className="p-4 md:p-6">
1231
+ {isLoading ? (
1232
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
1233
+ {Array.from({ length: 12 }).map((_, index) => (
1234
+ <Skeleton key={index} className="aspect-[4/3] rounded-lg" />
1235
+ ))}
1236
+ </div>
1237
+ ) : assets.length === 0 ? (
1238
+ <div className="flex min-h-64 items-center justify-center text-center">
1239
+ <div className="max-w-sm text-sm text-muted-foreground">
1240
+ {query
1241
+ ? t("library.noMatchingAssets")
1242
+ : t("library.noReusableAssets")}
1243
+ </div>
1244
+ </div>
1245
+ ) : (
1246
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
1247
+ {assets.map((asset) => (
1248
+ <div
1249
+ key={asset.id}
1250
+ className="group relative overflow-hidden rounded-lg border border-border/80 bg-background transition hover:border-foreground/25 hover:bg-muted/10 focus-within:ring-2 focus-within:ring-ring"
1251
+ >
1252
+ <button
1253
+ type="button"
1254
+ aria-label={t("library.selectAsset", {
1255
+ title: assetDisplayTitle(asset),
1256
+ })}
1257
+ onClick={() => setPreviewAsset(asset)}
1258
+ title={assetDisplayTitle(asset)}
1259
+ className="block w-full text-left focus-visible:outline-none"
1260
+ >
1261
+ <div className="aspect-[4/3] bg-muted/40">
1262
+ {asset.mediaType === "video" ||
1263
+ asset.mimeType?.startsWith("video/") ? (
1264
+ <video
1265
+ src={asset.previewUrl ?? asset.downloadUrl ?? asset.url}
1266
+ poster={asset.thumbnailUrl}
1267
+ muted
1268
+ playsInline
1269
+ className="h-full w-full object-cover transition group-hover:scale-[1.02]"
1270
+ />
1271
+ ) : (
1272
+ <AssetThumbnail asset={asset} />
1273
+ )}
1274
+ </div>
1275
+ </button>
1276
+ {(asset as any).libraryTitle ? (
1277
+ <button
1278
+ type="button"
1279
+ onClick={() => navigate(`/library/${asset.libraryId}`)}
1280
+ className="absolute bottom-2 left-2 z-10 max-w-[calc(100%-1rem)] truncate rounded-full bg-background/90 px-2.5 py-1 text-[11px] font-medium shadow-sm backdrop-blur transition hover:bg-background"
1281
+ >
1282
+ {(asset as any).libraryTitle}
1283
+ </button>
1284
+ ) : null}
1285
+ <TooltipProvider>
1286
+ <Tooltip>
1287
+ <TooltipTrigger asChild>
1288
+ <button
1289
+ type="button"
1290
+ aria-label={t("library.copyToClipboard")}
1291
+ onClick={(event) => {
1292
+ event.stopPropagation();
1293
+ chooseAsset(asset);
1294
+ }}
1295
+ className="absolute right-2 top-2 z-10 inline-flex h-8 w-8 items-center justify-center rounded-full bg-background/80 text-foreground opacity-0 shadow-sm backdrop-blur transition hover:bg-primary hover:text-primary-foreground focus:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100"
1296
+ >
1297
+ <IconClipboard className="h-4 w-4" />
1298
+ </button>
1299
+ </TooltipTrigger>
1300
+ <TooltipContent>
1301
+ {t("library.copyToClipboard")}
1302
+ </TooltipContent>
1303
+ </Tooltip>
1304
+ </TooltipProvider>
1305
+ </div>
1306
+ ))}
1307
+ </div>
1308
+ )}
1309
+ </main>
1310
+
1311
+ <AssetPreviewDialog
1312
+ asset={previewAsset}
1313
+ assets={assets}
1314
+ onAssetChange={setPreviewAsset}
1315
+ />
1316
+ </div>
1317
+ );
1318
+ }
1319
+
1320
+ function AssetPreviewDialog({
1321
+ asset,
1322
+ assets,
1323
+ onAssetChange,
1324
+ }: {
1325
+ asset: Asset | null;
1326
+ assets: Asset[];
1327
+ onAssetChange: (asset: Asset | null) => void;
1328
+ }) {
1329
+ const t = useT();
1330
+ return (
1331
+ <Dialog
1332
+ open={Boolean(asset)}
1333
+ onOpenChange={(open) => {
1334
+ if (!open) onAssetChange(null);
1335
+ }}
1336
+ >
1337
+ {asset &&
1338
+ (() => {
1339
+ const previewIndex = assets.findIndex(
1340
+ (candidate) => candidate.id === asset.id,
1341
+ );
1342
+ const hasPrev = previewIndex > 0;
1343
+ const hasNext = previewIndex >= 0 && previewIndex < assets.length - 1;
1344
+ const showPreviousAsset = () => {
1345
+ if (hasPrev) onAssetChange(assets[previewIndex - 1]);
1346
+ };
1347
+ const showNextAsset = () => {
1348
+ if (hasNext) onAssetChange(assets[previewIndex + 1]);
1349
+ };
1350
+ return (
1351
+ <DialogContent
1352
+ hideClose
1353
+ onKeyDown={(event) => {
1354
+ if (event.key === "ArrowLeft") showPreviousAsset();
1355
+ if (event.key === "ArrowRight") showNextAsset();
1356
+ }}
1357
+ className="max-w-4xl border-0 bg-transparent p-0 shadow-none"
1358
+ >
1359
+ <DialogTitle className="sr-only">
1360
+ {assetDisplayTitle(asset)}
1361
+ </DialogTitle>
1362
+ <DialogDescription className="sr-only">
1363
+ {t("library.fullSizePreview", {
1364
+ title: assetDisplayTitle(asset),
1365
+ })}
1366
+ </DialogDescription>
1367
+ <div className="relative">
1368
+ <div className="absolute right-2 top-2 z-10 flex items-center gap-2">
1369
+ <Button asChild variant="outline" size="sm">
1370
+ <Link to={`/asset/${encodeURIComponent(asset.id)}`}>
1371
+ {t("library.viewDetails")}
1372
+ </Link>
1373
+ </Button>
1374
+ <DialogClose
1375
+ aria-label={t("library.closePreview")}
1376
+ className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
1377
+ >
1378
+ <IconX className="h-5 w-5" />
1379
+ </DialogClose>
1380
+ </div>
1381
+ {asset.mediaType === "video" ||
1382
+ asset.mimeType?.startsWith("video/") ? (
1383
+ <video
1384
+ src={asset.previewUrl ?? asset.downloadUrl ?? asset.url}
1385
+ poster={asset.thumbnailUrl}
1386
+ controls
1387
+ autoPlay
1388
+ playsInline
1389
+ className="max-h-[85vh] w-full rounded-lg bg-black object-contain"
1390
+ />
1391
+ ) : (
1392
+ <AssetOverlayImage asset={asset} />
1393
+ )}
1394
+ </div>
1395
+ {(hasPrev || hasNext) && (
1396
+ <div className="mt-5 flex justify-center gap-2">
1397
+ <button
1398
+ type="button"
1399
+ aria-label={t("library.previousImage")}
1400
+ onClick={showPreviousAsset}
1401
+ disabled={!hasPrev}
1402
+ className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:cursor-not-allowed disabled:opacity-40"
1403
+ >
1404
+ <IconChevronLeft className="h-5 w-5" />
1405
+ </button>
1406
+ <button
1407
+ type="button"
1408
+ aria-label={t("library.nextImage")}
1409
+ onClick={showNextAsset}
1410
+ disabled={!hasNext}
1411
+ className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:cursor-not-allowed disabled:opacity-40"
1412
+ >
1413
+ <IconChevronRight className="h-5 w-5" />
1414
+ </button>
1415
+ </div>
1416
+ )}
1417
+ </DialogContent>
1418
+ );
1419
+ })()}
1420
+ </Dialog>
1421
+ );
1422
+ }
1423
+
1424
+ function variantSlotTime(slot: AssetVariantState["slots"][number]): number {
1425
+ const raw = slot.updatedAt ?? slot.createdAt ?? "";
1426
+ const time = Date.parse(raw);
1427
+ return Number.isNaN(time) ? 0 : time;
1428
+ }
1429
+
1430
+ function assetVariantStateKey(scopeId?: string | null) {
1431
+ const scopedId = typeof scopeId === "string" ? scopeId.trim() : "";
1432
+ return scopedId ? `asset-variants:${scopedId}` : "asset-variants";
1433
+ }
1434
+
1435
+ function referenceRoleForLibraryCandidate(asset?: Asset | null) {
1436
+ if (asset?.mediaType === "video" || asset?.mimeType?.startsWith("video/")) {
1437
+ return "video_reference";
1438
+ }
1439
+ return "style_reference";
1440
+ }
1441
+
1442
+ function setLibraryAssetSavedInCache(
1443
+ queryClient: ReturnType<typeof useQueryClient>,
1444
+ libraryId: string,
1445
+ assetId: string,
1446
+ savedAsset: unknown,
1447
+ ) {
1448
+ queryClient.setQueryData(
1449
+ ["action", "get-library", { id: libraryId }],
1450
+ (current: any) => {
1451
+ if (!current || !Array.isArray(current.assets)) return current;
1452
+ let changed = false;
1453
+ const assets = current.assets.map((asset: any) => {
1454
+ if (asset.id !== assetId) return asset;
1455
+ changed = true;
1456
+ return {
1457
+ ...asset,
1458
+ ...(savedAsset && typeof savedAsset === "object" ? savedAsset : {}),
1459
+ status: "saved",
1460
+ };
1461
+ });
1462
+ return changed ? { ...current, assets } : current;
1463
+ },
1464
+ );
1465
+ }
1466
+
1467
+ function setLibraryAssetReferenceInCache(
1468
+ queryClient: ReturnType<typeof useQueryClient>,
1469
+ libraryId: string,
1470
+ assetId: string,
1471
+ ) {
1472
+ queryClient.setQueryData(
1473
+ ["action", "get-library", { id: libraryId }],
1474
+ (current: any) => {
1475
+ if (!current || !Array.isArray(current.assets)) return current;
1476
+ let changed = false;
1477
+ const assets = current.assets.map((asset: any) => {
1478
+ if (asset.id !== assetId) return asset;
1479
+ changed = true;
1480
+ return {
1481
+ ...asset,
1482
+ status: "reference",
1483
+ role: referenceRoleForLibraryCandidate(asset),
1484
+ updatedAt: new Date().toISOString(),
1485
+ };
1486
+ });
1487
+ return changed ? { ...current, assets } : current;
1488
+ },
1489
+ );
1490
+ }
1491
+
1492
+ function updateLibraryVariantSlotsInCache(
1493
+ queryClient: ReturnType<typeof useQueryClient>,
1494
+ shouldRemove: (slot: any) => boolean,
1495
+ variantScopeId?: string | null,
1496
+ ) {
1497
+ const stateKeys = new Set([
1498
+ "asset-variants",
1499
+ assetVariantStateKey(variantScopeId),
1500
+ ]);
1501
+ for (const stateKey of stateKeys) {
1502
+ queryClient.setQueryData(["app-state", stateKey], (current: any) => {
1503
+ if (!current || !Array.isArray(current.slots)) return current;
1504
+ const slots = current.slots.filter((slot: any) => !shouldRemove(slot));
1505
+ if (slots.length === current.slots.length) return current;
1506
+ if (slots.length === 0) return null;
1507
+ return { ...current, slots, updatedAt: new Date().toISOString() };
1508
+ });
1509
+ }
1510
+ }
1511
+
1512
+ function removeLibraryVariantSlotFromCache(
1513
+ queryClient: ReturnType<typeof useQueryClient>,
1514
+ slot: VariantSlot,
1515
+ variantScopeId?: string | null,
1516
+ ) {
1517
+ updateLibraryVariantSlotsInCache(
1518
+ queryClient,
1519
+ (candidate) =>
1520
+ candidate.slotId === slot.slotId ||
1521
+ (!!slot.assetId && candidate.assetId === slot.assetId),
1522
+ variantScopeId,
1523
+ );
1524
+ }
1525
+
1526
+ function LibraryCandidateStage({
1527
+ activeLibraryId = null,
1528
+ foldersByLibraryId = {},
1529
+ variantScopeId = null,
1530
+ onUseAsset,
1531
+ inline = false,
1532
+ }: {
1533
+ activeLibraryId?: string | null;
1534
+ foldersByLibraryId?: Record<string, any[]>;
1535
+ variantScopeId?: string | null;
1536
+ onUseAsset?: (asset: Asset) => void;
1537
+ inline?: boolean;
1538
+ }) {
1539
+ const t = useT();
1540
+ const queryClient = useQueryClient();
1541
+ const [savingCandidateSlotId, setSavingCandidateSlotId] = useState<
1542
+ string | null
1543
+ >(null);
1544
+ const [promotingReferenceKeys, setPromotingReferenceKeys] = useState<
1545
+ Set<string>
1546
+ >(() => new Set());
1547
+ const { data: variants } = useQuery({
1548
+ queryKey: ["app-state", assetVariantStateKey(variantScopeId)],
1549
+ queryFn: ({ signal }) => {
1550
+ return readClientAppState<AssetVariantState>(
1551
+ assetVariantStateKey(variantScopeId),
1552
+ { signal },
1553
+ );
1554
+ },
1555
+ refetchInterval: 1000,
1556
+ });
1557
+ const isAllAssetsStage = !activeLibraryId;
1558
+ const liveLibraryId = activeLibraryId ?? variants?.libraryId ?? null;
1559
+ const liveVariantsForLibrary =
1560
+ liveLibraryId && variants?.libraryId === liveLibraryId ? variants : null;
1561
+ const { data: libraryData } = useActionQuery(
1562
+ "get-library",
1563
+ { id: activeLibraryId ?? "" } as any,
1564
+ { enabled: Boolean(activeLibraryId) } as any,
1565
+ ) as { data?: { library?: Library; assets?: Asset[]; folders?: any[] } };
1566
+ const { data: allCandidateData } = useActionQuery(
1567
+ "list-assets",
1568
+ { includeCandidates: true, status: "candidate" } as any,
1569
+ { enabled: isAllAssetsStage } as any,
1570
+ ) as { data?: { assets?: Asset[] } };
1571
+ const saveGenerated = useActionMutation("save-generated-image");
1572
+ const updateAsset = useActionMutation("update-asset");
1573
+ const libraryAssets = isAllAssetsStage
1574
+ ? (allCandidateData?.assets ?? [])
1575
+ : (libraryData?.assets ?? []);
1576
+ const assetById = useMemo(
1577
+ () => new Map(libraryAssets.map((asset) => [asset.id, asset])),
1578
+ [libraryAssets],
1579
+ );
1580
+ const slots = useMemo(
1581
+ () =>
1582
+ (liveVariantsForLibrary?.slots ?? [])
1583
+ .filter((slot) => ["pending", "ready", "failed"].includes(slot.status))
1584
+ .slice()
1585
+ .sort(
1586
+ (left, right) =>
1587
+ variantSlotTime(right) - variantSlotTime(left) ||
1588
+ right.slotId.localeCompare(left.slotId),
1589
+ ),
1590
+ [liveVariantsForLibrary?.slots],
1591
+ );
1592
+ const liveAssetIds = useMemo(
1593
+ () =>
1594
+ new Set(
1595
+ slots
1596
+ .map((slot) => slot.assetId)
1597
+ .filter((assetId): assetId is string => Boolean(assetId)),
1598
+ ),
1599
+ [slots],
1600
+ );
1601
+ const draftAssets = useMemo(
1602
+ () =>
1603
+ libraryAssets
1604
+ .filter(
1605
+ (asset) =>
1606
+ asset.status === "candidate" &&
1607
+ isGeneratedAsset(asset) &&
1608
+ !liveAssetIds.has(asset.id),
1609
+ )
1610
+ .slice()
1611
+ .sort((left, right) => String(right.id).localeCompare(String(left.id))),
1612
+ [libraryAssets, liveAssetIds],
1613
+ );
1614
+ const totalCount = slots.length + draftAssets.length;
1615
+
1616
+ if (totalCount === 0) return null;
1617
+ const stageLibraryId = liveLibraryId ?? draftAssets[0]?.libraryId ?? null;
1618
+ if (!stageLibraryId) return null;
1619
+
1620
+ function invalidateStage(
1621
+ libraryIdToInvalidate: string | null = stageLibraryId,
1622
+ ) {
1623
+ void queryClient.invalidateQueries({
1624
+ queryKey: ["app-state"],
1625
+ refetchType: "active",
1626
+ });
1627
+ if (libraryIdToInvalidate) {
1628
+ void queryClient.invalidateQueries({
1629
+ queryKey: ["action", "get-library", { id: libraryIdToInvalidate }],
1630
+ refetchType: "active",
1631
+ });
1632
+ }
1633
+ void queryClient.invalidateQueries({
1634
+ queryKey: ["action", "get-library"],
1635
+ refetchType: "active",
1636
+ });
1637
+ void queryClient.invalidateQueries({
1638
+ queryKey: ["action", "list-assets"],
1639
+ refetchType: "active",
1640
+ });
1641
+ }
1642
+
1643
+ function setReferencePromoting(key: string, promoting: boolean) {
1644
+ setPromotingReferenceKeys((current) => {
1645
+ const next = new Set(current);
1646
+ if (promoting) next.add(key);
1647
+ else next.delete(key);
1648
+ return next.size === current.size ? current : next;
1649
+ });
1650
+ }
1651
+
1652
+ async function handleSaveLiveCandidate(
1653
+ slot: VariantSlot,
1654
+ folderId?: string | null,
1655
+ ) {
1656
+ if (savingCandidateSlotId || (!slot.assetId && !slot.slotId)) return;
1657
+ if (!liveLibraryId) return;
1658
+ setSavingCandidateSlotId(slot.slotId);
1659
+ try {
1660
+ const savedAsset = await saveGenerated.mutateAsync({
1661
+ assetId: slot.assetId,
1662
+ slotId: slot.slotId,
1663
+ folderId,
1664
+ } as any);
1665
+ if (slot.assetId) {
1666
+ setLibraryAssetSavedInCache(
1667
+ queryClient,
1668
+ liveLibraryId,
1669
+ slot.assetId,
1670
+ savedAsset,
1671
+ );
1672
+ }
1673
+ removeLibraryVariantSlotFromCache(queryClient, slot, variantScopeId);
1674
+ invalidateStage(liveLibraryId);
1675
+ toast.success(t("library.savedToLibrary"));
1676
+ } catch (error) {
1677
+ toast.error(
1678
+ error instanceof Error
1679
+ ? error.message
1680
+ : t("library.couldNotSaveCandidate"),
1681
+ );
1682
+ } finally {
1683
+ setSavingCandidateSlotId(null);
1684
+ }
1685
+ }
1686
+
1687
+ async function handleSaveDraftCandidate(
1688
+ asset: Asset,
1689
+ folderId?: string | null,
1690
+ ) {
1691
+ if (!asset?.id || savingCandidateSlotId) return;
1692
+ const key = `draft:${asset.id}`;
1693
+ setSavingCandidateSlotId(key);
1694
+ const targetLibraryId = asset.libraryId ?? stageLibraryId;
1695
+ try {
1696
+ const savedAsset = await saveGenerated.mutateAsync({
1697
+ assetId: asset.id,
1698
+ folderId,
1699
+ } as any);
1700
+ setLibraryAssetSavedInCache(
1701
+ queryClient,
1702
+ targetLibraryId,
1703
+ asset.id,
1704
+ savedAsset,
1705
+ );
1706
+ invalidateStage(targetLibraryId);
1707
+ toast.success(t("library.savedToLibrary"));
1708
+ } catch (error) {
1709
+ toast.error(
1710
+ error instanceof Error ? error.message : t("library.couldNotSaveDraft"),
1711
+ );
1712
+ } finally {
1713
+ setSavingCandidateSlotId(null);
1714
+ }
1715
+ }
1716
+
1717
+ async function handleMoveToReferences(asset: Asset, slot?: VariantSlot) {
1718
+ const key = slot?.slotId
1719
+ ? `slot:${slot.slotId}`
1720
+ : asset?.id
1721
+ ? `asset:${asset.id}`
1722
+ : "";
1723
+ if (!asset?.id || !key || promotingReferenceKeys.has(key)) return;
1724
+ setReferencePromoting(key, true);
1725
+ const targetLibraryId = asset.libraryId ?? stageLibraryId;
1726
+ try {
1727
+ await updateAsset.mutateAsync({
1728
+ id: asset.id,
1729
+ status: "reference",
1730
+ role: referenceRoleForLibraryCandidate(asset),
1731
+ } as any);
1732
+ setLibraryAssetReferenceInCache(queryClient, targetLibraryId, asset.id);
1733
+ updateLibraryVariantSlotsInCache(
1734
+ queryClient,
1735
+ (candidate) =>
1736
+ candidate.assetId === asset.id ||
1737
+ (!!slot?.slotId && candidate.slotId === slot.slotId),
1738
+ variantScopeId,
1739
+ );
1740
+ invalidateStage(targetLibraryId);
1741
+ toast.success(t("library.addedToReferences"));
1742
+ } catch (error) {
1743
+ toast.error(
1744
+ error instanceof Error
1745
+ ? error.message
1746
+ : t("library.couldNotAddToReferences"),
1747
+ );
1748
+ } finally {
1749
+ setReferencePromoting(key, false);
1750
+ }
1751
+ }
1752
+
1753
+ function handleMoveLiveCandidateToReferences(slot: VariantSlot) {
1754
+ if (!slot.assetId) return;
1755
+ void handleMoveToReferences(
1756
+ assetById.get(slot.assetId) ?? {
1757
+ id: slot.assetId,
1758
+ libraryId: liveLibraryId ?? stageLibraryId,
1759
+ mediaType: "image",
1760
+ status: "candidate",
1761
+ },
1762
+ slot,
1763
+ );
1764
+ }
1765
+
1766
+ function handleUseLiveCandidate(slot: VariantSlot) {
1767
+ if (!slot.assetId || !onUseAsset) return;
1768
+ onUseAsset(
1769
+ assetById.get(slot.assetId) ?? {
1770
+ id: slot.assetId,
1771
+ libraryId: liveLibraryId ?? stageLibraryId,
1772
+ mediaType: "image",
1773
+ status: "candidate",
1774
+ title: t("library.readyCandidate"),
1775
+ previewUrl: slot.previewUrl,
1776
+ thumbnailUrl: slot.thumbnailUrl,
1777
+ downloadUrl: slot.previewUrl,
1778
+ url: slot.previewUrl,
1779
+ },
1780
+ );
1781
+ }
1782
+
1783
+ return (
1784
+ <section
1785
+ className={cn(
1786
+ "min-w-0 overflow-hidden",
1787
+ inline
1788
+ ? "mb-3"
1789
+ : "shrink-0 border-b border-border bg-background px-3 py-3 sm:px-4 md:px-6",
1790
+ )}
1791
+ >
1792
+ <LiveCandidatesStage
1793
+ slots={slots}
1794
+ draftAssets={draftAssets}
1795
+ libraryId={stageLibraryId}
1796
+ folders={
1797
+ libraryData?.folders ?? foldersByLibraryId[stageLibraryId] ?? []
1798
+ }
1799
+ foldersByLibraryId={foldersByLibraryId}
1800
+ savingSlotId={savingCandidateSlotId}
1801
+ promotingReferenceKeys={promotingReferenceKeys}
1802
+ onSave={(slot, folderId) => {
1803
+ void handleSaveLiveCandidate(slot, folderId);
1804
+ }}
1805
+ onSaveDraft={(asset, folderId) => {
1806
+ void handleSaveDraftCandidate(asset, folderId);
1807
+ }}
1808
+ onMoveToReferences={handleMoveLiveCandidateToReferences}
1809
+ onMoveDraftToReferences={(asset) => {
1810
+ void handleMoveToReferences(asset);
1811
+ }}
1812
+ onUse={onUseAsset ? handleUseLiveCandidate : undefined}
1813
+ onUseDraft={onUseAsset}
1814
+ />
1815
+ </section>
1816
+ );
1817
+ }
1818
+
1819
+ function useLibraryRouteSelectedId(explicitId: string | null) {
1820
+ const params = useParams();
1821
+ const location = useLocation();
1822
+
1823
+ return useMemo(() => {
1824
+ if (explicitId) return explicitId;
1825
+ if (typeof params.id === "string" && params.id.trim()) return params.id;
1826
+
1827
+ const segments = location.pathname
1828
+ .split("/")
1829
+ .filter(Boolean)
1830
+ .map((segment) => {
1831
+ try {
1832
+ return decodeURIComponent(segment);
1833
+ } catch {
1834
+ return segment;
1835
+ }
1836
+ });
1837
+ const libraryIndex = segments.lastIndexOf("library");
1838
+ const nextSegment =
1839
+ libraryIndex >= 0 ? segments[libraryIndex + 1] : undefined;
1840
+ return nextSegment?.trim() || null;
1841
+ }, [explicitId, location.pathname, params.id]);
1842
+ }
1843
+
1844
+ export function LibraryWorkspace({
1845
+ selectedLibraryId = null,
1846
+ }: {
1847
+ selectedLibraryId?: string | null;
1848
+ }) {
1849
+ const navigate = useNavigate();
1850
+ const routeSelectedLibraryId = useLibraryRouteSelectedId(selectedLibraryId);
1851
+ const [createOpen, setCreateOpen] = useState(false);
1852
+ const { data, isLoading } = useActionQuery("list-libraries", {
1853
+ includeFolders: true,
1854
+ } as any);
1855
+ const libraries = ((data as any)?.libraries ?? []) as ImageLibrarySummary[];
1856
+ const foldersByLibraryId = useMemo(() => {
1857
+ const result: Record<string, any[]> = {};
1858
+ for (const library of libraries) {
1859
+ result[library.id] = Array.isArray(library.folders)
1860
+ ? library.folders
1861
+ : [];
1862
+ }
1863
+ return result;
1864
+ }, [libraries]);
1865
+ const hasLibraries = isLoading || libraries.length > 0;
1866
+ const currentLibrary = useMemo(
1867
+ () =>
1868
+ routeSelectedLibraryId
1869
+ ? libraries.find((library) => library.id === routeSelectedLibraryId)
1870
+ : null,
1871
+ [libraries, routeSelectedLibraryId],
1872
+ );
1873
+
1874
+ useEffect(() => {
1875
+ if (!routeSelectedLibraryId || !currentLibrary?.title) return;
1876
+ insertAgentComposerReference({
1877
+ label: currentLibrary.title,
1878
+ icon: "folder",
1879
+ source: "assets",
1880
+ refType: "brand-kit",
1881
+ refId: routeSelectedLibraryId,
1882
+ refPath: `/library/${encodeURIComponent(routeSelectedLibraryId)}`,
1883
+ slotKey: "brand-kit",
1884
+ slotLabel: "Brand kit",
1885
+ clearsSlots: ["preset"],
1886
+ metadata: {
1887
+ libraryId: routeSelectedLibraryId,
1888
+ },
1889
+ });
1890
+ }, [currentLibrary?.title, routeSelectedLibraryId]);
1891
+
1892
+ return (
1893
+ <div className="flex h-full min-h-0 flex-col bg-background text-foreground">
1894
+ <section className="min-h-0 min-w-0 flex-1 overflow-hidden">
1895
+ {routeSelectedLibraryId || hasLibraries ? (
1896
+ <div className="h-full min-h-0 min-w-0 overflow-y-auto">
1897
+ <LibraryShellHeader
1898
+ selectedLibraryId={routeSelectedLibraryId}
1899
+ libraries={libraries}
1900
+ isLoading={isLoading}
1901
+ onCreateKit={() => setCreateOpen(true)}
1902
+ />
1903
+ <LibraryCandidateStage
1904
+ activeLibraryId={routeSelectedLibraryId}
1905
+ foldersByLibraryId={foldersByLibraryId}
1906
+ />
1907
+ <div className="min-w-0">
1908
+ {routeSelectedLibraryId ? (
1909
+ <BrandKitDetailRoute
1910
+ libraryId={routeSelectedLibraryId}
1911
+ headerMode="actions"
1912
+ />
1913
+ ) : (
1914
+ <AllAssetsBrowser />
1915
+ )}
1916
+ </div>
1917
+ </div>
1918
+ ) : (
1919
+ <EmptyLibraryStarter onCreateBlank={() => setCreateOpen(true)} />
1920
+ )}
1921
+ </section>
1922
+ <CreateLibraryDialog
1923
+ open={createOpen}
1924
+ onOpenChange={setCreateOpen}
1925
+ onCreated={(library) => navigate(`/library/${library.id}`)}
1926
+ />
1927
+ </div>
1928
+ );
1929
+ }
1930
+
1931
+ export function AssetPickerSurface() {
720
1932
  const t = useT();
721
1933
  const [searchParams] = useSearchParams();
722
1934
  const searchParamsKey = searchParams.toString();
@@ -748,6 +1960,11 @@ export default function AssetPicker() {
748
1960
  }, [searchParamsKey]);
749
1961
  const bridgeRef = useRef<EmbeddedAppBridge | null>(null);
750
1962
  const embedded = useMemo(() => isEmbeddedWindow() || isEmbedAuthActive(), []);
1963
+ const pickerVariantScopeId = useMemo(
1964
+ () =>
1965
+ typeof window === "undefined" ? null : `picker:${getBrowserTabId()}`,
1966
+ [],
1967
+ );
751
1968
  const [hostConfig, setHostConfig] = useState<HostConfig>(() => urlHostConfig);
752
1969
  const [mediaType, setMediaType] = useState<PickerMediaType>(
753
1970
  () => hostConfig.mediaType ?? "image",
@@ -794,7 +2011,7 @@ export default function AssetPicker() {
794
2011
  const starterLibrary: Library = useMemo(
795
2012
  () => ({
796
2013
  id: STARTER_LIBRARY_ID,
797
- title: t("assetPicker.starterAssets"),
2014
+ title: t("library.starterAssets"),
798
2015
  description: STARTER_PRESET.description,
799
2016
  }),
800
2017
  [t],
@@ -840,7 +2057,6 @@ export default function AssetPicker() {
840
2057
  const {
841
2058
  data: presetData,
842
2059
  isLoading: presetsLoading,
843
- isFetching: presetsFetching,
844
2060
  isPending: presetsPending,
845
2061
  } = useActionQuery(
846
2062
  "list-generation-presets",
@@ -849,7 +2065,6 @@ export default function AssetPicker() {
849
2065
  ) as {
850
2066
  data?: { presets?: GenerationPreset[] };
851
2067
  isLoading?: boolean;
852
- isFetching?: boolean;
853
2068
  isPending?: boolean;
854
2069
  };
855
2070
  const generationPresets =
@@ -859,50 +2074,16 @@ export default function AssetPicker() {
859
2074
  ? null
860
2075
  : (generationPresets.find((preset) => preset.id === presetId) ?? null);
861
2076
  const effectiveAspectRatio = selectedPreset?.aspectRatio || aspectRatio;
2077
+ const waitingForPresetData =
2078
+ !presetData && (presetsLoading || presetsPending);
862
2079
  const waitingForRequestedPreset =
863
2080
  mediaType === "image" &&
864
2081
  presetId !== "none" &&
865
2082
  !usingStarterLibrary &&
866
2083
  Boolean(selectedLibraryId) &&
867
- (presetsLoading || presetsFetching || presetsPending || !selectedPreset);
868
- const mediaLabelText =
869
- mediaType === "video"
870
- ? t("brandKitDetail.video").toLowerCase()
871
- : t("brandKitDetail.image").toLowerCase();
872
- const generateBatch = useActionMutation(
873
- "generate-image-batch" as any,
874
- {
875
- onSuccess: (result: any) => {
876
- const images = Array.isArray(result?.images) ? result.images : [];
877
- const generatedCount = images.filter((image: any) => image?.ok).length;
878
- const failedCount = images.length - generatedCount;
879
- setVisibleCandidateRunIds(
880
- images
881
- .map((image: any) =>
882
- image?.ok && typeof image.runId === "string" ? image.runId : null,
883
- )
884
- .filter((runId: string | null): runId is string => Boolean(runId)),
885
- );
886
- if (generatedCount > 0) {
887
- toast.success(
888
- t("assetPicker.generatedCandidates", { count: generatedCount }),
889
- {
890
- description:
891
- failedCount > 0
892
- ? t("assetPicker.failedCandidates", { count: failedCount })
893
- : t("assetPicker.pickCandidate"),
894
- },
895
- );
896
- setQuery("");
897
- } else {
898
- toast.error(images[0]?.error || t("assetPicker.noUsableCandidates"));
899
- }
900
- },
901
- onError: (error: Error) => {
902
- toast.error(error.message || t("assetPicker.generationFailed"));
903
- },
904
- } as any,
905
- );
2084
+ !selectedPreset &&
2085
+ Boolean(waitingForPresetData);
2086
+ const mediaLabel = mediaType === "video" ? "video" : "image";
906
2087
  const assetsParams = useMemo(
907
2088
  () => ({
908
2089
  libraryId: selectedLibraryId,
@@ -951,6 +2132,12 @@ export default function AssetPicker() {
951
2132
  ? visibleStarterAssets
952
2133
  : []
953
2134
  : (assetData?.assets ?? []);
2135
+ const currentLibrary = useMemo(
2136
+ () =>
2137
+ displayLibraries.find((library) => library.id === selectedLibraryId) ??
2138
+ null,
2139
+ [displayLibraries, selectedLibraryId],
2140
+ );
954
2141
  const assets = useMemo(
955
2142
  () => allAssets.filter((asset) => assetMatchesTab(asset, assetTab)),
956
2143
  [allAssets, assetTab],
@@ -982,11 +2169,11 @@ export default function AssetPicker() {
982
2169
  try {
983
2170
  await navigator.clipboard.writeText(text);
984
2171
  setStandaloneCopyOk(true);
985
- toast.success(t("assetPicker.selectionCopied"));
2172
+ toast.success(t("library.selectionCopied"));
986
2173
  return true;
987
2174
  } catch {
988
2175
  setStandaloneCopyOk(false);
989
- toast.info(t("assetPicker.selectionReady"));
2176
+ toast.info(t("library.selectionReady"));
990
2177
  return false;
991
2178
  }
992
2179
  },
@@ -1024,7 +2211,7 @@ export default function AssetPicker() {
1024
2211
  }),
1025
2212
  );
1026
2213
  } else {
1027
- toast.error(t("assetPicker.sendBackFailed"));
2214
+ toast.error(t("library.selectedAssetSendFailed"));
1028
2215
  }
1029
2216
  });
1030
2217
  return;
@@ -1041,7 +2228,7 @@ export default function AssetPicker() {
1041
2228
  if (!result?.id) return;
1042
2229
  const library = {
1043
2230
  id: result.id,
1044
- title: result.title || t("assetPicker.mcpImagePicks"),
2231
+ title: result.title || t("library.mcpImagePicks"),
1045
2232
  description: result.description ?? null,
1046
2233
  };
1047
2234
  setCreatedPickerLibrary(library);
@@ -1052,18 +2239,86 @@ export default function AssetPicker() {
1052
2239
  // Allow the auto-create effect to retry after a transient failure;
1053
2240
  // otherwise the picker stays stuck on "Preparing..." until reload.
1054
2241
  autoCreateLibraryRef.current = false;
1055
- toast.error(error.message || t("assetPicker.prepareLibraryFailed"));
2242
+ toast.error(error.message || t("library.couldNotPrepareImageLibrary"));
2243
+ },
2244
+ } as any,
2245
+ );
2246
+ const generateBatch = useActionMutation(
2247
+ "generate-image-batch" as any,
2248
+ {
2249
+ onSuccess: (result: any) => {
2250
+ const images = Array.isArray(result?.images) ? result.images : [];
2251
+ const generatedCount = images.filter((image: any) => image?.ok).length;
2252
+ const failedCount = images.length - generatedCount;
2253
+ setVisibleCandidateRunIds(
2254
+ images
2255
+ .map((image: any) =>
2256
+ image?.ok && typeof image.runId === "string" ? image.runId : null,
2257
+ )
2258
+ .filter((runId: string | null): runId is string => Boolean(runId)),
2259
+ );
2260
+ if (generatedCount > 0) {
2261
+ toast.success(
2262
+ `Generated ${generatedCount} image candidate${
2263
+ generatedCount === 1 ? "" : "s"
2264
+ }`,
2265
+ {
2266
+ description:
2267
+ failedCount > 0
2268
+ ? `${failedCount} candidate${
2269
+ failedCount === 1 ? "" : "s"
2270
+ } failed.`
2271
+ : "Pick the one you want to send back.",
2272
+ },
2273
+ );
2274
+ setQuery("");
2275
+ } else {
2276
+ toast.error(
2277
+ images[0]?.error ||
2278
+ "Image generation finished without usable candidates.",
2279
+ );
2280
+ }
2281
+ },
2282
+ onError: (error: Error) => {
2283
+ toast.error(error.message || "Image generation failed");
1056
2284
  },
1057
2285
  } as any,
1058
2286
  );
2287
+ const [chatGenerating, sendToAgent] = useAgentChatGenerating();
1059
2288
 
1060
2289
  const runGenerate = useCallback(() => {
1061
2290
  if (!selectedLibraryId || !prompt.trim()) return;
1062
2291
  if (waitingForRequestedPreset) return;
2292
+ if (!embedded) {
2293
+ sendToAgent({
2294
+ message: buildPickerChatHandoffPrompt({
2295
+ mediaType,
2296
+ prompt,
2297
+ count,
2298
+ aspectRatio: effectiveAspectRatio,
2299
+ libraryId: usingStarterLibrary ? null : selectedLibraryId,
2300
+ libraryTitle: usingStarterLibrary
2301
+ ? null
2302
+ : (currentLibrary?.title ?? null),
2303
+ presetId: selectedPreset?.id ?? null,
2304
+ presetTitle: selectedPreset?.title ?? null,
2305
+ tier: hostConfig.tier,
2306
+ styleStrength: hostConfig.styleStrength ?? "balanced",
2307
+ includeLogo: hostConfig.includeLogo ?? false,
2308
+ }),
2309
+ submit: true,
2310
+ openSidebar: true,
2311
+ newTab: true,
2312
+ });
2313
+ return;
2314
+ }
1063
2315
  setVisibleCandidateRunIds([]);
2316
+ const variantScopeId =
2317
+ pickerVariantScopeId ?? `picker:${getBrowserTabId()}`;
1064
2318
  generateBatch.mutate({
1065
2319
  libraryId: selectedLibraryId,
1066
2320
  presetId: selectedPreset?.id,
2321
+ variantScopeId,
1067
2322
  slots: Array.from({ length: count }, (_, index) => ({
1068
2323
  slotId: `picker-candidate-${index + 1}`,
1069
2324
  prompt: prompt.trim(),
@@ -1079,48 +2334,44 @@ export default function AssetPicker() {
1079
2334
  } as any);
1080
2335
  }, [
1081
2336
  count,
2337
+ currentLibrary?.title,
2338
+ embedded,
1082
2339
  effectiveAspectRatio,
1083
2340
  generateBatch,
1084
2341
  hostConfig.callerAppId,
1085
2342
  hostConfig.includeLogo,
1086
2343
  hostConfig.styleStrength,
1087
2344
  hostConfig.tier,
2345
+ mediaType,
1088
2346
  prompt,
2347
+ pickerVariantScopeId,
2348
+ sendToAgent,
1089
2349
  setVisibleCandidateRunIds,
1090
2350
  selectedLibraryId,
1091
2351
  selectedPreset,
2352
+ usingStarterLibrary,
1092
2353
  waitingForRequestedPreset,
1093
2354
  ]);
1094
2355
 
1095
2356
  useEffect(() => {
1096
- const hostCandidateFilterMatches =
1097
- hostConfig.candidateRunIds?.length &&
1098
- normalizeMediaType(hostConfig.mediaType) === mediaType &&
1099
- (hostConfig.libraryId ?? "") === (selectedLibraryId ?? "") &&
1100
- (hostConfig.prompt ?? "") === prompt &&
1101
- (hostConfig.aspectRatio ?? "16:9") === aspectRatio &&
1102
- (hostConfig.presetId ?? "none") === presetId &&
1103
- (hostConfig.count ?? 3) === count;
1104
- if (hostCandidateFilterMatches) return;
1105
- setVisibleCandidateRunIds([]);
2357
+ if (!visibleCandidateRunIds.length) return;
2358
+ if (normalizeMediaType(hostConfig.mediaType) !== mediaType) {
2359
+ setVisibleCandidateRunIds([]);
2360
+ return;
2361
+ }
2362
+ if (
2363
+ hostConfig.libraryId &&
2364
+ selectedLibraryId &&
2365
+ hostConfig.libraryId !== selectedLibraryId
2366
+ ) {
2367
+ setVisibleCandidateRunIds([]);
2368
+ }
1106
2369
  }, [
1107
- aspectRatio,
1108
- count,
1109
- hostConfig.aspectRatio,
1110
- hostConfig.candidateRunIds,
1111
- hostConfig.count,
1112
- hostConfig.callerAppId,
1113
- hostConfig.includeLogo,
1114
2370
  hostConfig.libraryId,
1115
2371
  hostConfig.mediaType,
1116
- hostConfig.presetId,
1117
- hostConfig.prompt,
1118
- hostConfig.styleStrength,
1119
- hostConfig.tier,
1120
2372
  mediaType,
1121
- presetId,
1122
- prompt,
1123
2373
  selectedLibraryId,
2374
+ visibleCandidateRunIds.length,
1124
2375
  ]);
1125
2376
 
1126
2377
  useEffect(() => {
@@ -1150,25 +2401,17 @@ export default function AssetPicker() {
1150
2401
  }, []);
1151
2402
 
1152
2403
  useEffect(() => {
1153
- fetch(
1154
- agentNativePath(
1155
- `/_agent-native/application-state/navigation:${getBrowserTabId()}`,
1156
- ),
2404
+ void writeClientAppState(
2405
+ `navigation:${getBrowserTabId()}`,
1157
2406
  {
1158
- method: "PUT",
1159
- headers: {
1160
- "content-type": "application/json",
1161
- "x-request-source": "assets-picker-ui",
1162
- },
1163
- body: JSON.stringify({
1164
- view: "picker",
1165
- mediaType,
1166
- libraryId: selectedLibraryId || null,
1167
- query,
1168
- prompt,
1169
- aspectRatio,
1170
- }),
2407
+ view: "picker",
2408
+ mediaType,
2409
+ libraryId: selectedLibraryId || null,
2410
+ query,
2411
+ prompt,
2412
+ aspectRatio,
1171
2413
  },
2414
+ { requestSource: "assets-picker-ui" },
1172
2415
  ).catch(() => {});
1173
2416
  }, [aspectRatio, mediaType, prompt, query, selectedLibraryId]);
1174
2417
 
@@ -1178,14 +2421,14 @@ export default function AssetPicker() {
1178
2421
  !usingStarterLibrary &&
1179
2422
  Boolean(prompt.trim()) &&
1180
2423
  !waitingForRequestedPreset &&
1181
- !generateBatch.isPending;
2424
+ !(embedded ? generateBatch.isPending : chatGenerating);
1182
2425
  const setupNeeded = mediaType === "image" && config?.configured === false;
1183
2426
  const setupMessage =
1184
2427
  typeof config?.lastIssue?.message === "string"
1185
2428
  ? config.lastIssue.message
1186
2429
  : config?.builderEnabled === false
1187
- ? t("assetPicker.addGenerationKey")
1188
- : t("assetPicker.connectGenerationModels");
2430
+ ? t("library.addGenerationKey")
2431
+ : t("library.connectGenerationModels");
1189
2432
  const needsGenerationLibrary =
1190
2433
  mediaType === "image" &&
1191
2434
  libraryListReady &&
@@ -1201,24 +2444,30 @@ export default function AssetPicker() {
1201
2444
  (waitingForLibraries ||
1202
2445
  needsGenerationLibrary ||
1203
2446
  preparingGenerationLibrary);
1204
- const generationButtonLabel = generateBatch.isPending
1205
- ? t("brandKitDetail.generating")
2447
+ const generationButtonLabel = (
2448
+ embedded ? generateBatch.isPending : chatGenerating
2449
+ )
2450
+ ? t("library.generating")
1206
2451
  : waitingForRequestedPreset
1207
- ? t("assetPicker.loadingPreset")
2452
+ ? t("library.loadingPreset")
1208
2453
  : setupNeeded
1209
- ? t("assetPicker.setupNeeded")
2454
+ ? t("library.setupNeeded")
1210
2455
  : preparingAutoLibrary
1211
- ? t("assetPicker.preparing")
1212
- : t("brandKitDetail.generate");
1213
- const generationStatus = generateBatch.isPending
1214
- ? t("assetPicker.generatingCandidates")
1215
- : preparingAutoLibrary
1216
- ? waitingForLibraries
1217
- ? t("assetPicker.checkingLibraries")
1218
- : t("assetPicker.preparingImageLibrary")
1219
- : waitingForRequestedPreset
1220
- ? t("assetPicker.loadingRequestedPreset")
1221
- : null;
2456
+ ? t("library.preparing")
2457
+ : embedded
2458
+ ? t("library.generate")
2459
+ : t("library.openChat");
2460
+ const generationStatus = preparingAutoLibrary
2461
+ ? waitingForLibraries
2462
+ ? t("library.checkingImageLibraries")
2463
+ : t("library.preparingImageLibrary")
2464
+ : embedded && generateBatch.isPending
2465
+ ? t("library.generatingCandidates")
2466
+ : !embedded && chatGenerating
2467
+ ? t("library.generating")
2468
+ : waitingForRequestedPreset
2469
+ ? t("library.loadingRequestedPreset")
2470
+ : null;
1222
2471
 
1223
2472
  useEffect(() => {
1224
2473
  if (!selectedPreset?.aspectRatio) return;
@@ -1228,10 +2477,10 @@ export default function AssetPicker() {
1228
2477
  const prepareGenerationLibrary = useCallback(() => {
1229
2478
  createPickerLibrary.mutate({
1230
2479
  presetId: STARTER_PRESET.id,
1231
- title: t("assetPicker.mcpImagePicks"),
1232
- description: t("assetPicker.mcpImagePicksDescription"),
2480
+ title: t("library.mcpImagePicks"),
2481
+ description: t("library.mcpImagePicksDescription"),
1233
2482
  } as any);
1234
- }, [createPickerLibrary]);
2483
+ }, [createPickerLibrary, t]);
1235
2484
 
1236
2485
  useEffect(() => {
1237
2486
  if (!prompt.trim()) return;
@@ -1251,6 +2500,7 @@ export default function AssetPicker() {
1251
2500
  useEffect(() => {
1252
2501
  if (!hostConfig.autoGenerate) return;
1253
2502
  if (!prompt.trim() || mediaType !== "image") return;
2503
+ if (!embedded) return;
1254
2504
  if (!canGenerate || setupNeeded || generateBatch.isPending) return;
1255
2505
  if (waitingForRequestedPreset) return;
1256
2506
  const key = [
@@ -1266,6 +2516,7 @@ export default function AssetPicker() {
1266
2516
  }, [
1267
2517
  canGenerate,
1268
2518
  count,
2519
+ embedded,
1269
2520
  effectiveAspectRatio,
1270
2521
  generateBatch.isPending,
1271
2522
  hostConfig.autoGenerate,
@@ -1287,7 +2538,7 @@ export default function AssetPicker() {
1287
2538
  >
1288
2539
  <header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-3">
1289
2540
  <div className="min-w-0 truncate text-sm font-semibold">
1290
- {embedded ? t("navigation.brand") : t("navigation.library")}
2541
+ {embedded ? t("navigation.brand") : t("library.library")}
1291
2542
  </div>
1292
2543
  <div className="flex shrink-0 items-center gap-2">
1293
2544
  {mediaType === "image" && (
@@ -1302,9 +2553,7 @@ export default function AssetPicker() {
1302
2553
  ) : (
1303
2554
  <IconPhotoPlus className="h-3.5 w-3.5" />
1304
2555
  )}
1305
- {showCreatePane
1306
- ? t("brandKitDetail.close")
1307
- : t("navigation.create")}
2556
+ {showCreatePane ? t("library.close") : t("library.createPane")}
1308
2557
  </Button>
1309
2558
  )}
1310
2559
  {embedded && (
@@ -1313,7 +2562,7 @@ export default function AssetPicker() {
1313
2562
  asChild
1314
2563
  variant="ghost"
1315
2564
  size="icon"
1316
- title={t("assetPicker.openAssets")}
2565
+ title={t("library.openAssets")}
1317
2566
  >
1318
2567
  <a href={absoluteAppUrl("/")} target="_blank" rel="noreferrer">
1319
2568
  <IconArrowUpRight className="h-4 w-4" />
@@ -1322,7 +2571,7 @@ export default function AssetPicker() {
1322
2571
  <Button
1323
2572
  variant="ghost"
1324
2573
  size="icon"
1325
- title={t("brandKitDetail.close")}
2574
+ title={t("library.close")}
1326
2575
  onClick={() => bridgeRef.current?.close()}
1327
2576
  >
1328
2577
  <IconX className="h-4 w-4" />
@@ -1336,9 +2585,8 @@ export default function AssetPicker() {
1336
2585
  <section className="shrink-0 border-b border-border px-3 py-3">
1337
2586
  {mediaType === "image" ? (
1338
2587
  <div className="mt-2 rounded-lg border border-border/80 bg-background focus-within:ring-1 focus-within:ring-ring">
1339
- <Textarea
1340
- autoGrow
1341
- rows={1}
2588
+ <Input
2589
+ type="text"
1342
2590
  value={prompt}
1343
2591
  onChange={(event) => setPrompt(event.target.value)}
1344
2592
  onKeyDown={(event) => {
@@ -1352,8 +2600,8 @@ export default function AssetPicker() {
1352
2600
  event.preventDefault();
1353
2601
  if (canGenerate) runGenerate();
1354
2602
  }}
1355
- placeholder={t("assetPicker.generatePlaceholder")}
1356
- className="min-h-11 max-h-40 border-0 bg-transparent px-3 py-2.5 leading-6 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
2603
+ placeholder={t("library.generateImagePlaceholder")}
2604
+ className="h-11 border-0 bg-transparent px-3 py-2.5 leading-6 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
1357
2605
  />
1358
2606
  <div className="flex items-center gap-1 px-2 pb-2">
1359
2607
  <div className="flex min-w-0 flex-1 items-center justify-end gap-0.5 sm:gap-1">
@@ -1421,18 +2669,21 @@ export default function AssetPicker() {
1421
2669
  ) : null}
1422
2670
  </div>
1423
2671
  <Button
1424
- className="h-7 shrink-0 px-3 text-xs"
2672
+ className="h-7 min-w-[5.75rem] shrink-0 px-3 text-xs"
1425
2673
  disabled={!canGenerate}
1426
2674
  onClick={runGenerate}
1427
2675
  >
1428
2676
  {generationButtonLabel}
1429
2677
  </Button>
1430
2678
  </div>
1431
- {generationStatus && (
1432
- <div className="-mt-1 px-3 pb-2 text-[11px] leading-4 text-muted-foreground">
1433
- {generationStatus}
1434
- </div>
1435
- )}
2679
+ <div
2680
+ className={cn(
2681
+ "-mt-1 min-h-6 px-3 pb-2 text-[11px] leading-4 text-muted-foreground",
2682
+ !generationStatus && "invisible",
2683
+ )}
2684
+ >
2685
+ {generationStatus || t("library.ready")}
2686
+ </div>
1436
2687
  </div>
1437
2688
  ) : null}
1438
2689
 
@@ -1450,7 +2701,7 @@ export default function AssetPicker() {
1450
2701
  target="_blank"
1451
2702
  rel="noreferrer"
1452
2703
  >
1453
- {t("navigation.settings")}
2704
+ {t("library.settings")}
1454
2705
  </a>
1455
2706
  </Button>
1456
2707
  </div>
@@ -1460,8 +2711,8 @@ export default function AssetPicker() {
1460
2711
  <div className="mt-2 flex items-center justify-between gap-3 rounded-md border border-border bg-muted/60 px-3 py-2 text-xs text-muted-foreground">
1461
2712
  <span className="min-w-0 truncate">
1462
2713
  {preparingGenerationLibrary
1463
- ? t("assetPicker.preparingGeneratedLibrary")
1464
- : t("assetPicker.createLibraryForCandidates")}
2714
+ ? t("library.preparingImageLibraryCandidates")
2715
+ : t("library.createImageLibraryToGenerate")}
1465
2716
  </span>
1466
2717
  <Button
1467
2718
  variant="outline"
@@ -1471,8 +2722,8 @@ export default function AssetPicker() {
1471
2722
  disabled={preparingGenerationLibrary}
1472
2723
  >
1473
2724
  {preparingGenerationLibrary
1474
- ? t("assetPicker.preparing")
1475
- : t("assetPicker.createLibrary")}
2725
+ ? t("library.preparing")
2726
+ : t("library.createLibrary")}
1476
2727
  </Button>
1477
2728
  </div>
1478
2729
  )}
@@ -1480,9 +2731,9 @@ export default function AssetPicker() {
1480
2731
  )}
1481
2732
 
1482
2733
  {!embedded && standaloneSelection && (
1483
- <section className="shrink-0 border-b border-border bg-muted/30 px-3 py-3">
1484
- <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
1485
- <div className="min-w-0">
2734
+ <section className="shrink-0 overflow-hidden border-b border-border bg-muted/30 px-3 py-3">
2735
+ <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
2736
+ <div className="min-w-0 flex-1">
1486
2737
  <div className="truncate text-sm font-medium">
1487
2738
  {selectedAssetLabel(standaloneSelection)}
1488
2739
  </div>
@@ -1492,11 +2743,11 @@ export default function AssetPicker() {
1492
2743
  </div>
1493
2744
  )}
1494
2745
  </div>
1495
- <div className="flex shrink-0 gap-2">
2746
+ <div className="flex w-full min-w-0 flex-wrap gap-2 sm:w-auto sm:flex-nowrap sm:justify-end">
1496
2747
  <Button
1497
2748
  variant="outline"
1498
2749
  size="sm"
1499
- className="h-8 gap-1.5"
2750
+ className="h-8 shrink-0 gap-1.5"
1500
2751
  onClick={() => copyStandaloneSelection(standaloneSelection)}
1501
2752
  >
1502
2753
  {standaloneCopyOk ? (
@@ -1504,16 +2755,14 @@ export default function AssetPicker() {
1504
2755
  ) : (
1505
2756
  <IconClipboard className="h-3.5 w-3.5" />
1506
2757
  )}
1507
- {standaloneCopyOk
1508
- ? t("assetPicker.copied")
1509
- : t("assetPicker.copy")}
2758
+ {standaloneCopyOk ? t("library.copied") : t("library.copy")}
1510
2759
  </Button>
1511
2760
  {canOpenStandaloneAsset && (
1512
2761
  <Button
1513
2762
  asChild
1514
2763
  variant="ghost"
1515
2764
  size="sm"
1516
- className="h-8 gap-1.5"
2765
+ className="h-8 shrink-0 gap-1.5"
1517
2766
  >
1518
2767
  <Link
1519
2768
  to={`/asset/${encodeURIComponent(
@@ -1521,16 +2770,16 @@ export default function AssetPicker() {
1521
2770
  )}`}
1522
2771
  >
1523
2772
  <IconArrowUpRight className="h-3.5 w-3.5" />
1524
- {t("brandKitDetail.open")}
2773
+ {t("library.open")}
1525
2774
  </Link>
1526
2775
  </Button>
1527
2776
  )}
1528
2777
  <Button
1529
2778
  variant="ghost"
1530
2779
  size="icon"
1531
- title={t("brandKitDetail.close")}
1532
- aria-label={t("brandKitDetail.close")}
1533
- className="h-8 w-8"
2780
+ title={t("library.close")}
2781
+ aria-label={t("library.close")}
2782
+ className="h-8 w-8 shrink-0"
1534
2783
  onClick={() => {
1535
2784
  setStandaloneSelection(null);
1536
2785
  setStandaloneCopyOk(false);
@@ -1542,17 +2791,18 @@ export default function AssetPicker() {
1542
2791
  </div>
1543
2792
  <p className="mt-2 text-xs text-muted-foreground">
1544
2793
  {standaloneCopyOk
1545
- ? t("assetPicker.copiedHelp")
1546
- : t("assetPicker.copyHelp")}
2794
+ ? t("library.copiedPasteInstruction")
2795
+ : t("library.copyPasteInstruction")}
2796
+ . {t("library.tellAgentUseThis")}
1547
2797
  </p>
1548
2798
  <details className="mt-2">
1549
2799
  <summary className="cursor-pointer select-none text-xs text-muted-foreground hover:text-foreground">
1550
- {t("assetPicker.showPasteText")}
2800
+ {t("library.showPasteText")}
1551
2801
  </summary>
1552
2802
  <Textarea
1553
2803
  readOnly
1554
2804
  value={standaloneSelectionText}
1555
- className="mt-2 h-24 resize-none border-border/70 bg-background font-mono text-[11px] leading-relaxed"
2805
+ className="mt-2 h-24 max-w-full resize-none border-border/70 bg-background font-mono text-[11px] leading-relaxed"
1556
2806
  onFocus={(event) => event.currentTarget.select()}
1557
2807
  />
1558
2808
  </details>
@@ -1568,7 +2818,7 @@ export default function AssetPicker() {
1568
2818
  onValueChange={setSelectedLibraryId}
1569
2819
  >
1570
2820
  <SelectTrigger className="h-9 w-full border-border/70 bg-background sm:w-48">
1571
- <SelectValue placeholder={t("navigation.library")} />
2821
+ <SelectValue placeholder={t("library.library")} />
1572
2822
  </SelectTrigger>
1573
2823
  <SelectContent>
1574
2824
  <SelectGroup>
@@ -1587,13 +2837,13 @@ export default function AssetPicker() {
1587
2837
  >
1588
2838
  <TabsList>
1589
2839
  <TabsTrigger value="all">
1590
- {t("brandKitDetail.all")}
2840
+ {t("library.tabsAll")}
1591
2841
  </TabsTrigger>
1592
2842
  <TabsTrigger value="generated">
1593
- {t("brandKitDetail.generated")}
2843
+ {t("library.generated")}
1594
2844
  </TabsTrigger>
1595
2845
  <TabsTrigger value="references">
1596
- {t("brandKitDetail.references")}
2846
+ {t("library.references")}
1597
2847
  </TabsTrigger>
1598
2848
  </TabsList>
1599
2849
  </Tabs>
@@ -1605,19 +2855,26 @@ export default function AssetPicker() {
1605
2855
  value={query}
1606
2856
  onInput={(event) => setQuery(event.currentTarget.value)}
1607
2857
  onChange={(event) => setQuery(event.target.value)}
1608
- placeholder={t("assetPicker.searchMedia", {
1609
- media: mediaLabelText,
1610
- })}
2858
+ placeholder={t("library.searchMedia", { mediaLabel })}
1611
2859
  className="h-9 border-border/70 bg-background sm:max-w-xs"
1612
2860
  />
1613
2861
  )}
1614
2862
  </div>
1615
2863
  )}
1616
2864
 
2865
+ {mediaType === "image" && selectedLibraryId && pickerVariantScopeId && (
2866
+ <LibraryCandidateStage
2867
+ activeLibraryId={selectedLibraryId}
2868
+ variantScopeId={pickerVariantScopeId}
2869
+ onUseAsset={chooseAsset}
2870
+ inline
2871
+ />
2872
+ )}
2873
+
1617
2874
  {!selectedLibraryId && (
1618
2875
  <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
1619
2876
  <Button asChild variant="outline">
1620
- <Link to="/brand-kits">{t("brandKits.createDialogTitle")}</Link>
2877
+ <Link to="/library">{t("library.createBrandKit")}</Link>
1621
2878
  </Button>
1622
2879
  </div>
1623
2880
  )}
@@ -1634,10 +2891,8 @@ export default function AssetPicker() {
1634
2891
  <div className="flex h-full items-center justify-center text-center">
1635
2892
  <div className="max-w-sm text-sm text-muted-foreground">
1636
2893
  {query
1637
- ? t("assetPicker.noMatchingAssets", {
1638
- media: mediaLabelText,
1639
- })
1640
- : t("assetPicker.noAssetsYet", { media: mediaLabelText })}
2894
+ ? t("library.noMatchingLibraryAssets", { mediaLabel })
2895
+ : t("library.noAssetsInLibrary", { mediaLabel })}
1641
2896
  </div>
1642
2897
  </div>
1643
2898
  )}
@@ -1651,7 +2906,7 @@ export default function AssetPicker() {
1651
2906
  >
1652
2907
  <button
1653
2908
  type="button"
1654
- aria-label={t("assetPicker.openAsset", {
2909
+ aria-label={t("library.selectAsset", {
1655
2910
  title: assetDisplayTitle(asset),
1656
2911
  })}
1657
2912
  onClick={() => {
@@ -1684,7 +2939,7 @@ export default function AssetPicker() {
1684
2939
  <TooltipTrigger asChild>
1685
2940
  <button
1686
2941
  type="button"
1687
- aria-label={t("assetPicker.copyAsset", {
2942
+ aria-label={t("library.copyAsset", {
1688
2943
  title: assetDisplayTitle(asset),
1689
2944
  })}
1690
2945
  onClick={(event) => {
@@ -1697,7 +2952,7 @@ export default function AssetPicker() {
1697
2952
  </button>
1698
2953
  </TooltipTrigger>
1699
2954
  <TooltipContent>
1700
- {t("assetPicker.copyToClipboard")}
2955
+ {t("library.copyToClipboard")}
1701
2956
  </TooltipContent>
1702
2957
  </Tooltip>
1703
2958
  </TooltipProvider>
@@ -1740,7 +2995,7 @@ export default function AssetPicker() {
1740
2995
  {assetDisplayTitle(previewAsset)}
1741
2996
  </DialogTitle>
1742
2997
  <DialogDescription className="sr-only">
1743
- {t("assetPicker.fullSizePreview", {
2998
+ {t("library.fullSizePreview", {
1744
2999
  title: assetDisplayTitle(previewAsset),
1745
3000
  })}
1746
3001
  </DialogDescription>
@@ -1750,11 +3005,11 @@ export default function AssetPicker() {
1750
3005
  <Link
1751
3006
  to={`/asset/${encodeURIComponent(previewAsset.id)}`}
1752
3007
  >
1753
- {t("brandKitDetail.viewDetails")}
3008
+ {t("library.viewDetails")}
1754
3009
  </Link>
1755
3010
  </Button>
1756
3011
  <DialogClose
1757
- aria-label={t("assetPicker.closePreview")}
3012
+ aria-label={t("library.closePreview")}
1758
3013
  className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
1759
3014
  >
1760
3015
  <IconX className="h-5 w-5" />
@@ -1782,7 +3037,7 @@ export default function AssetPicker() {
1782
3037
  <div className="mt-5 flex justify-center gap-2">
1783
3038
  <button
1784
3039
  type="button"
1785
- aria-label={t("assetPicker.previousImage")}
3040
+ aria-label={t("library.previousImage")}
1786
3041
  onClick={showPreviousAsset}
1787
3042
  disabled={!hasPrev}
1788
3043
  className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:cursor-not-allowed disabled:opacity-40"
@@ -1791,7 +3046,7 @@ export default function AssetPicker() {
1791
3046
  </button>
1792
3047
  <button
1793
3048
  type="button"
1794
- aria-label={t("assetPicker.nextImage")}
3049
+ aria-label={t("library.nextImage")}
1795
3050
  onClick={showNextAsset}
1796
3051
  disabled={!hasNext}
1797
3052
  className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:cursor-not-allowed disabled:opacity-40"
@@ -1807,3 +3062,27 @@ export default function AssetPicker() {
1807
3062
  </div>
1808
3063
  );
1809
3064
  }
3065
+
3066
+ export default function LibraryRoute() {
3067
+ const [searchParams] = useSearchParams();
3068
+ const searchParamsKey = searchParams.toString();
3069
+ const embedded = useMemo(() => isEmbeddedWindow() || isEmbedAuthActive(), []);
3070
+ const { pickerRequested, queryLibraryId } = useMemo(() => {
3071
+ const params = new URLSearchParams(searchParamsKey);
3072
+ const requested =
3073
+ params.get("__an_picker") === "1" ||
3074
+ params.get("__an_mcp_chat_bridge") === "1";
3075
+ return {
3076
+ pickerRequested: requested,
3077
+ queryLibraryId: requested ? null : params.get("libraryId"),
3078
+ };
3079
+ }, [searchParamsKey]);
3080
+ if (queryLibraryId) {
3081
+ return <LibraryWorkspace selectedLibraryId={queryLibraryId} />;
3082
+ }
3083
+ return embedded || pickerRequested ? (
3084
+ <AssetPickerSurface />
3085
+ ) : (
3086
+ <LibraryWorkspace />
3087
+ );
3088
+ }