@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +20 -0
- package/corpus/core/docs/content/template-analytics.md +5 -0
- package/corpus/core/docs/content/tracking.md +45 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/agent/types.ts +22 -0
- package/corpus/core/src/client/AgentPanel.tsx +34 -18
- package/corpus/core/src/client/AssistantChat.tsx +40 -0
- package/corpus/core/src/client/agent-chat.ts +173 -0
- package/corpus/core/src/client/analytics.ts +141 -5
- package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
- package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
- package/corpus/core/src/client/composer/types.ts +22 -0
- package/corpus/core/src/client/index.ts +14 -0
- package/corpus/core/src/client/session-replay.ts +800 -0
- package/corpus/core/src/db/schema.ts +12 -0
- package/corpus/core/src/deploy/build.ts +75 -0
- package/corpus/core/src/private-blob/index.ts +18 -0
- package/corpus/core/src/private-blob/registry.ts +241 -0
- package/corpus/core/src/private-blob/types.ts +55 -0
- package/corpus/core/src/scripts/utils.ts +34 -6
- package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
- package/corpus/core/src/server/email.ts +5 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/styles/agent-native.css +24 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
- package/corpus/templates/analytics/AGENTS.md +32 -1
- package/corpus/templates/analytics/DEVELOPING.md +9 -1
- package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
- package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
- package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
- package/corpus/templates/analytics/actions/navigate.ts +15 -5
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
- package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
- package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
- package/corpus/templates/analytics/actions/view-screen.ts +50 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
- package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
- package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
- package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
- package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
- package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
- package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
- package/corpus/templates/analytics/package.json +5 -2
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
- package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
- package/corpus/templates/analytics/server/db/index.ts +12 -0
- package/corpus/templates/analytics/server/db/schema.ts +131 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
- package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
- package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
- package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
- package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
- package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
- package/corpus/templates/analytics/server/plugins/db.ts +200 -0
- package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
- package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
- package/corpus/templates/assets/AGENTS.md +18 -3
- package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
- package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
- package/corpus/templates/assets/actions/generate-asset.ts +30 -23
- package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
- package/corpus/templates/assets/actions/generate-image.ts +46 -30
- package/corpus/templates/assets/actions/generate-video.ts +18 -3
- package/corpus/templates/assets/actions/list-assets.ts +39 -8
- package/corpus/templates/assets/actions/list-libraries.ts +24 -2
- package/corpus/templates/assets/actions/navigate.ts +1 -1
- package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
- package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
- package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
- package/corpus/templates/assets/actions/variant-slots.ts +131 -28
- package/corpus/templates/assets/actions/view-screen.ts +9 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
- package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
- package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
- package/corpus/templates/assets/app/i18n-data.ts +1815 -0
- package/corpus/templates/assets/app/lib/libraries.ts +6 -0
- package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
- package/corpus/templates/assets/app/routes/audit.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
- package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
- package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
- package/corpus/templates/assets/app/routes/library.tsx +1454 -175
- package/corpus/templates/assets/server/db/index.ts +1 -1
- package/corpus/templates/assets/server/lib/generation.ts +86 -15
- package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
- package/corpus/templates/assets/server/plugins/db.ts +5 -0
- package/corpus/templates/assets/shared/api.ts +6 -3
- package/dist/agent/types.d.ts +21 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +6 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +10 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +15 -3
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +32 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +113 -0
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/analytics.d.ts +22 -3
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +96 -4
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +2 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +228 -56
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
- package/dist/client/composer/extensions/MentionReference.js +3 -0
- package/dist/client/composer/extensions/MentionReference.js.map +1 -1
- package/dist/client/composer/types.d.ts +21 -0
- package/dist/client/composer/types.d.ts.map +1 -1
- package/dist/client/composer/types.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +41 -0
- package/dist/client/session-replay.d.ts.map +1 -0
- package/dist/client/session-replay.js +595 -0
- package/dist/client/session-replay.js.map +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/schema.d.ts +3 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +6 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/deploy/build.d.ts +8 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +66 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/private-blob/index.d.ts +3 -0
- package/dist/private-blob/index.d.ts.map +1 -0
- package/dist/private-blob/index.js +2 -0
- package/dist/private-blob/index.js.map +1 -0
- package/dist/private-blob/registry.d.ts +10 -0
- package/dist/private-blob/registry.d.ts.map +1 -0
- package/dist/private-blob/registry.js +152 -0
- package/dist/private-blob/registry.js.map +1 -0
- package/dist/private-blob/types.d.ts +51 -0
- package/dist/private-blob/types.d.ts.map +1 -0
- package/dist/private-blob/types.js +2 -0
- package/dist/private-blob/types.js.map +1 -0
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/scripts/utils.d.ts +4 -4
- package/dist/scripts/utils.d.ts.map +1 -1
- package/dist/scripts/utils.js +30 -6
- package/dist/scripts/utils.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +5 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/email.d.ts +2 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +3 -0
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +24 -0
- package/docs/content/template-analytics.md +5 -0
- package/docs/content/tracking.md +45 -0
- package/package.json +3 -1
- package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +0 -209
|
@@ -3,14 +3,18 @@ import {
|
|
|
3
3
|
appBasePath,
|
|
4
4
|
agentNativePath,
|
|
5
5
|
getBrowserTabId,
|
|
6
|
+
readClientAppState,
|
|
6
7
|
sendToAgentChat,
|
|
7
8
|
useT,
|
|
8
9
|
useActionMutation,
|
|
9
10
|
useActionQuery,
|
|
10
11
|
} from "@agent-native/core/client";
|
|
11
12
|
import {
|
|
13
|
+
IconCheck,
|
|
14
|
+
IconClipboard,
|
|
12
15
|
IconCopy,
|
|
13
16
|
IconDotsVertical,
|
|
17
|
+
IconArrowUpRight,
|
|
14
18
|
IconArchive,
|
|
15
19
|
IconFolder,
|
|
16
20
|
IconFolderPlus,
|
|
@@ -38,10 +42,19 @@ import {
|
|
|
38
42
|
type ReactNode,
|
|
39
43
|
type SetStateAction,
|
|
40
44
|
useEffect,
|
|
45
|
+
useMemo,
|
|
41
46
|
useRef,
|
|
42
47
|
useState,
|
|
43
48
|
} from "react";
|
|
44
|
-
import {
|
|
49
|
+
import { createPortal } from "react-dom";
|
|
50
|
+
import {
|
|
51
|
+
Link,
|
|
52
|
+
LoaderFunctionArgs,
|
|
53
|
+
redirect,
|
|
54
|
+
useNavigate,
|
|
55
|
+
useParams,
|
|
56
|
+
useSearchParams,
|
|
57
|
+
} from "react-router";
|
|
45
58
|
import { toast } from "sonner";
|
|
46
59
|
|
|
47
60
|
import { EditLibraryDialog } from "@/components/library/EditLibraryDialog";
|
|
@@ -78,11 +91,6 @@ import {
|
|
|
78
91
|
} from "@/components/ui/dropdown-menu";
|
|
79
92
|
import { Input } from "@/components/ui/input";
|
|
80
93
|
import { Label } from "@/components/ui/label";
|
|
81
|
-
import {
|
|
82
|
-
Popover,
|
|
83
|
-
PopoverContent,
|
|
84
|
-
PopoverTrigger,
|
|
85
|
-
} from "@/components/ui/popover";
|
|
86
94
|
import {
|
|
87
95
|
Select,
|
|
88
96
|
SelectContent,
|
|
@@ -114,26 +122,13 @@ import {
|
|
|
114
122
|
import {
|
|
115
123
|
IMAGE_CATEGORIES,
|
|
116
124
|
ASPECT_RATIOS,
|
|
117
|
-
|
|
118
|
-
IMAGE_SIZES,
|
|
119
|
-
VIDEO_ASPECT_RATIOS,
|
|
120
|
-
VIDEO_DURATIONS,
|
|
121
|
-
VIDEO_MODELS,
|
|
122
|
-
VIDEO_RESOLUTIONS,
|
|
125
|
+
type AssetVariantState,
|
|
123
126
|
type AspectRatio,
|
|
124
127
|
type ImageCategory,
|
|
125
128
|
type ImageRole,
|
|
126
129
|
} from "../../shared/api";
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
if (typeof slot?.assetId === "string" && slot.assetId) {
|
|
130
|
-
return `asset:${slot.assetId}`;
|
|
131
|
-
}
|
|
132
|
-
if (typeof slot?.slotId === "string" && slot.slotId) {
|
|
133
|
-
return `slot:${slot.slotId}`;
|
|
134
|
-
}
|
|
135
|
-
return "";
|
|
136
|
-
}
|
|
131
|
+
export type VariantSlot = AssetVariantState["slots"][number];
|
|
137
132
|
|
|
138
133
|
function referencePromotionKey(asset: any, slot?: any): string {
|
|
139
134
|
if (typeof slot?.slotId === "string" && slot.slotId) {
|
|
@@ -293,27 +288,6 @@ function removeVariantSlotsByScopeFromCache(
|
|
|
293
288
|
);
|
|
294
289
|
}
|
|
295
290
|
|
|
296
|
-
function variantSlotTime(slot: any): number {
|
|
297
|
-
const raw = slot?.createdAt ?? slot?.updatedAt ?? "";
|
|
298
|
-
const time = Date.parse(String(raw));
|
|
299
|
-
return Number.isNaN(time) ? 0 : time;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function compareVariantSlotsNewestFirst(left: any, right: any): number {
|
|
303
|
-
return (
|
|
304
|
-
variantSlotTime(right) - variantSlotTime(left) ||
|
|
305
|
-
String(right?.slotId ?? "").localeCompare(String(left?.slotId ?? ""))
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function stalePendingVariantRunId(slot: any): string | null {
|
|
310
|
-
if (slot?.status !== "pending") return null;
|
|
311
|
-
if (typeof slot?.runId !== "string" || !slot.runId) return null;
|
|
312
|
-
const timestamp = variantSlotTime(slot);
|
|
313
|
-
if (!timestamp) return null;
|
|
314
|
-
return Date.now() - timestamp >= 2 * 60 * 1000 ? slot.runId : null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
291
|
function paletteDraftFromColors(colors: unknown): string {
|
|
318
292
|
return Array.isArray(colors)
|
|
319
293
|
? colors.filter((color) => typeof color === "string").join(", ")
|
|
@@ -331,6 +305,18 @@ function referenceRoleForAsset(asset: any): ImageRole {
|
|
|
331
305
|
return "style_reference";
|
|
332
306
|
}
|
|
333
307
|
|
|
308
|
+
function variantSlotTime(slot: VariantSlot): number {
|
|
309
|
+
const raw = slot.createdAt ?? slot.updatedAt ?? "";
|
|
310
|
+
const time = Date.parse(raw);
|
|
311
|
+
return Number.isNaN(time) ? 0 : time;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function assetUpdatedTime(asset: any): number {
|
|
315
|
+
const raw = asset?.updatedAt ?? asset?.createdAt ?? "";
|
|
316
|
+
const time = Date.parse(String(raw));
|
|
317
|
+
return Number.isNaN(time) ? 0 : time;
|
|
318
|
+
}
|
|
319
|
+
|
|
334
320
|
function parsePaletteDraft(value: string): string[] {
|
|
335
321
|
const seen = new Set<string>();
|
|
336
322
|
const colors: string[] = [];
|
|
@@ -347,6 +333,15 @@ function parsePaletteDraft(value: string): string[] {
|
|
|
347
333
|
return colors;
|
|
348
334
|
}
|
|
349
335
|
|
|
336
|
+
export function loader({ params, request }: LoaderFunctionArgs) {
|
|
337
|
+
const url = new URL(request.url);
|
|
338
|
+
return redirect(`/library/${params.id}${url.search}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export default function BrandKitDetailRedirect() {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
350
345
|
function libraryTabFromValue(value: unknown): LibraryTab | null {
|
|
351
346
|
return value === "references" ||
|
|
352
347
|
value === "generated" ||
|
|
@@ -356,26 +351,31 @@ function libraryTabFromValue(value: unknown): LibraryTab | null {
|
|
|
356
351
|
: null;
|
|
357
352
|
}
|
|
358
353
|
|
|
359
|
-
export
|
|
354
|
+
export function BrandKitDetailRoute({
|
|
355
|
+
libraryId: explicitLibraryId = null,
|
|
356
|
+
headerMode = "full",
|
|
357
|
+
}: {
|
|
358
|
+
libraryId?: string | null;
|
|
359
|
+
headerMode?: "full" | "actions";
|
|
360
|
+
} = {}) {
|
|
360
361
|
const t = useT();
|
|
361
362
|
const { id } = useParams();
|
|
362
363
|
const navigate = useNavigate();
|
|
363
364
|
const [searchParams] = useSearchParams();
|
|
364
365
|
const urlTab = libraryTabFromValue(searchParams.get("tab"));
|
|
365
|
-
const libraryId = id!;
|
|
366
|
+
const libraryId = explicitLibraryId ?? id!;
|
|
366
367
|
const { data } = useActionQuery("get-library", { id: libraryId }) as any;
|
|
367
368
|
const updateLibrary = useActionMutation("update-library");
|
|
368
369
|
const archiveLibrary = useActionMutation("archive-library");
|
|
369
370
|
const duplicateLibrary = useActionMutation("duplicate-library");
|
|
370
|
-
const saveGenerated = useActionMutation("save-generated-image");
|
|
371
371
|
const updateAsset = useActionMutation("update-asset");
|
|
372
|
+
const saveGenerated = useActionMutation("save-generated-image");
|
|
372
373
|
const rerunGeneration = useActionMutation("rerun-generation-run");
|
|
373
374
|
const refreshGeneration = useActionMutation("refresh-generation-run");
|
|
374
375
|
const createSession = useActionMutation("create-generation-session");
|
|
375
376
|
const prepareSessionContinuation = useActionMutation(
|
|
376
377
|
"prepare-generation-session-continuation",
|
|
377
378
|
);
|
|
378
|
-
const { data: variants } = useVariantState();
|
|
379
379
|
const { data: presetData } = useActionQuery("list-generation-presets", {
|
|
380
380
|
libraryId,
|
|
381
381
|
}) as any;
|
|
@@ -383,17 +383,20 @@ export default function LibraryPage() {
|
|
|
383
383
|
libraryId,
|
|
384
384
|
}) as any;
|
|
385
385
|
const queryClient = useQueryClient();
|
|
386
|
-
const [generateOpen, setGenerateOpen] = useState(false);
|
|
387
386
|
const [folderOpen, setFolderOpen] = useState(false);
|
|
388
387
|
const [uploading, setUploading] = useState(false);
|
|
389
388
|
const [pendingUploads, setPendingUploads] = useState<PendingUpload[]>([]);
|
|
390
389
|
const [editOpen, setEditOpen] = useState(false);
|
|
391
390
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
|
391
|
+
const [headerPrimaryActionsTarget, setHeaderPrimaryActionsTarget] =
|
|
392
|
+
useState<HTMLElement | null>(null);
|
|
393
|
+
const [headerMoreActionsTarget, setHeaderMoreActionsTarget] =
|
|
394
|
+
useState<HTMLElement | null>(null);
|
|
392
395
|
const [activeFolderId, setActiveFolderId] = useState<string | null>("all");
|
|
393
396
|
const [activeTab, setActiveTab] = useState<LibraryTab>(
|
|
394
397
|
() => urlTab ?? "references",
|
|
395
398
|
);
|
|
396
|
-
const [assetViewMode, setAssetViewMode] = useState<AssetViewMode>("
|
|
399
|
+
const [assetViewMode, setAssetViewMode] = useState<AssetViewMode>("cards");
|
|
397
400
|
const [assetScope, setAssetScope] = useState<AssetLibraryScope>("all");
|
|
398
401
|
const [selectedAssetIds, setSelectedAssetIds] = useState<Set<string>>(
|
|
399
402
|
() => new Set(),
|
|
@@ -402,12 +405,12 @@ export default function LibraryPage() {
|
|
|
402
405
|
useState<Set<string>>(() => new Set());
|
|
403
406
|
const [optimisticallySavedAssetIds, setOptimisticallySavedAssetIds] =
|
|
404
407
|
useState<Set<string>>(() => new Set());
|
|
405
|
-
const [savingCandidateKeys, setSavingCandidateKeys] = useState<Set<string>>(
|
|
406
|
-
() => new Set(),
|
|
407
|
-
);
|
|
408
408
|
const [promotingReferenceKeys, setPromotingReferenceKeys] = useState<
|
|
409
409
|
Set<string>
|
|
410
410
|
>(() => new Set());
|
|
411
|
+
const [savingCandidateSlotId, setSavingCandidateSlotId] = useState<
|
|
412
|
+
string | null
|
|
413
|
+
>(null);
|
|
411
414
|
const [mediaFilter, setMediaFilter] = useState<"all" | "image" | "video">(
|
|
412
415
|
"all",
|
|
413
416
|
);
|
|
@@ -416,30 +419,43 @@ export default function LibraryPage() {
|
|
|
416
419
|
const [customInstructionsDraft, setCustomInstructionsDraft] = useState("");
|
|
417
420
|
const [paletteDraft, setPaletteDraft] = useState("");
|
|
418
421
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
419
|
-
const refreshingVariantRunIdsRef = useRef<Set<string>>(new Set());
|
|
420
422
|
const dragCounterRef = useRef(0);
|
|
421
423
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
422
424
|
const createFolder = useActionMutation("create-folder");
|
|
425
|
+
const { data: liveVariants } = useQuery({
|
|
426
|
+
queryKey: ["app-state", "asset-variants"],
|
|
427
|
+
queryFn: ({ signal }) => {
|
|
428
|
+
return readClientAppState<AssetVariantState>("asset-variants", {
|
|
429
|
+
signal,
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
refetchInterval: 1000,
|
|
433
|
+
});
|
|
423
434
|
|
|
424
435
|
useEffect(() => {
|
|
425
436
|
if (!urlTab) return;
|
|
426
437
|
setActiveTab((current) => (current === urlTab ? current : urlTab));
|
|
427
438
|
}, [urlTab]);
|
|
428
439
|
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (headerMode !== "actions" || typeof document === "undefined") {
|
|
442
|
+
setHeaderPrimaryActionsTarget(null);
|
|
443
|
+
setHeaderMoreActionsTarget(null);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
setHeaderPrimaryActionsTarget(
|
|
447
|
+
document.getElementById("assets-library-detail-primary-actions"),
|
|
448
|
+
);
|
|
449
|
+
setHeaderMoreActionsTarget(
|
|
450
|
+
document.getElementById("assets-library-detail-more-actions"),
|
|
451
|
+
);
|
|
452
|
+
}, [headerMode, libraryId]);
|
|
453
|
+
|
|
429
454
|
const library = data?.library;
|
|
430
455
|
const folders = (data?.folders ?? []) as any[];
|
|
431
456
|
const generationPresets = ((presetData as any)?.presets ?? []) as any[];
|
|
432
457
|
const generationSessions = ((sessionData as any)?.sessions ?? []) as any[];
|
|
433
458
|
const serverAssets = (data?.assets ?? []) as any[];
|
|
434
|
-
const finishedVariantAssetIds = new Set(
|
|
435
|
-
serverAssets
|
|
436
|
-
.filter(
|
|
437
|
-
(asset) =>
|
|
438
|
-
asset.status !== "candidate" ||
|
|
439
|
-
optimisticallySavedAssetIds.has(asset.id),
|
|
440
|
-
)
|
|
441
|
-
.map((asset) => asset.id),
|
|
442
|
-
);
|
|
443
459
|
const assets = serverAssets
|
|
444
460
|
.map((asset) =>
|
|
445
461
|
optimisticallySavedAssetIds.has(asset.id)
|
|
@@ -447,9 +463,6 @@ export default function LibraryPage() {
|
|
|
447
463
|
: asset,
|
|
448
464
|
)
|
|
449
465
|
.filter((asset) => !optimisticallyDeletedAssetIds.has(asset.id));
|
|
450
|
-
const candidateAssets = assets.filter(
|
|
451
|
-
(asset) => asset.role === "generated" && asset.status === "candidate",
|
|
452
|
-
);
|
|
453
466
|
const libraryAssets = assets.filter((asset) => asset.status !== "candidate");
|
|
454
467
|
const visibleAssets = libraryAssets.filter((asset) => {
|
|
455
468
|
if (activeFolderId !== "all") {
|
|
@@ -495,6 +508,45 @@ export default function LibraryPage() {
|
|
|
495
508
|
const libraryPaletteDraft = paletteDraftFromColors(
|
|
496
509
|
library?.styleBrief?.palette,
|
|
497
510
|
);
|
|
511
|
+
const liveVariantsForLibrary =
|
|
512
|
+
liveVariants?.libraryId === libraryId ? liveVariants : null;
|
|
513
|
+
const liveCandidateSlots = useMemo(
|
|
514
|
+
() =>
|
|
515
|
+
(liveVariantsForLibrary?.slots ?? [])
|
|
516
|
+
.filter(
|
|
517
|
+
(slot) =>
|
|
518
|
+
slot.status === "pending" ||
|
|
519
|
+
slot.status === "ready" ||
|
|
520
|
+
slot.status === "failed",
|
|
521
|
+
)
|
|
522
|
+
.slice()
|
|
523
|
+
.sort(
|
|
524
|
+
(left, right) =>
|
|
525
|
+
variantSlotTime(right) - variantSlotTime(left) ||
|
|
526
|
+
right.slotId.localeCompare(left.slotId),
|
|
527
|
+
),
|
|
528
|
+
[liveVariantsForLibrary?.slots],
|
|
529
|
+
);
|
|
530
|
+
const draftCandidateAssets = useMemo(() => {
|
|
531
|
+
const liveAssetIds = new Set(
|
|
532
|
+
liveCandidateSlots
|
|
533
|
+
.map((slot) => slot.assetId)
|
|
534
|
+
.filter((assetId): assetId is string => typeof assetId === "string"),
|
|
535
|
+
);
|
|
536
|
+
return assets
|
|
537
|
+
.filter(
|
|
538
|
+
(asset) =>
|
|
539
|
+
asset.status === "candidate" &&
|
|
540
|
+
asset.role === "generated" &&
|
|
541
|
+
!liveAssetIds.has(asset.id),
|
|
542
|
+
)
|
|
543
|
+
.slice()
|
|
544
|
+
.sort(
|
|
545
|
+
(left, right) =>
|
|
546
|
+
assetUpdatedTime(right) - assetUpdatedTime(left) ||
|
|
547
|
+
String(right.id).localeCompare(String(left.id)),
|
|
548
|
+
);
|
|
549
|
+
}, [assets, liveCandidateSlots]);
|
|
498
550
|
|
|
499
551
|
useEffect(() => {
|
|
500
552
|
setStyleDescriptionDraft(libraryStyleDescription);
|
|
@@ -514,41 +566,6 @@ export default function LibraryPage() {
|
|
|
514
566
|
return upload.folderId === activeFolderId;
|
|
515
567
|
});
|
|
516
568
|
|
|
517
|
-
const pendingVariants =
|
|
518
|
-
variants?.libraryId === libraryId
|
|
519
|
-
? (variants.slots ?? [])
|
|
520
|
-
.filter(
|
|
521
|
-
(slot: any) =>
|
|
522
|
-
!slot.assetId || !finishedVariantAssetIds.has(slot.assetId),
|
|
523
|
-
)
|
|
524
|
-
.sort(compareVariantSlotsNewestFirst)
|
|
525
|
-
: [];
|
|
526
|
-
|
|
527
|
-
useEffect(() => {
|
|
528
|
-
if (refreshGeneration.isPending) return;
|
|
529
|
-
const runId = pendingVariants
|
|
530
|
-
.map(stalePendingVariantRunId)
|
|
531
|
-
.find((id: string | null): id is string =>
|
|
532
|
-
Boolean(id && !refreshingVariantRunIdsRef.current.has(id)),
|
|
533
|
-
);
|
|
534
|
-
if (!runId) return;
|
|
535
|
-
refreshingVariantRunIdsRef.current.add(runId);
|
|
536
|
-
refreshGeneration.mutate(
|
|
537
|
-
{ runId },
|
|
538
|
-
{
|
|
539
|
-
onSettled: () => {
|
|
540
|
-
window.setTimeout(() => {
|
|
541
|
-
refreshingVariantRunIdsRef.current.delete(runId);
|
|
542
|
-
}, 30_000);
|
|
543
|
-
void queryClient.invalidateQueries({
|
|
544
|
-
queryKey: ["app-state", "asset-variants"],
|
|
545
|
-
refetchType: "active",
|
|
546
|
-
});
|
|
547
|
-
},
|
|
548
|
-
},
|
|
549
|
-
);
|
|
550
|
-
}, [pendingVariants, queryClient, refreshGeneration]);
|
|
551
|
-
|
|
552
569
|
function markAssetsOptimisticallyDeleted(ids: string[]) {
|
|
553
570
|
setOptimisticallyDeletedAssetIds((current) => {
|
|
554
571
|
const next = new Set(current);
|
|
@@ -591,18 +608,6 @@ export default function LibraryPage() {
|
|
|
591
608
|
});
|
|
592
609
|
}, [serverAssets]);
|
|
593
610
|
|
|
594
|
-
function setCandidateSaving(key: string, saving: boolean) {
|
|
595
|
-
setSavingCandidateKeys((current) => {
|
|
596
|
-
const next = new Set(current);
|
|
597
|
-
if (saving) {
|
|
598
|
-
next.add(key);
|
|
599
|
-
} else {
|
|
600
|
-
next.delete(key);
|
|
601
|
-
}
|
|
602
|
-
return next.size === current.size ? current : next;
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
611
|
function setReferencePromoting(key: string, promoting: boolean) {
|
|
607
612
|
setPromotingReferenceKeys((current) => {
|
|
608
613
|
const next = new Set(current);
|
|
@@ -615,52 +620,103 @@ export default function LibraryPage() {
|
|
|
615
620
|
});
|
|
616
621
|
}
|
|
617
622
|
|
|
618
|
-
async function
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
+
async function handleSaveLiveCandidate(
|
|
624
|
+
slot: VariantSlot,
|
|
625
|
+
folderId?: string | null,
|
|
626
|
+
) {
|
|
627
|
+
if (savingCandidateSlotId || (!slot.assetId && !slot.slotId)) return;
|
|
628
|
+
setSavingCandidateSlotId(slot.slotId);
|
|
623
629
|
try {
|
|
624
630
|
const savedAsset = await saveGenerated.mutateAsync({
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
631
|
+
assetId: slot.assetId,
|
|
632
|
+
slotId: slot.slotId,
|
|
633
|
+
folderId,
|
|
628
634
|
});
|
|
629
|
-
|
|
630
|
-
typeof (savedAsset as any)?.id === "string"
|
|
631
|
-
? (savedAsset as any).id
|
|
632
|
-
: slot.assetId;
|
|
633
|
-
|
|
634
|
-
if (savedAssetId) {
|
|
635
|
+
if (slot.assetId) {
|
|
635
636
|
setOptimisticallySavedAssetIds((current) => {
|
|
636
637
|
const next = new Set(current);
|
|
637
|
-
next.add(
|
|
638
|
+
next.add(slot.assetId!);
|
|
638
639
|
return next;
|
|
639
640
|
});
|
|
640
641
|
markLibraryAssetSavedInCache(
|
|
641
642
|
queryClient,
|
|
642
643
|
libraryId,
|
|
643
|
-
|
|
644
|
+
slot.assetId,
|
|
644
645
|
savedAsset,
|
|
645
646
|
);
|
|
646
647
|
}
|
|
647
648
|
removeVariantSlotFromCache(queryClient, slot);
|
|
648
649
|
void queryClient.invalidateQueries({
|
|
649
|
-
queryKey: ["
|
|
650
|
+
queryKey: ["action", "get-library", { id: libraryId }],
|
|
651
|
+
refetchType: "active",
|
|
652
|
+
});
|
|
653
|
+
void queryClient.invalidateQueries({
|
|
654
|
+
queryKey: ["app-state"],
|
|
650
655
|
refetchType: "active",
|
|
651
656
|
});
|
|
652
|
-
toast.success(t("
|
|
657
|
+
toast.success(t("library.savedToLibrary"));
|
|
653
658
|
} catch (error) {
|
|
654
659
|
toast.error(
|
|
655
660
|
error instanceof Error
|
|
656
661
|
? error.message
|
|
657
|
-
: t("
|
|
662
|
+
: t("library.couldNotSaveCandidate"),
|
|
663
|
+
);
|
|
664
|
+
} finally {
|
|
665
|
+
setSavingCandidateSlotId(null);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function handleSaveDraftCandidate(
|
|
670
|
+
asset: any,
|
|
671
|
+
folderId?: string | null,
|
|
672
|
+
) {
|
|
673
|
+
if (!asset?.id || savingCandidateSlotId) return;
|
|
674
|
+
const key = `draft:${asset.id}`;
|
|
675
|
+
setSavingCandidateSlotId(key);
|
|
676
|
+
try {
|
|
677
|
+
const savedAsset = await saveGenerated.mutateAsync({
|
|
678
|
+
assetId: asset.id,
|
|
679
|
+
folderId,
|
|
680
|
+
});
|
|
681
|
+
setOptimisticallySavedAssetIds((current) => {
|
|
682
|
+
const next = new Set(current);
|
|
683
|
+
next.add(asset.id);
|
|
684
|
+
return next;
|
|
685
|
+
});
|
|
686
|
+
markLibraryAssetSavedInCache(
|
|
687
|
+
queryClient,
|
|
688
|
+
libraryId,
|
|
689
|
+
asset.id,
|
|
690
|
+
savedAsset,
|
|
691
|
+
);
|
|
692
|
+
void queryClient.invalidateQueries({
|
|
693
|
+
queryKey: ["action", "get-library", { id: libraryId }],
|
|
694
|
+
refetchType: "active",
|
|
695
|
+
});
|
|
696
|
+
void queryClient.invalidateQueries({
|
|
697
|
+
queryKey: ["app-state"],
|
|
698
|
+
refetchType: "active",
|
|
699
|
+
});
|
|
700
|
+
toast.success(t("library.savedToLibrary"));
|
|
701
|
+
} catch (error) {
|
|
702
|
+
toast.error(
|
|
703
|
+
error instanceof Error ? error.message : t("library.couldNotSaveDraft"),
|
|
658
704
|
);
|
|
659
705
|
} finally {
|
|
660
|
-
|
|
706
|
+
setSavingCandidateSlotId(null);
|
|
661
707
|
}
|
|
662
708
|
}
|
|
663
709
|
|
|
710
|
+
function handleMoveLiveCandidateToReferences(slot: VariantSlot) {
|
|
711
|
+
if (!slot.assetId) return;
|
|
712
|
+
const asset = assetById.get(slot.assetId) ?? {
|
|
713
|
+
id: slot.assetId,
|
|
714
|
+
mediaType: "image",
|
|
715
|
+
status: "candidate",
|
|
716
|
+
};
|
|
717
|
+
void handleMoveToReferences(asset, slot);
|
|
718
|
+
}
|
|
719
|
+
|
|
664
720
|
async function handleMoveToReferences(asset: any, slot?: any) {
|
|
665
721
|
const key = referencePromotionKey(asset, slot);
|
|
666
722
|
if (!asset?.id || !key || promotingReferenceKeys.has(key)) return;
|
|
@@ -684,7 +740,7 @@ export default function LibraryPage() {
|
|
|
684
740
|
refetchType: "active",
|
|
685
741
|
});
|
|
686
742
|
void queryClient.invalidateQueries({
|
|
687
|
-
queryKey: ["app-state"
|
|
743
|
+
queryKey: ["app-state"],
|
|
688
744
|
refetchType: "active",
|
|
689
745
|
});
|
|
690
746
|
toast.success(t("brandKitDetail.addedToReferences"));
|
|
@@ -889,50 +945,41 @@ export default function LibraryPage() {
|
|
|
889
945
|
}
|
|
890
946
|
if (failedCount > 0) {
|
|
891
947
|
toast.warning(
|
|
892
|
-
t("
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
failed: failedCount,
|
|
948
|
+
t("library.uploadedWithFailures", {
|
|
949
|
+
uploadedCount,
|
|
950
|
+
failedCount,
|
|
896
951
|
}),
|
|
897
952
|
{
|
|
898
953
|
id: toastId,
|
|
899
954
|
description:
|
|
900
955
|
skippedCount > 0
|
|
901
|
-
? t("
|
|
902
|
-
count: skippedCount,
|
|
903
|
-
plural: skippedCount === 1 ? "" : "s",
|
|
904
|
-
})
|
|
956
|
+
? t("library.skippedDuplicates", { count: skippedCount })
|
|
905
957
|
: null,
|
|
906
958
|
},
|
|
907
959
|
);
|
|
908
960
|
} else if (uploadedCount > 0 && skippedCount > 0) {
|
|
909
961
|
toast.success(
|
|
910
|
-
t("
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
skipped: skippedCount,
|
|
914
|
-
skippedPlural: skippedCount === 1 ? "" : "s",
|
|
962
|
+
t("library.uploadedAndSkippedDuplicates", {
|
|
963
|
+
uploadedCount,
|
|
964
|
+
skippedCount,
|
|
915
965
|
}),
|
|
916
966
|
{ id: toastId, description: null },
|
|
917
967
|
);
|
|
918
968
|
} else if (uploadedCount > 0) {
|
|
919
|
-
toast.success(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
description: null,
|
|
924
|
-
},
|
|
925
|
-
);
|
|
969
|
+
toast.success(t("library.uploadedAssets", { count: uploadedCount }), {
|
|
970
|
+
id: toastId,
|
|
971
|
+
description: null,
|
|
972
|
+
});
|
|
926
973
|
} else if (skippedCount > 0) {
|
|
927
974
|
toast.warning(
|
|
928
|
-
t("
|
|
975
|
+
t("library.skippedDuplicateAssets", { count: skippedCount }),
|
|
929
976
|
{
|
|
930
977
|
id: toastId,
|
|
931
|
-
description: t("
|
|
978
|
+
description: t("library.alreadyInThisBrandKit"),
|
|
932
979
|
},
|
|
933
980
|
);
|
|
934
981
|
} else {
|
|
935
|
-
toast.warning(t("
|
|
982
|
+
toast.warning(t("library.noNewAssetsUploaded"), {
|
|
936
983
|
id: toastId,
|
|
937
984
|
description: null,
|
|
938
985
|
});
|
|
@@ -940,7 +987,7 @@ export default function LibraryPage() {
|
|
|
940
987
|
await refreshLibrary();
|
|
941
988
|
} catch (e) {
|
|
942
989
|
const message =
|
|
943
|
-
e instanceof Error ? e.message : t("
|
|
990
|
+
e instanceof Error ? e.message : t("library.uploadFailed");
|
|
944
991
|
const indeterminate =
|
|
945
992
|
/(?:\b408\b|\b504\b|timeout|timed out|network|failed to fetch|load failed)/i.test(
|
|
946
993
|
message,
|
|
@@ -950,9 +997,9 @@ export default function LibraryPage() {
|
|
|
950
997
|
setPendingUploads(
|
|
951
998
|
pending.map((upload) => ({ ...upload, status: "checking" })),
|
|
952
999
|
);
|
|
953
|
-
toast.warning(t("
|
|
1000
|
+
toast.warning(t("library.uploadTakingLonger"), {
|
|
954
1001
|
id: toastId,
|
|
955
|
-
description: t("
|
|
1002
|
+
description: t("library.uploadTakingLongerDescription"),
|
|
956
1003
|
});
|
|
957
1004
|
void refreshLibrary();
|
|
958
1005
|
window.setTimeout(() => void refreshLibrary(), 4_000);
|
|
@@ -974,8 +1021,8 @@ export default function LibraryPage() {
|
|
|
974
1021
|
if (!library || archiveLibrary.isPending) return;
|
|
975
1022
|
try {
|
|
976
1023
|
await archiveLibrary.mutateAsync({ id: library.id });
|
|
977
|
-
toast.success(t("
|
|
978
|
-
navigate("/
|
|
1024
|
+
toast.success(t("library.brandKitArchived"));
|
|
1025
|
+
navigate("/library");
|
|
979
1026
|
} catch (error) {
|
|
980
1027
|
toast.error(
|
|
981
1028
|
error instanceof Error
|
|
@@ -991,8 +1038,8 @@ export default function LibraryPage() {
|
|
|
991
1038
|
const copy = (await duplicateLibrary.mutateAsync({
|
|
992
1039
|
id: library.id,
|
|
993
1040
|
})) as any;
|
|
994
|
-
toast.success(t("
|
|
995
|
-
navigate(`/
|
|
1041
|
+
toast.success(t("library.privateBrandKitCopyCreated"));
|
|
1042
|
+
navigate(`/library/${copy.id}`);
|
|
996
1043
|
} catch (error) {
|
|
997
1044
|
toast.error(
|
|
998
1045
|
error instanceof Error
|
|
@@ -1002,61 +1049,6 @@ export default function LibraryPage() {
|
|
|
1002
1049
|
}
|
|
1003
1050
|
}
|
|
1004
1051
|
|
|
1005
|
-
function generate(prompt: string, options: GenerateOptions) {
|
|
1006
|
-
const selectedPreset = options.presetId
|
|
1007
|
-
? generationPresets.find((preset) => preset.id === options.presetId)
|
|
1008
|
-
: null;
|
|
1009
|
-
const trimmedPrompt = prompt.trim();
|
|
1010
|
-
const chatMessage =
|
|
1011
|
-
trimmedPrompt ||
|
|
1012
|
-
(options.mediaType === "video"
|
|
1013
|
-
? "Generate a video for this library."
|
|
1014
|
-
: "Generate images for this library.");
|
|
1015
|
-
const context = [
|
|
1016
|
-
"## Assets library context",
|
|
1017
|
-
options.mediaType === "video"
|
|
1018
|
-
? "Requested output: 1 video candidate for this library"
|
|
1019
|
-
: `Requested output: ${options.count} image candidate${options.count === 1 ? "" : "s"} for this library`,
|
|
1020
|
-
`User prompt: ${trimmedPrompt || "(no text prompt provided)"}`,
|
|
1021
|
-
options.presetId ? `Preset ID: ${options.presetId}` : "Preset ID: none",
|
|
1022
|
-
`Aspect ratio: ${options.aspectRatio}`,
|
|
1023
|
-
options.mediaType === "video"
|
|
1024
|
-
? `Duration: ${options.durationSeconds}s\nResolution: ${options.resolution}`
|
|
1025
|
-
: `Image size: ${options.imageSize}`,
|
|
1026
|
-
`Model: ${options.model}`,
|
|
1027
|
-
activeFolderId && activeFolderId !== "all"
|
|
1028
|
-
? `Folder ID: ${activeFolderId}`
|
|
1029
|
-
: "Folder ID: none",
|
|
1030
|
-
`Reference categories: ${options.category}`,
|
|
1031
|
-
`Include canonical logo: ${options.includeLogo ? "yes" : "no"}`,
|
|
1032
|
-
"",
|
|
1033
|
-
"## Selected library",
|
|
1034
|
-
`Library: ${library.title} (${library.id})`,
|
|
1035
|
-
`Description: ${library.description || ""}`,
|
|
1036
|
-
`Folder: ${activeFolderId && activeFolderId !== "all" ? folders.find((folder) => folder.id === activeFolderId)?.title : "All assets"}`, // i18n-ignore: agent prompt context, not UI
|
|
1037
|
-
`References: ${references.length}`,
|
|
1038
|
-
`Saved assets: ${saved.length}`,
|
|
1039
|
-
`Style brief: ${JSON.stringify(library.styleBrief ?? {})}`,
|
|
1040
|
-
customInstructions
|
|
1041
|
-
? `Custom instructions: ${customInstructions}`
|
|
1042
|
-
: "Custom instructions: none",
|
|
1043
|
-
selectedPreset
|
|
1044
|
-
? `Generation preset: ${selectedPreset.title} (${selectedPreset.id}); ${selectedPreset.aspectRatio}; text policy: ${selectedPreset.textPolicy || "none"}`
|
|
1045
|
-
: "Generation preset: none",
|
|
1046
|
-
"",
|
|
1047
|
-
options.mediaType === "video"
|
|
1048
|
-
? "Use generate-video, then call refresh-generation-run until the run completes and returns a video asset. Use save-generated-asset when the user approves it."
|
|
1049
|
-
: "Use the asset generation actions. If a generation preset ID is present, pass presetId to generate-image or generate-image-batch. Generate candidates, show previews, ask for feedback, and refine by assetId until the user is happy.",
|
|
1050
|
-
].join("\n");
|
|
1051
|
-
sendToAgentChat({
|
|
1052
|
-
message: chatMessage,
|
|
1053
|
-
context,
|
|
1054
|
-
submit: true,
|
|
1055
|
-
newTab: true,
|
|
1056
|
-
});
|
|
1057
|
-
setGenerateOpen(false);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
1052
|
function continueSession(sessionId: string) {
|
|
1061
1053
|
prepareSessionContinuation.mutate(
|
|
1062
1054
|
{ id: sessionId },
|
|
@@ -1070,9 +1062,7 @@ export default function LibraryPage() {
|
|
|
1070
1062
|
});
|
|
1071
1063
|
},
|
|
1072
1064
|
onError: (error: Error) => {
|
|
1073
|
-
toast.error(
|
|
1074
|
-
error.message || t("brandKitDetail.couldNotPrepareHandoff"),
|
|
1075
|
-
);
|
|
1065
|
+
toast.error(error.message || t("library.couldNotPrepareHandoff"));
|
|
1076
1066
|
},
|
|
1077
1067
|
},
|
|
1078
1068
|
);
|
|
@@ -1081,11 +1071,11 @@ export default function LibraryPage() {
|
|
|
1081
1071
|
function createHandoffFromRun(run: any) {
|
|
1082
1072
|
const outputIds = outputAssetIds(run);
|
|
1083
1073
|
if (!outputIds.length) {
|
|
1084
|
-
toast.error(t("
|
|
1074
|
+
toast.error(t("library.runHasNoGeneratedAssets"));
|
|
1085
1075
|
return;
|
|
1086
1076
|
}
|
|
1087
1077
|
const prompt =
|
|
1088
|
-
run.originalPrompt || run.prompt || t("
|
|
1078
|
+
run.originalPrompt || run.prompt || t("library.generatedAsset");
|
|
1089
1079
|
createSession.mutate(
|
|
1090
1080
|
{
|
|
1091
1081
|
libraryId,
|
|
@@ -1113,7 +1103,7 @@ export default function LibraryPage() {
|
|
|
1113
1103
|
if (!library) {
|
|
1114
1104
|
return (
|
|
1115
1105
|
<div className="p-6 text-sm text-muted-foreground">
|
|
1116
|
-
{t("
|
|
1106
|
+
{t("library.loadingBrandKit")}
|
|
1117
1107
|
</div>
|
|
1118
1108
|
);
|
|
1119
1109
|
}
|
|
@@ -1122,121 +1112,123 @@ export default function LibraryPage() {
|
|
|
1122
1112
|
activeTab === "runs" || activeTab === "settings" ? activeTab : "assets";
|
|
1123
1113
|
const hideEmptyLanes =
|
|
1124
1114
|
activeFolderId !== "all" || mediaFilter !== "all" || search.trim() !== "";
|
|
1115
|
+
const uploadAction = (
|
|
1116
|
+
<Button
|
|
1117
|
+
variant="outline"
|
|
1118
|
+
className="gap-2"
|
|
1119
|
+
onClick={() => fileInputRef.current?.click()}
|
|
1120
|
+
disabled={uploading}
|
|
1121
|
+
>
|
|
1122
|
+
{uploading ? (
|
|
1123
|
+
<Spinner className="h-4 w-4" />
|
|
1124
|
+
) : (
|
|
1125
|
+
<IconUpload className="h-4 w-4" />
|
|
1126
|
+
)}
|
|
1127
|
+
{uploading
|
|
1128
|
+
? t("library.uploadingCount", { count: pendingUploads.length })
|
|
1129
|
+
: t("library.upload")}
|
|
1130
|
+
</Button>
|
|
1131
|
+
);
|
|
1132
|
+
const moreActions = (
|
|
1133
|
+
<DropdownMenu>
|
|
1134
|
+
<DropdownMenuTrigger asChild>
|
|
1135
|
+
<Button
|
|
1136
|
+
variant="outline"
|
|
1137
|
+
size="icon"
|
|
1138
|
+
aria-label={t("library.kitActions")}
|
|
1139
|
+
disabled={archiveLibrary.isPending || duplicateLibrary.isPending}
|
|
1140
|
+
>
|
|
1141
|
+
<IconDotsVertical className="h-4 w-4" />
|
|
1142
|
+
</Button>
|
|
1143
|
+
</DropdownMenuTrigger>
|
|
1144
|
+
<DropdownMenuContent align="end">
|
|
1145
|
+
<DropdownMenuItem
|
|
1146
|
+
onSelect={(event) => {
|
|
1147
|
+
event.preventDefault();
|
|
1148
|
+
setFolderOpen(true);
|
|
1149
|
+
}}
|
|
1150
|
+
>
|
|
1151
|
+
<IconFolderPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
1152
|
+
{t("library.newFolder")}
|
|
1153
|
+
</DropdownMenuItem>
|
|
1154
|
+
<ShareButton
|
|
1155
|
+
resourceType="asset-library"
|
|
1156
|
+
resourceId={library.id}
|
|
1157
|
+
resourceTitle={library.title}
|
|
1158
|
+
triggerClassName="w-full justify-start border-0 bg-transparent px-2 py-1.5 text-sm font-normal shadow-none hover:bg-accent hover:text-accent-foreground"
|
|
1159
|
+
/>
|
|
1160
|
+
<DropdownMenuSeparator />
|
|
1161
|
+
<DropdownMenuItem
|
|
1162
|
+
disabled={duplicateLibrary.isPending}
|
|
1163
|
+
onSelect={(event) => {
|
|
1164
|
+
event.preventDefault();
|
|
1165
|
+
void duplicateCurrentLibrary();
|
|
1166
|
+
}}
|
|
1167
|
+
>
|
|
1168
|
+
<IconCopy className="mr-2 h-4 w-4 shrink-0" />
|
|
1169
|
+
{duplicateLibrary.isPending
|
|
1170
|
+
? t("library.duplicating")
|
|
1171
|
+
: t("library.duplicate")}
|
|
1172
|
+
</DropdownMenuItem>
|
|
1173
|
+
<DropdownMenuSeparator />
|
|
1174
|
+
<DropdownMenuItem
|
|
1175
|
+
onSelect={(event) => {
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
setArchiveOpen(true);
|
|
1178
|
+
}}
|
|
1179
|
+
>
|
|
1180
|
+
<IconArchive className="mr-2 h-4 w-4 shrink-0" />
|
|
1181
|
+
{t("library.archiveBrandKit")}
|
|
1182
|
+
</DropdownMenuItem>
|
|
1183
|
+
</DropdownMenuContent>
|
|
1184
|
+
</DropdownMenu>
|
|
1185
|
+
);
|
|
1186
|
+
const headerActions = (
|
|
1187
|
+
<div className="flex flex-wrap items-center gap-2 sm:flex-nowrap lg:shrink-0">
|
|
1188
|
+
{uploadAction}
|
|
1189
|
+
{moreActions}
|
|
1190
|
+
</div>
|
|
1191
|
+
);
|
|
1192
|
+
const headerPrimaryActionsPortal =
|
|
1193
|
+
headerMode === "actions" && headerPrimaryActionsTarget
|
|
1194
|
+
? createPortal(uploadAction, headerPrimaryActionsTarget)
|
|
1195
|
+
: null;
|
|
1196
|
+
const headerMoreActionsPortal =
|
|
1197
|
+
headerMode === "actions" && headerMoreActionsTarget
|
|
1198
|
+
? createPortal(moreActions, headerMoreActionsTarget)
|
|
1199
|
+
: null;
|
|
1125
1200
|
|
|
1126
1201
|
return (
|
|
1127
|
-
<div className="flex
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
1140
|
-
onClick={() => setEditOpen(true)}
|
|
1141
|
-
aria-label={t("brandKitDetail.editBrandKit")}
|
|
1142
|
-
>
|
|
1143
|
-
<IconPencil className="h-4 w-4" />
|
|
1144
|
-
</Button>
|
|
1145
|
-
</div>
|
|
1146
|
-
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
|
|
1147
|
-
{library.description || t("brandKitDetail.defaultDescription")}
|
|
1148
|
-
</p>
|
|
1149
|
-
</div>
|
|
1150
|
-
<div className="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:flex-nowrap lg:shrink-0">
|
|
1151
|
-
<ShareButton
|
|
1152
|
-
resourceType="asset-library"
|
|
1153
|
-
resourceId={library.id}
|
|
1154
|
-
resourceTitle={library.title}
|
|
1155
|
-
/>
|
|
1156
|
-
<Button
|
|
1157
|
-
variant="outline"
|
|
1158
|
-
className="gap-2"
|
|
1159
|
-
onClick={() => fileInputRef.current?.click()}
|
|
1160
|
-
disabled={uploading}
|
|
1161
|
-
>
|
|
1162
|
-
{uploading ? (
|
|
1163
|
-
<Spinner className="h-4 w-4" />
|
|
1164
|
-
) : (
|
|
1165
|
-
<IconUpload className="h-4 w-4" />
|
|
1166
|
-
)}
|
|
1167
|
-
{uploading
|
|
1168
|
-
? t("brandKitDetail.uploadingCount", {
|
|
1169
|
-
count: pendingUploads.length,
|
|
1170
|
-
})
|
|
1171
|
-
: t("brandKitDetail.upload")}
|
|
1172
|
-
</Button>
|
|
1173
|
-
<Button
|
|
1174
|
-
variant="outline"
|
|
1175
|
-
className="hidden gap-2 xl:inline-flex"
|
|
1176
|
-
onClick={() => setFolderOpen(true)}
|
|
1177
|
-
>
|
|
1178
|
-
<IconFolderPlus className="h-4 w-4" />
|
|
1179
|
-
{t("brandKitDetail.folder")}
|
|
1180
|
-
</Button>
|
|
1181
|
-
<GeneratePopover
|
|
1182
|
-
open={generateOpen}
|
|
1183
|
-
onOpenChange={setGenerateOpen}
|
|
1184
|
-
onSubmit={generate}
|
|
1185
|
-
hasLogo={!!library.canonicalLogoAssetId}
|
|
1186
|
-
presets={generationPresets}
|
|
1187
|
-
/>
|
|
1188
|
-
<DropdownMenu>
|
|
1189
|
-
<DropdownMenuTrigger asChild>
|
|
1202
|
+
<div className="flex min-w-0 flex-col">
|
|
1203
|
+
{headerPrimaryActionsPortal}
|
|
1204
|
+
{headerMoreActionsPortal}
|
|
1205
|
+
{headerMode === "full" ? (
|
|
1206
|
+
<div className="border-b border-border px-6 py-4">
|
|
1207
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
1208
|
+
<div className="min-w-0">
|
|
1209
|
+
<div className="flex items-center gap-2">
|
|
1210
|
+
<h2 className="truncate text-2xl font-semibold tracking-tight">
|
|
1211
|
+
{library.title}
|
|
1212
|
+
</h2>
|
|
1213
|
+
<Badge variant="outline">{library.visibility}</Badge>
|
|
1190
1214
|
<Button
|
|
1191
|
-
variant="
|
|
1215
|
+
variant="ghost"
|
|
1192
1216
|
size="icon"
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
}
|
|
1217
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
1218
|
+
onClick={() => setEditOpen(true)}
|
|
1219
|
+
aria-label={t("library.editBrandKit")}
|
|
1197
1220
|
>
|
|
1198
|
-
<
|
|
1221
|
+
<IconPencil className="h-4 w-4" />
|
|
1199
1222
|
</Button>
|
|
1200
|
-
</
|
|
1201
|
-
<
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
setFolderOpen(true);
|
|
1207
|
-
}}
|
|
1208
|
-
>
|
|
1209
|
-
<IconFolderPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
1210
|
-
{t("brandKitDetail.newFolder")}
|
|
1211
|
-
</DropdownMenuItem>
|
|
1212
|
-
<DropdownMenuSeparator className="xl:hidden" />
|
|
1213
|
-
<DropdownMenuItem
|
|
1214
|
-
disabled={duplicateLibrary.isPending}
|
|
1215
|
-
onSelect={(event) => {
|
|
1216
|
-
event.preventDefault();
|
|
1217
|
-
void duplicateCurrentLibrary();
|
|
1218
|
-
}}
|
|
1219
|
-
>
|
|
1220
|
-
<IconCopy className="mr-2 h-4 w-4 shrink-0" />
|
|
1221
|
-
{duplicateLibrary.isPending
|
|
1222
|
-
? t("brandKitDetail.duplicating")
|
|
1223
|
-
: t("brandKitDetail.duplicate")}
|
|
1224
|
-
</DropdownMenuItem>
|
|
1225
|
-
<DropdownMenuSeparator />
|
|
1226
|
-
<DropdownMenuItem
|
|
1227
|
-
onSelect={(event) => {
|
|
1228
|
-
event.preventDefault();
|
|
1229
|
-
setArchiveOpen(true);
|
|
1230
|
-
}}
|
|
1231
|
-
>
|
|
1232
|
-
<IconArchive className="mr-2 h-4 w-4 shrink-0" />
|
|
1233
|
-
{t("brandKitDetail.archiveBrandKit")}
|
|
1234
|
-
</DropdownMenuItem>
|
|
1235
|
-
</DropdownMenuContent>
|
|
1236
|
-
</DropdownMenu>
|
|
1223
|
+
</div>
|
|
1224
|
+
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
|
|
1225
|
+
{library.description || t("library.defaultKitDescription")}
|
|
1226
|
+
</p>
|
|
1227
|
+
</div>
|
|
1228
|
+
{headerActions}
|
|
1237
1229
|
</div>
|
|
1238
1230
|
</div>
|
|
1239
|
-
|
|
1231
|
+
) : null}
|
|
1240
1232
|
|
|
1241
1233
|
<input
|
|
1242
1234
|
ref={fileInputRef}
|
|
@@ -1255,15 +1247,13 @@ export default function LibraryPage() {
|
|
|
1255
1247
|
<AlertDialog open={archiveOpen} onOpenChange={setArchiveOpen}>
|
|
1256
1248
|
<AlertDialogContent>
|
|
1257
1249
|
<AlertDialogHeader>
|
|
1258
|
-
<AlertDialogTitle>
|
|
1259
|
-
{t("brandKitDetail.archiveThisBrandKit")}
|
|
1260
|
-
</AlertDialogTitle>
|
|
1250
|
+
<AlertDialogTitle>{t("library.archiveKitTitle")}</AlertDialogTitle>
|
|
1261
1251
|
<AlertDialogDescription>
|
|
1262
|
-
{t("
|
|
1252
|
+
{t("library.archiveKitDescription")}
|
|
1263
1253
|
</AlertDialogDescription>
|
|
1264
1254
|
</AlertDialogHeader>
|
|
1265
1255
|
<AlertDialogFooter>
|
|
1266
|
-
<AlertDialogCancel>{t("
|
|
1256
|
+
<AlertDialogCancel>{t("library.cancel")}</AlertDialogCancel>
|
|
1267
1257
|
<AlertDialogAction
|
|
1268
1258
|
disabled={archiveLibrary.isPending}
|
|
1269
1259
|
onClick={() => {
|
|
@@ -1271,8 +1261,8 @@ export default function LibraryPage() {
|
|
|
1271
1261
|
}}
|
|
1272
1262
|
>
|
|
1273
1263
|
{archiveLibrary.isPending
|
|
1274
|
-
? t("
|
|
1275
|
-
: t("
|
|
1264
|
+
? t("library.archiving")
|
|
1265
|
+
: t("library.archive")}
|
|
1276
1266
|
</AlertDialogAction>
|
|
1277
1267
|
</AlertDialogFooter>
|
|
1278
1268
|
</AlertDialogContent>
|
|
@@ -1298,7 +1288,7 @@ export default function LibraryPage() {
|
|
|
1298
1288
|
) : null}
|
|
1299
1289
|
|
|
1300
1290
|
<div
|
|
1301
|
-
className="relative
|
|
1291
|
+
className="relative px-6 py-5"
|
|
1302
1292
|
onDragEnter={(e: DragEvent<HTMLDivElement>) => {
|
|
1303
1293
|
if (!e.dataTransfer.types.includes("Files")) return;
|
|
1304
1294
|
e.preventDefault();
|
|
@@ -1338,38 +1328,24 @@ export default function LibraryPage() {
|
|
|
1338
1328
|
className="space-y-4"
|
|
1339
1329
|
>
|
|
1340
1330
|
<TabsList>
|
|
1341
|
-
<TabsTrigger value="assets">
|
|
1342
|
-
|
|
1343
|
-
</TabsTrigger>
|
|
1344
|
-
<TabsTrigger value="runs">{t("brandKitDetail.runs")}</TabsTrigger>
|
|
1345
|
-
<TabsTrigger value="settings">
|
|
1346
|
-
{t("brandKitDetail.settings")}
|
|
1347
|
-
</TabsTrigger>
|
|
1331
|
+
<TabsTrigger value="assets">{t("library.assetsTab")}</TabsTrigger>
|
|
1332
|
+
<TabsTrigger value="runs">{t("library.runs")}</TabsTrigger>
|
|
1333
|
+
<TabsTrigger value="settings">{t("library.settings")}</TabsTrigger>
|
|
1348
1334
|
</TabsList>
|
|
1349
1335
|
|
|
1350
1336
|
<TabsContent value="assets" className="space-y-5">
|
|
1351
|
-
<CandidateStage
|
|
1352
|
-
candidates={candidateAssets}
|
|
1353
|
-
pendingVariants={pendingVariants}
|
|
1354
|
-
libraryId={libraryId}
|
|
1355
|
-
folders={folders}
|
|
1356
|
-
savingCandidateKeys={savingCandidateKeys}
|
|
1357
|
-
onSaveCandidate={(slot) => {
|
|
1358
|
-
void handleSaveCandidate(slot);
|
|
1359
|
-
}}
|
|
1360
|
-
/>
|
|
1361
1337
|
<section className="space-y-3">
|
|
1362
1338
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
1363
1339
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
1364
1340
|
<FolderChip
|
|
1365
1341
|
active={activeFolderId === "all"}
|
|
1366
|
-
label={t("
|
|
1342
|
+
label={t("library.allAssets")}
|
|
1367
1343
|
count={libraryAssets.length}
|
|
1368
1344
|
onClick={() => setActiveFolderId("all")}
|
|
1369
1345
|
/>
|
|
1370
1346
|
<FolderChip
|
|
1371
1347
|
active={activeFolderId === null}
|
|
1372
|
-
label={t("
|
|
1348
|
+
label={t("library.unfiled")}
|
|
1373
1349
|
count={unfiledCount}
|
|
1374
1350
|
onClick={() => setActiveFolderId(null)}
|
|
1375
1351
|
/>
|
|
@@ -1393,13 +1369,13 @@ export default function LibraryPage() {
|
|
|
1393
1369
|
<Input
|
|
1394
1370
|
value={search}
|
|
1395
1371
|
onChange={(event) => setSearch(event.target.value)}
|
|
1396
|
-
placeholder={t("
|
|
1372
|
+
placeholder={t("library.searchAssets")}
|
|
1397
1373
|
className="h-9 w-full pl-8 pr-8 sm:w-64"
|
|
1398
1374
|
/>
|
|
1399
1375
|
{search && (
|
|
1400
1376
|
<button
|
|
1401
1377
|
type="button"
|
|
1402
|
-
aria-label={t("
|
|
1378
|
+
aria-label={t("library.clearSearch")}
|
|
1403
1379
|
className="absolute right-2 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
1404
1380
|
onClick={() => setSearch("")}
|
|
1405
1381
|
>
|
|
@@ -1624,19 +1600,6 @@ export default function LibraryPage() {
|
|
|
1624
1600
|
);
|
|
1625
1601
|
}
|
|
1626
1602
|
|
|
1627
|
-
type GenerateOptions = {
|
|
1628
|
-
mediaType: "image" | "video";
|
|
1629
|
-
presetId?: string;
|
|
1630
|
-
count: number;
|
|
1631
|
-
aspectRatio: string;
|
|
1632
|
-
imageSize: string;
|
|
1633
|
-
durationSeconds: number;
|
|
1634
|
-
resolution: string;
|
|
1635
|
-
model: string;
|
|
1636
|
-
category: string;
|
|
1637
|
-
includeLogo: boolean;
|
|
1638
|
-
};
|
|
1639
|
-
|
|
1640
1603
|
type PendingUpload = {
|
|
1641
1604
|
id: string;
|
|
1642
1605
|
name: string;
|
|
@@ -1655,10 +1618,12 @@ type LaneGalleryItem = {
|
|
|
1655
1618
|
subtitle?: string | null;
|
|
1656
1619
|
metadata?: string | null;
|
|
1657
1620
|
status?: string | null;
|
|
1621
|
+
asset?: any;
|
|
1658
1622
|
mediaType?: "image" | "video";
|
|
1659
1623
|
href?: string;
|
|
1660
1624
|
selected?: boolean;
|
|
1661
1625
|
busy?: boolean;
|
|
1626
|
+
showBusyOverlay?: boolean;
|
|
1662
1627
|
deleting?: boolean;
|
|
1663
1628
|
preview: ReactNode;
|
|
1664
1629
|
thumbnail: ReactNode; // i18n-ignore structural preview slot name
|
|
@@ -1912,6 +1877,63 @@ function assetLineageSourceText(asset: any): string | null {
|
|
|
1912
1877
|
: null;
|
|
1913
1878
|
}
|
|
1914
1879
|
|
|
1880
|
+
function detailAssetPayload(asset: any) {
|
|
1881
|
+
const mediaType =
|
|
1882
|
+
asset?.mediaType === "video" || asset?.mimeType?.startsWith("video/")
|
|
1883
|
+
? "video"
|
|
1884
|
+
: "image";
|
|
1885
|
+
const title = assetDisplayTitle(asset);
|
|
1886
|
+
const url = assetMediaUrl(
|
|
1887
|
+
asset?.previewUrl ?? asset?.downloadUrl ?? asset?.url,
|
|
1888
|
+
);
|
|
1889
|
+
const width = Number(asset?.width);
|
|
1890
|
+
const height = Number(asset?.height);
|
|
1891
|
+
return {
|
|
1892
|
+
assetId: asset?.id,
|
|
1893
|
+
title,
|
|
1894
|
+
mediaType,
|
|
1895
|
+
url,
|
|
1896
|
+
previewUrl: assetMediaUrl(asset?.previewUrl),
|
|
1897
|
+
downloadUrl: assetMediaUrl(asset?.downloadUrl),
|
|
1898
|
+
...(Number.isFinite(width) && Number.isFinite(height) && width && height
|
|
1899
|
+
? { width, height }
|
|
1900
|
+
: {}),
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function detailAssetClipboardText(asset: any) {
|
|
1905
|
+
const payload = detailAssetPayload(asset);
|
|
1906
|
+
const url = payload.url ?? payload.downloadUrl ?? payload.previewUrl;
|
|
1907
|
+
const previewTip =
|
|
1908
|
+
payload.mediaType === "image" && url
|
|
1909
|
+
? [
|
|
1910
|
+
`Markdown preview: `,
|
|
1911
|
+
"If this remote preview does not render in Codex or Claude Code, download the image locally and embed the absolute local file path.",
|
|
1912
|
+
]
|
|
1913
|
+
: [];
|
|
1914
|
+
return [
|
|
1915
|
+
`Use this selected ${payload.mediaType} in the current work: ${payload.title}`,
|
|
1916
|
+
url ? `URL: ${url}` : null,
|
|
1917
|
+
...previewTip,
|
|
1918
|
+
"",
|
|
1919
|
+
JSON.stringify(
|
|
1920
|
+
{
|
|
1921
|
+
assetId: payload.assetId,
|
|
1922
|
+
title: payload.title,
|
|
1923
|
+
mediaType: payload.mediaType,
|
|
1924
|
+
url,
|
|
1925
|
+
...(payload.width && payload.height
|
|
1926
|
+
? { width: payload.width, height: payload.height }
|
|
1927
|
+
: {}),
|
|
1928
|
+
},
|
|
1929
|
+
null,
|
|
1930
|
+
2,
|
|
1931
|
+
),
|
|
1932
|
+
]
|
|
1933
|
+
.filter((line): line is string => line !== null)
|
|
1934
|
+
.join("\n");
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1915
1937
|
function shortId(id: string) {
|
|
1916
1938
|
return id.length > 12 ? `${id.slice(0, 6)}...${id.slice(-4)}` : id;
|
|
1917
1939
|
}
|
|
@@ -1997,287 +2019,30 @@ function SessionCard({
|
|
|
1997
2019
|
);
|
|
1998
2020
|
}
|
|
1999
2021
|
|
|
2000
|
-
function
|
|
2001
|
-
|
|
2002
|
-
onOpenChange,
|
|
2003
|
-
onSubmit,
|
|
2004
|
-
hasLogo,
|
|
2022
|
+
function GenerationPresetsPanel({
|
|
2023
|
+
libraryId,
|
|
2005
2024
|
presets,
|
|
2006
2025
|
}: {
|
|
2007
|
-
|
|
2008
|
-
onOpenChange: (open: boolean) => void;
|
|
2009
|
-
onSubmit: (prompt: string, options: GenerateOptions) => void;
|
|
2010
|
-
hasLogo: boolean;
|
|
2026
|
+
libraryId: string;
|
|
2011
2027
|
presets: any[];
|
|
2012
2028
|
}) {
|
|
2013
2029
|
const t = useT();
|
|
2014
|
-
const
|
|
2015
|
-
const
|
|
2016
|
-
const [
|
|
2017
|
-
const [
|
|
2018
|
-
const [
|
|
2019
|
-
const [
|
|
2020
|
-
const [
|
|
2021
|
-
const [
|
|
2022
|
-
const [
|
|
2023
|
-
const [category, setCategory] = useState("hero");
|
|
2024
|
-
const [includeLogo, setIncludeLogo] = useState(false);
|
|
2025
|
-
const selectedPreset = presets.find((preset) => preset.id === presetId);
|
|
2026
|
-
|
|
2027
|
-
return (
|
|
2028
|
-
<Popover open={open} onOpenChange={onOpenChange}>
|
|
2029
|
-
<PopoverTrigger asChild>
|
|
2030
|
-
<Button className="gap-2">
|
|
2031
|
-
<IconMessageCircle className="h-4 w-4" />
|
|
2032
|
-
{t("brandKitDetail.generate")}
|
|
2033
|
-
</Button>
|
|
2034
|
-
</PopoverTrigger>
|
|
2035
|
-
<PopoverContent
|
|
2036
|
-
align="end"
|
|
2037
|
-
className="w-[420px] max-w-[calc(100vw-2rem)]"
|
|
2038
|
-
>
|
|
2039
|
-
<div className="space-y-4">
|
|
2040
|
-
<div>
|
|
2041
|
-
<div className="text-sm font-semibold">
|
|
2042
|
-
{t("brandKitDetail.generateWithChat")}
|
|
2043
|
-
</div>
|
|
2044
|
-
</div>
|
|
2045
|
-
{presets.length ? (
|
|
2046
|
-
<Select
|
|
2047
|
-
value={presetId}
|
|
2048
|
-
onValueChange={(value) => {
|
|
2049
|
-
setPresetId(value);
|
|
2050
|
-
const preset = presets.find((item) => item.id === value);
|
|
2051
|
-
if (!preset) return;
|
|
2052
|
-
setMediaType(preset.mediaType === "video" ? "video" : "image");
|
|
2053
|
-
setAspectRatio(preset.aspectRatio || "16:9");
|
|
2054
|
-
setImageSize(preset.imageSize || "2K");
|
|
2055
|
-
setModel(preset.model || "gemini-3.1-flash-image");
|
|
2056
|
-
setCategory(preset.category || "hero");
|
|
2057
|
-
}}
|
|
2058
|
-
>
|
|
2059
|
-
<SelectTrigger>
|
|
2060
|
-
<SelectValue placeholder={t("brandKitDetail.preset")} />
|
|
2061
|
-
</SelectTrigger>
|
|
2062
|
-
<SelectContent>
|
|
2063
|
-
<SelectItem value="none">
|
|
2064
|
-
{t("brandKitDetail.noPreset")}
|
|
2065
|
-
</SelectItem>
|
|
2066
|
-
{presets.map((preset) => (
|
|
2067
|
-
<SelectItem key={preset.id} value={preset.id}>
|
|
2068
|
-
{preset.title}
|
|
2069
|
-
</SelectItem>
|
|
2070
|
-
))}
|
|
2071
|
-
</SelectContent>
|
|
2072
|
-
</Select>
|
|
2073
|
-
) : null}
|
|
2074
|
-
{selectedPreset?.textPolicy ? (
|
|
2075
|
-
<p className="rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
|
2076
|
-
{selectedPreset.textPolicy}
|
|
2077
|
-
</p>
|
|
2078
|
-
) : null}
|
|
2079
|
-
<Select
|
|
2080
|
-
value={mediaType}
|
|
2081
|
-
onValueChange={(value) => {
|
|
2082
|
-
const next = value as "image" | "video";
|
|
2083
|
-
setMediaType(next);
|
|
2084
|
-
setModel(
|
|
2085
|
-
next === "video"
|
|
2086
|
-
? "veo-3.1-generate-preview"
|
|
2087
|
-
: "gemini-3.1-flash-image",
|
|
2088
|
-
);
|
|
2089
|
-
setCategory(next === "video" ? "video" : "hero");
|
|
2090
|
-
setAspectRatio(next === "video" ? "16:9" : aspectRatio);
|
|
2091
|
-
}}
|
|
2092
|
-
>
|
|
2093
|
-
<SelectTrigger>
|
|
2094
|
-
<SelectValue />
|
|
2095
|
-
</SelectTrigger>
|
|
2096
|
-
<SelectContent>
|
|
2097
|
-
<SelectItem value="image">
|
|
2098
|
-
{t("brandKitDetail.imageCandidates")}
|
|
2099
|
-
</SelectItem>
|
|
2100
|
-
<SelectItem value="video">
|
|
2101
|
-
{t("brandKitDetail.videoCandidate")}
|
|
2102
|
-
</SelectItem>
|
|
2103
|
-
</SelectContent>
|
|
2104
|
-
</Select>
|
|
2105
|
-
<Textarea
|
|
2106
|
-
autoGrow
|
|
2107
|
-
value={prompt}
|
|
2108
|
-
onChange={(event) => setPrompt(event.target.value)}
|
|
2109
|
-
placeholder={
|
|
2110
|
-
mediaType === "video"
|
|
2111
|
-
? t("brandKitDetail.videoPromptPlaceholder")
|
|
2112
|
-
: t("brandKitDetail.imagePromptPlaceholder")
|
|
2113
|
-
}
|
|
2114
|
-
className="min-h-28 max-h-48 resize-none overflow-y-auto"
|
|
2115
|
-
/>
|
|
2116
|
-
<div className="grid grid-cols-2 gap-3">
|
|
2117
|
-
{mediaType === "image" ? (
|
|
2118
|
-
<Select
|
|
2119
|
-
value={String(count)}
|
|
2120
|
-
onValueChange={(v) => setCount(Number(v))}
|
|
2121
|
-
>
|
|
2122
|
-
<SelectTrigger>
|
|
2123
|
-
<SelectValue />
|
|
2124
|
-
</SelectTrigger>
|
|
2125
|
-
<SelectContent>
|
|
2126
|
-
{[1, 2, 3, 4].map((n) => (
|
|
2127
|
-
<SelectItem key={n} value={String(n)}>
|
|
2128
|
-
{t("brandKitDetail.variants", { count: n })}
|
|
2129
|
-
</SelectItem>
|
|
2130
|
-
))}
|
|
2131
|
-
</SelectContent>
|
|
2132
|
-
</Select>
|
|
2133
|
-
) : (
|
|
2134
|
-
<Select
|
|
2135
|
-
value={String(durationSeconds)}
|
|
2136
|
-
onValueChange={(v) => setDurationSeconds(Number(v))}
|
|
2137
|
-
>
|
|
2138
|
-
<SelectTrigger>
|
|
2139
|
-
<SelectValue />
|
|
2140
|
-
</SelectTrigger>
|
|
2141
|
-
<SelectContent>
|
|
2142
|
-
{VIDEO_DURATIONS.map((n) => (
|
|
2143
|
-
<SelectItem key={n} value={String(n)}>
|
|
2144
|
-
{n}s
|
|
2145
|
-
</SelectItem>
|
|
2146
|
-
))}
|
|
2147
|
-
</SelectContent>
|
|
2148
|
-
</Select>
|
|
2149
|
-
)}
|
|
2150
|
-
<Select value={aspectRatio} onValueChange={setAspectRatio}>
|
|
2151
|
-
<SelectTrigger>
|
|
2152
|
-
<SelectValue />
|
|
2153
|
-
</SelectTrigger>
|
|
2154
|
-
<SelectContent>
|
|
2155
|
-
{(mediaType === "video"
|
|
2156
|
-
? VIDEO_ASPECT_RATIOS
|
|
2157
|
-
: ASPECT_RATIOS
|
|
2158
|
-
).map((ratio) => (
|
|
2159
|
-
<SelectItem key={ratio} value={ratio}>
|
|
2160
|
-
{ratio}
|
|
2161
|
-
</SelectItem>
|
|
2162
|
-
))}
|
|
2163
|
-
</SelectContent>
|
|
2164
|
-
</Select>
|
|
2165
|
-
{mediaType === "image" ? (
|
|
2166
|
-
<Select value={imageSize} onValueChange={setImageSize}>
|
|
2167
|
-
<SelectTrigger>
|
|
2168
|
-
<SelectValue />
|
|
2169
|
-
</SelectTrigger>
|
|
2170
|
-
<SelectContent>
|
|
2171
|
-
{IMAGE_SIZES.map((size) => (
|
|
2172
|
-
<SelectItem key={size} value={size}>
|
|
2173
|
-
{size}
|
|
2174
|
-
</SelectItem>
|
|
2175
|
-
))}
|
|
2176
|
-
</SelectContent>
|
|
2177
|
-
</Select>
|
|
2178
|
-
) : (
|
|
2179
|
-
<Select value={resolution} onValueChange={setResolution}>
|
|
2180
|
-
<SelectTrigger>
|
|
2181
|
-
<SelectValue />
|
|
2182
|
-
</SelectTrigger>
|
|
2183
|
-
<SelectContent>
|
|
2184
|
-
{VIDEO_RESOLUTIONS.map((item) => (
|
|
2185
|
-
<SelectItem key={item} value={item}>
|
|
2186
|
-
{item}
|
|
2187
|
-
</SelectItem>
|
|
2188
|
-
))}
|
|
2189
|
-
</SelectContent>
|
|
2190
|
-
</Select>
|
|
2191
|
-
)}
|
|
2192
|
-
<Select value={category} onValueChange={setCategory}>
|
|
2193
|
-
<SelectTrigger>
|
|
2194
|
-
<SelectValue />
|
|
2195
|
-
</SelectTrigger>
|
|
2196
|
-
<SelectContent>
|
|
2197
|
-
{IMAGE_CATEGORIES.map((item) => (
|
|
2198
|
-
<SelectItem key={item} value={item}>
|
|
2199
|
-
{item}
|
|
2200
|
-
</SelectItem>
|
|
2201
|
-
))}
|
|
2202
|
-
</SelectContent>
|
|
2203
|
-
</Select>
|
|
2204
|
-
</div>
|
|
2205
|
-
<Select value={model} onValueChange={setModel}>
|
|
2206
|
-
<SelectTrigger>
|
|
2207
|
-
<SelectValue />
|
|
2208
|
-
</SelectTrigger>
|
|
2209
|
-
<SelectContent>
|
|
2210
|
-
{(mediaType === "video" ? VIDEO_MODELS : IMAGE_MODELS).map(
|
|
2211
|
-
(item) => (
|
|
2212
|
-
<SelectItem key={item} value={item}>
|
|
2213
|
-
{item}
|
|
2214
|
-
</SelectItem>
|
|
2215
|
-
),
|
|
2216
|
-
)}
|
|
2217
|
-
</SelectContent>
|
|
2218
|
-
</Select>
|
|
2219
|
-
{mediaType === "image" && (
|
|
2220
|
-
<label className="flex items-center gap-2 text-sm">
|
|
2221
|
-
<Checkbox
|
|
2222
|
-
checked={includeLogo}
|
|
2223
|
-
disabled={!hasLogo}
|
|
2224
|
-
onCheckedChange={(checked) => setIncludeLogo(checked === true)}
|
|
2225
|
-
/>
|
|
2226
|
-
{t("brandKitDetail.compositeCanonicalLogo")}
|
|
2227
|
-
</label>
|
|
2228
|
-
)}
|
|
2229
|
-
<Button
|
|
2230
|
-
className="w-full"
|
|
2231
|
-
disabled={!prompt.trim()}
|
|
2232
|
-
onClick={() =>
|
|
2233
|
-
onSubmit(prompt, {
|
|
2234
|
-
mediaType,
|
|
2235
|
-
presetId: presetId === "none" ? undefined : presetId,
|
|
2236
|
-
count,
|
|
2237
|
-
aspectRatio,
|
|
2238
|
-
imageSize,
|
|
2239
|
-
durationSeconds,
|
|
2240
|
-
resolution,
|
|
2241
|
-
model,
|
|
2242
|
-
category,
|
|
2243
|
-
includeLogo,
|
|
2244
|
-
})
|
|
2245
|
-
}
|
|
2246
|
-
>
|
|
2247
|
-
{t("brandKitDetail.openChat")}
|
|
2248
|
-
</Button>
|
|
2249
|
-
</div>
|
|
2250
|
-
</PopoverContent>
|
|
2251
|
-
</Popover>
|
|
2252
|
-
);
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
function GenerationPresetsPanel({
|
|
2256
|
-
libraryId,
|
|
2257
|
-
presets,
|
|
2258
|
-
}: {
|
|
2259
|
-
libraryId: string;
|
|
2260
|
-
presets: any[];
|
|
2261
|
-
}) {
|
|
2262
|
-
const t = useT();
|
|
2263
|
-
const createPreset = useActionMutation("create-generation-preset");
|
|
2264
|
-
const deletePreset = useActionMutation("delete-generation-preset");
|
|
2265
|
-
const [open, setOpen] = useState(false);
|
|
2266
|
-
const [confirmPresetId, setConfirmPresetId] = useState<string | null>(null);
|
|
2267
|
-
const [title, setTitle] = useState("");
|
|
2268
|
-
const [category, setCategory] = useState<ImageCategory>("social");
|
|
2269
|
-
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
|
|
2270
|
-
const [promptTemplate, setPromptTemplate] = useState("");
|
|
2271
|
-
const [textPolicy, setTextPolicy] = useState(
|
|
2272
|
-
t("brandKitDetail.defaultTextPolicy"),
|
|
2273
|
-
);
|
|
2030
|
+
const createPreset = useActionMutation("create-generation-preset");
|
|
2031
|
+
const deletePreset = useActionMutation("delete-generation-preset");
|
|
2032
|
+
const [open, setOpen] = useState(false);
|
|
2033
|
+
const [confirmPresetId, setConfirmPresetId] = useState<string | null>(null);
|
|
2034
|
+
const [title, setTitle] = useState("");
|
|
2035
|
+
const [category, setCategory] = useState<ImageCategory>("social");
|
|
2036
|
+
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
|
|
2037
|
+
const [promptTemplate, setPromptTemplate] = useState("");
|
|
2038
|
+
const [textPolicy, setTextPolicy] = useState(t("library.defaultTextPolicy"));
|
|
2274
2039
|
|
|
2275
2040
|
function reset() {
|
|
2276
2041
|
setTitle("");
|
|
2277
2042
|
setCategory("social");
|
|
2278
2043
|
setAspectRatio("1:1");
|
|
2279
2044
|
setPromptTemplate("");
|
|
2280
|
-
setTextPolicy(t("
|
|
2045
|
+
setTextPolicy(t("library.defaultTextPolicy"));
|
|
2281
2046
|
}
|
|
2282
2047
|
|
|
2283
2048
|
function submit() {
|
|
@@ -2401,7 +2166,7 @@ function GenerationPresetsPanel({
|
|
|
2401
2166
|
);
|
|
2402
2167
|
}}
|
|
2403
2168
|
>
|
|
2404
|
-
{t("
|
|
2169
|
+
{t("assetDetail.delete")}
|
|
2405
2170
|
</AlertDialogAction>
|
|
2406
2171
|
</AlertDialogFooter>
|
|
2407
2172
|
</AlertDialogContent>
|
|
@@ -2473,9 +2238,7 @@ function GenerationPresetsPanel({
|
|
|
2473
2238
|
id="preset-template"
|
|
2474
2239
|
value={promptTemplate}
|
|
2475
2240
|
onChange={(event) => setPromptTemplate(event.target.value)}
|
|
2476
|
-
placeholder={t("
|
|
2477
|
-
prompt: "{{prompt}}",
|
|
2478
|
-
})}
|
|
2241
|
+
placeholder={t("library.promptTemplatePlaceholder")}
|
|
2479
2242
|
/>
|
|
2480
2243
|
</div>
|
|
2481
2244
|
<div className="grid gap-2">
|
|
@@ -2603,767 +2366,118 @@ function AssetPreview({
|
|
|
2603
2366
|
);
|
|
2604
2367
|
}
|
|
2605
2368
|
|
|
2606
|
-
function
|
|
2607
|
-
candidates,
|
|
2608
|
-
pendingVariants,
|
|
2369
|
+
function AssetSwimlaneBoard({
|
|
2609
2370
|
libraryId,
|
|
2371
|
+
viewMode,
|
|
2372
|
+
onViewModeChange,
|
|
2373
|
+
scope,
|
|
2374
|
+
onScopeChange,
|
|
2375
|
+
hideEmptyLanes,
|
|
2376
|
+
assets,
|
|
2377
|
+
pendingUploads,
|
|
2610
2378
|
folders,
|
|
2611
|
-
|
|
2612
|
-
|
|
2379
|
+
promotingReferenceKeys,
|
|
2380
|
+
onUploadClick,
|
|
2381
|
+
onDrop,
|
|
2382
|
+
onMoveToReferences,
|
|
2383
|
+
onRemoveFromReferences,
|
|
2384
|
+
selectedIds,
|
|
2385
|
+
onSelectedIdsChange,
|
|
2386
|
+
onOptimisticDelete,
|
|
2387
|
+
onRestoreOptimisticDelete,
|
|
2613
2388
|
}: {
|
|
2614
|
-
candidates: any[];
|
|
2615
|
-
pendingVariants: any[];
|
|
2616
2389
|
libraryId: string;
|
|
2390
|
+
viewMode: AssetViewMode;
|
|
2391
|
+
onViewModeChange: (mode: AssetViewMode) => void;
|
|
2392
|
+
scope: AssetLibraryScope;
|
|
2393
|
+
onScopeChange: (scope: AssetLibraryScope) => void;
|
|
2394
|
+
hideEmptyLanes: boolean;
|
|
2395
|
+
assets: any[];
|
|
2396
|
+
pendingUploads: PendingUpload[];
|
|
2617
2397
|
folders: any[];
|
|
2618
|
-
|
|
2619
|
-
|
|
2398
|
+
promotingReferenceKeys: Set<string>;
|
|
2399
|
+
onUploadClick: () => void;
|
|
2400
|
+
onDrop: (files: FileList) => void;
|
|
2401
|
+
onMoveToReferences: (asset: any, slot?: any) => void;
|
|
2402
|
+
onRemoveFromReferences: (asset: any) => void;
|
|
2403
|
+
selectedIds: Set<string>;
|
|
2404
|
+
onSelectedIdsChange: Dispatch<SetStateAction<Set<string>>>;
|
|
2405
|
+
onOptimisticDelete?: (ids: string[]) => void;
|
|
2406
|
+
onRestoreOptimisticDelete?: (ids: string[]) => void;
|
|
2620
2407
|
}) {
|
|
2621
2408
|
const t = useT();
|
|
2622
|
-
const dismissSlot = useActionMutation("dismiss-variant-slots");
|
|
2623
2409
|
const deleteAsset = useActionMutation("delete-asset");
|
|
2410
|
+
const deleteAssets = useActionMutation("delete-assets");
|
|
2411
|
+
const updateAsset = useActionMutation("update-asset");
|
|
2624
2412
|
const queryClient = useQueryClient();
|
|
2625
|
-
const [
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
const
|
|
2413
|
+
const [confirmDeleteIds, setConfirmDeleteIds] = useState<string[]>([]);
|
|
2414
|
+
const [deletingIds, setDeletingIds] = useState<Set<string>>(() => new Set());
|
|
2415
|
+
const [bulkReferenceAction, setBulkReferenceAction] = useState<
|
|
2416
|
+
"add" | "remove" | null
|
|
2417
|
+
>(null);
|
|
2418
|
+
const visiblePendingUploads = scope === "all" ? pendingUploads : [];
|
|
2419
|
+
const referenceAssets = assets.filter(
|
|
2420
|
+
(asset) => asset.status === "reference",
|
|
2421
|
+
);
|
|
2422
|
+
const savedAssets = assets.filter((asset) => asset.status === "saved");
|
|
2423
|
+
const visibleLibraryAssets =
|
|
2424
|
+
scope === "references" ? referenceAssets : assets;
|
|
2425
|
+
const boardAssets = visibleLibraryAssets;
|
|
2426
|
+
const selectedAssets = boardAssets.filter((asset) =>
|
|
2427
|
+
selectedIds.has(asset.id),
|
|
2428
|
+
);
|
|
2429
|
+
const selectedReferenceAssets = selectedAssets.filter(
|
|
2430
|
+
(asset) => asset.status === "reference",
|
|
2431
|
+
);
|
|
2432
|
+
const selectedSavedAssets = selectedAssets.filter(
|
|
2433
|
+
(asset) => asset.status === "saved",
|
|
2434
|
+
);
|
|
2435
|
+
const selectedCount = selectedAssets.length;
|
|
2436
|
+
const allSelected =
|
|
2437
|
+
boardAssets.length > 0 && selectedCount === boardAssets.length;
|
|
2438
|
+
const pendingDeleteCount = deletingIds.size;
|
|
2439
|
+
const deleting =
|
|
2440
|
+
deleteAsset.isPending || deleteAssets.isPending || pendingDeleteCount > 0;
|
|
2441
|
+
const changingReference = bulkReferenceAction !== null;
|
|
2442
|
+
const hasAnyBoardItem =
|
|
2443
|
+
assets.length > 0 || pendingUploads.length > 0 || pendingDeleteCount > 0;
|
|
2632
2444
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
if (
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
dismissTarget.slot.assetId,
|
|
2641
|
-
]);
|
|
2642
|
-
void queryClient.invalidateQueries({
|
|
2643
|
-
queryKey: ["app-state", "asset-variants"],
|
|
2644
|
-
refetchType: "active",
|
|
2645
|
-
});
|
|
2646
|
-
} else if (dismissTarget.kind === "asset" && dismissTarget.asset?.id) {
|
|
2647
|
-
await deleteAsset.mutateAsync({ id: dismissTarget.asset.id });
|
|
2648
|
-
removeAssetsFromLibraryCache(queryClient, libraryId, [
|
|
2649
|
-
dismissTarget.asset.id,
|
|
2650
|
-
]);
|
|
2445
|
+
function toggleAsset(assetId: string, checked: boolean) {
|
|
2446
|
+
onSelectedIdsChange((current) => {
|
|
2447
|
+
const next = new Set(current);
|
|
2448
|
+
if (checked) {
|
|
2449
|
+
next.add(assetId);
|
|
2450
|
+
} else {
|
|
2451
|
+
next.delete(assetId);
|
|
2651
2452
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
} catch (error) {
|
|
2655
|
-
toast.error(
|
|
2656
|
-
error instanceof Error
|
|
2657
|
-
? error.message
|
|
2658
|
-
: t("brandKitDetail.couldNotDismissCandidate"),
|
|
2659
|
-
);
|
|
2660
|
-
}
|
|
2453
|
+
return next;
|
|
2454
|
+
});
|
|
2661
2455
|
}
|
|
2662
2456
|
|
|
2663
|
-
function
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
return {
|
|
2668
|
-
id: `slot:${slot.slotId}`,
|
|
2669
|
-
title: isFailed
|
|
2670
|
-
? t("brandKitDetail.failedCandidate")
|
|
2671
|
-
: slot.status === "ready"
|
|
2672
|
-
? t("brandKitDetail.readyCandidate")
|
|
2673
|
-
: t("brandKitDetail.generatingCandidate"),
|
|
2674
|
-
subtitle: slot.slotId
|
|
2675
|
-
? shortId(String(slot.slotId))
|
|
2676
|
-
: t("brandKitDetail.liveSlot"),
|
|
2677
|
-
metadata: t("brandKitDetail.candidate"),
|
|
2678
|
-
status: slot.status,
|
|
2679
|
-
mediaType: "image",
|
|
2680
|
-
href: slot.assetId ? `/asset/${slot.assetId}` : undefined,
|
|
2681
|
-
busy,
|
|
2682
|
-
preview: <VariantPreview slot={slot} fit="contain" />, // i18n-ignore structural preview slot name
|
|
2683
|
-
thumbnail: <VariantPreview slot={slot} />, // i18n-ignore structural preview slot name
|
|
2684
|
-
menu: (
|
|
2685
|
-
<VariantActionsMenu slot={slot} libraryId={libraryId} busy={busy} />
|
|
2686
|
-
),
|
|
2687
|
-
primaryActions:
|
|
2688
|
-
slot.status === "ready" ? (
|
|
2689
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2690
|
-
<CandidateSaveMenu
|
|
2691
|
-
libraryId={libraryId}
|
|
2692
|
-
folders={folders}
|
|
2693
|
-
saving={saving}
|
|
2694
|
-
disabled={busy}
|
|
2695
|
-
onSave={(folderId) => onSaveCandidate({ ...slot, folderId })}
|
|
2696
|
-
/>
|
|
2697
|
-
<Button
|
|
2698
|
-
variant="ghost"
|
|
2699
|
-
size="sm"
|
|
2700
|
-
className="h-8 px-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
2701
|
-
onClick={() =>
|
|
2702
|
-
setDismissTarget({
|
|
2703
|
-
kind: "slot",
|
|
2704
|
-
title: isFailed
|
|
2705
|
-
? t("brandKitDetail.failedCandidate")
|
|
2706
|
-
: t("brandKitDetail.readyCandidate"),
|
|
2707
|
-
slot,
|
|
2708
|
-
})
|
|
2709
|
-
}
|
|
2710
|
-
disabled={busy || dismissing}
|
|
2711
|
-
>
|
|
2712
|
-
{t("brandKitDetail.dismiss")}
|
|
2713
|
-
</Button>
|
|
2714
|
-
</div>
|
|
2715
|
-
) : (
|
|
2716
|
-
<Button
|
|
2717
|
-
variant="ghost"
|
|
2718
|
-
size="sm"
|
|
2719
|
-
className="h-8 w-full px-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
2720
|
-
onClick={() =>
|
|
2721
|
-
setDismissTarget({
|
|
2722
|
-
kind: "slot",
|
|
2723
|
-
title: isFailed
|
|
2724
|
-
? t("brandKitDetail.failedCandidate")
|
|
2725
|
-
: t("brandKitDetail.generatingCandidate"),
|
|
2726
|
-
slot,
|
|
2727
|
-
})
|
|
2728
|
-
}
|
|
2729
|
-
disabled={busy || dismissing}
|
|
2730
|
-
>
|
|
2731
|
-
{t("brandKitDetail.dismiss")}
|
|
2732
|
-
</Button>
|
|
2733
|
-
),
|
|
2734
|
-
};
|
|
2457
|
+
function toggleAll(checked: boolean) {
|
|
2458
|
+
onSelectedIdsChange(
|
|
2459
|
+
checked ? new Set(boardAssets.map((asset) => asset.id)) : new Set(),
|
|
2460
|
+
);
|
|
2735
2461
|
}
|
|
2736
2462
|
|
|
2737
|
-
function
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2740
|
-
return {
|
|
2741
|
-
id: `candidate:${asset.id}`,
|
|
2742
|
-
title: assetDisplayTitle(asset),
|
|
2743
|
-
subtitle: assetLineageSourceText(asset) || assetCategoryLabel(asset),
|
|
2744
|
-
metadata:
|
|
2745
|
-
asset.mediaType === "video"
|
|
2746
|
-
? t("brandKitDetail.video")
|
|
2747
|
-
: asset.mimeType?.startsWith("image/")
|
|
2748
|
-
? t("brandKitDetail.image")
|
|
2749
|
-
: t("brandKitDetail.candidate"),
|
|
2750
|
-
status: "candidate",
|
|
2751
|
-
mediaType: asset.mediaType === "video" ? "video" : "image",
|
|
2752
|
-
href: `/asset/${asset.id}`,
|
|
2753
|
-
busy,
|
|
2754
|
-
preview: <AssetPreview asset={asset} fit="contain" />, // i18n-ignore structural preview slot name
|
|
2755
|
-
thumbnail: <AssetPreview asset={asset} />, // i18n-ignore structural preview slot name
|
|
2756
|
-
primaryActions: (
|
|
2757
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2758
|
-
<CandidateSaveMenu
|
|
2759
|
-
libraryId={libraryId}
|
|
2760
|
-
folders={folders}
|
|
2761
|
-
saving={saving}
|
|
2762
|
-
disabled={busy}
|
|
2763
|
-
onSave={(folderId) =>
|
|
2764
|
-
onSaveCandidate({ assetId: asset.id, folderId })
|
|
2765
|
-
}
|
|
2766
|
-
/>
|
|
2767
|
-
<Button
|
|
2768
|
-
variant="ghost"
|
|
2769
|
-
size="sm"
|
|
2770
|
-
className="h-8 px-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
2771
|
-
onClick={() =>
|
|
2772
|
-
setDismissTarget({
|
|
2773
|
-
kind: "asset",
|
|
2774
|
-
title: assetDisplayTitle(asset),
|
|
2775
|
-
asset,
|
|
2776
|
-
})
|
|
2777
|
-
}
|
|
2778
|
-
disabled={busy || dismissing}
|
|
2779
|
-
>
|
|
2780
|
-
{t("brandKitDetail.dismiss")}
|
|
2781
|
-
</Button>
|
|
2782
|
-
</div>
|
|
2783
|
-
),
|
|
2784
|
-
};
|
|
2463
|
+
function confirmDelete(ids: string[]) {
|
|
2464
|
+
const uniqueIds = [...new Set(ids)];
|
|
2465
|
+
if (uniqueIds.length) setConfirmDeleteIds(uniqueIds);
|
|
2785
2466
|
}
|
|
2786
2467
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
.
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
];
|
|
2801
|
-
const [activeItemId, setActiveItemId] = useState<string | null>(null);
|
|
2802
|
-
const itemIds = items.map((item) => item.id).join("\n");
|
|
2803
|
-
const activeItem =
|
|
2804
|
-
items.find((item) => item.id === activeItemId) ?? items[0] ?? null;
|
|
2805
|
-
|
|
2806
|
-
useEffect(() => {
|
|
2807
|
-
if (!items.length) {
|
|
2808
|
-
setActiveItemId(null);
|
|
2809
|
-
return;
|
|
2810
|
-
}
|
|
2811
|
-
setActiveItemId((current) =>
|
|
2812
|
-
current && items.some((item) => item.id === current)
|
|
2813
|
-
? current
|
|
2814
|
-
: items[0].id,
|
|
2815
|
-
);
|
|
2816
|
-
}, [itemIds, items]);
|
|
2817
|
-
|
|
2818
|
-
if (!items.length) return null;
|
|
2819
|
-
|
|
2820
|
-
return (
|
|
2821
|
-
<>
|
|
2822
|
-
<AlertDialog
|
|
2823
|
-
open={dismissTarget !== null}
|
|
2824
|
-
onOpenChange={(open) => {
|
|
2825
|
-
if (!open && !dismissing) setDismissTarget(null);
|
|
2826
|
-
}}
|
|
2827
|
-
>
|
|
2828
|
-
<AlertDialogContent>
|
|
2829
|
-
<AlertDialogHeader>
|
|
2830
|
-
<AlertDialogTitle>
|
|
2831
|
-
{t("brandKitDetail.dismissThisCandidate")}
|
|
2832
|
-
</AlertDialogTitle>
|
|
2833
|
-
<AlertDialogDescription>
|
|
2834
|
-
{t("brandKitDetail.dismissCandidateDescription", {
|
|
2835
|
-
title:
|
|
2836
|
-
dismissTarget?.title ?? t("brandKitDetail.thisCandidate"),
|
|
2837
|
-
})}
|
|
2838
|
-
</AlertDialogDescription>
|
|
2839
|
-
</AlertDialogHeader>
|
|
2840
|
-
<AlertDialogFooter>
|
|
2841
|
-
<AlertDialogCancel disabled={dismissing}>
|
|
2842
|
-
{t("brandKitDetail.cancel")}
|
|
2843
|
-
</AlertDialogCancel>
|
|
2844
|
-
<AlertDialogAction
|
|
2845
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
2846
|
-
disabled={dismissing}
|
|
2847
|
-
onClick={(event) => {
|
|
2848
|
-
event.preventDefault();
|
|
2849
|
-
void handleDismissCandidate();
|
|
2850
|
-
}}
|
|
2851
|
-
>
|
|
2852
|
-
{dismissing ? (
|
|
2853
|
-
<>
|
|
2854
|
-
<Spinner className="h-4 w-4" />
|
|
2855
|
-
{t("brandKitDetail.dismissing")}
|
|
2856
|
-
</>
|
|
2857
|
-
) : (
|
|
2858
|
-
t("brandKitDetail.dismiss")
|
|
2859
|
-
)}
|
|
2860
|
-
</AlertDialogAction>
|
|
2861
|
-
</AlertDialogFooter>
|
|
2862
|
-
</AlertDialogContent>
|
|
2863
|
-
</AlertDialog>
|
|
2864
|
-
|
|
2865
|
-
<section className="overflow-hidden rounded-lg border border-border bg-background">
|
|
2866
|
-
<div className="flex flex-col gap-3 border-b border-border px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
2867
|
-
<div className="min-w-0">
|
|
2868
|
-
<div className="flex items-center gap-2">
|
|
2869
|
-
<h3 className="truncate text-sm font-semibold">
|
|
2870
|
-
{t("brandKitDetail.candidates")}
|
|
2871
|
-
</h3>
|
|
2872
|
-
<Badge variant="outline">{items.length}</Badge>
|
|
2873
|
-
</div>
|
|
2874
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
2875
|
-
{t("brandKitDetail.generationQueue")}
|
|
2876
|
-
</p>
|
|
2877
|
-
</div>
|
|
2878
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
2879
|
-
<CandidateStageActions
|
|
2880
|
-
slots={pendingVariants}
|
|
2881
|
-
detachedCandidates={detachedCandidates}
|
|
2882
|
-
libraryId={libraryId}
|
|
2883
|
-
/>
|
|
2884
|
-
</div>
|
|
2885
|
-
</div>
|
|
2886
|
-
<div className="grid lg:grid-cols-[minmax(0,1fr)_300px]">
|
|
2887
|
-
<div className="min-w-0 bg-muted/10 p-3">
|
|
2888
|
-
<div
|
|
2889
|
-
className={[
|
|
2890
|
-
"group relative overflow-hidden rounded-lg border border-border bg-background",
|
|
2891
|
-
activeItem?.busy ? "opacity-80" : "",
|
|
2892
|
-
].join(" ")}
|
|
2893
|
-
aria-busy={activeItem?.busy}
|
|
2894
|
-
>
|
|
2895
|
-
<div className="aspect-[16/9] bg-muted/30">
|
|
2896
|
-
{activeItem?.href ? (
|
|
2897
|
-
<Link to={activeItem.href} className="block h-full w-full">
|
|
2898
|
-
{activeItem.preview}
|
|
2899
|
-
</Link>
|
|
2900
|
-
) : (
|
|
2901
|
-
activeItem?.preview
|
|
2902
|
-
)}
|
|
2903
|
-
</div>
|
|
2904
|
-
{activeItem?.menu ? (
|
|
2905
|
-
<div className="absolute right-3 top-3 z-10">
|
|
2906
|
-
{activeItem.menu}
|
|
2907
|
-
</div>
|
|
2908
|
-
) : null}
|
|
2909
|
-
{activeItem?.busy ? (
|
|
2910
|
-
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background/20">
|
|
2911
|
-
<Spinner className="h-5 w-5" />
|
|
2912
|
-
</div>
|
|
2913
|
-
) : null}
|
|
2914
|
-
</div>
|
|
2915
|
-
<div className="mt-3 flex gap-2 overflow-x-auto pb-1">
|
|
2916
|
-
{items.map((item) => {
|
|
2917
|
-
const active = item.id === activeItem?.id;
|
|
2918
|
-
return (
|
|
2919
|
-
<button
|
|
2920
|
-
key={item.id}
|
|
2921
|
-
type="button"
|
|
2922
|
-
onClick={() => setActiveItemId(item.id)}
|
|
2923
|
-
className={[
|
|
2924
|
-
"group relative h-16 w-24 shrink-0 overflow-hidden rounded-md border bg-background transition",
|
|
2925
|
-
active
|
|
2926
|
-
? "border-primary ring-2 ring-primary/25"
|
|
2927
|
-
: "border-border/80 hover:border-foreground/30",
|
|
2928
|
-
].join(" ")}
|
|
2929
|
-
aria-label={t("brandKitDetail.showItem", {
|
|
2930
|
-
title: item.title,
|
|
2931
|
-
})}
|
|
2932
|
-
aria-pressed={active}
|
|
2933
|
-
>
|
|
2934
|
-
{item.thumbnail}
|
|
2935
|
-
<span className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background/90 to-transparent" />
|
|
2936
|
-
{item.busy ? (
|
|
2937
|
-
<span className="absolute right-1.5 top-1.5 rounded-full bg-background/90 p-1 shadow-sm">
|
|
2938
|
-
<Spinner className="h-3 w-3" />
|
|
2939
|
-
</span>
|
|
2940
|
-
) : null}
|
|
2941
|
-
</button>
|
|
2942
|
-
);
|
|
2943
|
-
})}
|
|
2944
|
-
</div>
|
|
2945
|
-
</div>
|
|
2946
|
-
<aside className="flex min-h-56 flex-col justify-between gap-4 border-t border-border bg-background p-4 lg:border-l lg:border-t-0">
|
|
2947
|
-
<div className="min-w-0 space-y-4">
|
|
2948
|
-
<div className="min-w-0">
|
|
2949
|
-
<div className="truncate text-sm font-medium">
|
|
2950
|
-
{activeItem?.title}
|
|
2951
|
-
</div>
|
|
2952
|
-
{activeItem?.subtitle ? (
|
|
2953
|
-
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
2954
|
-
{activeItem.subtitle}
|
|
2955
|
-
</div>
|
|
2956
|
-
) : null}
|
|
2957
|
-
</div>
|
|
2958
|
-
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
2959
|
-
{activeItem?.status ? (
|
|
2960
|
-
<div className="rounded-md border border-border bg-muted/20 px-2 py-1.5">
|
|
2961
|
-
<div className="text-[10px] font-medium uppercase text-muted-foreground">
|
|
2962
|
-
{t("brandKitDetail.status")}
|
|
2963
|
-
</div>
|
|
2964
|
-
<div className="mt-0.5 truncate">{activeItem.status}</div>
|
|
2965
|
-
</div>
|
|
2966
|
-
) : null}
|
|
2967
|
-
{activeItem?.metadata ? (
|
|
2968
|
-
<div className="rounded-md border border-border bg-muted/20 px-2 py-1.5">
|
|
2969
|
-
<div className="text-[10px] font-medium uppercase text-muted-foreground">
|
|
2970
|
-
{t("brandKitDetail.type")}
|
|
2971
|
-
</div>
|
|
2972
|
-
<div className="mt-0.5 truncate">{activeItem.metadata}</div>
|
|
2973
|
-
</div>
|
|
2974
|
-
) : null}
|
|
2975
|
-
</div>
|
|
2976
|
-
{activeItem?.primaryActions ? (
|
|
2977
|
-
<div>{activeItem.primaryActions}</div>
|
|
2978
|
-
) : null}
|
|
2979
|
-
</div>
|
|
2980
|
-
{activeItem?.href ? (
|
|
2981
|
-
<Button asChild variant="outline" size="sm">
|
|
2982
|
-
<Link to={activeItem.href}>{t("brandKitDetail.open")}</Link>
|
|
2983
|
-
</Button>
|
|
2984
|
-
) : null}
|
|
2985
|
-
</aside>
|
|
2986
|
-
</div>
|
|
2987
|
-
</section>
|
|
2988
|
-
</>
|
|
2989
|
-
);
|
|
2990
|
-
}
|
|
2991
|
-
|
|
2992
|
-
function CandidateSaveMenu({
|
|
2993
|
-
libraryId,
|
|
2994
|
-
folders,
|
|
2995
|
-
saving,
|
|
2996
|
-
disabled,
|
|
2997
|
-
onSave,
|
|
2998
|
-
}: {
|
|
2999
|
-
libraryId: string;
|
|
3000
|
-
folders: any[];
|
|
3001
|
-
saving?: boolean;
|
|
3002
|
-
disabled?: boolean;
|
|
3003
|
-
onSave: (folderId: string | null) => void;
|
|
3004
|
-
}) {
|
|
3005
|
-
const t = useT();
|
|
3006
|
-
const createFolder = useActionMutation("create-folder");
|
|
3007
|
-
const queryClient = useQueryClient();
|
|
3008
|
-
const [createOpen, setCreateOpen] = useState(false);
|
|
3009
|
-
const pending = saving || createFolder.isPending;
|
|
3010
|
-
|
|
3011
|
-
return (
|
|
3012
|
-
<>
|
|
3013
|
-
<CreateFolderDialog
|
|
3014
|
-
open={createOpen}
|
|
3015
|
-
onOpenChange={setCreateOpen}
|
|
3016
|
-
onSubmit={async (title) => {
|
|
3017
|
-
const folder = (await createFolder.mutateAsync({
|
|
3018
|
-
libraryId,
|
|
3019
|
-
title,
|
|
3020
|
-
parentId: null,
|
|
3021
|
-
})) as any;
|
|
3022
|
-
void queryClient.invalidateQueries({
|
|
3023
|
-
queryKey: ["action", "get-library", { id: libraryId }],
|
|
3024
|
-
refetchType: "active",
|
|
3025
|
-
});
|
|
3026
|
-
setCreateOpen(false);
|
|
3027
|
-
if (folder?.id) onSave(folder.id);
|
|
3028
|
-
}}
|
|
3029
|
-
pending={createFolder.isPending}
|
|
3030
|
-
/>
|
|
3031
|
-
<DropdownMenu>
|
|
3032
|
-
<DropdownMenuTrigger asChild>
|
|
3033
|
-
<Button size="sm" className="h-8 px-2 text-xs" disabled={disabled}>
|
|
3034
|
-
{pending ? (
|
|
3035
|
-
<Spinner className="h-3.5 w-3.5" />
|
|
3036
|
-
) : (
|
|
3037
|
-
t("brandKitDetail.saveTo")
|
|
3038
|
-
)}
|
|
3039
|
-
</Button>
|
|
3040
|
-
</DropdownMenuTrigger>
|
|
3041
|
-
<DropdownMenuContent align="start">
|
|
3042
|
-
<DropdownMenuItem onSelect={() => onSave(null)}>
|
|
3043
|
-
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
3044
|
-
{t("brandKitDetail.unfiled")}
|
|
3045
|
-
</DropdownMenuItem>
|
|
3046
|
-
{folders.map((folder) => (
|
|
3047
|
-
<DropdownMenuItem
|
|
3048
|
-
key={folder.id}
|
|
3049
|
-
onSelect={() => onSave(folder.id)}
|
|
3050
|
-
>
|
|
3051
|
-
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
3052
|
-
{t("brandKitDetail.folderLabel", { title: folder.title })}
|
|
3053
|
-
</DropdownMenuItem>
|
|
3054
|
-
))}
|
|
3055
|
-
<DropdownMenuSeparator />
|
|
3056
|
-
<DropdownMenuItem
|
|
3057
|
-
onSelect={(event) => {
|
|
3058
|
-
event.preventDefault();
|
|
3059
|
-
setCreateOpen(true);
|
|
3060
|
-
}}
|
|
3061
|
-
>
|
|
3062
|
-
<IconFolderPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
3063
|
-
{t("brandKitDetail.newFolderEllipsis")}
|
|
3064
|
-
</DropdownMenuItem>
|
|
3065
|
-
</DropdownMenuContent>
|
|
3066
|
-
</DropdownMenu>
|
|
3067
|
-
</>
|
|
3068
|
-
);
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
function CandidateStageActions({
|
|
3072
|
-
slots,
|
|
3073
|
-
detachedCandidates,
|
|
3074
|
-
libraryId,
|
|
3075
|
-
}: {
|
|
3076
|
-
slots: any[];
|
|
3077
|
-
detachedCandidates: any[];
|
|
3078
|
-
libraryId: string;
|
|
3079
|
-
}) {
|
|
3080
|
-
const t = useT();
|
|
3081
|
-
const dismissSlots = useActionMutation("dismiss-variant-slots");
|
|
3082
|
-
const deleteAssets = useActionMutation("delete-assets");
|
|
3083
|
-
const queryClient = useQueryClient();
|
|
3084
|
-
const [pending, setPending] = useState<"failed" | "all" | null>(null);
|
|
3085
|
-
const failedSlots = slots.filter((slot) => slot.status === "failed");
|
|
3086
|
-
const failedCount = failedSlots.length;
|
|
3087
|
-
const detachedCount = detachedCandidates.length;
|
|
3088
|
-
const totalCount = slots.length + detachedCount;
|
|
3089
|
-
const isClearing = dismissSlots.isPending || deleteAssets.isPending;
|
|
3090
|
-
const actionLabel =
|
|
3091
|
-
pending === "failed"
|
|
3092
|
-
? t("brandKitDetail.dismissFailed")
|
|
3093
|
-
: t("brandKitDetail.clearAll");
|
|
3094
|
-
const busyLabel =
|
|
3095
|
-
pending === "failed"
|
|
3096
|
-
? t("brandKitDetail.dismissing")
|
|
3097
|
-
: t("brandKitDetail.clearing");
|
|
3098
|
-
|
|
3099
|
-
async function handleClear(scope: "failed" | "all") {
|
|
3100
|
-
const slotAssetIds = slots
|
|
3101
|
-
.filter((slot) => scope === "all" || slot.status === "failed")
|
|
3102
|
-
.map((slot) => slot.assetId)
|
|
3103
|
-
.filter(
|
|
3104
|
-
(assetId: unknown): assetId is string => typeof assetId === "string",
|
|
3105
|
-
);
|
|
3106
|
-
const detachedAssetIds =
|
|
3107
|
-
scope === "all" ? detachedCandidates.map((asset) => asset.id) : [];
|
|
3108
|
-
const removedAssetIds = [
|
|
3109
|
-
...new Set([...slotAssetIds, ...detachedAssetIds]),
|
|
3110
|
-
];
|
|
3111
|
-
|
|
3112
|
-
if (
|
|
3113
|
-
scope === "all" &&
|
|
3114
|
-
slots.length === 0 &&
|
|
3115
|
-
detachedAssetIds.length === 0
|
|
3116
|
-
) {
|
|
3117
|
-
setPending(null);
|
|
3118
|
-
return;
|
|
3119
|
-
}
|
|
3120
|
-
if (scope === "failed" && failedCount === 0) {
|
|
3121
|
-
setPending(null);
|
|
3122
|
-
return;
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
try {
|
|
3126
|
-
if (slots.length > 0 && (scope === "all" || failedCount > 0)) {
|
|
3127
|
-
await dismissSlots.mutateAsync({ scope });
|
|
3128
|
-
removeVariantSlotsByScopeFromCache(queryClient, scope);
|
|
3129
|
-
}
|
|
3130
|
-
if (detachedAssetIds.length > 0) {
|
|
3131
|
-
await deleteAssets.mutateAsync({ ids: detachedAssetIds });
|
|
3132
|
-
}
|
|
3133
|
-
if (removedAssetIds.length > 0) {
|
|
3134
|
-
removeAssetsFromLibraryCache(queryClient, libraryId, removedAssetIds);
|
|
3135
|
-
}
|
|
3136
|
-
setPending(null);
|
|
3137
|
-
void queryClient.invalidateQueries({
|
|
3138
|
-
queryKey: ["app-state", "asset-variants"],
|
|
3139
|
-
refetchType: "active",
|
|
3140
|
-
});
|
|
3141
|
-
if (scope === "failed") {
|
|
3142
|
-
toast.success(
|
|
3143
|
-
t("brandKitDetail.dismissedFailedCandidates", {
|
|
3144
|
-
count: failedCount,
|
|
3145
|
-
}),
|
|
3146
|
-
);
|
|
3147
|
-
} else {
|
|
3148
|
-
toast.success(
|
|
3149
|
-
t("brandKitDetail.clearedCandidates", { count: totalCount }),
|
|
3150
|
-
);
|
|
3151
|
-
}
|
|
3152
|
-
} catch (error) {
|
|
3153
|
-
toast.error(
|
|
3154
|
-
error instanceof Error
|
|
3155
|
-
? error.message
|
|
3156
|
-
: t("brandKitDetail.couldNotClearCandidates"),
|
|
3157
|
-
);
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
if (totalCount === 0) return null;
|
|
3162
|
-
|
|
3163
|
-
return (
|
|
3164
|
-
<>
|
|
3165
|
-
<AlertDialog
|
|
3166
|
-
open={pending !== null}
|
|
3167
|
-
onOpenChange={(open) => {
|
|
3168
|
-
if (!open && !isClearing) setPending(null);
|
|
3169
|
-
}}
|
|
3170
|
-
>
|
|
3171
|
-
<AlertDialogContent>
|
|
3172
|
-
<AlertDialogHeader>
|
|
3173
|
-
<AlertDialogTitle>
|
|
3174
|
-
{pending === "failed"
|
|
3175
|
-
? t("brandKitDetail.dismissFailedCandidatesTitle", {
|
|
3176
|
-
count: failedCount,
|
|
3177
|
-
})
|
|
3178
|
-
: t("brandKitDetail.clearCandidatesTitle", {
|
|
3179
|
-
count: totalCount,
|
|
3180
|
-
})}
|
|
3181
|
-
</AlertDialogTitle>
|
|
3182
|
-
<AlertDialogDescription>
|
|
3183
|
-
{pending === "failed"
|
|
3184
|
-
? t("brandKitDetail.dismissFailedDescription")
|
|
3185
|
-
: t("brandKitDetail.clearCandidatesDescription")}
|
|
3186
|
-
</AlertDialogDescription>
|
|
3187
|
-
</AlertDialogHeader>
|
|
3188
|
-
<AlertDialogFooter>
|
|
3189
|
-
<AlertDialogCancel disabled={isClearing}>
|
|
3190
|
-
{t("brandKitDetail.cancel")}
|
|
3191
|
-
</AlertDialogCancel>
|
|
3192
|
-
<AlertDialogAction
|
|
3193
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
3194
|
-
disabled={isClearing || pending === null}
|
|
3195
|
-
onClick={(event) => {
|
|
3196
|
-
event.preventDefault();
|
|
3197
|
-
if (pending) void handleClear(pending);
|
|
3198
|
-
}}
|
|
3199
|
-
>
|
|
3200
|
-
{isClearing ? (
|
|
3201
|
-
<>
|
|
3202
|
-
<Spinner className="h-4 w-4" />
|
|
3203
|
-
{busyLabel}
|
|
3204
|
-
</>
|
|
3205
|
-
) : (
|
|
3206
|
-
actionLabel
|
|
3207
|
-
)}
|
|
3208
|
-
</AlertDialogAction>
|
|
3209
|
-
</AlertDialogFooter>
|
|
3210
|
-
</AlertDialogContent>
|
|
3211
|
-
</AlertDialog>
|
|
3212
|
-
|
|
3213
|
-
<DropdownMenu>
|
|
3214
|
-
<DropdownMenuTrigger asChild>
|
|
3215
|
-
<Button
|
|
3216
|
-
type="button"
|
|
3217
|
-
variant="outline"
|
|
3218
|
-
size="sm"
|
|
3219
|
-
className="gap-2"
|
|
3220
|
-
aria-label={t("brandKitDetail.candidateActions")}
|
|
3221
|
-
disabled={isClearing}
|
|
3222
|
-
>
|
|
3223
|
-
<IconDotsVertical className="h-4 w-4" />
|
|
3224
|
-
{t("brandKitDetail.clear")}
|
|
3225
|
-
</Button>
|
|
3226
|
-
</DropdownMenuTrigger>
|
|
3227
|
-
<DropdownMenuContent align="end">
|
|
3228
|
-
<DropdownMenuItem
|
|
3229
|
-
disabled={failedCount === 0 || isClearing}
|
|
3230
|
-
onSelect={(event) => {
|
|
3231
|
-
event.preventDefault();
|
|
3232
|
-
setPending("failed");
|
|
3233
|
-
}}
|
|
3234
|
-
>
|
|
3235
|
-
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
3236
|
-
{t("brandKitDetail.dismissFailed")} ({failedCount})
|
|
3237
|
-
</DropdownMenuItem>
|
|
3238
|
-
<DropdownMenuItem
|
|
3239
|
-
className="text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
3240
|
-
disabled={isClearing}
|
|
3241
|
-
onSelect={(event) => {
|
|
3242
|
-
event.preventDefault();
|
|
3243
|
-
setPending("all");
|
|
3244
|
-
}}
|
|
3245
|
-
>
|
|
3246
|
-
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
3247
|
-
{t("brandKitDetail.clearAll")}
|
|
3248
|
-
</DropdownMenuItem>
|
|
3249
|
-
</DropdownMenuContent>
|
|
3250
|
-
</DropdownMenu>
|
|
3251
|
-
</>
|
|
3252
|
-
);
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
function AssetSwimlaneBoard({
|
|
3256
|
-
libraryId,
|
|
3257
|
-
viewMode,
|
|
3258
|
-
onViewModeChange,
|
|
3259
|
-
scope,
|
|
3260
|
-
onScopeChange,
|
|
3261
|
-
hideEmptyLanes,
|
|
3262
|
-
assets,
|
|
3263
|
-
pendingUploads,
|
|
3264
|
-
folders,
|
|
3265
|
-
promotingReferenceKeys,
|
|
3266
|
-
onUploadClick,
|
|
3267
|
-
onDrop,
|
|
3268
|
-
onMoveToReferences,
|
|
3269
|
-
onRemoveFromReferences,
|
|
3270
|
-
selectedIds,
|
|
3271
|
-
onSelectedIdsChange,
|
|
3272
|
-
onOptimisticDelete,
|
|
3273
|
-
onRestoreOptimisticDelete,
|
|
3274
|
-
}: {
|
|
3275
|
-
libraryId: string;
|
|
3276
|
-
viewMode: AssetViewMode;
|
|
3277
|
-
onViewModeChange: (mode: AssetViewMode) => void;
|
|
3278
|
-
scope: AssetLibraryScope;
|
|
3279
|
-
onScopeChange: (scope: AssetLibraryScope) => void;
|
|
3280
|
-
hideEmptyLanes: boolean;
|
|
3281
|
-
assets: any[];
|
|
3282
|
-
pendingUploads: PendingUpload[];
|
|
3283
|
-
folders: any[];
|
|
3284
|
-
promotingReferenceKeys: Set<string>;
|
|
3285
|
-
onUploadClick: () => void;
|
|
3286
|
-
onDrop: (files: FileList) => void;
|
|
3287
|
-
onMoveToReferences: (asset: any, slot?: any) => void;
|
|
3288
|
-
onRemoveFromReferences: (asset: any) => void;
|
|
3289
|
-
selectedIds: Set<string>;
|
|
3290
|
-
onSelectedIdsChange: Dispatch<SetStateAction<Set<string>>>;
|
|
3291
|
-
onOptimisticDelete?: (ids: string[]) => void;
|
|
3292
|
-
onRestoreOptimisticDelete?: (ids: string[]) => void;
|
|
3293
|
-
}) {
|
|
3294
|
-
const t = useT();
|
|
3295
|
-
const deleteAsset = useActionMutation("delete-asset");
|
|
3296
|
-
const deleteAssets = useActionMutation("delete-assets");
|
|
3297
|
-
const updateAsset = useActionMutation("update-asset");
|
|
3298
|
-
const queryClient = useQueryClient();
|
|
3299
|
-
const [confirmDeleteIds, setConfirmDeleteIds] = useState<string[]>([]);
|
|
3300
|
-
const [deletingIds, setDeletingIds] = useState<Set<string>>(() => new Set());
|
|
3301
|
-
const [bulkReferenceAction, setBulkReferenceAction] = useState<
|
|
3302
|
-
"add" | "remove" | null
|
|
3303
|
-
>(null);
|
|
3304
|
-
const visiblePendingUploads = scope === "all" ? pendingUploads : [];
|
|
3305
|
-
const referenceAssets = assets.filter(
|
|
3306
|
-
(asset) => asset.status === "reference",
|
|
3307
|
-
);
|
|
3308
|
-
const savedAssets = assets.filter((asset) => asset.status === "saved");
|
|
3309
|
-
const visibleLibraryAssets =
|
|
3310
|
-
scope === "references" ? referenceAssets : assets;
|
|
3311
|
-
const boardAssets = visibleLibraryAssets;
|
|
3312
|
-
const selectedAssets = boardAssets.filter((asset) =>
|
|
3313
|
-
selectedIds.has(asset.id),
|
|
3314
|
-
);
|
|
3315
|
-
const selectedReferenceAssets = selectedAssets.filter(
|
|
3316
|
-
(asset) => asset.status === "reference",
|
|
3317
|
-
);
|
|
3318
|
-
const selectedSavedAssets = selectedAssets.filter(
|
|
3319
|
-
(asset) => asset.status === "saved",
|
|
3320
|
-
);
|
|
3321
|
-
const selectedCount = selectedAssets.length;
|
|
3322
|
-
const allSelected =
|
|
3323
|
-
boardAssets.length > 0 && selectedCount === boardAssets.length;
|
|
3324
|
-
const pendingDeleteCount = deletingIds.size;
|
|
3325
|
-
const deleting =
|
|
3326
|
-
deleteAsset.isPending || deleteAssets.isPending || pendingDeleteCount > 0;
|
|
3327
|
-
const changingReference = bulkReferenceAction !== null;
|
|
3328
|
-
const hasAnyBoardItem =
|
|
3329
|
-
assets.length > 0 || pendingUploads.length > 0 || pendingDeleteCount > 0;
|
|
3330
|
-
|
|
3331
|
-
function toggleAsset(assetId: string, checked: boolean) {
|
|
3332
|
-
onSelectedIdsChange((current) => {
|
|
3333
|
-
const next = new Set(current);
|
|
3334
|
-
if (checked) {
|
|
3335
|
-
next.add(assetId);
|
|
3336
|
-
} else {
|
|
3337
|
-
next.delete(assetId);
|
|
3338
|
-
}
|
|
3339
|
-
return next;
|
|
3340
|
-
});
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
function toggleAll(checked: boolean) {
|
|
3344
|
-
onSelectedIdsChange(
|
|
3345
|
-
checked ? new Set(boardAssets.map((asset) => asset.id)) : new Set(),
|
|
3346
|
-
);
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
function confirmDelete(ids: string[]) {
|
|
3350
|
-
const uniqueIds = [...new Set(ids)];
|
|
3351
|
-
if (uniqueIds.length) setConfirmDeleteIds(uniqueIds);
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
|
-
function markDeleting(ids: string[]) {
|
|
3355
|
-
setDeletingIds((current) => {
|
|
3356
|
-
const next = new Set(current);
|
|
3357
|
-
for (const id of ids) next.add(id);
|
|
3358
|
-
return next;
|
|
3359
|
-
});
|
|
3360
|
-
onSelectedIdsChange((current) => {
|
|
3361
|
-
const next = new Set(current);
|
|
3362
|
-
for (const id of ids) next.delete(id);
|
|
3363
|
-
return next;
|
|
3364
|
-
});
|
|
3365
|
-
onOptimisticDelete?.(ids);
|
|
3366
|
-
}
|
|
2468
|
+
function markDeleting(ids: string[]) {
|
|
2469
|
+
setDeletingIds((current) => {
|
|
2470
|
+
const next = new Set(current);
|
|
2471
|
+
for (const id of ids) next.add(id);
|
|
2472
|
+
return next;
|
|
2473
|
+
});
|
|
2474
|
+
onSelectedIdsChange((current) => {
|
|
2475
|
+
const next = new Set(current);
|
|
2476
|
+
for (const id of ids) next.delete(id);
|
|
2477
|
+
return next;
|
|
2478
|
+
});
|
|
2479
|
+
onOptimisticDelete?.(ids);
|
|
2480
|
+
}
|
|
3367
2481
|
|
|
3368
2482
|
function finishDeleting(ids: string[]) {
|
|
3369
2483
|
setDeletingIds((current) => {
|
|
@@ -3395,13 +2509,11 @@ function AssetSwimlaneBoard({
|
|
|
3395
2509
|
{
|
|
3396
2510
|
onSuccess: () => {
|
|
3397
2511
|
finishDeleting(ids);
|
|
3398
|
-
toast.success(t("
|
|
2512
|
+
toast.success(t("library.deletedAsset"));
|
|
3399
2513
|
},
|
|
3400
2514
|
onError: (error) => {
|
|
3401
2515
|
restoreAfterDeleteError(ids);
|
|
3402
|
-
toast.error(
|
|
3403
|
-
error.message || t("brandKitDetail.couldNotDeleteAsset"),
|
|
3404
|
-
);
|
|
2516
|
+
toast.error(error.message || t("library.couldNotDeleteAsset"));
|
|
3405
2517
|
},
|
|
3406
2518
|
},
|
|
3407
2519
|
);
|
|
@@ -3416,12 +2528,12 @@ function AssetSwimlaneBoard({
|
|
|
3416
2528
|
onSuccess: (result: any) => {
|
|
3417
2529
|
finishDeleting(ids);
|
|
3418
2530
|
const count = Number(result?.deletedCount ?? ids.length);
|
|
3419
|
-
toast.success(t("
|
|
2531
|
+
toast.success(t("library.deletedAssets", { count }));
|
|
3420
2532
|
},
|
|
3421
2533
|
onError: (error) => {
|
|
3422
2534
|
restoreAfterDeleteError(ids);
|
|
3423
2535
|
toast.error(
|
|
3424
|
-
error.message || t("
|
|
2536
|
+
error.message || t("library.couldNotDeleteSelectedAssets"),
|
|
3425
2537
|
);
|
|
3426
2538
|
},
|
|
3427
2539
|
},
|
|
@@ -3477,10 +2589,10 @@ function AssetSwimlaneBoard({
|
|
|
3477
2589
|
}
|
|
3478
2590
|
toast.success(
|
|
3479
2591
|
enabled
|
|
3480
|
-
? t("
|
|
2592
|
+
? t("library.addedAssetsToReferences", {
|
|
3481
2593
|
count: assetList.length,
|
|
3482
2594
|
})
|
|
3483
|
-
: t("
|
|
2595
|
+
: t("library.removedAssetsFromReferences", {
|
|
3484
2596
|
count: assetList.length,
|
|
3485
2597
|
}),
|
|
3486
2598
|
);
|
|
@@ -3489,8 +2601,8 @@ function AssetSwimlaneBoard({
|
|
|
3489
2601
|
error instanceof Error
|
|
3490
2602
|
? error.message
|
|
3491
2603
|
: enabled
|
|
3492
|
-
? t("
|
|
3493
|
-
: t("
|
|
2604
|
+
? t("library.couldNotAddSelectedToReferences")
|
|
2605
|
+
: t("library.couldNotRemoveSelectedFromReferences"),
|
|
3494
2606
|
);
|
|
3495
2607
|
return;
|
|
3496
2608
|
} finally {
|
|
@@ -3504,15 +2616,14 @@ function AssetSwimlaneBoard({
|
|
|
3504
2616
|
id: `upload:${upload.id}`,
|
|
3505
2617
|
title: upload.name,
|
|
3506
2618
|
subtitle: isChecking
|
|
3507
|
-
? t("
|
|
3508
|
-
: t("
|
|
3509
|
-
status: isChecking
|
|
3510
|
-
? t("brandKitDetail.checking")
|
|
3511
|
-
: t("brandKitDetail.uploading"),
|
|
2619
|
+
? t("library.checkingUpload")
|
|
2620
|
+
: t("library.uploading"),
|
|
2621
|
+
status: isChecking ? t("library.checking") : t("library.uploading"),
|
|
3512
2622
|
mediaType: upload.mediaType,
|
|
3513
2623
|
busy: true,
|
|
2624
|
+
showBusyOverlay: false,
|
|
3514
2625
|
preview: <PendingUploadPreview upload={upload} fit="contain" />, // i18n-ignore structural preview slot name
|
|
3515
|
-
thumbnail: <PendingUploadPreview upload={upload} />,
|
|
2626
|
+
thumbnail: <PendingUploadPreview upload={upload} />,
|
|
3516
2627
|
};
|
|
3517
2628
|
}
|
|
3518
2629
|
|
|
@@ -3547,15 +2658,14 @@ function AssetSwimlaneBoard({
|
|
|
3547
2658
|
id: `asset:${asset.id}`,
|
|
3548
2659
|
title: displayTitle,
|
|
3549
2660
|
subtitle: sourceText || categoryLabel || asset.status,
|
|
2661
|
+
asset,
|
|
3550
2662
|
metadata:
|
|
3551
2663
|
asset.mediaType === "video"
|
|
3552
|
-
? t("
|
|
2664
|
+
? t("library.video")
|
|
3553
2665
|
: asset.mimeType?.startsWith("image/")
|
|
3554
|
-
? t("
|
|
3555
|
-
: asset.mimeType || t("
|
|
3556
|
-
status: isReference
|
|
3557
|
-
? t("brandKitDetail.reference")
|
|
3558
|
-
: t("brandKitDetail.saved"),
|
|
2666
|
+
? t("library.image")
|
|
2667
|
+
: asset.mimeType || t("library.asset"),
|
|
2668
|
+
status: isReference ? t("library.reference") : t("library.saved"),
|
|
3559
2669
|
mediaType: asset.mediaType === "video" ? "video" : "image",
|
|
3560
2670
|
href: `/asset/${asset.id}`,
|
|
3561
2671
|
selected: selectedIds.has(asset.id),
|
|
@@ -3594,7 +2704,7 @@ function AssetSwimlaneBoard({
|
|
|
3594
2704
|
{saving ? (
|
|
3595
2705
|
<Spinner className="h-3.5 w-3.5" />
|
|
3596
2706
|
) : (
|
|
3597
|
-
t("
|
|
2707
|
+
t("library.save")
|
|
3598
2708
|
)}
|
|
3599
2709
|
</Button>
|
|
3600
2710
|
) : null}
|
|
@@ -3607,12 +2717,12 @@ function AssetSwimlaneBoard({
|
|
|
3607
2717
|
}
|
|
3608
2718
|
onClick={onMoveToReferences}
|
|
3609
2719
|
disabled={busy}
|
|
3610
|
-
title={t("
|
|
2720
|
+
title={t("library.addToReferences")}
|
|
3611
2721
|
>
|
|
3612
2722
|
{promoting ? (
|
|
3613
2723
|
<Spinner className="h-3.5 w-3.5" />
|
|
3614
2724
|
) : (
|
|
3615
|
-
t("
|
|
2725
|
+
t("library.addToReferences")
|
|
3616
2726
|
)}
|
|
3617
2727
|
</Button>
|
|
3618
2728
|
) : null}
|
|
@@ -3625,12 +2735,12 @@ function AssetSwimlaneBoard({
|
|
|
3625
2735
|
}
|
|
3626
2736
|
onClick={onRemoveFromReferences}
|
|
3627
2737
|
disabled={busy}
|
|
3628
|
-
title={t("
|
|
2738
|
+
title={t("library.removeFromReferences")}
|
|
3629
2739
|
>
|
|
3630
2740
|
{promoting ? (
|
|
3631
2741
|
<Spinner className="h-3.5 w-3.5" />
|
|
3632
2742
|
) : (
|
|
3633
|
-
t("
|
|
2743
|
+
t("library.removeFromReferences")
|
|
3634
2744
|
)}
|
|
3635
2745
|
</Button>
|
|
3636
2746
|
) : null}
|
|
@@ -3665,10 +2775,10 @@ function AssetSwimlaneBoard({
|
|
|
3665
2775
|
<div className="flex min-h-[280px] w-full flex-col items-center justify-center rounded-lg border border-dashed border-border bg-muted/15 p-8 text-center">
|
|
3666
2776
|
<IconSearch className="h-9 w-9 text-muted-foreground" />
|
|
3667
2777
|
<span className="mt-4 text-base font-semibold">
|
|
3668
|
-
{t("
|
|
2778
|
+
{t("library.noAssetsMatchView")}
|
|
3669
2779
|
</span>
|
|
3670
2780
|
<span className="mt-2 max-w-md text-sm text-muted-foreground">
|
|
3671
|
-
{t("
|
|
2781
|
+
{t("library.noAssetsMatchViewBody")}
|
|
3672
2782
|
</span>
|
|
3673
2783
|
</div>
|
|
3674
2784
|
);
|
|
@@ -3688,10 +2798,10 @@ function AssetSwimlaneBoard({
|
|
|
3688
2798
|
>
|
|
3689
2799
|
<IconPhotoPlus className="h-10 w-10 text-muted-foreground" />
|
|
3690
2800
|
<span className="mt-4 text-base font-semibold">
|
|
3691
|
-
{t("
|
|
2801
|
+
{t("library.addAssets")}
|
|
3692
2802
|
</span>
|
|
3693
2803
|
<span className="mt-2 max-w-md text-sm text-muted-foreground">
|
|
3694
|
-
{t("
|
|
2804
|
+
{t("library.addAssetsDescription")}
|
|
3695
2805
|
</span>
|
|
3696
2806
|
</button>
|
|
3697
2807
|
);
|
|
@@ -3709,19 +2819,19 @@ function AssetSwimlaneBoard({
|
|
|
3709
2819
|
<AlertDialogHeader>
|
|
3710
2820
|
<AlertDialogTitle>
|
|
3711
2821
|
{confirmDeleteIds.length > 1
|
|
3712
|
-
? t("
|
|
2822
|
+
? t("library.deleteAssetsTitle", {
|
|
3713
2823
|
count: confirmDeleteIds.length,
|
|
3714
2824
|
})
|
|
3715
|
-
: t("
|
|
2825
|
+
: t("library.deleteAssetTitle")}
|
|
3716
2826
|
</AlertDialogTitle>
|
|
3717
2827
|
<AlertDialogDescription>
|
|
3718
2828
|
{confirmDeleteIds.length > 1
|
|
3719
|
-
? t("
|
|
3720
|
-
: t("
|
|
2829
|
+
? t("library.deleteAssetsDescription")
|
|
2830
|
+
: t("library.deleteAssetDescription")}
|
|
3721
2831
|
</AlertDialogDescription>
|
|
3722
2832
|
</AlertDialogHeader>
|
|
3723
2833
|
<AlertDialogFooter>
|
|
3724
|
-
<AlertDialogCancel>{t("
|
|
2834
|
+
<AlertDialogCancel>{t("library.cancel")}</AlertDialogCancel>
|
|
3725
2835
|
<AlertDialogAction
|
|
3726
2836
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
3727
2837
|
disabled={!confirmDeleteIds.length || deleting}
|
|
@@ -3743,18 +2853,14 @@ function AssetSwimlaneBoard({
|
|
|
3743
2853
|
checked={allSelected}
|
|
3744
2854
|
disabled={!boardAssets.length || deleting}
|
|
3745
2855
|
onCheckedChange={(checked) => toggleAll(checked === true)}
|
|
3746
|
-
aria-label={t("
|
|
2856
|
+
aria-label={t("library.selectAllVisibleAssets")}
|
|
3747
2857
|
/>
|
|
3748
|
-
{allSelected
|
|
3749
|
-
? t("brandKitDetail.deselectAll")
|
|
3750
|
-
: t("brandKitDetail.selectAll")}
|
|
2858
|
+
{allSelected ? t("library.deselectAll") : t("library.selectAll")}
|
|
3751
2859
|
</label>
|
|
3752
2860
|
<span className="text-xs text-muted-foreground">
|
|
3753
|
-
{t("
|
|
3754
|
-
count: boardAssets.length,
|
|
3755
|
-
})}
|
|
2861
|
+
{t("library.visibleAssetsCount", { count: boardAssets.length })}
|
|
3756
2862
|
{referenceAssets.length > 0
|
|
3757
|
-
? ` · ${t("
|
|
2863
|
+
? ` · ${t("library.referencesCount", {
|
|
3758
2864
|
count: referenceAssets.length,
|
|
3759
2865
|
})}`
|
|
3760
2866
|
: ""}
|
|
@@ -3766,7 +2872,7 @@ function AssetSwimlaneBoard({
|
|
|
3766
2872
|
size="sm"
|
|
3767
2873
|
onClick={() => onSelectedIdsChange(new Set())}
|
|
3768
2874
|
>
|
|
3769
|
-
{t("
|
|
2875
|
+
{t("library.clear")}
|
|
3770
2876
|
</Button>
|
|
3771
2877
|
) : null}
|
|
3772
2878
|
</div>
|
|
@@ -3787,9 +2893,7 @@ function AssetSwimlaneBoard({
|
|
|
3787
2893
|
<div className="flex min-w-0 items-center gap-2 text-sm font-medium">
|
|
3788
2894
|
<Spinner className="h-4 w-4" />
|
|
3789
2895
|
<span className="truncate">
|
|
3790
|
-
{t("
|
|
3791
|
-
count: pendingDeleteCount,
|
|
3792
|
-
})}
|
|
2896
|
+
{t("library.deletingAssets", { count: pendingDeleteCount })}
|
|
3793
2897
|
</span>
|
|
3794
2898
|
</div>
|
|
3795
2899
|
) : (
|
|
@@ -3797,19 +2901,17 @@ function AssetSwimlaneBoard({
|
|
|
3797
2901
|
<Checkbox
|
|
3798
2902
|
checked={allSelected}
|
|
3799
2903
|
onCheckedChange={(checked) => toggleAll(checked === true)}
|
|
3800
|
-
aria-label={t("
|
|
2904
|
+
aria-label={t("library.selectAllAssetsInBoard")}
|
|
3801
2905
|
/>
|
|
3802
2906
|
<span className="truncate">
|
|
3803
2907
|
{selectedCount > 0
|
|
3804
|
-
? t("
|
|
3805
|
-
: t("
|
|
3806
|
-
count: boardAssets.length,
|
|
3807
|
-
})}
|
|
2908
|
+
? t("library.selectedCount", { count: selectedCount })
|
|
2909
|
+
: t("library.assetCount", { count: boardAssets.length })}
|
|
3808
2910
|
</span>
|
|
3809
2911
|
<span className="hidden text-xs text-muted-foreground sm:inline">
|
|
3810
|
-
{t("
|
|
3811
|
-
|
|
3812
|
-
|
|
2912
|
+
{t("library.referencesAndSavedCount", {
|
|
2913
|
+
referenceCount: referenceAssets.length,
|
|
2914
|
+
savedCount: savedAssets.length,
|
|
3813
2915
|
})}
|
|
3814
2916
|
</span>
|
|
3815
2917
|
</div>
|
|
@@ -3874,7 +2976,7 @@ function AssetSwimlaneBoard({
|
|
|
3874
2976
|
) : (
|
|
3875
2977
|
<IconTrash className="h-4 w-4" />
|
|
3876
2978
|
)}
|
|
3877
|
-
{t("
|
|
2979
|
+
{t("assetDetail.delete")}
|
|
3878
2980
|
</Button>
|
|
3879
2981
|
</div>
|
|
3880
2982
|
) : null}
|
|
@@ -3887,39 +2989,39 @@ function AssetSwimlaneBoard({
|
|
|
3887
2989
|
<SwimLane
|
|
3888
2990
|
title={
|
|
3889
2991
|
scope === "references"
|
|
3890
|
-
? t("
|
|
3891
|
-
: t("
|
|
2992
|
+
? t("library.references")
|
|
2993
|
+
: t("library.library")
|
|
3892
2994
|
}
|
|
3893
2995
|
eyebrow={
|
|
3894
2996
|
scope === "references"
|
|
3895
|
-
? t("
|
|
3896
|
-
: t("
|
|
2997
|
+
? t("library.referencesEyebrow")
|
|
2998
|
+
: t("library.libraryEyebrow")
|
|
3897
2999
|
}
|
|
3898
3000
|
items={visibleGalleryItems}
|
|
3899
3001
|
action={
|
|
3900
3002
|
<Button variant="outline" size="sm" onClick={onUploadClick}>
|
|
3901
|
-
{t("
|
|
3003
|
+
{t("library.add")}
|
|
3902
3004
|
</Button>
|
|
3903
3005
|
}
|
|
3904
3006
|
empty={
|
|
3905
3007
|
scope === "references" && assets.length > 0 ? (
|
|
3906
3008
|
<LaneActionEmpty
|
|
3907
|
-
title={t("
|
|
3908
|
-
body={t("
|
|
3009
|
+
title={t("library.noReferencesInView")}
|
|
3010
|
+
body={t("library.noReferencesInViewBody")}
|
|
3909
3011
|
onClick={() => onScopeChange("all")}
|
|
3910
|
-
action={t("
|
|
3012
|
+
action={t("library.showAll")}
|
|
3911
3013
|
/>
|
|
3912
3014
|
) : hideEmptyLanes ? (
|
|
3913
3015
|
<LaneActionEmpty
|
|
3914
|
-
title={t("
|
|
3915
|
-
body={t("
|
|
3016
|
+
title={t("library.noAssetsMatchView")}
|
|
3017
|
+
body={t("library.noAssetsMatchViewBody")}
|
|
3916
3018
|
onClick={() => onScopeChange("all")}
|
|
3917
|
-
action={t("
|
|
3019
|
+
action={t("library.showAll")}
|
|
3918
3020
|
/>
|
|
3919
3021
|
) : (
|
|
3920
3022
|
<LaneDropTarget
|
|
3921
|
-
title={t("
|
|
3922
|
-
body={t("
|
|
3023
|
+
title={t("library.dropAssetsHere")}
|
|
3024
|
+
body={t("library.dropAssetsHereBody")}
|
|
3923
3025
|
onClick={onUploadClick}
|
|
3924
3026
|
onDrop={onDrop}
|
|
3925
3027
|
/>
|
|
@@ -3946,12 +3048,12 @@ function AssetViewModeToggle({
|
|
|
3946
3048
|
}> = [
|
|
3947
3049
|
{
|
|
3948
3050
|
value: "lanes",
|
|
3949
|
-
label: t("
|
|
3051
|
+
label: t("library.lanes"),
|
|
3950
3052
|
icon: <IconLayoutBottombar className="h-4 w-4" />,
|
|
3951
3053
|
},
|
|
3952
3054
|
{
|
|
3953
3055
|
value: "cards",
|
|
3954
|
-
label: t("
|
|
3056
|
+
label: t("library.cards"),
|
|
3955
3057
|
icon: <IconLayoutGrid className="h-4 w-4" />,
|
|
3956
3058
|
},
|
|
3957
3059
|
];
|
|
@@ -3960,7 +3062,7 @@ function AssetViewModeToggle({
|
|
|
3960
3062
|
<TooltipProvider delayDuration={150}>
|
|
3961
3063
|
<div
|
|
3962
3064
|
role="group"
|
|
3963
|
-
aria-label={t("
|
|
3065
|
+
aria-label={t("library.assetView")}
|
|
3964
3066
|
className="inline-flex shrink-0 gap-1 rounded-md border border-border bg-muted/20 p-1"
|
|
3965
3067
|
>
|
|
3966
3068
|
{options.map((option) => {
|
|
@@ -3977,19 +3079,17 @@ function AssetViewModeToggle({
|
|
|
3977
3079
|
? "bg-background text-foreground shadow-sm"
|
|
3978
3080
|
: "text-muted-foreground hover:bg-background/60 hover:text-foreground",
|
|
3979
3081
|
].join(" ")}
|
|
3980
|
-
aria-label={t("
|
|
3981
|
-
|
|
3082
|
+
aria-label={t("library.assetViewMode", {
|
|
3083
|
+
mode: option.label,
|
|
3982
3084
|
})}
|
|
3983
3085
|
aria-pressed={active}
|
|
3984
|
-
title={t("
|
|
3985
|
-
label: option.label,
|
|
3986
|
-
})}
|
|
3086
|
+
title={t("library.assetViewMode", { mode: option.label })}
|
|
3987
3087
|
>
|
|
3988
3088
|
{option.icon}
|
|
3989
3089
|
</button>
|
|
3990
3090
|
</TooltipTrigger>
|
|
3991
3091
|
<TooltipContent side="bottom">
|
|
3992
|
-
{t("
|
|
3092
|
+
{t("library.assetViewMode", { mode: option.label })}
|
|
3993
3093
|
</TooltipContent>
|
|
3994
3094
|
</Tooltip>
|
|
3995
3095
|
);
|
|
@@ -4016,10 +3116,10 @@ function AssetScopeToggle({
|
|
|
4016
3116
|
label: string;
|
|
4017
3117
|
count: number;
|
|
4018
3118
|
}> = [
|
|
4019
|
-
{ value: "all", label: t("
|
|
3119
|
+
{ value: "all", label: t("library.tabsAll"), count: allCount },
|
|
4020
3120
|
{
|
|
4021
3121
|
value: "references",
|
|
4022
|
-
label: t("
|
|
3122
|
+
label: t("library.references"),
|
|
4023
3123
|
count: referenceCount,
|
|
4024
3124
|
},
|
|
4025
3125
|
];
|
|
@@ -4027,7 +3127,7 @@ function AssetScopeToggle({
|
|
|
4027
3127
|
return (
|
|
4028
3128
|
<div
|
|
4029
3129
|
role="group"
|
|
4030
|
-
aria-label={t("
|
|
3130
|
+
aria-label={t("library.assetScope")}
|
|
4031
3131
|
className="inline-flex shrink-0 gap-1 rounded-md border border-border bg-muted/20 p-1"
|
|
4032
3132
|
>
|
|
4033
3133
|
{options.map((option) => {
|
|
@@ -4065,78 +3165,141 @@ function AssetScopeToggle({
|
|
|
4065
3165
|
|
|
4066
3166
|
function AssetCardsView({ items }: { items: LaneGalleryItem[] }) {
|
|
4067
3167
|
const t = useT();
|
|
3168
|
+
const [copiedItemId, setCopiedItemId] = useState<string | null>(null);
|
|
3169
|
+
|
|
3170
|
+
async function copyItem(item: LaneGalleryItem) {
|
|
3171
|
+
if (!item.asset) return;
|
|
3172
|
+
try {
|
|
3173
|
+
await navigator.clipboard.writeText(detailAssetClipboardText(item.asset));
|
|
3174
|
+
setCopiedItemId(item.id);
|
|
3175
|
+
toast.success(t("library.selectionCopied"));
|
|
3176
|
+
window.setTimeout(() => {
|
|
3177
|
+
setCopiedItemId((current) => (current === item.id ? null : current));
|
|
3178
|
+
}, 1400);
|
|
3179
|
+
} catch {
|
|
3180
|
+
toast.info(t("library.selectionReady"));
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
|
|
4068
3184
|
if (!items.length) {
|
|
4069
3185
|
return (
|
|
4070
3186
|
<div className="flex min-h-[220px] items-center justify-center rounded-lg border border-dashed border-border bg-muted/15 p-8 text-center text-sm text-muted-foreground">
|
|
4071
|
-
{t("
|
|
3187
|
+
{t("library.noAssetsToShow")}
|
|
4072
3188
|
</div>
|
|
4073
3189
|
);
|
|
4074
3190
|
}
|
|
4075
3191
|
|
|
4076
3192
|
return (
|
|
4077
3193
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
|
4078
|
-
{items.map((item) =>
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
item.
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
3194
|
+
{items.map((item) => {
|
|
3195
|
+
const copied = copiedItemId === item.id;
|
|
3196
|
+
const secondary =
|
|
3197
|
+
item.subtitle &&
|
|
3198
|
+
item.subtitle.toLowerCase() !== item.status?.toLowerCase()
|
|
3199
|
+
? item.subtitle
|
|
3200
|
+
: null;
|
|
3201
|
+
return (
|
|
3202
|
+
<article
|
|
3203
|
+
key={item.id}
|
|
3204
|
+
className={[
|
|
3205
|
+
"group overflow-hidden rounded-lg border border-border/80 bg-background transition hover:border-foreground/25",
|
|
3206
|
+
item.selected ? "border-primary ring-2 ring-primary/25" : "",
|
|
3207
|
+
item.deleting ? "opacity-60" : "",
|
|
3208
|
+
].join(" ")}
|
|
3209
|
+
aria-busy={item.busy}
|
|
3210
|
+
>
|
|
3211
|
+
<div className="relative aspect-[4/3] bg-muted/30">
|
|
3212
|
+
{item.href ? (
|
|
3213
|
+
<Link to={item.href} className="block h-full w-full">
|
|
3214
|
+
{item.thumbnail}
|
|
3215
|
+
</Link>
|
|
3216
|
+
) : (
|
|
3217
|
+
item.thumbnail
|
|
3218
|
+
)}
|
|
3219
|
+
<div className="absolute left-2 top-2 z-10">
|
|
3220
|
+
{item.onToggle ? (
|
|
3221
|
+
<Checkbox
|
|
3222
|
+
checked={item.selected}
|
|
3223
|
+
onCheckedChange={(checked) =>
|
|
3224
|
+
item.onToggle?.(checked === true)
|
|
3225
|
+
}
|
|
3226
|
+
aria-label={t("library.selectAsset", {
|
|
3227
|
+
title: item.title,
|
|
3228
|
+
})}
|
|
3229
|
+
className="border-background bg-background/90 shadow-sm"
|
|
3230
|
+
/>
|
|
3231
|
+
) : null}
|
|
4116
3232
|
</div>
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
3233
|
+
<div className="absolute right-2 top-2 z-10 flex items-center gap-1.5 opacity-100 transition md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
|
|
3234
|
+
{item.asset ? (
|
|
3235
|
+
<TooltipProvider delayDuration={200}>
|
|
3236
|
+
<Tooltip>
|
|
3237
|
+
<TooltipTrigger asChild>
|
|
3238
|
+
<Button
|
|
3239
|
+
type="button"
|
|
3240
|
+
size="icon"
|
|
3241
|
+
variant="secondary"
|
|
3242
|
+
className="size-8 border border-border/80 bg-background/90 shadow-sm backdrop-blur hover:bg-background"
|
|
3243
|
+
onClick={(event) => {
|
|
3244
|
+
event.preventDefault();
|
|
3245
|
+
event.stopPropagation();
|
|
3246
|
+
void copyItem(item);
|
|
3247
|
+
}}
|
|
3248
|
+
aria-label={t("library.copyAsset", {
|
|
3249
|
+
title: item.title,
|
|
3250
|
+
})}
|
|
3251
|
+
>
|
|
3252
|
+
{copied ? (
|
|
3253
|
+
<IconCheck className="h-4 w-4" />
|
|
3254
|
+
) : (
|
|
3255
|
+
<IconClipboard className="h-4 w-4" />
|
|
3256
|
+
)}
|
|
3257
|
+
</Button>
|
|
3258
|
+
</TooltipTrigger>
|
|
3259
|
+
<TooltipContent>
|
|
3260
|
+
{copied
|
|
3261
|
+
? t("library.copied")
|
|
3262
|
+
: t("library.copyToClipboard")}
|
|
3263
|
+
</TooltipContent>
|
|
3264
|
+
</Tooltip>
|
|
3265
|
+
</TooltipProvider>
|
|
3266
|
+
) : null}
|
|
3267
|
+
{item.menu}
|
|
3268
|
+
</div>
|
|
3269
|
+
{item.busy && item.showBusyOverlay !== false ? (
|
|
3270
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background/20">
|
|
3271
|
+
<Spinner className="h-5 w-5" />
|
|
4125
3272
|
</div>
|
|
4126
3273
|
) : null}
|
|
4127
3274
|
</div>
|
|
4128
|
-
<div className="
|
|
4129
|
-
|
|
4130
|
-
<
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
3275
|
+
<div className="p-3">
|
|
3276
|
+
<div className="min-w-0">
|
|
3277
|
+
<div className="truncate text-sm font-medium">{item.title}</div>
|
|
3278
|
+
<div className="mt-2 flex min-h-5 min-w-0 flex-wrap items-center gap-1.5">
|
|
3279
|
+
{item.status ? (
|
|
3280
|
+
<Badge
|
|
3281
|
+
variant="secondary"
|
|
3282
|
+
className="h-5 rounded-full px-2"
|
|
3283
|
+
>
|
|
3284
|
+
{item.status}
|
|
3285
|
+
</Badge>
|
|
3286
|
+
) : null}
|
|
3287
|
+
{item.metadata ? (
|
|
3288
|
+
<Badge variant="outline" className="h-5 rounded-full px-2">
|
|
3289
|
+
{item.metadata}
|
|
3290
|
+
</Badge>
|
|
3291
|
+
) : null}
|
|
3292
|
+
{secondary ? (
|
|
3293
|
+
<span className="min-w-0 truncate text-xs text-muted-foreground">
|
|
3294
|
+
{secondary}
|
|
3295
|
+
</span>
|
|
3296
|
+
) : null}
|
|
3297
|
+
</div>
|
|
3298
|
+
</div>
|
|
4135
3299
|
</div>
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
))}
|
|
3300
|
+
</article>
|
|
3301
|
+
);
|
|
3302
|
+
})}
|
|
4140
3303
|
</div>
|
|
4141
3304
|
);
|
|
4142
3305
|
}
|
|
@@ -4233,7 +3396,7 @@ function SwimLane({
|
|
|
4233
3396
|
>
|
|
4234
3397
|
{item.thumbnail}
|
|
4235
3398
|
<span className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background/90 to-transparent" />
|
|
4236
|
-
{item.busy ? (
|
|
3399
|
+
{item.busy && item.showBusyOverlay !== false ? (
|
|
4237
3400
|
<span className="absolute right-1.5 top-1.5 rounded-full bg-background/90 p-1 shadow-sm">
|
|
4238
3401
|
<Spinner className="h-3 w-3" />
|
|
4239
3402
|
</span>
|
|
@@ -4401,9 +3564,7 @@ function PendingUploadPreview({
|
|
|
4401
3564
|
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
4402
3565
|
<Spinner className={fit === "contain" ? "h-6 w-6" : "h-4 w-4"} />
|
|
4403
3566
|
<span className="text-xs font-medium">
|
|
4404
|
-
{isChecking
|
|
4405
|
-
? t("brandKitDetail.checking")
|
|
4406
|
-
: t("brandKitDetail.uploading")}
|
|
3567
|
+
{isChecking ? t("library.checking") : t("library.uploading")}
|
|
4407
3568
|
</span>
|
|
4408
3569
|
</div>
|
|
4409
3570
|
</div>
|
|
@@ -4436,7 +3597,7 @@ function AssetActionsMenu({
|
|
|
4436
3597
|
variant="secondary"
|
|
4437
3598
|
size="icon"
|
|
4438
3599
|
className="h-8 w-8 shadow-sm"
|
|
4439
|
-
aria-label={t("
|
|
3600
|
+
aria-label={t("library.assetActions")}
|
|
4440
3601
|
disabled={busy}
|
|
4441
3602
|
>
|
|
4442
3603
|
<IconDotsVertical className="h-4 w-4" />
|
|
@@ -4445,7 +3606,8 @@ function AssetActionsMenu({
|
|
|
4445
3606
|
<DropdownMenuContent align="end">
|
|
4446
3607
|
<DropdownMenuItem asChild>
|
|
4447
3608
|
<Link to={`/asset/${asset.id}`}>
|
|
4448
|
-
|
|
3609
|
+
<IconArrowUpRight className="mr-2 h-4 w-4 shrink-0" />
|
|
3610
|
+
{t("library.viewDetails")}
|
|
4449
3611
|
</Link>
|
|
4450
3612
|
</DropdownMenuItem>
|
|
4451
3613
|
{onMoveToReferences ? (
|
|
@@ -4456,7 +3618,7 @@ function AssetActionsMenu({
|
|
|
4456
3618
|
}}
|
|
4457
3619
|
>
|
|
4458
3620
|
<IconPhotoPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
4459
|
-
{t("
|
|
3621
|
+
{t("library.addToReferences")}
|
|
4460
3622
|
</DropdownMenuItem>
|
|
4461
3623
|
) : null}
|
|
4462
3624
|
{onRemoveFromReferences ? (
|
|
@@ -4467,13 +3629,13 @@ function AssetActionsMenu({
|
|
|
4467
3629
|
}}
|
|
4468
3630
|
>
|
|
4469
3631
|
<IconX className="mr-2 h-4 w-4 shrink-0" />
|
|
4470
|
-
{t("
|
|
3632
|
+
{t("library.removeFromReferences")}
|
|
4471
3633
|
</DropdownMenuItem>
|
|
4472
3634
|
) : null}
|
|
4473
3635
|
<DropdownMenuSub>
|
|
4474
3636
|
<DropdownMenuSubTrigger>
|
|
4475
3637
|
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
4476
|
-
{t("
|
|
3638
|
+
{t("library.moveTo")}
|
|
4477
3639
|
</DropdownMenuSubTrigger>
|
|
4478
3640
|
<DropdownMenuSubContent>
|
|
4479
3641
|
<DropdownMenuItem
|
|
@@ -4484,7 +3646,7 @@ function AssetActionsMenu({
|
|
|
4484
3646
|
})
|
|
4485
3647
|
}
|
|
4486
3648
|
>
|
|
4487
|
-
{t("
|
|
3649
|
+
{t("library.unfiled")}
|
|
4488
3650
|
</DropdownMenuItem>
|
|
4489
3651
|
{folders.map((folder) => (
|
|
4490
3652
|
<DropdownMenuItem
|
|
@@ -4507,661 +3669,887 @@ function AssetActionsMenu({
|
|
|
4507
3669
|
onSelect={onDelete}
|
|
4508
3670
|
>
|
|
4509
3671
|
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
4510
|
-
{t("
|
|
3672
|
+
{t("assetDetail.delete")}
|
|
4511
3673
|
</DropdownMenuItem>
|
|
4512
3674
|
</DropdownMenuContent>
|
|
4513
3675
|
</DropdownMenu>
|
|
4514
3676
|
);
|
|
4515
3677
|
}
|
|
4516
3678
|
|
|
4517
|
-
function
|
|
4518
|
-
|
|
4519
|
-
|
|
3679
|
+
function PendingUploadLaneTile({ upload }: { upload: PendingUpload }) {
|
|
3680
|
+
const t = useT();
|
|
3681
|
+
const isChecking = upload.status === "checking";
|
|
3682
|
+
return (
|
|
3683
|
+
<div className="w-[144px] shrink-0 overflow-hidden rounded-md border border-dashed border-border bg-background sm:w-[156px]">
|
|
3684
|
+
<div className="flex aspect-[4/3] items-center justify-center bg-muted/30">
|
|
3685
|
+
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
3686
|
+
<Spinner className="h-5 w-5" />
|
|
3687
|
+
<span className="text-xs font-medium">
|
|
3688
|
+
{isChecking ? t("library.checking") : t("library.uploading")}
|
|
3689
|
+
</span>
|
|
3690
|
+
</div>
|
|
3691
|
+
</div>
|
|
3692
|
+
<div className="p-2.5">
|
|
3693
|
+
<div className="flex items-center gap-2 truncate text-xs font-medium">
|
|
3694
|
+
{upload.mediaType === "video" ? (
|
|
3695
|
+
<IconVideo className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
3696
|
+
) : (
|
|
3697
|
+
<IconPhoto className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
3698
|
+
)}
|
|
3699
|
+
<span className="truncate">{upload.name}</span>
|
|
3700
|
+
</div>
|
|
3701
|
+
</div>
|
|
3702
|
+
</div>
|
|
3703
|
+
);
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
function AssetLaneTile({
|
|
3707
|
+
asset,
|
|
3708
|
+
folders,
|
|
3709
|
+
selected,
|
|
3710
|
+
deleting,
|
|
3711
|
+
saving,
|
|
3712
|
+
promoting,
|
|
3713
|
+
onToggle,
|
|
3714
|
+
onDelete,
|
|
3715
|
+
updateAsset,
|
|
3716
|
+
onSave,
|
|
3717
|
+
onMoveToReferences,
|
|
4520
3718
|
}: {
|
|
4521
|
-
|
|
4522
|
-
|
|
3719
|
+
asset: any;
|
|
3720
|
+
folders: any[];
|
|
3721
|
+
selected: boolean;
|
|
3722
|
+
deleting?: boolean;
|
|
3723
|
+
saving?: boolean;
|
|
3724
|
+
promoting?: boolean;
|
|
3725
|
+
onToggle: (checked: boolean) => void;
|
|
3726
|
+
onDelete: () => void;
|
|
3727
|
+
updateAsset: any;
|
|
3728
|
+
onSave?: () => void;
|
|
3729
|
+
onMoveToReferences?: () => void;
|
|
4523
3730
|
}) {
|
|
4524
3731
|
const t = useT();
|
|
4525
|
-
const
|
|
4526
|
-
const
|
|
4527
|
-
const
|
|
4528
|
-
const
|
|
4529
|
-
const
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4532
|
-
useEffect(() => {
|
|
4533
|
-
setSourceIndex(0);
|
|
4534
|
-
setPreviewUnavailable(false);
|
|
4535
|
-
}, [previewSourcesKey]);
|
|
3732
|
+
const displayTitle = assetDisplayTitle(asset);
|
|
3733
|
+
const sourceText = assetLineageSourceText(asset);
|
|
3734
|
+
const canMoveToReferences = Boolean(onMoveToReferences);
|
|
3735
|
+
const hasPrimaryActions = Boolean(onSave || canMoveToReferences);
|
|
3736
|
+
const categoryLabel = assetCategoryLabel(asset);
|
|
3737
|
+
const busy = deleting || saving || promoting;
|
|
4536
3738
|
|
|
4537
3739
|
return (
|
|
4538
|
-
<div
|
|
4539
|
-
{
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
3740
|
+
<div
|
|
3741
|
+
className={[
|
|
3742
|
+
"group relative w-[144px] shrink-0 overflow-hidden rounded-md border bg-background transition sm:w-[156px]",
|
|
3743
|
+
selected
|
|
3744
|
+
? "border-primary ring-2 ring-primary/25"
|
|
3745
|
+
: "border-border/80 hover:border-foreground/20",
|
|
3746
|
+
deleting ? "opacity-60" : "",
|
|
3747
|
+
].join(" ")}
|
|
3748
|
+
aria-busy={busy}
|
|
3749
|
+
>
|
|
3750
|
+
<div className="absolute left-2 top-2 z-10">
|
|
3751
|
+
<Checkbox
|
|
3752
|
+
checked={selected}
|
|
3753
|
+
onCheckedChange={(checked) => onToggle(checked === true)}
|
|
3754
|
+
aria-label={t("library.selectAsset", { title: displayTitle })}
|
|
4543
3755
|
className={[
|
|
4544
|
-
"
|
|
4545
|
-
|
|
3756
|
+
"border-background bg-background/90 shadow-sm opacity-100 transition sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100",
|
|
3757
|
+
selected ? "sm:opacity-100" : "",
|
|
4546
3758
|
].join(" ")}
|
|
4547
|
-
onError={() => {
|
|
4548
|
-
const nextIndex = sourceIndex + 1;
|
|
4549
|
-
if (nextIndex < previewSources.length) {
|
|
4550
|
-
setSourceIndex(nextIndex);
|
|
4551
|
-
} else {
|
|
4552
|
-
setPreviewUnavailable(true);
|
|
4553
|
-
}
|
|
4554
|
-
}}
|
|
4555
3759
|
/>
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
3760
|
+
</div>
|
|
3761
|
+
<div className="absolute right-2 top-2 z-10">
|
|
3762
|
+
<DropdownMenu>
|
|
3763
|
+
<DropdownMenuTrigger asChild>
|
|
3764
|
+
<Button
|
|
3765
|
+
type="button"
|
|
3766
|
+
variant="secondary"
|
|
3767
|
+
size="icon"
|
|
3768
|
+
className="h-8 w-8 shadow-sm opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100 data-[state=open]:opacity-100"
|
|
3769
|
+
aria-label={t("library.assetActions")}
|
|
3770
|
+
disabled={busy}
|
|
3771
|
+
>
|
|
3772
|
+
<IconDotsVertical className="h-4 w-4" />
|
|
3773
|
+
</Button>
|
|
3774
|
+
</DropdownMenuTrigger>
|
|
3775
|
+
<DropdownMenuContent align="end">
|
|
3776
|
+
<DropdownMenuItem asChild>
|
|
3777
|
+
<Link to={`/asset/${asset.id}`}>
|
|
3778
|
+
<IconArrowUpRight className="mr-2 h-4 w-4 shrink-0" />
|
|
3779
|
+
{t("library.viewDetails")}
|
|
3780
|
+
</Link>
|
|
3781
|
+
</DropdownMenuItem>
|
|
3782
|
+
{canMoveToReferences ? (
|
|
3783
|
+
<DropdownMenuItem
|
|
3784
|
+
onSelect={(event) => {
|
|
3785
|
+
event.preventDefault();
|
|
3786
|
+
onMoveToReferences?.();
|
|
3787
|
+
}}
|
|
3788
|
+
>
|
|
3789
|
+
<IconPhotoPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
3790
|
+
{t("library.addToReferences")}
|
|
3791
|
+
</DropdownMenuItem>
|
|
3792
|
+
) : null}
|
|
3793
|
+
<DropdownMenuSub>
|
|
3794
|
+
<DropdownMenuSubTrigger>
|
|
3795
|
+
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
3796
|
+
{t("library.moveTo")}
|
|
3797
|
+
</DropdownMenuSubTrigger>
|
|
3798
|
+
<DropdownMenuSubContent>
|
|
3799
|
+
<DropdownMenuItem
|
|
3800
|
+
onSelect={() =>
|
|
3801
|
+
updateAsset.mutate({
|
|
3802
|
+
id: asset.id,
|
|
3803
|
+
folderId: null,
|
|
3804
|
+
})
|
|
3805
|
+
}
|
|
3806
|
+
>
|
|
3807
|
+
{t("library.unfiled")}
|
|
3808
|
+
</DropdownMenuItem>
|
|
3809
|
+
{folders.map((folder) => (
|
|
3810
|
+
<DropdownMenuItem
|
|
3811
|
+
key={folder.id}
|
|
3812
|
+
onSelect={() =>
|
|
3813
|
+
updateAsset.mutate({
|
|
3814
|
+
id: asset.id,
|
|
3815
|
+
folderId: folder.id,
|
|
3816
|
+
})
|
|
3817
|
+
}
|
|
3818
|
+
>
|
|
3819
|
+
{folder.title}
|
|
3820
|
+
</DropdownMenuItem>
|
|
3821
|
+
))}
|
|
3822
|
+
</DropdownMenuSubContent>
|
|
3823
|
+
</DropdownMenuSub>
|
|
3824
|
+
<DropdownMenuSeparator />
|
|
3825
|
+
<DropdownMenuItem
|
|
3826
|
+
className="text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
3827
|
+
onSelect={onDelete}
|
|
3828
|
+
>
|
|
3829
|
+
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
3830
|
+
{t("assetDetail.delete")}
|
|
3831
|
+
</DropdownMenuItem>
|
|
3832
|
+
</DropdownMenuContent>
|
|
3833
|
+
</DropdownMenu>
|
|
3834
|
+
</div>
|
|
3835
|
+
<Link to={`/asset/${asset.id}`} className="block outline-none">
|
|
3836
|
+
<div className="relative aspect-[4/3] bg-muted">
|
|
3837
|
+
<AssetPreview asset={asset} />
|
|
3838
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-background via-background/90 to-transparent px-2 pb-2 pt-8">
|
|
3839
|
+
<div className="flex items-center gap-1.5 truncate text-xs font-medium">
|
|
3840
|
+
{asset.mediaType === "video" ? (
|
|
3841
|
+
<IconVideo className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
3842
|
+
) : (
|
|
3843
|
+
<IconPhoto className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
3844
|
+
)}
|
|
3845
|
+
<span className="truncate">{displayTitle}</span>
|
|
3846
|
+
</div>
|
|
3847
|
+
<div className="mt-1 flex min-w-0 items-center gap-1 overflow-hidden text-[10px] font-medium text-muted-foreground">
|
|
3848
|
+
{sourceText ? (
|
|
3849
|
+
<span className="truncate">{sourceText}</span>
|
|
3850
|
+
) : (
|
|
3851
|
+
<>
|
|
3852
|
+
<span className="truncate">{asset.status}</span>
|
|
3853
|
+
{categoryLabel ? (
|
|
3854
|
+
<>
|
|
3855
|
+
<span className="shrink-0 text-muted-foreground/60">
|
|
3856
|
+
/
|
|
3857
|
+
</span>
|
|
3858
|
+
<span className="truncate">{categoryLabel}</span>
|
|
3859
|
+
</>
|
|
3860
|
+
) : null}
|
|
3861
|
+
</>
|
|
3862
|
+
)}
|
|
3863
|
+
</div>
|
|
3864
|
+
</div>
|
|
4559
3865
|
</div>
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
3866
|
+
</Link>
|
|
3867
|
+
{hasPrimaryActions ? (
|
|
3868
|
+
<div className="space-y-2 border-t border-border/70 p-2">
|
|
3869
|
+
<div
|
|
3870
|
+
className={
|
|
3871
|
+
onSave && canMoveToReferences
|
|
3872
|
+
? "grid grid-cols-1 gap-2"
|
|
3873
|
+
: "grid grid-cols-2 gap-2"
|
|
3874
|
+
}
|
|
3875
|
+
>
|
|
3876
|
+
{onSave ? (
|
|
3877
|
+
<Button
|
|
3878
|
+
size="sm"
|
|
3879
|
+
className="h-8 px-2 text-xs"
|
|
3880
|
+
onClick={onSave}
|
|
3881
|
+
disabled={busy}
|
|
3882
|
+
>
|
|
3883
|
+
{saving ? (
|
|
3884
|
+
<Spinner className="h-3.5 w-3.5" />
|
|
3885
|
+
) : (
|
|
3886
|
+
t("library.save")
|
|
3887
|
+
)}
|
|
3888
|
+
</Button>
|
|
3889
|
+
) : null}
|
|
3890
|
+
{canMoveToReferences ? (
|
|
3891
|
+
<Button
|
|
3892
|
+
variant="outline"
|
|
3893
|
+
size="sm"
|
|
3894
|
+
className={
|
|
3895
|
+
onSave ? "h-8 px-2 text-xs" : "col-span-2 h-8 px-2 text-xs"
|
|
3896
|
+
}
|
|
3897
|
+
onClick={onMoveToReferences}
|
|
3898
|
+
disabled={busy}
|
|
3899
|
+
title={t("library.addToReferences")}
|
|
3900
|
+
>
|
|
3901
|
+
{promoting ? (
|
|
3902
|
+
<Spinner className="h-3.5 w-3.5" />
|
|
3903
|
+
) : (
|
|
3904
|
+
t("library.addToReferences")
|
|
3905
|
+
)}
|
|
3906
|
+
</Button>
|
|
3907
|
+
) : null}
|
|
3908
|
+
</div>
|
|
4563
3909
|
</div>
|
|
4564
|
-
) :
|
|
4565
|
-
<IconPhoto className="h-8 w-8 animate-pulse text-muted-foreground" />
|
|
4566
|
-
)}
|
|
3910
|
+
) : null}
|
|
4567
3911
|
</div>
|
|
4568
3912
|
);
|
|
4569
3913
|
}
|
|
4570
3914
|
|
|
4571
|
-
function
|
|
4572
|
-
|
|
3915
|
+
export function LiveCandidatesStage({
|
|
3916
|
+
slots,
|
|
3917
|
+
draftAssets,
|
|
4573
3918
|
libraryId,
|
|
4574
|
-
|
|
3919
|
+
folders,
|
|
3920
|
+
foldersByLibraryId = {},
|
|
3921
|
+
allowCreateFolder = true,
|
|
3922
|
+
savingSlotId,
|
|
3923
|
+
promotingReferenceKeys,
|
|
3924
|
+
onSave,
|
|
3925
|
+
onSaveDraft,
|
|
4575
3926
|
onMoveToReferences,
|
|
3927
|
+
onMoveDraftToReferences,
|
|
3928
|
+
onUse,
|
|
3929
|
+
onUseDraft,
|
|
4576
3930
|
}: {
|
|
4577
|
-
|
|
3931
|
+
slots: VariantSlot[];
|
|
3932
|
+
draftAssets: any[];
|
|
4578
3933
|
libraryId: string;
|
|
4579
|
-
|
|
4580
|
-
|
|
3934
|
+
folders: any[];
|
|
3935
|
+
foldersByLibraryId?: Record<string, any[]>;
|
|
3936
|
+
allowCreateFolder?: boolean;
|
|
3937
|
+
savingSlotId: string | null;
|
|
3938
|
+
promotingReferenceKeys: Set<string>;
|
|
3939
|
+
onSave: (slot: VariantSlot, folderId: string | null) => void;
|
|
3940
|
+
onSaveDraft: (asset: any, folderId: string | null) => void;
|
|
3941
|
+
onMoveToReferences: (slot: VariantSlot) => void;
|
|
3942
|
+
onMoveDraftToReferences: (asset: any) => void;
|
|
3943
|
+
onUse?: (slot: VariantSlot) => void;
|
|
3944
|
+
onUseDraft?: (asset: any) => void;
|
|
4581
3945
|
}) {
|
|
4582
3946
|
const t = useT();
|
|
4583
3947
|
const dismissSlot = useActionMutation("dismiss-variant-slots");
|
|
3948
|
+
const deleteAsset = useActionMutation("delete-asset");
|
|
4584
3949
|
const queryClient = useQueryClient();
|
|
4585
|
-
const [
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
3950
|
+
const [dismissTarget, setDismissTarget] = useState<{
|
|
3951
|
+
kind: "slot" | "asset";
|
|
3952
|
+
title: string;
|
|
3953
|
+
slot?: VariantSlot;
|
|
3954
|
+
asset?: any;
|
|
3955
|
+
} | null>(null);
|
|
3956
|
+
const dismissing = dismissSlot.isPending || deleteAsset.isPending;
|
|
3957
|
+
const totalCount = slots.length + draftAssets.length;
|
|
3958
|
+
const [activeItemId, setActiveItemId] = useState<string | null>(null);
|
|
4590
3959
|
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
3960
|
+
async function handleDismissCandidate() {
|
|
3961
|
+
if (!dismissTarget || dismissing) return;
|
|
3962
|
+
try {
|
|
3963
|
+
if (dismissTarget.kind === "slot" && dismissTarget.slot) {
|
|
3964
|
+
await dismissSlot.mutateAsync({ slotId: dismissTarget.slot.slotId });
|
|
3965
|
+
removeVariantSlotFromCache(queryClient, dismissTarget.slot);
|
|
3966
|
+
removeAssetsFromLibraryCache(queryClient, libraryId, [
|
|
3967
|
+
dismissTarget.slot.assetId,
|
|
3968
|
+
]);
|
|
3969
|
+
void queryClient.invalidateQueries({
|
|
3970
|
+
queryKey: ["app-state"],
|
|
3971
|
+
refetchType: "active",
|
|
3972
|
+
});
|
|
3973
|
+
} else if (dismissTarget.kind === "asset" && dismissTarget.asset?.id) {
|
|
3974
|
+
await deleteAsset.mutateAsync({ id: dismissTarget.asset.id });
|
|
3975
|
+
removeAssetsFromLibraryCache(queryClient, libraryId, [
|
|
3976
|
+
dismissTarget.asset.id,
|
|
3977
|
+
]);
|
|
3978
|
+
}
|
|
3979
|
+
setDismissTarget(null);
|
|
3980
|
+
void queryClient.invalidateQueries({
|
|
3981
|
+
queryKey: ["action", "get-library", { id: libraryId }],
|
|
3982
|
+
refetchType: "active",
|
|
3983
|
+
});
|
|
3984
|
+
void queryClient.invalidateQueries({
|
|
3985
|
+
queryKey: ["action", "get-library"],
|
|
3986
|
+
refetchType: "active",
|
|
3987
|
+
});
|
|
3988
|
+
void queryClient.invalidateQueries({
|
|
3989
|
+
queryKey: ["action", "list-assets"],
|
|
3990
|
+
refetchType: "active",
|
|
3991
|
+
});
|
|
3992
|
+
toast.success(t("library.dismissedCandidate"));
|
|
3993
|
+
} catch (error) {
|
|
3994
|
+
toast.error(
|
|
3995
|
+
error instanceof Error
|
|
3996
|
+
? error.message
|
|
3997
|
+
: t("library.couldNotDismissCandidate"),
|
|
3998
|
+
);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
function candidateActions({
|
|
4003
|
+
canUseCandidate,
|
|
4004
|
+
saving,
|
|
4005
|
+
promoting,
|
|
4006
|
+
candidateLibraryId,
|
|
4007
|
+
onSaveCandidate,
|
|
4008
|
+
onAddToReferences,
|
|
4009
|
+
onUseCandidate,
|
|
4010
|
+
onDismiss,
|
|
4011
|
+
}: {
|
|
4012
|
+
canUseCandidate: boolean;
|
|
4013
|
+
saving?: boolean;
|
|
4014
|
+
promoting?: boolean;
|
|
4015
|
+
candidateLibraryId?: string | null;
|
|
4016
|
+
onSaveCandidate?: (folderId: string | null) => void;
|
|
4017
|
+
onAddToReferences?: () => void;
|
|
4018
|
+
onUseCandidate?: () => void;
|
|
4019
|
+
onDismiss: () => void;
|
|
4020
|
+
}) {
|
|
4021
|
+
const busy = saving || promoting || dismissing;
|
|
4022
|
+
const actionLibraryId = candidateLibraryId || libraryId;
|
|
4023
|
+
const candidateFolders =
|
|
4024
|
+
foldersByLibraryId[actionLibraryId] ??
|
|
4025
|
+
(actionLibraryId === libraryId ? folders : []);
|
|
4026
|
+
if (!canUseCandidate) {
|
|
4027
|
+
return (
|
|
4028
|
+
<Button
|
|
4029
|
+
variant="outline"
|
|
4030
|
+
size="sm"
|
|
4031
|
+
className="h-8 w-full justify-center px-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
4032
|
+
onClick={onDismiss}
|
|
4033
|
+
disabled={busy}
|
|
4034
|
+
>
|
|
4035
|
+
{t("library.dismiss")}
|
|
4036
|
+
</Button>
|
|
4037
|
+
);
|
|
4038
|
+
}
|
|
4039
|
+
return (
|
|
4040
|
+
<div className="grid min-w-0 gap-2">
|
|
4041
|
+
{onUseCandidate ? (
|
|
4595
4042
|
<Button
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
aria-label={t("brandKitDetail.candidateActions")}
|
|
4601
|
-
disabled={busy || dismissSlot.isPending}
|
|
4043
|
+
size="sm"
|
|
4044
|
+
className="h-8 min-w-0 justify-center px-2 text-xs"
|
|
4045
|
+
onClick={onUseCandidate}
|
|
4046
|
+
disabled={busy}
|
|
4602
4047
|
>
|
|
4603
|
-
|
|
4048
|
+
{t("library.useCandidate")}
|
|
4604
4049
|
</Button>
|
|
4605
|
-
|
|
4606
|
-
<
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
event.preventDefault();
|
|
4622
|
-
setConfirmOpen(true);
|
|
4623
|
-
}}
|
|
4050
|
+
) : null}
|
|
4051
|
+
<div className="grid min-w-0 grid-cols-1 gap-2 min-[420px]:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
|
|
4052
|
+
<CandidateSaveMenu
|
|
4053
|
+
libraryId={actionLibraryId}
|
|
4054
|
+
folders={candidateFolders}
|
|
4055
|
+
allowCreateFolder={allowCreateFolder}
|
|
4056
|
+
saving={saving}
|
|
4057
|
+
disabled={busy}
|
|
4058
|
+
onSave={(folderId) => onSaveCandidate?.(folderId)}
|
|
4059
|
+
/>
|
|
4060
|
+
<Button
|
|
4061
|
+
variant="outline"
|
|
4062
|
+
size="sm"
|
|
4063
|
+
className="h-8 min-w-0 px-2 text-xs"
|
|
4064
|
+
onClick={onAddToReferences}
|
|
4065
|
+
disabled={busy}
|
|
4624
4066
|
>
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4067
|
+
{promoting ? (
|
|
4068
|
+
<Spinner className="h-3.5 w-3.5" />
|
|
4069
|
+
) : (
|
|
4070
|
+
t("library.addToReferences")
|
|
4071
|
+
)}
|
|
4072
|
+
</Button>
|
|
4073
|
+
</div>
|
|
4074
|
+
<Button
|
|
4075
|
+
variant="ghost"
|
|
4076
|
+
size="sm"
|
|
4077
|
+
className="h-8 min-w-0 justify-center px-2 text-xs text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
|
4078
|
+
onClick={onDismiss}
|
|
4079
|
+
disabled={busy}
|
|
4080
|
+
>
|
|
4081
|
+
{t("library.dismiss")}
|
|
4082
|
+
</Button>
|
|
4083
|
+
</div>
|
|
4084
|
+
);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
function slotItem(slot: VariantSlot): LaneGalleryItem {
|
|
4088
|
+
const isFailed = slot.status === "failed";
|
|
4089
|
+
const canUseCandidate = slot.status === "ready" && Boolean(slot.assetId);
|
|
4090
|
+
const promotingKey = referencePromotionKey(
|
|
4091
|
+
slot.assetId ? { id: slot.assetId } : null,
|
|
4092
|
+
slot,
|
|
4093
|
+
);
|
|
4094
|
+
const saving = savingSlotId === slot.slotId;
|
|
4095
|
+
const promoting =
|
|
4096
|
+
Boolean(promotingKey) && promotingReferenceKeys.has(promotingKey);
|
|
4097
|
+
const busy = saving || promoting || dismissing;
|
|
4098
|
+
const title = isFailed
|
|
4099
|
+
? t("library.failedCandidate")
|
|
4100
|
+
: slot.status === "ready"
|
|
4101
|
+
? t("library.readyCandidate")
|
|
4102
|
+
: t("library.generatingCandidate");
|
|
4103
|
+
return {
|
|
4104
|
+
id: `slot:${slot.slotId}`,
|
|
4105
|
+
title,
|
|
4106
|
+
subtitle: slot.slotId
|
|
4107
|
+
? shortId(String(slot.slotId))
|
|
4108
|
+
: t("library.liveSlot"),
|
|
4109
|
+
metadata: t("library.live"),
|
|
4110
|
+
status: slot.status,
|
|
4111
|
+
mediaType: "image",
|
|
4112
|
+
href: slot.assetId ? `/asset/${slot.assetId}` : undefined,
|
|
4113
|
+
busy,
|
|
4114
|
+
preview: <VariantPreview slot={slot} fit="contain" />, // i18n-ignore structural preview slot name
|
|
4115
|
+
thumbnail: <VariantPreview slot={slot} />,
|
|
4116
|
+
primaryActions: candidateActions({
|
|
4117
|
+
canUseCandidate,
|
|
4118
|
+
saving,
|
|
4119
|
+
promoting,
|
|
4120
|
+
candidateLibraryId: libraryId,
|
|
4121
|
+
onSaveCandidate: (folderId) => onSave(slot, folderId),
|
|
4122
|
+
onAddToReferences: () => onMoveToReferences(slot),
|
|
4123
|
+
onUseCandidate: onUse ? () => onUse(slot) : undefined,
|
|
4124
|
+
onDismiss: () =>
|
|
4125
|
+
setDismissTarget({
|
|
4126
|
+
kind: "slot",
|
|
4127
|
+
title,
|
|
4128
|
+
slot,
|
|
4129
|
+
}),
|
|
4130
|
+
}),
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
function draftItem(asset: any): LaneGalleryItem {
|
|
4135
|
+
const promotingKey = referencePromotionKey(asset);
|
|
4136
|
+
const saving = savingSlotId === `draft:${asset.id}`;
|
|
4137
|
+
const promoting =
|
|
4138
|
+
Boolean(promotingKey) && promotingReferenceKeys.has(promotingKey);
|
|
4139
|
+
const busy = saving || promoting || dismissing;
|
|
4140
|
+
return {
|
|
4141
|
+
id: `draft:${asset.id}`,
|
|
4142
|
+
title: assetDisplayTitle(asset),
|
|
4143
|
+
subtitle:
|
|
4144
|
+
[asset.libraryTitle, assetLineageSourceText(asset)]
|
|
4145
|
+
.filter(Boolean)
|
|
4146
|
+
.join(" / ") || assetCategoryLabel(asset),
|
|
4147
|
+
metadata:
|
|
4148
|
+
asset.mediaType === "video"
|
|
4149
|
+
? t("library.video")
|
|
4150
|
+
: asset.mimeType?.startsWith("image/")
|
|
4151
|
+
? t("library.image")
|
|
4152
|
+
: t("library.draft"),
|
|
4153
|
+
status: "draft",
|
|
4154
|
+
mediaType: asset.mediaType === "video" ? "video" : "image",
|
|
4155
|
+
href: `/asset/${asset.id}`,
|
|
4156
|
+
busy,
|
|
4157
|
+
preview: <AssetPreview asset={asset} fit="contain" />, // i18n-ignore structural preview slot name
|
|
4158
|
+
thumbnail: <AssetPreview asset={asset} />,
|
|
4159
|
+
primaryActions: candidateActions({
|
|
4160
|
+
canUseCandidate: true,
|
|
4161
|
+
saving,
|
|
4162
|
+
promoting,
|
|
4163
|
+
candidateLibraryId: asset.libraryId,
|
|
4164
|
+
onSaveCandidate: (folderId) => onSaveDraft(asset, folderId),
|
|
4165
|
+
onAddToReferences: () => onMoveDraftToReferences(asset),
|
|
4166
|
+
onUseCandidate: onUseDraft ? () => onUseDraft(asset) : undefined,
|
|
4167
|
+
onDismiss: () =>
|
|
4168
|
+
setDismissTarget({
|
|
4169
|
+
kind: "asset",
|
|
4170
|
+
title: assetDisplayTitle(asset),
|
|
4171
|
+
asset,
|
|
4172
|
+
}),
|
|
4173
|
+
}),
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
const items = [...slots.map(slotItem), ...draftAssets.map(draftItem)];
|
|
4178
|
+
const itemIds = items.map((item) => item.id).join("\n");
|
|
4179
|
+
const activeItem =
|
|
4180
|
+
items.find((item) => item.id === activeItemId) ?? items[0] ?? null;
|
|
4181
|
+
|
|
4182
|
+
useEffect(() => {
|
|
4183
|
+
if (!items.length) {
|
|
4184
|
+
setActiveItemId(null);
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
setActiveItemId((current) =>
|
|
4188
|
+
current && items.some((item) => item.id === current)
|
|
4189
|
+
? current
|
|
4190
|
+
: items[0].id,
|
|
4191
|
+
);
|
|
4192
|
+
}, [itemIds, items]);
|
|
4630
4193
|
|
|
4194
|
+
return (
|
|
4195
|
+
<>
|
|
4631
4196
|
<AlertDialog
|
|
4632
|
-
open={
|
|
4197
|
+
open={dismissTarget !== null}
|
|
4633
4198
|
onOpenChange={(open) => {
|
|
4634
|
-
if (!
|
|
4199
|
+
if (!open && !dismissing) setDismissTarget(null);
|
|
4635
4200
|
}}
|
|
4636
4201
|
>
|
|
4637
4202
|
<AlertDialogContent>
|
|
4638
4203
|
<AlertDialogHeader>
|
|
4639
4204
|
<AlertDialogTitle>
|
|
4640
|
-
{
|
|
4641
|
-
? t("brandKitDetail.dismissThisSlot")
|
|
4642
|
-
: t("brandKitDetail.deleteCandidate")}
|
|
4205
|
+
{t("library.dismissCandidateTitle")}
|
|
4643
4206
|
</AlertDialogTitle>
|
|
4644
4207
|
<AlertDialogDescription>
|
|
4645
|
-
{
|
|
4646
|
-
|
|
4647
|
-
|
|
4208
|
+
{t("library.dismissCandidateDescription", {
|
|
4209
|
+
title: dismissTarget?.title ?? t("library.thisCandidate"),
|
|
4210
|
+
})}
|
|
4648
4211
|
</AlertDialogDescription>
|
|
4649
4212
|
</AlertDialogHeader>
|
|
4650
4213
|
<AlertDialogFooter>
|
|
4651
|
-
<AlertDialogCancel disabled={
|
|
4652
|
-
{t("
|
|
4214
|
+
<AlertDialogCancel disabled={dismissing}>
|
|
4215
|
+
{t("library.cancel")}
|
|
4653
4216
|
</AlertDialogCancel>
|
|
4654
4217
|
<AlertDialogAction
|
|
4655
4218
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
4656
|
-
disabled={
|
|
4219
|
+
disabled={dismissing}
|
|
4657
4220
|
onClick={(event) => {
|
|
4658
4221
|
event.preventDefault();
|
|
4659
|
-
|
|
4660
|
-
{ slotId: slot.slotId },
|
|
4661
|
-
{
|
|
4662
|
-
onSuccess: () => {
|
|
4663
|
-
removeVariantSlotFromCache(queryClient, slot);
|
|
4664
|
-
removeAssetsFromLibraryCache(queryClient, libraryId, [
|
|
4665
|
-
slot.assetId,
|
|
4666
|
-
]);
|
|
4667
|
-
setConfirmOpen(false);
|
|
4668
|
-
void queryClient.invalidateQueries({
|
|
4669
|
-
queryKey: ["app-state", "asset-variants"],
|
|
4670
|
-
refetchType: "active",
|
|
4671
|
-
});
|
|
4672
|
-
},
|
|
4673
|
-
onError: (error) =>
|
|
4674
|
-
toast.error(
|
|
4675
|
-
error.message ||
|
|
4676
|
-
t("brandKitDetail.couldNotClearCandidate"),
|
|
4677
|
-
),
|
|
4678
|
-
},
|
|
4679
|
-
);
|
|
4222
|
+
void handleDismissCandidate();
|
|
4680
4223
|
}}
|
|
4681
4224
|
>
|
|
4682
|
-
{
|
|
4225
|
+
{dismissing ? (
|
|
4683
4226
|
<>
|
|
4684
4227
|
<Spinner className="h-4 w-4" />
|
|
4685
|
-
{
|
|
4686
|
-
? t("brandKitDetail.dismissing")
|
|
4687
|
-
: t("brandKitDetail.deleting")}
|
|
4228
|
+
{t("library.dismissing")}
|
|
4688
4229
|
</>
|
|
4689
4230
|
) : (
|
|
4690
|
-
|
|
4231
|
+
t("library.dismiss")
|
|
4691
4232
|
)}
|
|
4692
4233
|
</AlertDialogAction>
|
|
4693
4234
|
</AlertDialogFooter>
|
|
4694
4235
|
</AlertDialogContent>
|
|
4695
4236
|
</AlertDialog>
|
|
4237
|
+
|
|
4238
|
+
<section className="min-w-0 overflow-hidden rounded-lg border border-border bg-background">
|
|
4239
|
+
<div className="flex min-w-0 items-center justify-between gap-3 border-b border-border px-3 py-2.5 sm:px-4">
|
|
4240
|
+
<div className="flex min-w-0 flex-1 items-center">
|
|
4241
|
+
<h3 className="shrink-0 text-sm font-semibold">
|
|
4242
|
+
{t("library.candidates")}
|
|
4243
|
+
</h3>
|
|
4244
|
+
</div>
|
|
4245
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
4246
|
+
<LiveCandidatesActions
|
|
4247
|
+
slots={slots}
|
|
4248
|
+
draftAssets={draftAssets}
|
|
4249
|
+
libraryId={libraryId}
|
|
4250
|
+
/>
|
|
4251
|
+
</div>
|
|
4252
|
+
</div>
|
|
4253
|
+
<div className="grid min-w-0 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,22rem)]">
|
|
4254
|
+
<div className="min-w-0 bg-muted/10 p-2.5 sm:p-3">
|
|
4255
|
+
<div
|
|
4256
|
+
className={[
|
|
4257
|
+
"group relative overflow-hidden rounded-lg border border-border bg-background shadow-sm",
|
|
4258
|
+
activeItem?.busy ? "opacity-80" : "",
|
|
4259
|
+
].join(" ")}
|
|
4260
|
+
aria-busy={activeItem?.busy}
|
|
4261
|
+
>
|
|
4262
|
+
<div className="h-36 bg-muted/30 sm:h-44 lg:h-56 2xl:h-64">
|
|
4263
|
+
{activeItem?.href ? (
|
|
4264
|
+
<Link to={activeItem.href} className="block h-full w-full">
|
|
4265
|
+
{activeItem.preview}
|
|
4266
|
+
</Link>
|
|
4267
|
+
) : (
|
|
4268
|
+
activeItem?.preview
|
|
4269
|
+
)}
|
|
4270
|
+
</div>
|
|
4271
|
+
{activeItem?.busy ? (
|
|
4272
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background/20">
|
|
4273
|
+
<Spinner className="h-5 w-5" />
|
|
4274
|
+
</div>
|
|
4275
|
+
) : null}
|
|
4276
|
+
{activeItem?.href ? (
|
|
4277
|
+
<Button
|
|
4278
|
+
asChild
|
|
4279
|
+
variant="secondary"
|
|
4280
|
+
size="sm"
|
|
4281
|
+
className="absolute right-2 top-2 h-8 gap-1.5 bg-background/85 px-2.5 text-xs opacity-0 shadow-sm backdrop-blur transition group-hover:opacity-100 focus-within:opacity-100"
|
|
4282
|
+
>
|
|
4283
|
+
<Link to={activeItem.href}>
|
|
4284
|
+
<IconArrowUpRight className="h-3.5 w-3.5" />
|
|
4285
|
+
{t("library.details")}
|
|
4286
|
+
</Link>
|
|
4287
|
+
</Button>
|
|
4288
|
+
) : null}
|
|
4289
|
+
</div>
|
|
4290
|
+
<div className="mt-2.5 flex gap-2 overflow-x-auto pb-1">
|
|
4291
|
+
{items.map((item) => {
|
|
4292
|
+
const active = item.id === activeItem?.id;
|
|
4293
|
+
return (
|
|
4294
|
+
<button
|
|
4295
|
+
key={item.id}
|
|
4296
|
+
type="button"
|
|
4297
|
+
onClick={() => setActiveItemId(item.id)}
|
|
4298
|
+
className={[
|
|
4299
|
+
"group relative h-16 w-24 shrink-0 overflow-hidden rounded-md border bg-background transition",
|
|
4300
|
+
active
|
|
4301
|
+
? "border-primary ring-2 ring-primary/25"
|
|
4302
|
+
: "border-border/80 hover:border-foreground/30",
|
|
4303
|
+
].join(" ")}
|
|
4304
|
+
aria-label={t("library.showCandidate", {
|
|
4305
|
+
title: item.title,
|
|
4306
|
+
})}
|
|
4307
|
+
aria-pressed={active}
|
|
4308
|
+
>
|
|
4309
|
+
{item.thumbnail}
|
|
4310
|
+
<span className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background/90 to-transparent" />
|
|
4311
|
+
{item.busy && item.showBusyOverlay !== false ? (
|
|
4312
|
+
<span className="absolute right-1.5 top-1.5 rounded-full bg-background/90 p-1 shadow-sm">
|
|
4313
|
+
<Spinner className="h-3 w-3" />
|
|
4314
|
+
</span>
|
|
4315
|
+
) : null}
|
|
4316
|
+
</button>
|
|
4317
|
+
);
|
|
4318
|
+
})}
|
|
4319
|
+
</div>
|
|
4320
|
+
</div>
|
|
4321
|
+
<aside className="flex min-w-0 flex-col justify-between gap-3 border-t border-border bg-background p-3 lg:border-l lg:border-t-0 lg:p-4">
|
|
4322
|
+
<div className="min-w-0 space-y-3">
|
|
4323
|
+
<div className="min-w-0">
|
|
4324
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
4325
|
+
{activeItem?.status ? (
|
|
4326
|
+
<CandidateStatusPill status={activeItem.status} />
|
|
4327
|
+
) : null}
|
|
4328
|
+
{activeItem?.metadata ? (
|
|
4329
|
+
<Badge
|
|
4330
|
+
variant="outline"
|
|
4331
|
+
className="h-6 max-w-full rounded-full px-2 text-[11px]"
|
|
4332
|
+
>
|
|
4333
|
+
{activeItem.metadata}
|
|
4334
|
+
</Badge>
|
|
4335
|
+
) : null}
|
|
4336
|
+
</div>
|
|
4337
|
+
<div className="mt-2 truncate text-sm font-semibold">
|
|
4338
|
+
{activeItem?.title}
|
|
4339
|
+
</div>
|
|
4340
|
+
{activeItem?.subtitle ? (
|
|
4341
|
+
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
4342
|
+
{activeItem.subtitle}
|
|
4343
|
+
</div>
|
|
4344
|
+
) : null}
|
|
4345
|
+
</div>
|
|
4346
|
+
{activeItem?.primaryActions ? (
|
|
4347
|
+
<div>{activeItem.primaryActions}</div>
|
|
4348
|
+
) : null}
|
|
4349
|
+
</div>
|
|
4350
|
+
{activeItem?.href ? (
|
|
4351
|
+
<Button asChild variant="ghost" size="sm" className="gap-1.5">
|
|
4352
|
+
<Link to={activeItem.href}>
|
|
4353
|
+
<IconArrowUpRight className="h-3.5 w-3.5" />
|
|
4354
|
+
{t("library.openDetails")}
|
|
4355
|
+
</Link>
|
|
4356
|
+
</Button>
|
|
4357
|
+
) : null}
|
|
4358
|
+
</aside>
|
|
4359
|
+
</div>
|
|
4360
|
+
</section>
|
|
4696
4361
|
</>
|
|
4697
4362
|
);
|
|
4698
4363
|
}
|
|
4699
4364
|
|
|
4700
|
-
function
|
|
4365
|
+
function CandidateStatusPill({ status }: { status: string }) {
|
|
4701
4366
|
const t = useT();
|
|
4702
|
-
const
|
|
4367
|
+
const normalized = status.toLowerCase();
|
|
4368
|
+
const label =
|
|
4369
|
+
normalized === "pending"
|
|
4370
|
+
? t("library.generating")
|
|
4371
|
+
: normalized === "ready"
|
|
4372
|
+
? t("library.ready")
|
|
4373
|
+
: normalized === "failed"
|
|
4374
|
+
? t("library.failed")
|
|
4375
|
+
: normalized === "draft"
|
|
4376
|
+
? t("library.draft")
|
|
4377
|
+
: status;
|
|
4378
|
+
const className =
|
|
4379
|
+
normalized === "ready"
|
|
4380
|
+
? "border-primary/30 bg-primary/10 text-primary"
|
|
4381
|
+
: normalized === "failed"
|
|
4382
|
+
? "border-destructive/30 bg-destructive/10 text-destructive"
|
|
4383
|
+
: normalized === "pending"
|
|
4384
|
+
? "border-border bg-muted/70 text-muted-foreground"
|
|
4385
|
+
: "border-border bg-background text-muted-foreground";
|
|
4386
|
+
|
|
4703
4387
|
return (
|
|
4704
|
-
<
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
</div>
|
|
4714
|
-
</div>
|
|
4715
|
-
<div className="p-2.5">
|
|
4716
|
-
<div className="flex items-center gap-2 truncate text-xs font-medium">
|
|
4717
|
-
{upload.mediaType === "video" ? (
|
|
4718
|
-
<IconVideo className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
4719
|
-
) : (
|
|
4720
|
-
<IconPhoto className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
4721
|
-
)}
|
|
4722
|
-
<span className="truncate">{upload.name}</span>
|
|
4723
|
-
</div>
|
|
4724
|
-
</div>
|
|
4725
|
-
</div>
|
|
4388
|
+
<span
|
|
4389
|
+
className={[
|
|
4390
|
+
"inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border px-2 text-[11px] font-medium",
|
|
4391
|
+
className,
|
|
4392
|
+
].join(" ")}
|
|
4393
|
+
>
|
|
4394
|
+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-current" />
|
|
4395
|
+
<span className="truncate">{label}</span>
|
|
4396
|
+
</span>
|
|
4726
4397
|
);
|
|
4727
4398
|
}
|
|
4728
4399
|
|
|
4729
|
-
function
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
selected,
|
|
4733
|
-
deleting,
|
|
4734
|
-
saving,
|
|
4735
|
-
promoting,
|
|
4736
|
-
onToggle,
|
|
4737
|
-
onDelete,
|
|
4738
|
-
updateAsset,
|
|
4739
|
-
onSave,
|
|
4740
|
-
onMoveToReferences,
|
|
4400
|
+
function VariantPreview({
|
|
4401
|
+
slot,
|
|
4402
|
+
fit = "cover",
|
|
4741
4403
|
}: {
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
selected: boolean;
|
|
4745
|
-
deleting?: boolean;
|
|
4746
|
-
saving?: boolean;
|
|
4747
|
-
promoting?: boolean;
|
|
4748
|
-
onToggle: (checked: boolean) => void;
|
|
4749
|
-
onDelete: () => void;
|
|
4750
|
-
updateAsset: any;
|
|
4751
|
-
onSave?: () => void;
|
|
4752
|
-
onMoveToReferences?: () => void;
|
|
4404
|
+
slot: VariantSlot;
|
|
4405
|
+
fit?: "cover" | "contain";
|
|
4753
4406
|
}) {
|
|
4754
4407
|
const t = useT();
|
|
4755
|
-
const
|
|
4756
|
-
const
|
|
4757
|
-
const
|
|
4758
|
-
const
|
|
4759
|
-
const
|
|
4760
|
-
const
|
|
4408
|
+
const [sourceIndex, setSourceIndex] = useState(0);
|
|
4409
|
+
const [previewUnavailable, setPreviewUnavailable] = useState(false);
|
|
4410
|
+
const previewSources = assetPreviewSources(slot, "thumbnail");
|
|
4411
|
+
const previewSourcesKey = previewSources.join("\n");
|
|
4412
|
+
const isFailed = slot.status === "failed";
|
|
4413
|
+
const previewSrc = previewSources[sourceIndex];
|
|
4414
|
+
|
|
4415
|
+
useEffect(() => {
|
|
4416
|
+
setSourceIndex(0);
|
|
4417
|
+
setPreviewUnavailable(false);
|
|
4418
|
+
}, [previewSourcesKey]);
|
|
4761
4419
|
|
|
4762
4420
|
return (
|
|
4763
|
-
<div
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
: "border-border/80 hover:border-foreground/20",
|
|
4769
|
-
deleting ? "opacity-60" : "",
|
|
4770
|
-
].join(" ")}
|
|
4771
|
-
aria-busy={busy}
|
|
4772
|
-
>
|
|
4773
|
-
<div className="absolute left-2 top-2 z-10">
|
|
4774
|
-
<Checkbox
|
|
4775
|
-
checked={selected}
|
|
4776
|
-
onCheckedChange={(checked) => onToggle(checked === true)}
|
|
4777
|
-
aria-label={t("brandKitDetail.selectItem", { title: displayTitle })}
|
|
4421
|
+
<div className="flex h-full w-full items-center justify-center bg-muted">
|
|
4422
|
+
{previewSrc && !previewUnavailable ? (
|
|
4423
|
+
<img
|
|
4424
|
+
src={previewSrc}
|
|
4425
|
+
alt=""
|
|
4778
4426
|
className={[
|
|
4779
|
-
"
|
|
4780
|
-
|
|
4781
|
-
].join(" ")}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
>
|
|
4795
|
-
<IconDotsVertical className="h-4 w-4" />
|
|
4796
|
-
</Button>
|
|
4797
|
-
</DropdownMenuTrigger>
|
|
4798
|
-
<DropdownMenuContent align="end">
|
|
4799
|
-
<DropdownMenuItem asChild>
|
|
4800
|
-
<Link to={`/asset/${asset.id}`}>
|
|
4801
|
-
{t("brandKitDetail.viewDetails")}
|
|
4802
|
-
</Link>
|
|
4803
|
-
</DropdownMenuItem>
|
|
4804
|
-
{canMoveToReferences ? (
|
|
4805
|
-
<DropdownMenuItem
|
|
4806
|
-
onSelect={(event) => {
|
|
4807
|
-
event.preventDefault();
|
|
4808
|
-
onMoveToReferences?.();
|
|
4809
|
-
}}
|
|
4810
|
-
>
|
|
4811
|
-
<IconPhotoPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
4812
|
-
{t("brandKitDetail.addToReferences")}
|
|
4813
|
-
</DropdownMenuItem>
|
|
4814
|
-
) : null}
|
|
4815
|
-
<DropdownMenuSub>
|
|
4816
|
-
<DropdownMenuSubTrigger>
|
|
4817
|
-
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
4818
|
-
{t("brandKitDetail.moveTo")}
|
|
4819
|
-
</DropdownMenuSubTrigger>
|
|
4820
|
-
<DropdownMenuSubContent>
|
|
4821
|
-
<DropdownMenuItem
|
|
4822
|
-
onSelect={() =>
|
|
4823
|
-
updateAsset.mutate({
|
|
4824
|
-
id: asset.id,
|
|
4825
|
-
folderId: null,
|
|
4826
|
-
})
|
|
4827
|
-
}
|
|
4828
|
-
>
|
|
4829
|
-
{t("brandKitDetail.unfiled")}
|
|
4830
|
-
</DropdownMenuItem>
|
|
4831
|
-
{folders.map((folder) => (
|
|
4832
|
-
<DropdownMenuItem
|
|
4833
|
-
key={folder.id}
|
|
4834
|
-
onSelect={() =>
|
|
4835
|
-
updateAsset.mutate({
|
|
4836
|
-
id: asset.id,
|
|
4837
|
-
folderId: folder.id,
|
|
4838
|
-
})
|
|
4839
|
-
}
|
|
4840
|
-
>
|
|
4841
|
-
{folder.title}
|
|
4842
|
-
</DropdownMenuItem>
|
|
4843
|
-
))}
|
|
4844
|
-
</DropdownMenuSubContent>
|
|
4845
|
-
</DropdownMenuSub>
|
|
4846
|
-
<DropdownMenuSeparator />
|
|
4847
|
-
<DropdownMenuItem
|
|
4848
|
-
className="text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
4849
|
-
onSelect={onDelete}
|
|
4850
|
-
>
|
|
4851
|
-
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
4852
|
-
{t("brandKitDetail.delete")}
|
|
4853
|
-
</DropdownMenuItem>
|
|
4854
|
-
</DropdownMenuContent>
|
|
4855
|
-
</DropdownMenu>
|
|
4856
|
-
</div>
|
|
4857
|
-
<Link to={`/asset/${asset.id}`} className="block outline-none">
|
|
4858
|
-
<div className="relative aspect-[4/3] bg-muted">
|
|
4859
|
-
<AssetPreview asset={asset} />
|
|
4860
|
-
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-background via-background/90 to-transparent px-2 pb-2 pt-8">
|
|
4861
|
-
<div className="flex items-center gap-1.5 truncate text-xs font-medium">
|
|
4862
|
-
{asset.mediaType === "video" ? (
|
|
4863
|
-
<IconVideo className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
4864
|
-
) : (
|
|
4865
|
-
<IconPhoto className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
4866
|
-
)}
|
|
4867
|
-
<span className="truncate">{displayTitle}</span>
|
|
4868
|
-
</div>
|
|
4869
|
-
<div className="mt-1 flex min-w-0 items-center gap-1 overflow-hidden text-[10px] font-medium text-muted-foreground">
|
|
4870
|
-
{sourceText ? (
|
|
4871
|
-
<span className="truncate">{sourceText}</span>
|
|
4872
|
-
) : (
|
|
4873
|
-
<>
|
|
4874
|
-
<span className="truncate">{asset.status}</span>
|
|
4875
|
-
{categoryLabel ? (
|
|
4876
|
-
<>
|
|
4877
|
-
<span className="shrink-0 text-muted-foreground/60">
|
|
4878
|
-
/
|
|
4879
|
-
</span>
|
|
4880
|
-
<span className="truncate">{categoryLabel}</span>
|
|
4881
|
-
</>
|
|
4882
|
-
) : null}
|
|
4883
|
-
</>
|
|
4884
|
-
)}
|
|
4885
|
-
</div>
|
|
4886
|
-
</div>
|
|
4887
|
-
</div>
|
|
4888
|
-
</Link>
|
|
4889
|
-
{hasPrimaryActions ? (
|
|
4890
|
-
<div className="space-y-2 border-t border-border/70 p-2">
|
|
4891
|
-
<div
|
|
4892
|
-
className={
|
|
4893
|
-
onSave && canMoveToReferences
|
|
4894
|
-
? "grid grid-cols-1 gap-2"
|
|
4895
|
-
: "grid grid-cols-2 gap-2"
|
|
4896
|
-
}
|
|
4897
|
-
>
|
|
4898
|
-
{onSave ? (
|
|
4899
|
-
<Button
|
|
4900
|
-
size="sm"
|
|
4901
|
-
className="h-8 px-2 text-xs"
|
|
4902
|
-
onClick={onSave}
|
|
4903
|
-
disabled={busy}
|
|
4904
|
-
>
|
|
4905
|
-
{saving ? (
|
|
4906
|
-
<Spinner className="h-3.5 w-3.5" />
|
|
4907
|
-
) : (
|
|
4908
|
-
t("brandKitDetail.save")
|
|
4909
|
-
)}
|
|
4910
|
-
</Button>
|
|
4911
|
-
) : null}
|
|
4912
|
-
{canMoveToReferences ? (
|
|
4913
|
-
<Button
|
|
4914
|
-
variant="outline"
|
|
4915
|
-
size="sm"
|
|
4916
|
-
className={
|
|
4917
|
-
onSave ? "h-8 px-2 text-xs" : "col-span-2 h-8 px-2 text-xs"
|
|
4918
|
-
}
|
|
4919
|
-
onClick={onMoveToReferences}
|
|
4920
|
-
disabled={busy}
|
|
4921
|
-
title={t("brandKitDetail.addToReferences")}
|
|
4922
|
-
>
|
|
4923
|
-
{promoting ? (
|
|
4924
|
-
<Spinner className="h-3.5 w-3.5" />
|
|
4925
|
-
) : (
|
|
4926
|
-
t("brandKitDetail.addToReferences")
|
|
4927
|
-
)}
|
|
4928
|
-
</Button>
|
|
4929
|
-
) : null}
|
|
4930
|
-
</div>
|
|
4427
|
+
"h-full w-full",
|
|
4428
|
+
fit === "contain" ? "object-contain" : "object-cover",
|
|
4429
|
+
].join(" ")}
|
|
4430
|
+
onError={() => {
|
|
4431
|
+
const nextIndex = sourceIndex + 1;
|
|
4432
|
+
if (nextIndex < previewSources.length) {
|
|
4433
|
+
setSourceIndex(nextIndex);
|
|
4434
|
+
} else {
|
|
4435
|
+
setPreviewUnavailable(true);
|
|
4436
|
+
}
|
|
4437
|
+
}}
|
|
4438
|
+
/>
|
|
4439
|
+
) : isFailed ? (
|
|
4440
|
+
<div className="p-4 text-center text-xs text-destructive">
|
|
4441
|
+
{slot.error}
|
|
4931
4442
|
</div>
|
|
4932
|
-
) :
|
|
4443
|
+
) : previewUnavailable ? (
|
|
4444
|
+
<div className="p-4 text-center text-xs text-muted-foreground">
|
|
4445
|
+
{t("assetDetail.previewUnavailable")}
|
|
4446
|
+
</div>
|
|
4447
|
+
) : (
|
|
4448
|
+
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
4449
|
+
<IconPhoto className="h-8 w-8 animate-pulse" />
|
|
4450
|
+
{fit === "contain" ? (
|
|
4451
|
+
<span className="text-xs font-medium">
|
|
4452
|
+
{t("library.rendering")}
|
|
4453
|
+
</span>
|
|
4454
|
+
) : null}
|
|
4455
|
+
</div>
|
|
4456
|
+
)}
|
|
4933
4457
|
</div>
|
|
4934
4458
|
);
|
|
4935
4459
|
}
|
|
4936
4460
|
|
|
4937
|
-
function
|
|
4938
|
-
slot,
|
|
4461
|
+
function CandidateSaveMenu({
|
|
4939
4462
|
libraryId,
|
|
4463
|
+
folders,
|
|
4464
|
+
allowCreateFolder = true,
|
|
4465
|
+
saving,
|
|
4466
|
+
disabled,
|
|
4940
4467
|
onSave,
|
|
4941
|
-
onMoveToReferences,
|
|
4942
|
-
saving = false,
|
|
4943
|
-
promoting = false,
|
|
4944
4468
|
}: {
|
|
4945
|
-
slot: any;
|
|
4946
4469
|
libraryId: string;
|
|
4947
|
-
|
|
4948
|
-
|
|
4470
|
+
folders: any[];
|
|
4471
|
+
allowCreateFolder?: boolean;
|
|
4949
4472
|
saving?: boolean;
|
|
4950
|
-
|
|
4473
|
+
disabled?: boolean;
|
|
4474
|
+
onSave: (folderId: string | null) => void;
|
|
4951
4475
|
}) {
|
|
4952
4476
|
const t = useT();
|
|
4953
|
-
const
|
|
4477
|
+
const createFolder = useActionMutation("create-folder");
|
|
4954
4478
|
const queryClient = useQueryClient();
|
|
4955
|
-
const [
|
|
4956
|
-
const
|
|
4957
|
-
const [previewUnavailable, setPreviewUnavailable] = useState(false);
|
|
4958
|
-
const previewSources = assetPreviewSources(slot, "thumbnail");
|
|
4959
|
-
const previewSourcesKey = previewSources.join("\n");
|
|
4960
|
-
const isFailed = slot.status === "failed";
|
|
4961
|
-
const label = isFailed
|
|
4962
|
-
? t("brandKitDetail.dismiss")
|
|
4963
|
-
: t("brandKitDetail.delete");
|
|
4964
|
-
const busy = saving || promoting || dismissSlot.isPending;
|
|
4965
|
-
const previewSrc = previewSources[sourceIndex];
|
|
4966
|
-
|
|
4967
|
-
useEffect(() => {
|
|
4968
|
-
setSourceIndex(0);
|
|
4969
|
-
setPreviewUnavailable(false);
|
|
4970
|
-
}, [previewSourcesKey]);
|
|
4479
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
4480
|
+
const pending = saving || createFolder.isPending;
|
|
4971
4481
|
|
|
4972
4482
|
return (
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4483
|
+
<>
|
|
4484
|
+
{allowCreateFolder ? (
|
|
4485
|
+
<CreateFolderDialog
|
|
4486
|
+
open={createOpen}
|
|
4487
|
+
onOpenChange={setCreateOpen}
|
|
4488
|
+
onSubmit={async (title) => {
|
|
4489
|
+
const folder = (await createFolder.mutateAsync({
|
|
4490
|
+
libraryId,
|
|
4491
|
+
title,
|
|
4492
|
+
parentId: null,
|
|
4493
|
+
})) as any;
|
|
4494
|
+
void queryClient.invalidateQueries({
|
|
4495
|
+
queryKey: ["action", "get-library", { id: libraryId }],
|
|
4496
|
+
refetchType: "active",
|
|
4497
|
+
});
|
|
4498
|
+
void queryClient.invalidateQueries({
|
|
4499
|
+
queryKey: ["action", "list-libraries"],
|
|
4500
|
+
refetchType: "active",
|
|
4501
|
+
});
|
|
4502
|
+
setCreateOpen(false);
|
|
4503
|
+
if (folder?.id) onSave(folder.id);
|
|
4504
|
+
}}
|
|
4505
|
+
pending={createFolder.isPending}
|
|
4506
|
+
/>
|
|
4507
|
+
) : null}
|
|
4508
|
+
<DropdownMenu>
|
|
4509
|
+
<DropdownMenuTrigger asChild>
|
|
4510
|
+
<Button
|
|
4511
|
+
size="sm"
|
|
4512
|
+
className="h-8 min-w-0 px-2 text-xs"
|
|
4513
|
+
disabled={disabled}
|
|
4514
|
+
>
|
|
4515
|
+
{pending ? (
|
|
4516
|
+
<Spinner className="h-3.5 w-3.5" />
|
|
4517
|
+
) : (
|
|
4518
|
+
t("library.saveTo")
|
|
4519
|
+
)}
|
|
4520
|
+
</Button>
|
|
4521
|
+
</DropdownMenuTrigger>
|
|
4522
|
+
<DropdownMenuContent align="start">
|
|
4523
|
+
<DropdownMenuItem onSelect={() => onSave(null)}>
|
|
4524
|
+
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
4525
|
+
{t("library.unfiled")}
|
|
4526
|
+
</DropdownMenuItem>
|
|
4527
|
+
{folders.map((folder) => (
|
|
4528
|
+
<DropdownMenuItem
|
|
4529
|
+
key={folder.id}
|
|
4530
|
+
onSelect={() => onSave(folder.id)}
|
|
4987
4531
|
>
|
|
4988
|
-
<
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4532
|
+
<IconFolder className="mr-2 h-4 w-4 shrink-0" />
|
|
4533
|
+
{t("library.folderLabel", { title: folder.title })}
|
|
4534
|
+
</DropdownMenuItem>
|
|
4535
|
+
))}
|
|
4536
|
+
{allowCreateFolder ? (
|
|
4537
|
+
<>
|
|
4538
|
+
<DropdownMenuSeparator />
|
|
4993
4539
|
<DropdownMenuItem
|
|
4994
4540
|
onSelect={(event) => {
|
|
4995
4541
|
event.preventDefault();
|
|
4996
|
-
|
|
4542
|
+
setCreateOpen(true);
|
|
4997
4543
|
}}
|
|
4998
4544
|
>
|
|
4999
|
-
<
|
|
5000
|
-
{t("
|
|
4545
|
+
<IconFolderPlus className="mr-2 h-4 w-4 shrink-0" />
|
|
4546
|
+
{t("library.newFolderEllipsis")}
|
|
5001
4547
|
</DropdownMenuItem>
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
setConfirmOpen(true);
|
|
5008
|
-
}}
|
|
5009
|
-
>
|
|
5010
|
-
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
5011
|
-
{label}
|
|
5012
|
-
</DropdownMenuItem>
|
|
5013
|
-
</DropdownMenuContent>
|
|
5014
|
-
</DropdownMenu>
|
|
5015
|
-
</div>
|
|
5016
|
-
|
|
5017
|
-
<AlertDialog
|
|
5018
|
-
open={confirmOpen}
|
|
5019
|
-
onOpenChange={(open) => {
|
|
5020
|
-
if (!dismissSlot.isPending) setConfirmOpen(open);
|
|
5021
|
-
}}
|
|
5022
|
-
>
|
|
5023
|
-
<AlertDialogContent>
|
|
5024
|
-
<AlertDialogHeader>
|
|
5025
|
-
<AlertDialogTitle>
|
|
5026
|
-
{isFailed
|
|
5027
|
-
? t("brandKitDetail.dismissThisSlot")
|
|
5028
|
-
: t("brandKitDetail.deleteCandidate")}
|
|
5029
|
-
</AlertDialogTitle>
|
|
5030
|
-
<AlertDialogDescription>
|
|
5031
|
-
{isFailed
|
|
5032
|
-
? t("brandKitDetail.dismissSlotDescription")
|
|
5033
|
-
: t("brandKitDetail.deleteCandidateDescription")}
|
|
5034
|
-
</AlertDialogDescription>
|
|
5035
|
-
</AlertDialogHeader>
|
|
5036
|
-
<AlertDialogFooter>
|
|
5037
|
-
<AlertDialogCancel disabled={dismissSlot.isPending}>
|
|
5038
|
-
{t("brandKitDetail.cancel")}
|
|
5039
|
-
</AlertDialogCancel>
|
|
5040
|
-
<AlertDialogAction
|
|
5041
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
5042
|
-
disabled={dismissSlot.isPending}
|
|
5043
|
-
onClick={(event) => {
|
|
5044
|
-
event.preventDefault();
|
|
5045
|
-
dismissSlot.mutate(
|
|
5046
|
-
{ slotId: slot.slotId },
|
|
5047
|
-
{
|
|
5048
|
-
onSuccess: () => {
|
|
5049
|
-
removeVariantSlotFromCache(queryClient, slot);
|
|
5050
|
-
removeAssetsFromLibraryCache(queryClient, libraryId, [
|
|
5051
|
-
slot.assetId,
|
|
5052
|
-
]);
|
|
5053
|
-
setConfirmOpen(false);
|
|
5054
|
-
void queryClient.invalidateQueries({
|
|
5055
|
-
queryKey: ["app-state", "asset-variants"],
|
|
5056
|
-
refetchType: "active",
|
|
5057
|
-
});
|
|
5058
|
-
},
|
|
5059
|
-
onError: (error) =>
|
|
5060
|
-
toast.error(
|
|
5061
|
-
error.message ||
|
|
5062
|
-
t("brandKitDetail.couldNotClearCandidate"),
|
|
5063
|
-
),
|
|
5064
|
-
},
|
|
5065
|
-
);
|
|
5066
|
-
}}
|
|
5067
|
-
>
|
|
5068
|
-
{dismissSlot.isPending ? (
|
|
5069
|
-
<>
|
|
5070
|
-
<Spinner className="h-4 w-4" />
|
|
5071
|
-
{isFailed
|
|
5072
|
-
? t("brandKitDetail.dismissing")
|
|
5073
|
-
: t("brandKitDetail.deleting")}
|
|
5074
|
-
</>
|
|
5075
|
-
) : (
|
|
5076
|
-
label
|
|
5077
|
-
)}
|
|
5078
|
-
</AlertDialogAction>
|
|
5079
|
-
</AlertDialogFooter>
|
|
5080
|
-
</AlertDialogContent>
|
|
5081
|
-
</AlertDialog>
|
|
5082
|
-
|
|
5083
|
-
<div className="relative flex aspect-[4/3] items-center justify-center bg-muted">
|
|
5084
|
-
{previewSrc && !previewUnavailable ? (
|
|
5085
|
-
<img
|
|
5086
|
-
src={previewSrc}
|
|
5087
|
-
alt=""
|
|
5088
|
-
className="h-full w-full object-cover"
|
|
5089
|
-
onError={() => {
|
|
5090
|
-
const nextIndex = sourceIndex + 1;
|
|
5091
|
-
if (nextIndex < previewSources.length) {
|
|
5092
|
-
setSourceIndex(nextIndex);
|
|
5093
|
-
} else {
|
|
5094
|
-
setPreviewUnavailable(true);
|
|
5095
|
-
}
|
|
5096
|
-
}}
|
|
5097
|
-
/>
|
|
5098
|
-
) : isFailed ? (
|
|
5099
|
-
<div className="p-4 text-center text-xs text-destructive">
|
|
5100
|
-
{slot.error}
|
|
5101
|
-
</div>
|
|
5102
|
-
) : previewUnavailable ? (
|
|
5103
|
-
<div className="p-4 text-center text-xs text-muted-foreground">
|
|
5104
|
-
{t("brandKitDetail.previewUnavailable")}
|
|
5105
|
-
</div>
|
|
5106
|
-
) : (
|
|
5107
|
-
<IconPhoto className="h-8 w-8 animate-pulse text-muted-foreground" />
|
|
5108
|
-
)}
|
|
5109
|
-
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-background via-background/90 to-transparent px-2 pb-2 pt-8">
|
|
5110
|
-
<div className="flex items-center justify-between gap-2">
|
|
5111
|
-
<span className="truncate text-xs font-medium">
|
|
5112
|
-
{slot.status === "ready"
|
|
5113
|
-
? t("brandKitDetail.readyCandidate")
|
|
5114
|
-
: t("brandKitDetail.generating")}
|
|
5115
|
-
</span>
|
|
5116
|
-
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
5117
|
-
{slot.slotId
|
|
5118
|
-
? shortId(String(slot.slotId))
|
|
5119
|
-
: t("brandKitDetail.slot")}
|
|
5120
|
-
</span>
|
|
5121
|
-
</div>
|
|
5122
|
-
</div>
|
|
5123
|
-
</div>
|
|
5124
|
-
{slot.status === "ready" ? (
|
|
5125
|
-
<div className="border-t border-border/70 p-2">
|
|
5126
|
-
<div
|
|
5127
|
-
className={
|
|
5128
|
-
onMoveToReferences
|
|
5129
|
-
? "grid grid-cols-1 gap-2"
|
|
5130
|
-
: "grid grid-cols-2 gap-2"
|
|
5131
|
-
}
|
|
5132
|
-
>
|
|
5133
|
-
<Button
|
|
5134
|
-
size="sm"
|
|
5135
|
-
className="h-8 px-2 text-xs"
|
|
5136
|
-
onClick={onSave}
|
|
5137
|
-
disabled={busy}
|
|
5138
|
-
>
|
|
5139
|
-
{saving ? (
|
|
5140
|
-
<Spinner className="h-3.5 w-3.5" />
|
|
5141
|
-
) : (
|
|
5142
|
-
t("brandKitDetail.save")
|
|
5143
|
-
)}
|
|
5144
|
-
</Button>
|
|
5145
|
-
{onMoveToReferences ? (
|
|
5146
|
-
<Button
|
|
5147
|
-
variant="outline"
|
|
5148
|
-
size="sm"
|
|
5149
|
-
className="h-8 px-2 text-xs"
|
|
5150
|
-
onClick={onMoveToReferences}
|
|
5151
|
-
disabled={busy}
|
|
5152
|
-
title={t("brandKitDetail.addToReferences")}
|
|
5153
|
-
>
|
|
5154
|
-
{promoting ? (
|
|
5155
|
-
<Spinner className="h-3.5 w-3.5" />
|
|
5156
|
-
) : (
|
|
5157
|
-
t("brandKitDetail.addToReferences")
|
|
5158
|
-
)}
|
|
5159
|
-
</Button>
|
|
5160
|
-
) : null}
|
|
5161
|
-
</div>
|
|
5162
|
-
</div>
|
|
5163
|
-
) : null}
|
|
5164
|
-
</div>
|
|
4548
|
+
</>
|
|
4549
|
+
) : null}
|
|
4550
|
+
</DropdownMenuContent>
|
|
4551
|
+
</DropdownMenu>
|
|
4552
|
+
</>
|
|
5165
4553
|
);
|
|
5166
4554
|
}
|
|
5167
4555
|
|
|
@@ -5188,7 +4576,7 @@ function CreateFolderDialog({
|
|
|
5188
4576
|
toast.error(
|
|
5189
4577
|
error instanceof Error
|
|
5190
4578
|
? error.message
|
|
5191
|
-
: t("
|
|
4579
|
+
: t("library.couldNotCreateFolder"),
|
|
5192
4580
|
);
|
|
5193
4581
|
}
|
|
5194
4582
|
}
|
|
@@ -5196,13 +4584,13 @@ function CreateFolderDialog({
|
|
|
5196
4584
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
5197
4585
|
<DialogContent className="sm:max-w-md">
|
|
5198
4586
|
<DialogHeader>
|
|
5199
|
-
<DialogTitle>{t("
|
|
4587
|
+
<DialogTitle>{t("library.newFolder")}</DialogTitle>
|
|
5200
4588
|
<DialogDescription>
|
|
5201
|
-
{t("
|
|
4589
|
+
{t("library.newFolderDescription")}
|
|
5202
4590
|
</DialogDescription>
|
|
5203
4591
|
</DialogHeader>
|
|
5204
4592
|
<div className="space-y-2">
|
|
5205
|
-
<Label htmlFor="folder-title">{t("
|
|
4593
|
+
<Label htmlFor="folder-title">{t("library.name")}</Label>
|
|
5206
4594
|
<Input
|
|
5207
4595
|
id="folder-title"
|
|
5208
4596
|
value={title}
|
|
@@ -5213,13 +4601,13 @@ function CreateFolderDialog({
|
|
|
5213
4601
|
void submit();
|
|
5214
4602
|
}
|
|
5215
4603
|
}}
|
|
5216
|
-
placeholder={t("
|
|
4604
|
+
placeholder={t("library.folderNamePlaceholder")}
|
|
5217
4605
|
autoFocus
|
|
5218
4606
|
/>
|
|
5219
4607
|
</div>
|
|
5220
4608
|
<DialogFooter>
|
|
5221
4609
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
5222
|
-
{t("
|
|
4610
|
+
{t("library.cancel")}
|
|
5223
4611
|
</Button>
|
|
5224
4612
|
<Button
|
|
5225
4613
|
disabled={!title.trim() || pending}
|
|
@@ -5227,7 +4615,7 @@ function CreateFolderDialog({
|
|
|
5227
4615
|
void submit();
|
|
5228
4616
|
}}
|
|
5229
4617
|
>
|
|
5230
|
-
{t("
|
|
4618
|
+
{t("library.create")}
|
|
5231
4619
|
</Button>
|
|
5232
4620
|
</DialogFooter>
|
|
5233
4621
|
</DialogContent>
|
|
@@ -5237,26 +4625,72 @@ function CreateFolderDialog({
|
|
|
5237
4625
|
|
|
5238
4626
|
function LiveCandidatesActions({
|
|
5239
4627
|
slots,
|
|
4628
|
+
draftAssets,
|
|
5240
4629
|
libraryId,
|
|
5241
4630
|
}: {
|
|
5242
4631
|
slots: any[];
|
|
4632
|
+
draftAssets: any[];
|
|
5243
4633
|
libraryId: string;
|
|
5244
4634
|
}) {
|
|
5245
4635
|
const t = useT();
|
|
5246
4636
|
const dismissSlots = useActionMutation("dismiss-variant-slots");
|
|
4637
|
+
const deleteAssets = useActionMutation("delete-assets");
|
|
5247
4638
|
const queryClient = useQueryClient();
|
|
5248
4639
|
const [pending, setPending] = useState<"failed" | "all" | null>(null);
|
|
5249
4640
|
const failedCount = slots.filter((s) => s.status === "failed").length;
|
|
4641
|
+
const draftCount = draftAssets.length;
|
|
4642
|
+
const totalCount = slots.length + draftCount;
|
|
5250
4643
|
const hasFailed = failedCount > 0;
|
|
5251
|
-
const isClearing = dismissSlots.isPending;
|
|
4644
|
+
const isClearing = dismissSlots.isPending || deleteAssets.isPending;
|
|
5252
4645
|
const actionLabel =
|
|
5253
|
-
pending === "failed"
|
|
5254
|
-
? t("brandKitDetail.dismissFailed")
|
|
5255
|
-
: t("brandKitDetail.clearAll");
|
|
4646
|
+
pending === "failed" ? t("library.dismissFailed") : t("library.clearAll");
|
|
5256
4647
|
const busyLabel =
|
|
5257
|
-
pending === "failed"
|
|
5258
|
-
|
|
5259
|
-
|
|
4648
|
+
pending === "failed" ? t("library.dismissing") : t("library.clearing");
|
|
4649
|
+
|
|
4650
|
+
async function handleClear(scope: "failed" | "all") {
|
|
4651
|
+
const slotAssetIds = slots
|
|
4652
|
+
.filter((slot) => scope === "all" || slot.status === "failed")
|
|
4653
|
+
.map((slot) => slot.assetId)
|
|
4654
|
+
.filter((assetId): assetId is string => typeof assetId === "string");
|
|
4655
|
+
const draftAssetIds =
|
|
4656
|
+
scope === "all" ? draftAssets.map((asset) => asset.id) : [];
|
|
4657
|
+
const removedAssetIds = [...new Set([...slotAssetIds, ...draftAssetIds])];
|
|
4658
|
+
try {
|
|
4659
|
+
if (slots.length > 0 && (scope === "all" || failedCount > 0)) {
|
|
4660
|
+
await dismissSlots.mutateAsync({ scope });
|
|
4661
|
+
removeVariantSlotsByScopeFromCache(queryClient, scope);
|
|
4662
|
+
}
|
|
4663
|
+
if (draftAssetIds.length > 0) {
|
|
4664
|
+
await deleteAssets.mutateAsync({ ids: draftAssetIds });
|
|
4665
|
+
}
|
|
4666
|
+
if (removedAssetIds.length > 0) {
|
|
4667
|
+
removeAssetsFromLibraryCache(queryClient, libraryId, removedAssetIds);
|
|
4668
|
+
}
|
|
4669
|
+
setPending(null);
|
|
4670
|
+
void queryClient.invalidateQueries({
|
|
4671
|
+
queryKey: ["app-state"],
|
|
4672
|
+
refetchType: "active",
|
|
4673
|
+
});
|
|
4674
|
+
void queryClient.invalidateQueries({
|
|
4675
|
+
queryKey: ["action", "get-library", { id: libraryId }],
|
|
4676
|
+
refetchType: "active",
|
|
4677
|
+
});
|
|
4678
|
+
void queryClient.invalidateQueries({
|
|
4679
|
+
queryKey: ["action", "get-library"],
|
|
4680
|
+
refetchType: "active",
|
|
4681
|
+
});
|
|
4682
|
+
void queryClient.invalidateQueries({
|
|
4683
|
+
queryKey: ["action", "list-assets"],
|
|
4684
|
+
refetchType: "active",
|
|
4685
|
+
});
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
toast.error(
|
|
4688
|
+
error instanceof Error
|
|
4689
|
+
? error.message
|
|
4690
|
+
: t("library.couldNotClearCandidates"),
|
|
4691
|
+
);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
5260
4694
|
|
|
5261
4695
|
return (
|
|
5262
4696
|
<>
|
|
@@ -5270,20 +4704,18 @@ function LiveCandidatesActions({
|
|
|
5270
4704
|
<AlertDialogHeader>
|
|
5271
4705
|
<AlertDialogTitle>
|
|
5272
4706
|
{pending === "failed"
|
|
5273
|
-
? t("
|
|
5274
|
-
|
|
5275
|
-
})
|
|
5276
|
-
: t("brandKitDetail.clearLiveCandidates")}
|
|
4707
|
+
? t("library.dismissFailedSlotsTitle", { count: failedCount })
|
|
4708
|
+
: t("library.clearCandidatesTitle", { count: totalCount })}
|
|
5277
4709
|
</AlertDialogTitle>
|
|
5278
4710
|
<AlertDialogDescription>
|
|
5279
4711
|
{pending === "failed"
|
|
5280
|
-
? t("
|
|
5281
|
-
: t("
|
|
4712
|
+
? t("library.dismissFailedSlotsDescription")
|
|
4713
|
+
: t("library.clearCandidatesDescription")}
|
|
5282
4714
|
</AlertDialogDescription>
|
|
5283
4715
|
</AlertDialogHeader>
|
|
5284
4716
|
<AlertDialogFooter>
|
|
5285
4717
|
<AlertDialogCancel disabled={isClearing}>
|
|
5286
|
-
{t("
|
|
4718
|
+
{t("library.cancel")}
|
|
5287
4719
|
</AlertDialogCancel>
|
|
5288
4720
|
<AlertDialogAction
|
|
5289
4721
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
@@ -5292,32 +4724,7 @@ function LiveCandidatesActions({
|
|
|
5292
4724
|
event.preventDefault();
|
|
5293
4725
|
const scope = pending;
|
|
5294
4726
|
if (!scope) return;
|
|
5295
|
-
|
|
5296
|
-
.filter((slot) => scope === "all" || slot.status === "failed")
|
|
5297
|
-
.map((slot) => slot.assetId);
|
|
5298
|
-
dismissSlots.mutate(
|
|
5299
|
-
{ scope },
|
|
5300
|
-
{
|
|
5301
|
-
onSuccess: () => {
|
|
5302
|
-
removeVariantSlotsByScopeFromCache(queryClient, scope);
|
|
5303
|
-
removeAssetsFromLibraryCache(
|
|
5304
|
-
queryClient,
|
|
5305
|
-
libraryId,
|
|
5306
|
-
removedAssetIds,
|
|
5307
|
-
);
|
|
5308
|
-
setPending(null);
|
|
5309
|
-
void queryClient.invalidateQueries({
|
|
5310
|
-
queryKey: ["app-state", "asset-variants"],
|
|
5311
|
-
refetchType: "active",
|
|
5312
|
-
});
|
|
5313
|
-
},
|
|
5314
|
-
onError: (error) =>
|
|
5315
|
-
toast.error(
|
|
5316
|
-
error.message ||
|
|
5317
|
-
t("brandKitDetail.couldNotClearLiveCandidates"),
|
|
5318
|
-
),
|
|
5319
|
-
},
|
|
5320
|
-
);
|
|
4727
|
+
void handleClear(scope);
|
|
5321
4728
|
}}
|
|
5322
4729
|
>
|
|
5323
4730
|
{isClearing ? (
|
|
@@ -5337,14 +4744,14 @@ function LiveCandidatesActions({
|
|
|
5337
4744
|
<DropdownMenuTrigger asChild>
|
|
5338
4745
|
<Button
|
|
5339
4746
|
type="button"
|
|
5340
|
-
variant="
|
|
5341
|
-
size="
|
|
5342
|
-
className="
|
|
5343
|
-
aria-label={t("
|
|
4747
|
+
variant="ghost"
|
|
4748
|
+
size="icon"
|
|
4749
|
+
className="h-8 w-8"
|
|
4750
|
+
aria-label={t("library.candidateActions")}
|
|
4751
|
+
title={t("library.candidateActions")}
|
|
5344
4752
|
disabled={isClearing}
|
|
5345
4753
|
>
|
|
5346
4754
|
<IconDotsVertical className="h-4 w-4" />
|
|
5347
|
-
{t("brandKitDetail.clear")}
|
|
5348
4755
|
</Button>
|
|
5349
4756
|
</DropdownMenuTrigger>
|
|
5350
4757
|
<DropdownMenuContent align="end">
|
|
@@ -5356,7 +4763,7 @@ function LiveCandidatesActions({
|
|
|
5356
4763
|
}}
|
|
5357
4764
|
>
|
|
5358
4765
|
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
5359
|
-
{t("
|
|
4766
|
+
{t("library.dismissFailedWithCount", { count: failedCount })}
|
|
5360
4767
|
</DropdownMenuItem>
|
|
5361
4768
|
<DropdownMenuItem
|
|
5362
4769
|
className="text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
@@ -5367,24 +4774,10 @@ function LiveCandidatesActions({
|
|
|
5367
4774
|
}}
|
|
5368
4775
|
>
|
|
5369
4776
|
<IconTrash className="mr-2 h-4 w-4 shrink-0" />
|
|
5370
|
-
{t("
|
|
4777
|
+
{t("library.clearAll")}
|
|
5371
4778
|
</DropdownMenuItem>
|
|
5372
4779
|
</DropdownMenuContent>
|
|
5373
4780
|
</DropdownMenu>
|
|
5374
4781
|
</>
|
|
5375
4782
|
);
|
|
5376
4783
|
}
|
|
5377
|
-
|
|
5378
|
-
function useVariantState() {
|
|
5379
|
-
return useQuery({
|
|
5380
|
-
queryKey: ["app-state", "asset-variants"],
|
|
5381
|
-
queryFn: async () => {
|
|
5382
|
-
const res = await fetch(
|
|
5383
|
-
agentNativePath("/_agent-native/application-state/asset-variants"),
|
|
5384
|
-
);
|
|
5385
|
-
if (!res.ok) return null;
|
|
5386
|
-
return res.json();
|
|
5387
|
-
},
|
|
5388
|
-
refetchInterval: 1000,
|
|
5389
|
-
}) as any;
|
|
5390
|
-
}
|