@agent-native/core 0.92.5 → 0.92.6
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 +13 -0
- package/corpus/core/docs/content/template-assets.mdx +139 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +96 -6
- package/corpus/core/src/client/AssistantChat.tsx +127 -15
- package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
- package/corpus/core/src/client/org/TeamPage.tsx +274 -177
- package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
- package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
- package/corpus/core/src/demo/config.ts +3 -3
- package/corpus/core/src/demo/redact.ts +720 -0
- package/corpus/core/src/extensions/url-safety.ts +13 -1
- package/corpus/core/src/localization/default-messages.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/analytics/AGENTS.md +6 -3
- package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
- package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
- package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
- package/corpus/templates/assets/AGENTS.md +13 -0
- package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
- package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
- package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
- package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
- package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
- package/corpus/templates/assets/actions/generate-image.ts +195 -26
- package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
- package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
- package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
- package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
- package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
- package/corpus/templates/assets/app/i18n-data.ts +93 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
- package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
- package/corpus/templates/assets/server/handlers/assets.ts +7 -27
- package/corpus/templates/assets/server/lib/generation.ts +83 -2
- package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
- package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
- package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
- package/corpus/templates/assets/shared/api.ts +20 -0
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
- package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
- package/corpus/templates/calendar/server/handlers/events.ts +9 -2
- package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
- package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
- package/corpus/templates/clips/desktop/src/styles.css +13 -9
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
- package/dist/agent/production-agent.d.ts +34 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +58 -5
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +3 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +96 -15
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +5 -1
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +32 -13
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +3 -3
- package/dist/client/settings/DemoModeSection.js +4 -4
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +38 -2
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +2 -2
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/config.js +3 -3
- package/dist/demo/config.js.map +1 -1
- package/dist/demo/redact.d.ts +1 -1
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +0 -0
- package/dist/demo/redact.js.map +1 -1
- package/dist/extensions/url-safety.d.ts +5 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +10 -0
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +7 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +7 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
- package/dist/templates/default/app/i18n/de-DE.ts +7 -0
- package/dist/templates/default/app/i18n/en-US.ts +7 -0
- package/dist/templates/default/app/i18n/es-ES.ts +7 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
- package/docs/content/template-assets.mdx +139 -0
- package/package.json +1 -1
- package/src/templates/default/app/i18n/ar-SA.ts +7 -0
- package/src/templates/default/app/i18n/de-DE.ts +7 -0
- package/src/templates/default/app/i18n/en-US.ts +7 -0
- package/src/templates/default/app/i18n/es-ES.ts +7 -0
- package/src/templates/default/app/i18n/fr-FR.ts +7 -0
- package/src/templates/default/app/i18n/hi-IN.ts +7 -0
- package/src/templates/default/app/i18n/ja-JP.ts +7 -0
- package/src/templates/default/app/i18n/ko-KR.ts +7 -0
- package/src/templates/default/app/i18n/pt-BR.ts +7 -0
- package/src/templates/default/app/i18n/zh-CN.ts +7 -0
- package/src/templates/default/app/i18n/zh-TW.ts +7 -0
|
@@ -37,35 +37,63 @@ import { Skeleton } from "@agent-native/toolkit/ui/skeleton";
|
|
|
37
37
|
import { Spinner } from "@agent-native/toolkit/ui/spinner";
|
|
38
38
|
import {
|
|
39
39
|
IconArrowLeft,
|
|
40
|
+
IconCheck,
|
|
40
41
|
IconChevronDown,
|
|
41
42
|
IconDeviceFloppy,
|
|
42
43
|
IconLock,
|
|
43
44
|
IconPhotoPlus,
|
|
45
|
+
IconPlus,
|
|
46
|
+
IconSearch,
|
|
44
47
|
IconTrash,
|
|
45
48
|
IconUpload,
|
|
49
|
+
IconUserPlus,
|
|
50
|
+
IconX,
|
|
46
51
|
} from "@tabler/icons-react";
|
|
47
52
|
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
48
53
|
import { Link, useNavigate, useParams } from "react-router";
|
|
49
54
|
import { toast } from "sonner";
|
|
50
55
|
|
|
56
|
+
import {
|
|
57
|
+
Dialog,
|
|
58
|
+
DialogContent,
|
|
59
|
+
DialogDescription,
|
|
60
|
+
DialogHeader,
|
|
61
|
+
DialogTitle,
|
|
62
|
+
} from "@/components/ui/dialog";
|
|
63
|
+
import {
|
|
64
|
+
Popover,
|
|
65
|
+
PopoverContent,
|
|
66
|
+
PopoverTrigger,
|
|
67
|
+
} from "@/components/ui/popover";
|
|
68
|
+
import {
|
|
69
|
+
Sheet,
|
|
70
|
+
SheetContent,
|
|
71
|
+
SheetDescription,
|
|
72
|
+
SheetHeader,
|
|
73
|
+
SheetTitle,
|
|
74
|
+
} from "@/components/ui/sheet";
|
|
51
75
|
import { Textarea } from "@/components/ui/textarea";
|
|
52
76
|
import { messagesByLocale } from "@/i18n-data";
|
|
53
77
|
import { assetMediaUrl } from "@/lib/asset-urls";
|
|
54
78
|
import type { AssetUploadResult } from "@/lib/upload-results";
|
|
55
79
|
import { cn } from "@/lib/utils";
|
|
56
80
|
|
|
81
|
+
import { normalizePresetReferences } from "../../server/lib/preset-references";
|
|
57
82
|
import {
|
|
58
83
|
ASPECT_RATIOS,
|
|
59
84
|
GENERATION_PRESET_REFERENCE_POLICIES,
|
|
60
85
|
IMAGE_CATEGORIES,
|
|
61
86
|
IMAGE_MODELS,
|
|
62
87
|
IMAGE_SIZES,
|
|
88
|
+
PRESET_REFERENCE_ROLES,
|
|
63
89
|
supportedAspectRatiosForModel,
|
|
64
90
|
type AspectRatio,
|
|
65
91
|
type GenerationPresetReferencePolicy,
|
|
66
92
|
type ImageCategory,
|
|
67
93
|
type ImageModel,
|
|
68
94
|
type ImageSize,
|
|
95
|
+
type PresetReference,
|
|
96
|
+
type PresetReferenceRole,
|
|
69
97
|
type PresetSkeletonSpec,
|
|
70
98
|
} from "../../shared/api";
|
|
71
99
|
|
|
@@ -101,11 +129,13 @@ type PresetFormState = {
|
|
|
101
129
|
skeletonDropShadow: boolean;
|
|
102
130
|
skeletonLogo: boolean;
|
|
103
131
|
skeletonLogoPlacement: SkeletonLogoPlacement;
|
|
132
|
+
presetReferences: PresetReference[];
|
|
104
133
|
};
|
|
105
134
|
|
|
106
135
|
const NO_COLLECTION = "__none__";
|
|
107
136
|
const NO_SKELETON_BACKGROUND = "__none__";
|
|
108
137
|
const NO_SKELETON_MASK = "__none__";
|
|
138
|
+
type ReferenceMode = "fixed" | "swappable" | "required";
|
|
109
139
|
|
|
110
140
|
function usesSkeletonInpaintMode(
|
|
111
141
|
form: Pick<PresetFormState, "model" | "skeletonContentMode">,
|
|
@@ -122,6 +152,10 @@ function skeletonFromPreset(preset: any): PresetSkeletonSpec | null {
|
|
|
122
152
|
return spec && typeof spec === "object" ? (spec as PresetSkeletonSpec) : null;
|
|
123
153
|
}
|
|
124
154
|
|
|
155
|
+
function presetReferencesFromPreset(preset: any): PresetReference[] {
|
|
156
|
+
return normalizePresetReferences(preset?.settings?.presetReferences);
|
|
157
|
+
}
|
|
158
|
+
|
|
125
159
|
function logoPlacementFromLayer(
|
|
126
160
|
layer: SkeletonForegroundLayer,
|
|
127
161
|
): SkeletonLogoPlacement {
|
|
@@ -177,6 +211,40 @@ function buildSkeletonSpec(
|
|
|
177
211
|
};
|
|
178
212
|
}
|
|
179
213
|
|
|
214
|
+
function normalizeReferenceEntries(entries: PresetReference[]) {
|
|
215
|
+
return normalizePresetReferences(
|
|
216
|
+
entries.map((entry) => ({
|
|
217
|
+
...entry,
|
|
218
|
+
id: entry.id.trim(),
|
|
219
|
+
// A cleared label must not delete the entry (normalize drops
|
|
220
|
+
// label-less entries); fall back to the durable id as the label.
|
|
221
|
+
label: entry.label.trim() || entry.id.trim(),
|
|
222
|
+
description: entry.description?.trim() || undefined,
|
|
223
|
+
assetIds: entry.assetIds.filter(Boolean).slice(0, 4),
|
|
224
|
+
variable: Boolean(entry.variable),
|
|
225
|
+
required: Boolean(entry.variable && entry.required),
|
|
226
|
+
})),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function kebabIdFromLabel(label: string, existingIds: string[] = []): string {
|
|
231
|
+
const base =
|
|
232
|
+
label
|
|
233
|
+
.toLowerCase()
|
|
234
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
235
|
+
.replace(/^-+|-+$/g, "")
|
|
236
|
+
.slice(0, 40)
|
|
237
|
+
.replace(/-+$/g, "") || "reference";
|
|
238
|
+
const existing = new Set(existingIds);
|
|
239
|
+
if (!existing.has(base)) return base;
|
|
240
|
+
for (let index = 2; index < 100; index += 1) {
|
|
241
|
+
const suffix = `-${index}`;
|
|
242
|
+
const candidate = `${base.slice(0, 40 - suffix.length).replace(/-+$/g, "")}${suffix}`;
|
|
243
|
+
if (!existing.has(candidate)) return candidate;
|
|
244
|
+
}
|
|
245
|
+
return base;
|
|
246
|
+
}
|
|
247
|
+
|
|
180
248
|
function formFromPreset(
|
|
181
249
|
preset: any,
|
|
182
250
|
assets: Array<Record<string, any>> = [],
|
|
@@ -224,6 +292,7 @@ function formFromPreset(
|
|
|
224
292
|
skeletonLogoPlacement: skeletonLogoLayer
|
|
225
293
|
? logoPlacementFromLayer(skeletonLogoLayer)
|
|
226
294
|
: "upper-right",
|
|
295
|
+
presetReferences: presetReferencesFromPreset(preset),
|
|
227
296
|
};
|
|
228
297
|
}
|
|
229
298
|
|
|
@@ -239,6 +308,7 @@ function normalizedForm(form: PresetFormState) {
|
|
|
239
308
|
: "0",
|
|
240
309
|
skeletonBackgroundPreviewUrl: "",
|
|
241
310
|
skeletonMaskPreviewUrl: "",
|
|
311
|
+
presetReferences: normalizeReferenceEntries(form.presetReferences),
|
|
242
312
|
};
|
|
243
313
|
}
|
|
244
314
|
|
|
@@ -250,6 +320,30 @@ function previewUrlForAsset(asset: any): string {
|
|
|
250
320
|
);
|
|
251
321
|
}
|
|
252
322
|
|
|
323
|
+
function referenceMode(entry: PresetReference): ReferenceMode {
|
|
324
|
+
if (!entry.variable) return "fixed";
|
|
325
|
+
return entry.required ? "required" : "swappable";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function referenceModePatch(mode: ReferenceMode): Partial<PresetReference> {
|
|
329
|
+
switch (mode) {
|
|
330
|
+
case "required":
|
|
331
|
+
return { variable: true, required: true };
|
|
332
|
+
case "swappable":
|
|
333
|
+
return { variable: true, required: false };
|
|
334
|
+
default:
|
|
335
|
+
return { variable: false, required: false };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function assetDisplayTitle(asset: any): string {
|
|
340
|
+
return asset?.title || asset?.metadata?.originalName || asset?.id || "Asset";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function truncateReferenceLabel(label: string): string {
|
|
344
|
+
return label.trim().slice(0, 60);
|
|
345
|
+
}
|
|
346
|
+
|
|
253
347
|
function isImageAsset(asset: any): boolean {
|
|
254
348
|
return (
|
|
255
349
|
asset?.mediaType === "image" ||
|
|
@@ -288,6 +382,23 @@ function FieldLabel({
|
|
|
288
382
|
);
|
|
289
383
|
}
|
|
290
384
|
|
|
385
|
+
function useMediaQuery(query: string): boolean {
|
|
386
|
+
const [matches, setMatches] = useState(false);
|
|
387
|
+
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (typeof window === "undefined") return;
|
|
390
|
+
const mediaQuery = window.matchMedia(query);
|
|
391
|
+
setMatches(mediaQuery.matches);
|
|
392
|
+
const listener = (event: MediaQueryListEvent) => {
|
|
393
|
+
setMatches(event.matches);
|
|
394
|
+
};
|
|
395
|
+
mediaQuery.addEventListener("change", listener);
|
|
396
|
+
return () => mediaQuery.removeEventListener("change", listener);
|
|
397
|
+
}, [query]);
|
|
398
|
+
|
|
399
|
+
return matches;
|
|
400
|
+
}
|
|
401
|
+
|
|
291
402
|
function SkeletonPreview({ form }: { form: PresetFormState }) {
|
|
292
403
|
const logoLayer = logoLayerFromPlacement(form.skeletonLogoPlacement);
|
|
293
404
|
return (
|
|
@@ -346,8 +457,26 @@ export default function GenerationPresetEditorRoute() {
|
|
|
346
457
|
const [skeletonUploadPending, setSkeletonUploadPending] = useState(false);
|
|
347
458
|
const [skeletonMaskUploadPending, setSkeletonMaskUploadPending] =
|
|
348
459
|
useState(false);
|
|
460
|
+
const [referenceUploadPending, setReferenceUploadPending] = useState<
|
|
461
|
+
string | null
|
|
462
|
+
>(null);
|
|
463
|
+
const [referencePreviewUrls, setReferencePreviewUrls] = useState<
|
|
464
|
+
Record<string, string>
|
|
465
|
+
>({});
|
|
466
|
+
const [activeReferenceIndex, setActiveReferenceIndex] = useState<
|
|
467
|
+
number | null
|
|
468
|
+
>(null);
|
|
469
|
+
const [pickerReferenceIndex, setPickerReferenceIndex] = useState<
|
|
470
|
+
number | null
|
|
471
|
+
>(null);
|
|
472
|
+
const [referenceAssetSearch, setReferenceAssetSearch] = useState("");
|
|
473
|
+
const [autofocusReferenceIndex, setAutofocusReferenceIndex] = useState<
|
|
474
|
+
number | null
|
|
475
|
+
>(null);
|
|
349
476
|
const skeletonFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
350
477
|
const skeletonMaskFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
478
|
+
const referenceUploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
479
|
+
const isSmallEditorViewport = useMediaQuery("(max-width: 640px)");
|
|
351
480
|
|
|
352
481
|
const library = libraryData?.library;
|
|
353
482
|
const assets = useMemo(
|
|
@@ -363,6 +492,12 @@ export default function GenerationPresetEditorRoute() {
|
|
|
363
492
|
);
|
|
364
493
|
const presets = Array.isArray(presetData?.presets) ? presetData.presets : [];
|
|
365
494
|
const preset = presets.find((item: any) => item.id === presetId);
|
|
495
|
+
// Saved entry ids are durable keys referenced by presetReferenceFills and
|
|
496
|
+
// past runs' boardAssignments; renaming a label must never change them.
|
|
497
|
+
const persistedReferenceIds = useMemo(
|
|
498
|
+
() => new Set(presetReferencesFromPreset(preset).map((entry) => entry.id)),
|
|
499
|
+
[preset],
|
|
500
|
+
);
|
|
366
501
|
const loading = libraryLoading || presetsLoading;
|
|
367
502
|
const accessRole = library?.accessRole;
|
|
368
503
|
const readOnly = Boolean(accessRole && !isEditableRole(accessRole));
|
|
@@ -478,6 +613,34 @@ export default function GenerationPresetEditorRoute() {
|
|
|
478
613
|
JSON.stringify(normalizedForm(form)) !==
|
|
479
614
|
JSON.stringify(normalizedForm(initialForm)),
|
|
480
615
|
);
|
|
616
|
+
const referenceImageCounts = useMemo(() => {
|
|
617
|
+
const entries = form?.presetReferences ?? [];
|
|
618
|
+
return {
|
|
619
|
+
total: entries.reduce((sum, entry) => sum + entry.assetIds.length, 0),
|
|
620
|
+
subject: entries
|
|
621
|
+
.filter((entry) => entry.role === "subject")
|
|
622
|
+
.reduce((sum, entry) => sum + entry.assetIds.length, 0),
|
|
623
|
+
};
|
|
624
|
+
}, [form?.presetReferences]);
|
|
625
|
+
const activeReferenceEntry =
|
|
626
|
+
activeReferenceIndex == null
|
|
627
|
+
? null
|
|
628
|
+
: (form?.presetReferences[activeReferenceIndex] ?? null);
|
|
629
|
+
const pickerReferenceEntry =
|
|
630
|
+
pickerReferenceIndex == null
|
|
631
|
+
? null
|
|
632
|
+
: (form?.presetReferences[pickerReferenceIndex] ?? null);
|
|
633
|
+
const filteredReferenceAssets = useMemo(() => {
|
|
634
|
+
const query = referenceAssetSearch.trim().toLowerCase();
|
|
635
|
+
if (!query) return skeletonBackgroundAssets;
|
|
636
|
+
return skeletonBackgroundAssets.filter((asset: any) =>
|
|
637
|
+
[assetDisplayTitle(asset), asset.id, asset.description]
|
|
638
|
+
.filter((value): value is string => typeof value === "string")
|
|
639
|
+
.join("\n")
|
|
640
|
+
.toLowerCase()
|
|
641
|
+
.includes(query),
|
|
642
|
+
);
|
|
643
|
+
}, [referenceAssetSearch, skeletonBackgroundAssets]);
|
|
481
644
|
const settingsHref = libraryId
|
|
482
645
|
? `/library/${encodeURIComponent(libraryId)}?tab=settings`
|
|
483
646
|
: "/library";
|
|
@@ -579,6 +742,217 @@ export default function GenerationPresetEditorRoute() {
|
|
|
579
742
|
});
|
|
580
743
|
}
|
|
581
744
|
|
|
745
|
+
function addPresetReference() {
|
|
746
|
+
if (!form || form.presetReferences.length >= 6 || readOnly) return;
|
|
747
|
+
const label = t("brandKitDetail.referenceDefaultLabel");
|
|
748
|
+
const id = kebabIdFromLabel(
|
|
749
|
+
label,
|
|
750
|
+
form.presetReferences.map((entry) => entry.id),
|
|
751
|
+
);
|
|
752
|
+
const nextIndex = form.presetReferences.length;
|
|
753
|
+
setForm({
|
|
754
|
+
...form,
|
|
755
|
+
presetReferences: [
|
|
756
|
+
...form.presetReferences,
|
|
757
|
+
{
|
|
758
|
+
id,
|
|
759
|
+
label,
|
|
760
|
+
role: "subject",
|
|
761
|
+
assetIds: [],
|
|
762
|
+
variable: true,
|
|
763
|
+
required: false,
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
});
|
|
767
|
+
setActiveReferenceIndex(nextIndex);
|
|
768
|
+
setPickerReferenceIndex(nextIndex);
|
|
769
|
+
setReferenceAssetSearch("");
|
|
770
|
+
setAutofocusReferenceIndex(nextIndex);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function updatePresetReference(
|
|
774
|
+
index: number,
|
|
775
|
+
patch: Partial<PresetReference>,
|
|
776
|
+
) {
|
|
777
|
+
setForm((current) => {
|
|
778
|
+
if (!current) return current;
|
|
779
|
+
const entries = current.presetReferences.map((entry, entryIndex) => {
|
|
780
|
+
if (entryIndex !== index) return entry;
|
|
781
|
+
const next = { ...entry, ...patch };
|
|
782
|
+
if (patch.label !== undefined && !persistedReferenceIds.has(entry.id)) {
|
|
783
|
+
next.id = kebabIdFromLabel(
|
|
784
|
+
patch.label,
|
|
785
|
+
current.presetReferences
|
|
786
|
+
.filter((_, otherIndex) => otherIndex !== index)
|
|
787
|
+
.map((item) => item.id),
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (!next.variable) {
|
|
791
|
+
next.required = false;
|
|
792
|
+
}
|
|
793
|
+
return next;
|
|
794
|
+
});
|
|
795
|
+
return { ...current, presetReferences: entries };
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function removePresetReference(index: number) {
|
|
800
|
+
setActiveReferenceIndex(null);
|
|
801
|
+
setPickerReferenceIndex(null);
|
|
802
|
+
setForm((current) =>
|
|
803
|
+
current
|
|
804
|
+
? {
|
|
805
|
+
...current,
|
|
806
|
+
presetReferences: current.presetReferences.filter(
|
|
807
|
+
(_, entryIndex) => entryIndex !== index,
|
|
808
|
+
),
|
|
809
|
+
}
|
|
810
|
+
: current,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function addReferenceAsset(index: number, assetId: string, title?: string) {
|
|
815
|
+
const entry = form?.presetReferences[index];
|
|
816
|
+
const asset = skeletonBackgroundAssets.find(
|
|
817
|
+
(item: any) => item.id === assetId,
|
|
818
|
+
);
|
|
819
|
+
if (!entry || entry.assetIds.includes(assetId)) return;
|
|
820
|
+
if (entry.assetIds.length >= 4) return;
|
|
821
|
+
if (referenceImageCounts.total >= 8) {
|
|
822
|
+
toast.error(t("brandKitDetail.referenceBoardTotalLimitError"));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (entry.role === "subject" && referenceImageCounts.subject >= 4) {
|
|
826
|
+
toast.error(t("brandKitDetail.referenceBoardSubjectLimitError"));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
setForm((current) => {
|
|
830
|
+
if (!current) return current;
|
|
831
|
+
const entries = current.presetReferences.map((entry, entryIndex) => {
|
|
832
|
+
if (
|
|
833
|
+
entryIndex !== index ||
|
|
834
|
+
entry.assetIds.includes(assetId) ||
|
|
835
|
+
entry.assetIds.length >= 4
|
|
836
|
+
) {
|
|
837
|
+
return entry;
|
|
838
|
+
}
|
|
839
|
+
const defaultLabel = t("brandKitDetail.referenceDefaultLabel");
|
|
840
|
+
const shouldPrefillLabel =
|
|
841
|
+
entry.assetIds.length === 0 && entry.label.trim() === defaultLabel;
|
|
842
|
+
const nextLabel = shouldPrefillLabel
|
|
843
|
+
? truncateReferenceLabel(title || assetDisplayTitle(asset)) ||
|
|
844
|
+
entry.label
|
|
845
|
+
: entry.label;
|
|
846
|
+
return {
|
|
847
|
+
...entry,
|
|
848
|
+
id: shouldPrefillLabel
|
|
849
|
+
? kebabIdFromLabel(
|
|
850
|
+
nextLabel,
|
|
851
|
+
current.presetReferences
|
|
852
|
+
.filter((_, otherIndex) => otherIndex !== index)
|
|
853
|
+
.map((item) => item.id),
|
|
854
|
+
)
|
|
855
|
+
: entry.id,
|
|
856
|
+
label: nextLabel,
|
|
857
|
+
assetIds: [...entry.assetIds, assetId],
|
|
858
|
+
};
|
|
859
|
+
});
|
|
860
|
+
return { ...current, presetReferences: entries };
|
|
861
|
+
});
|
|
862
|
+
setPickerReferenceIndex(null);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function removeReferenceAsset(index: number, assetId: string) {
|
|
866
|
+
setForm((current) =>
|
|
867
|
+
current
|
|
868
|
+
? {
|
|
869
|
+
...current,
|
|
870
|
+
presetReferences: current.presetReferences.map(
|
|
871
|
+
(entry, entryIndex) =>
|
|
872
|
+
entryIndex === index
|
|
873
|
+
? {
|
|
874
|
+
...entry,
|
|
875
|
+
assetIds: entry.assetIds.filter((id) => id !== assetId),
|
|
876
|
+
}
|
|
877
|
+
: entry,
|
|
878
|
+
),
|
|
879
|
+
}
|
|
880
|
+
: current,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function uploadReferenceImage(files: FileList | null, index: number) {
|
|
885
|
+
if (!files?.length || !libraryId || readOnly || referenceUploadPending) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const entry = form?.presetReferences[index];
|
|
889
|
+
if (!entry || entry.assetIds.length >= 4) return;
|
|
890
|
+
if (referenceImageCounts.total >= 8) {
|
|
891
|
+
toast.error(t("brandKitDetail.referenceBoardTotalLimitError"));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (entry.role === "subject" && referenceImageCounts.subject >= 4) {
|
|
895
|
+
toast.error(t("brandKitDetail.referenceBoardSubjectLimitError"));
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const file = files[0];
|
|
899
|
+
const localPreviewUrl = URL.createObjectURL(file);
|
|
900
|
+
const body = new FormData();
|
|
901
|
+
body.append("libraryId", libraryId);
|
|
902
|
+
body.append(
|
|
903
|
+
"category",
|
|
904
|
+
entry.role === "product"
|
|
905
|
+
? "product"
|
|
906
|
+
: entry.role === "subject"
|
|
907
|
+
? "other"
|
|
908
|
+
: "style-only",
|
|
909
|
+
);
|
|
910
|
+
if (entry.role === "subject") {
|
|
911
|
+
body.append("intent", "subject");
|
|
912
|
+
}
|
|
913
|
+
body.append("files", file);
|
|
914
|
+
setReferenceUploadPending(entry.id);
|
|
915
|
+
try {
|
|
916
|
+
const response = await fetch(`${appBasePath()}/api/assets/upload`, {
|
|
917
|
+
method: "POST",
|
|
918
|
+
body,
|
|
919
|
+
});
|
|
920
|
+
if (!response.ok) {
|
|
921
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
922
|
+
error?: string;
|
|
923
|
+
} | null;
|
|
924
|
+
throw new Error(
|
|
925
|
+
errorBody?.error || `Upload failed (${response.status})`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const result = (await response
|
|
929
|
+
.json()
|
|
930
|
+
.catch(() => null)) as AssetUploadResult | null;
|
|
931
|
+
const assetId = uploadedSkeletonAssetId(result);
|
|
932
|
+
if (!assetId) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
result?.errors?.[0]?.message ??
|
|
935
|
+
t("brandKitDetail.couldNotUploadReferenceImage"),
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
addReferenceAsset(index, assetId, file.name);
|
|
939
|
+
setReferencePreviewUrls((current) => ({
|
|
940
|
+
...current,
|
|
941
|
+
[assetId]: localPreviewUrl,
|
|
942
|
+
}));
|
|
943
|
+
toast.success(t("brandKitDetail.referenceImageUploaded"));
|
|
944
|
+
} catch (error) {
|
|
945
|
+
URL.revokeObjectURL(localPreviewUrl);
|
|
946
|
+
toast.error(
|
|
947
|
+
error instanceof Error
|
|
948
|
+
? error.message
|
|
949
|
+
: t("brandKitDetail.couldNotUploadReferenceImage"),
|
|
950
|
+
);
|
|
951
|
+
} finally {
|
|
952
|
+
setReferenceUploadPending(null);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
582
956
|
async function uploadSkeletonImage(
|
|
583
957
|
files: FileList | null,
|
|
584
958
|
target: "background" | "mask" = "background",
|
|
@@ -670,6 +1044,14 @@ export default function GenerationPresetEditorRoute() {
|
|
|
670
1044
|
toast.error(t("brandKitDetail.skeletonImageRequired"));
|
|
671
1045
|
return;
|
|
672
1046
|
}
|
|
1047
|
+
if (referenceImageCounts.total > 8) {
|
|
1048
|
+
toast.error(t("brandKitDetail.referenceBoardTotalLimitError"));
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (referenceImageCounts.subject > 4) {
|
|
1052
|
+
toast.error(t("brandKitDetail.referenceBoardSubjectLimitError"));
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
673
1055
|
try {
|
|
674
1056
|
const saved = await updatePreset.mutateAsync({
|
|
675
1057
|
id: preset.id,
|
|
@@ -688,6 +1070,9 @@ export default function GenerationPresetEditorRoute() {
|
|
|
688
1070
|
normalized,
|
|
689
1071
|
skeletonFromPreset(preset),
|
|
690
1072
|
),
|
|
1073
|
+
presetReferences: normalized.presetReferences.length
|
|
1074
|
+
? normalized.presetReferences
|
|
1075
|
+
: null,
|
|
691
1076
|
},
|
|
692
1077
|
collectionId: normalized.collectionId,
|
|
693
1078
|
sortOrder: Number(normalized.sortOrder),
|
|
@@ -729,6 +1114,332 @@ export default function GenerationPresetEditorRoute() {
|
|
|
729
1114
|
}
|
|
730
1115
|
}
|
|
731
1116
|
|
|
1117
|
+
const referenceUploadTargetIndex =
|
|
1118
|
+
pickerReferenceIndex ?? activeReferenceIndex;
|
|
1119
|
+
const referenceUploadTarget =
|
|
1120
|
+
referenceUploadTargetIndex == null
|
|
1121
|
+
? null
|
|
1122
|
+
: (form?.presetReferences[referenceUploadTargetIndex] ?? null);
|
|
1123
|
+
const referenceUploadDisabled =
|
|
1124
|
+
readOnly ||
|
|
1125
|
+
referenceUploadTargetIndex == null ||
|
|
1126
|
+
!referenceUploadTarget ||
|
|
1127
|
+
referenceUploadTarget.assetIds.length >= 4 ||
|
|
1128
|
+
Boolean(referenceUploadPending);
|
|
1129
|
+
|
|
1130
|
+
function openReferencePicker(index: number) {
|
|
1131
|
+
setPickerReferenceIndex(index);
|
|
1132
|
+
setReferenceAssetSearch("");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function renderReferenceEditor(entry: PresetReference, index: number) {
|
|
1136
|
+
const uploadPending = referenceUploadPending === entry.id;
|
|
1137
|
+
const mode = referenceMode(entry);
|
|
1138
|
+
return (
|
|
1139
|
+
<div className="grid gap-4">
|
|
1140
|
+
<div className="grid gap-2">
|
|
1141
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
1142
|
+
<div className="text-xs font-medium text-muted-foreground">
|
|
1143
|
+
{t("brandKitDetail.referenceImageCount", {
|
|
1144
|
+
count: entry.assetIds.length,
|
|
1145
|
+
max: 4,
|
|
1146
|
+
})}
|
|
1147
|
+
</div>
|
|
1148
|
+
<div className="flex shrink-0 gap-2">
|
|
1149
|
+
<Button
|
|
1150
|
+
type="button"
|
|
1151
|
+
variant="outline"
|
|
1152
|
+
size="sm"
|
|
1153
|
+
className="h-8 gap-2"
|
|
1154
|
+
disabled={readOnly || entry.assetIds.length >= 4}
|
|
1155
|
+
onClick={() => openReferencePicker(index)}
|
|
1156
|
+
>
|
|
1157
|
+
<IconPhotoPlus className="h-4 w-4" />
|
|
1158
|
+
{t("brandKitDetail.chooseReferenceImage")}
|
|
1159
|
+
</Button>
|
|
1160
|
+
<Button
|
|
1161
|
+
type="button"
|
|
1162
|
+
variant="outline"
|
|
1163
|
+
size="sm"
|
|
1164
|
+
className="h-8 gap-2"
|
|
1165
|
+
disabled={
|
|
1166
|
+
readOnly || uploadPending || entry.assetIds.length >= 4
|
|
1167
|
+
}
|
|
1168
|
+
onClick={() => referenceUploadInputRef.current?.click()}
|
|
1169
|
+
>
|
|
1170
|
+
{uploadPending ? (
|
|
1171
|
+
<Spinner className="h-4 w-4" />
|
|
1172
|
+
) : (
|
|
1173
|
+
<IconUpload className="h-4 w-4" />
|
|
1174
|
+
)}
|
|
1175
|
+
{uploadPending
|
|
1176
|
+
? t("brandKitDetail.uploadingReferenceImage")
|
|
1177
|
+
: t("brandKitDetail.uploadReferenceImage")}
|
|
1178
|
+
</Button>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
<div className="grid grid-cols-4 gap-2">
|
|
1182
|
+
{entry.assetIds.map((assetId) => {
|
|
1183
|
+
const asset = assets.find((item: any) => item.id === assetId);
|
|
1184
|
+
const src =
|
|
1185
|
+
referencePreviewUrls[assetId] || previewUrlForAsset(asset);
|
|
1186
|
+
return (
|
|
1187
|
+
<div
|
|
1188
|
+
key={assetId}
|
|
1189
|
+
className="group relative aspect-square overflow-hidden rounded-md border border-border bg-muted"
|
|
1190
|
+
>
|
|
1191
|
+
{src ? (
|
|
1192
|
+
<img
|
|
1193
|
+
src={src}
|
|
1194
|
+
alt={assetDisplayTitle(asset)}
|
|
1195
|
+
className="h-full w-full object-cover"
|
|
1196
|
+
/>
|
|
1197
|
+
) : (
|
|
1198
|
+
<div className="flex h-full items-center justify-center px-2 text-center text-[11px] text-muted-foreground">
|
|
1199
|
+
{t("brandKitDetail.previewUnavailable")}
|
|
1200
|
+
</div>
|
|
1201
|
+
)}
|
|
1202
|
+
<Button
|
|
1203
|
+
type="button"
|
|
1204
|
+
variant="secondary"
|
|
1205
|
+
size="icon"
|
|
1206
|
+
className="absolute right-1 top-1 h-6 w-6 opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"
|
|
1207
|
+
disabled={readOnly}
|
|
1208
|
+
onClick={() => removeReferenceAsset(index, assetId)}
|
|
1209
|
+
aria-label={t("brandKitDetail.removeReferenceImage")}
|
|
1210
|
+
>
|
|
1211
|
+
<IconX className="h-3.5 w-3.5" />
|
|
1212
|
+
</Button>
|
|
1213
|
+
</div>
|
|
1214
|
+
);
|
|
1215
|
+
})}
|
|
1216
|
+
{entry.assetIds.length === 0 ? (
|
|
1217
|
+
<button
|
|
1218
|
+
type="button"
|
|
1219
|
+
className="col-span-4 flex aspect-[4/1] items-center justify-center rounded-md border border-dashed border-border bg-muted/40 px-3 text-center text-xs text-muted-foreground transition hover:border-foreground/30 hover:text-foreground"
|
|
1220
|
+
disabled={readOnly}
|
|
1221
|
+
onClick={() => openReferencePicker(index)}
|
|
1222
|
+
>
|
|
1223
|
+
{entry.variable
|
|
1224
|
+
? t("brandKitDetail.filledPerGeneration")
|
|
1225
|
+
: t("brandKitDetail.noReferenceImages")}
|
|
1226
|
+
</button>
|
|
1227
|
+
) : null}
|
|
1228
|
+
</div>
|
|
1229
|
+
</div>
|
|
1230
|
+
|
|
1231
|
+
<div className="grid gap-2">
|
|
1232
|
+
<FieldLabel htmlFor={`reference-label-${entry.id}`}>
|
|
1233
|
+
{t("brandKitDetail.referenceLabel")}
|
|
1234
|
+
</FieldLabel>
|
|
1235
|
+
<Input
|
|
1236
|
+
id={`reference-label-${entry.id}`}
|
|
1237
|
+
value={entry.label}
|
|
1238
|
+
maxLength={60}
|
|
1239
|
+
disabled={readOnly}
|
|
1240
|
+
autoFocus={autofocusReferenceIndex === index}
|
|
1241
|
+
onFocus={() => setAutofocusReferenceIndex(null)}
|
|
1242
|
+
onChange={(event) =>
|
|
1243
|
+
updatePresetReference(index, {
|
|
1244
|
+
label: event.target.value,
|
|
1245
|
+
})
|
|
1246
|
+
}
|
|
1247
|
+
/>
|
|
1248
|
+
</div>
|
|
1249
|
+
|
|
1250
|
+
<div className="grid gap-2">
|
|
1251
|
+
<FieldLabel>{t("brandKitDetail.referenceRoleLabel")}</FieldLabel>
|
|
1252
|
+
<Select
|
|
1253
|
+
value={entry.role}
|
|
1254
|
+
disabled={readOnly}
|
|
1255
|
+
onValueChange={(value) =>
|
|
1256
|
+
updatePresetReference(index, {
|
|
1257
|
+
role: value as PresetReferenceRole,
|
|
1258
|
+
})
|
|
1259
|
+
}
|
|
1260
|
+
>
|
|
1261
|
+
<SelectTrigger>
|
|
1262
|
+
<SelectValue />
|
|
1263
|
+
</SelectTrigger>
|
|
1264
|
+
<SelectContent>
|
|
1265
|
+
{PRESET_REFERENCE_ROLES.map((role) => (
|
|
1266
|
+
<SelectItem key={role} value={role}>
|
|
1267
|
+
{t(`brandKitDetail.referenceRole_${role}` as any)}
|
|
1268
|
+
</SelectItem>
|
|
1269
|
+
))}
|
|
1270
|
+
</SelectContent>
|
|
1271
|
+
</Select>
|
|
1272
|
+
</div>
|
|
1273
|
+
|
|
1274
|
+
<div className="grid gap-2">
|
|
1275
|
+
<FieldLabel htmlFor={`reference-description-${entry.id}`}>
|
|
1276
|
+
{t("brandKitDetail.referenceDescription")}
|
|
1277
|
+
</FieldLabel>
|
|
1278
|
+
<Textarea
|
|
1279
|
+
id={`reference-description-${entry.id}`}
|
|
1280
|
+
value={entry.description ?? ""}
|
|
1281
|
+
maxLength={400}
|
|
1282
|
+
disabled={readOnly}
|
|
1283
|
+
placeholder={t("brandKitDetail.referenceDescriptionPlaceholder")}
|
|
1284
|
+
onChange={(event) =>
|
|
1285
|
+
updatePresetReference(index, {
|
|
1286
|
+
description: event.target.value,
|
|
1287
|
+
})
|
|
1288
|
+
}
|
|
1289
|
+
/>
|
|
1290
|
+
</div>
|
|
1291
|
+
|
|
1292
|
+
<div className="grid gap-2">
|
|
1293
|
+
<FieldLabel>{t("brandKitDetail.referenceMode")}</FieldLabel>
|
|
1294
|
+
<div
|
|
1295
|
+
className="grid grid-cols-3 rounded-md border border-border bg-muted/30 p-1"
|
|
1296
|
+
role="radiogroup"
|
|
1297
|
+
>
|
|
1298
|
+
{(["fixed", "swappable", "required"] as const).map((item) => {
|
|
1299
|
+
const selected = mode === item;
|
|
1300
|
+
const label =
|
|
1301
|
+
item === "fixed"
|
|
1302
|
+
? t("brandKitDetail.fixedReference")
|
|
1303
|
+
: item === "swappable"
|
|
1304
|
+
? t("brandKitDetail.swappableReference")
|
|
1305
|
+
: t("brandKitDetail.requiredPerRun");
|
|
1306
|
+
return (
|
|
1307
|
+
<button
|
|
1308
|
+
key={item}
|
|
1309
|
+
type="button"
|
|
1310
|
+
role="radio"
|
|
1311
|
+
aria-checked={selected}
|
|
1312
|
+
disabled={readOnly}
|
|
1313
|
+
className={cn(
|
|
1314
|
+
"h-8 rounded-sm px-2 text-xs font-medium text-muted-foreground transition",
|
|
1315
|
+
selected
|
|
1316
|
+
? "bg-background text-foreground shadow-sm"
|
|
1317
|
+
: "hover:text-foreground",
|
|
1318
|
+
)}
|
|
1319
|
+
onClick={() =>
|
|
1320
|
+
updatePresetReference(index, referenceModePatch(item))
|
|
1321
|
+
}
|
|
1322
|
+
>
|
|
1323
|
+
<span className="block truncate">{label}</span>
|
|
1324
|
+
</button>
|
|
1325
|
+
);
|
|
1326
|
+
})}
|
|
1327
|
+
</div>
|
|
1328
|
+
</div>
|
|
1329
|
+
|
|
1330
|
+
<div className="flex items-center justify-between gap-3 border-t pt-3">
|
|
1331
|
+
<Button
|
|
1332
|
+
type="button"
|
|
1333
|
+
variant="ghost"
|
|
1334
|
+
className="h-8 px-0 text-destructive hover:text-destructive"
|
|
1335
|
+
disabled={readOnly}
|
|
1336
|
+
onClick={() => removePresetReference(index)}
|
|
1337
|
+
>
|
|
1338
|
+
{t("brandKitDetail.removeReference")}
|
|
1339
|
+
</Button>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function renderReferenceTile(entry: PresetReference, index: number) {
|
|
1346
|
+
const firstAssetId = entry.assetIds[0];
|
|
1347
|
+
const firstAsset = firstAssetId
|
|
1348
|
+
? assets.find((item: any) => item.id === firstAssetId)
|
|
1349
|
+
: null;
|
|
1350
|
+
const src =
|
|
1351
|
+
(firstAssetId && referencePreviewUrls[firstAssetId]) ||
|
|
1352
|
+
previewUrlForAsset(firstAsset);
|
|
1353
|
+
const variableEmpty = entry.variable && entry.assetIds.length === 0;
|
|
1354
|
+
const tileButton = (
|
|
1355
|
+
<button
|
|
1356
|
+
type="button"
|
|
1357
|
+
className={cn(
|
|
1358
|
+
"group block min-w-0 text-left outline-none",
|
|
1359
|
+
readOnly && "cursor-default",
|
|
1360
|
+
)}
|
|
1361
|
+
disabled={readOnly}
|
|
1362
|
+
onClick={() => setActiveReferenceIndex(index)}
|
|
1363
|
+
onKeyDown={(event) => {
|
|
1364
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1365
|
+
event.preventDefault();
|
|
1366
|
+
setActiveReferenceIndex(index);
|
|
1367
|
+
}
|
|
1368
|
+
}}
|
|
1369
|
+
>
|
|
1370
|
+
<div
|
|
1371
|
+
className={cn(
|
|
1372
|
+
"relative aspect-square overflow-hidden rounded-md border border-border bg-muted transition group-hover:border-foreground/30 group-focus-visible:ring-2 group-focus-visible:ring-ring",
|
|
1373
|
+
entry.variable && "border-dashed",
|
|
1374
|
+
variableEmpty && "bg-muted/30",
|
|
1375
|
+
)}
|
|
1376
|
+
>
|
|
1377
|
+
{src && !variableEmpty ? (
|
|
1378
|
+
<img
|
|
1379
|
+
src={src}
|
|
1380
|
+
alt={entry.label}
|
|
1381
|
+
className="h-full w-full object-cover transition group-hover:scale-[1.02]"
|
|
1382
|
+
/>
|
|
1383
|
+
) : (
|
|
1384
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 px-3 text-center text-muted-foreground">
|
|
1385
|
+
<IconUserPlus className="h-8 w-8" />
|
|
1386
|
+
<span className="text-xs font-medium">
|
|
1387
|
+
{variableEmpty
|
|
1388
|
+
? t("brandKitDetail.filledPerGeneration")
|
|
1389
|
+
: t("brandKitDetail.noReferenceImages")}
|
|
1390
|
+
</span>
|
|
1391
|
+
</div>
|
|
1392
|
+
)}
|
|
1393
|
+
{entry.assetIds.length > 1 ? (
|
|
1394
|
+
<Badge className="absolute right-2 top-2 h-6 px-2 shadow-sm">
|
|
1395
|
+
{t("brandKitDetail.moreImagesCount", {
|
|
1396
|
+
count: entry.assetIds.length - 1,
|
|
1397
|
+
})}
|
|
1398
|
+
</Badge>
|
|
1399
|
+
) : null}
|
|
1400
|
+
{entry.required ? (
|
|
1401
|
+
<Badge
|
|
1402
|
+
variant="secondary"
|
|
1403
|
+
className="absolute left-2 top-2 h-6 px-2 shadow-sm"
|
|
1404
|
+
>
|
|
1405
|
+
{t("brandKitDetail.required")}
|
|
1406
|
+
</Badge>
|
|
1407
|
+
) : null}
|
|
1408
|
+
<Badge className="absolute bottom-2 left-2 max-w-[calc(100%-1rem)] truncate px-2 shadow-sm">
|
|
1409
|
+
{t(`brandKitDetail.referenceRole_${entry.role}` as any)}
|
|
1410
|
+
</Badge>
|
|
1411
|
+
</div>
|
|
1412
|
+
<div className="mt-2 min-w-0">
|
|
1413
|
+
<div className="truncate text-sm font-medium">
|
|
1414
|
+
{entry.label || t("brandKitDetail.referenceDefaultLabel")}
|
|
1415
|
+
</div>
|
|
1416
|
+
{variableEmpty ? (
|
|
1417
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
1418
|
+
{t("brandKitDetail.filledPerGeneration")}
|
|
1419
|
+
</div>
|
|
1420
|
+
) : null}
|
|
1421
|
+
</div>
|
|
1422
|
+
</button>
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1425
|
+
if (isSmallEditorViewport) return tileButton;
|
|
1426
|
+
|
|
1427
|
+
return (
|
|
1428
|
+
<Popover
|
|
1429
|
+
open={activeReferenceIndex === index}
|
|
1430
|
+
onOpenChange={(open) => {
|
|
1431
|
+
setActiveReferenceIndex(open ? index : null);
|
|
1432
|
+
if (!open) setAutofocusReferenceIndex(null);
|
|
1433
|
+
}}
|
|
1434
|
+
>
|
|
1435
|
+
<PopoverTrigger asChild>{tileButton}</PopoverTrigger>
|
|
1436
|
+
<PopoverContent align="start" className="w-[420px] max-w-[90vw] p-4">
|
|
1437
|
+
{renderReferenceEditor(entry, index)}
|
|
1438
|
+
</PopoverContent>
|
|
1439
|
+
</Popover>
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
732
1443
|
if (loading) {
|
|
733
1444
|
return (
|
|
734
1445
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-6">
|
|
@@ -909,6 +1620,94 @@ export default function GenerationPresetEditorRoute() {
|
|
|
909
1620
|
</Select>
|
|
910
1621
|
</div>
|
|
911
1622
|
</div>
|
|
1623
|
+
<div className="grid min-w-0 gap-4 rounded-md border border-border p-4">
|
|
1624
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
1625
|
+
<div className="min-w-0 flex-1">
|
|
1626
|
+
<div className="text-sm font-medium">
|
|
1627
|
+
{t("brandKitDetail.referenceBoard")}
|
|
1628
|
+
</div>
|
|
1629
|
+
{form.presetReferences.length ? (
|
|
1630
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
1631
|
+
<Badge variant="outline">
|
|
1632
|
+
{t("brandKitDetail.referenceImageCount", {
|
|
1633
|
+
count: referenceImageCounts.total,
|
|
1634
|
+
max: 8,
|
|
1635
|
+
})}
|
|
1636
|
+
</Badge>
|
|
1637
|
+
<Badge variant="outline">
|
|
1638
|
+
{t("brandKitDetail.referenceSubjectImageCount", {
|
|
1639
|
+
count: referenceImageCounts.subject,
|
|
1640
|
+
max: 4,
|
|
1641
|
+
})}
|
|
1642
|
+
</Badge>
|
|
1643
|
+
</div>
|
|
1644
|
+
) : (
|
|
1645
|
+
<div className="mt-2 max-w-xl text-sm text-muted-foreground">
|
|
1646
|
+
{t("brandKitDetail.referenceBoardEmptyDescription")}
|
|
1647
|
+
</div>
|
|
1648
|
+
)}
|
|
1649
|
+
</div>
|
|
1650
|
+
<div className="grid w-full min-w-0 gap-2 md:w-60">
|
|
1651
|
+
<FieldLabel>{t("brandKitDetail.referencePolicy")}</FieldLabel>
|
|
1652
|
+
<Select
|
|
1653
|
+
value={form.referencePolicy}
|
|
1654
|
+
disabled={readOnly}
|
|
1655
|
+
onValueChange={(value) =>
|
|
1656
|
+
updateForm({
|
|
1657
|
+
referencePolicy: value as GenerationPresetReferencePolicy,
|
|
1658
|
+
})
|
|
1659
|
+
}
|
|
1660
|
+
>
|
|
1661
|
+
<SelectTrigger>
|
|
1662
|
+
<SelectValue />
|
|
1663
|
+
</SelectTrigger>
|
|
1664
|
+
<SelectContent>
|
|
1665
|
+
{GENERATION_PRESET_REFERENCE_POLICIES.map((item) => (
|
|
1666
|
+
<SelectItem key={item} value={item}>
|
|
1667
|
+
{item}
|
|
1668
|
+
</SelectItem>
|
|
1669
|
+
))}
|
|
1670
|
+
</SelectContent>
|
|
1671
|
+
</Select>
|
|
1672
|
+
</div>
|
|
1673
|
+
</div>
|
|
1674
|
+
|
|
1675
|
+
{form.presetReferences.length === 0 ? (
|
|
1676
|
+
<Button
|
|
1677
|
+
type="button"
|
|
1678
|
+
variant="outline"
|
|
1679
|
+
className="w-fit gap-2"
|
|
1680
|
+
disabled={readOnly}
|
|
1681
|
+
onClick={addPresetReference}
|
|
1682
|
+
>
|
|
1683
|
+
<IconPlus className="h-4 w-4" />
|
|
1684
|
+
{t("brandKitDetail.addReference")}
|
|
1685
|
+
</Button>
|
|
1686
|
+
) : (
|
|
1687
|
+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
1688
|
+
{form.presetReferences.map((entry, index) => (
|
|
1689
|
+
<div key={entry.id} className="min-w-0">
|
|
1690
|
+
{renderReferenceTile(entry, index)}
|
|
1691
|
+
</div>
|
|
1692
|
+
))}
|
|
1693
|
+
{form.presetReferences.length < 6 ? (
|
|
1694
|
+
<button
|
|
1695
|
+
type="button"
|
|
1696
|
+
className="group block min-w-0 text-left outline-none"
|
|
1697
|
+
disabled={readOnly}
|
|
1698
|
+
onClick={addPresetReference}
|
|
1699
|
+
>
|
|
1700
|
+
<div className="flex aspect-square items-center justify-center rounded-md border border-dashed border-border bg-muted/30 text-muted-foreground transition group-hover:border-foreground/30 group-hover:text-foreground group-focus-visible:ring-2 group-focus-visible:ring-ring">
|
|
1701
|
+
<IconPlus className="h-8 w-8" />
|
|
1702
|
+
</div>
|
|
1703
|
+
<div className="mt-2 truncate text-sm font-medium">
|
|
1704
|
+
{t("brandKitDetail.addReference")}
|
|
1705
|
+
</div>
|
|
1706
|
+
</button>
|
|
1707
|
+
) : null}
|
|
1708
|
+
</div>
|
|
1709
|
+
)}
|
|
1710
|
+
</div>
|
|
912
1711
|
<div className="grid gap-2">
|
|
913
1712
|
<FieldLabel htmlFor="preset-template">
|
|
914
1713
|
{t("brandKitDetail.promptTemplate")}
|
|
@@ -1322,6 +2121,13 @@ export default function GenerationPresetEditorRoute() {
|
|
|
1322
2121
|
))}
|
|
1323
2122
|
</SelectContent>
|
|
1324
2123
|
</Select>
|
|
2124
|
+
{form.presetReferences.some(
|
|
2125
|
+
(entry) => entry.role === "subject",
|
|
2126
|
+
) ? (
|
|
2127
|
+
<p className="text-xs text-muted-foreground">
|
|
2128
|
+
{t("brandKitDetail.subjectReferenceModelTip")}
|
|
2129
|
+
</p>
|
|
2130
|
+
) : null}
|
|
1325
2131
|
</div>
|
|
1326
2132
|
<div className="grid gap-2">
|
|
1327
2133
|
<FieldLabel>{t("brandKitDetail.imageSize")}</FieldLabel>
|
|
@@ -1344,30 +2150,6 @@ export default function GenerationPresetEditorRoute() {
|
|
|
1344
2150
|
</SelectContent>
|
|
1345
2151
|
</Select>
|
|
1346
2152
|
</div>
|
|
1347
|
-
<div className="grid gap-2">
|
|
1348
|
-
<FieldLabel>{t("brandKitDetail.referencePolicy")}</FieldLabel>
|
|
1349
|
-
<Select
|
|
1350
|
-
value={form.referencePolicy}
|
|
1351
|
-
disabled={readOnly}
|
|
1352
|
-
onValueChange={(value) =>
|
|
1353
|
-
updateForm({
|
|
1354
|
-
referencePolicy:
|
|
1355
|
-
value as GenerationPresetReferencePolicy,
|
|
1356
|
-
})
|
|
1357
|
-
}
|
|
1358
|
-
>
|
|
1359
|
-
<SelectTrigger>
|
|
1360
|
-
<SelectValue />
|
|
1361
|
-
</SelectTrigger>
|
|
1362
|
-
<SelectContent>
|
|
1363
|
-
{GENERATION_PRESET_REFERENCE_POLICIES.map((item) => (
|
|
1364
|
-
<SelectItem key={item} value={item}>
|
|
1365
|
-
{item}
|
|
1366
|
-
</SelectItem>
|
|
1367
|
-
))}
|
|
1368
|
-
</SelectContent>
|
|
1369
|
-
</Select>
|
|
1370
|
-
</div>
|
|
1371
2153
|
<div className="grid gap-2">
|
|
1372
2154
|
<FieldLabel>{t("brandKitDetail.collection")}</FieldLabel>
|
|
1373
2155
|
<Select
|
|
@@ -1486,6 +2268,159 @@ export default function GenerationPresetEditorRoute() {
|
|
|
1486
2268
|
</AlertDialogFooter>
|
|
1487
2269
|
</AlertDialogContent>
|
|
1488
2270
|
</AlertDialog>
|
|
2271
|
+
|
|
2272
|
+
<input
|
|
2273
|
+
ref={referenceUploadInputRef}
|
|
2274
|
+
type="file"
|
|
2275
|
+
accept="image/*"
|
|
2276
|
+
className="hidden"
|
|
2277
|
+
disabled={referenceUploadDisabled}
|
|
2278
|
+
onChange={(event) => {
|
|
2279
|
+
if (referenceUploadTargetIndex != null) {
|
|
2280
|
+
void uploadReferenceImage(
|
|
2281
|
+
event.currentTarget.files,
|
|
2282
|
+
referenceUploadTargetIndex,
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
event.currentTarget.value = "";
|
|
2286
|
+
}}
|
|
2287
|
+
/>
|
|
2288
|
+
|
|
2289
|
+
<Sheet
|
|
2290
|
+
open={isSmallEditorViewport && activeReferenceEntry != null}
|
|
2291
|
+
onOpenChange={(open) => {
|
|
2292
|
+
if (!open) {
|
|
2293
|
+
setActiveReferenceIndex(null);
|
|
2294
|
+
setAutofocusReferenceIndex(null);
|
|
2295
|
+
}
|
|
2296
|
+
}}
|
|
2297
|
+
>
|
|
2298
|
+
<SheetContent side="bottom" className="max-h-[90vh] overflow-auto">
|
|
2299
|
+
<SheetHeader>
|
|
2300
|
+
<SheetTitle>
|
|
2301
|
+
{activeReferenceEntry?.label ||
|
|
2302
|
+
t("brandKitDetail.referenceDefaultLabel")}
|
|
2303
|
+
</SheetTitle>
|
|
2304
|
+
<SheetDescription>
|
|
2305
|
+
{t("brandKitDetail.editReference")}
|
|
2306
|
+
</SheetDescription>
|
|
2307
|
+
</SheetHeader>
|
|
2308
|
+
<div className="mt-4">
|
|
2309
|
+
{activeReferenceEntry && activeReferenceIndex != null
|
|
2310
|
+
? renderReferenceEditor(
|
|
2311
|
+
activeReferenceEntry,
|
|
2312
|
+
activeReferenceIndex,
|
|
2313
|
+
)
|
|
2314
|
+
: null}
|
|
2315
|
+
</div>
|
|
2316
|
+
</SheetContent>
|
|
2317
|
+
</Sheet>
|
|
2318
|
+
|
|
2319
|
+
<Dialog
|
|
2320
|
+
open={pickerReferenceEntry != null}
|
|
2321
|
+
onOpenChange={(open) => {
|
|
2322
|
+
if (!open) {
|
|
2323
|
+
setPickerReferenceIndex(null);
|
|
2324
|
+
setReferenceAssetSearch("");
|
|
2325
|
+
}
|
|
2326
|
+
}}
|
|
2327
|
+
>
|
|
2328
|
+
<DialogContent className="max-w-3xl">
|
|
2329
|
+
<DialogHeader>
|
|
2330
|
+
<DialogTitle>
|
|
2331
|
+
{t("brandKitDetail.chooseReferenceImage")}
|
|
2332
|
+
</DialogTitle>
|
|
2333
|
+
<DialogDescription>
|
|
2334
|
+
{t("brandKitDetail.searchLibraryImages")}
|
|
2335
|
+
</DialogDescription>
|
|
2336
|
+
</DialogHeader>
|
|
2337
|
+
<div className="grid gap-3">
|
|
2338
|
+
<div className="relative">
|
|
2339
|
+
<IconSearch className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
2340
|
+
<Input
|
|
2341
|
+
value={referenceAssetSearch}
|
|
2342
|
+
onChange={(event) =>
|
|
2343
|
+
setReferenceAssetSearch(event.target.value)
|
|
2344
|
+
}
|
|
2345
|
+
placeholder={t("brandKitDetail.searchLibraryImages")}
|
|
2346
|
+
className="pl-9"
|
|
2347
|
+
/>
|
|
2348
|
+
</div>
|
|
2349
|
+
<div className="max-h-[54vh] overflow-auto rounded-md border border-border p-3">
|
|
2350
|
+
{filteredReferenceAssets.length ? (
|
|
2351
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
|
|
2352
|
+
{filteredReferenceAssets.map((asset: any) => {
|
|
2353
|
+
const alreadyAdded = Boolean(
|
|
2354
|
+
pickerReferenceEntry?.assetIds.includes(asset.id),
|
|
2355
|
+
);
|
|
2356
|
+
const src = previewUrlForAsset(asset);
|
|
2357
|
+
return (
|
|
2358
|
+
<button
|
|
2359
|
+
key={asset.id}
|
|
2360
|
+
type="button"
|
|
2361
|
+
disabled={alreadyAdded || readOnly}
|
|
2362
|
+
title={`${assetDisplayTitle(asset)} · ${asset.id}`}
|
|
2363
|
+
className={cn(
|
|
2364
|
+
"group min-w-0 rounded-md border border-border bg-background p-1 text-left outline-none transition hover:border-foreground/30 focus-visible:ring-2 focus-visible:ring-ring",
|
|
2365
|
+
alreadyAdded && "cursor-default opacity-65",
|
|
2366
|
+
)}
|
|
2367
|
+
onClick={() => {
|
|
2368
|
+
if (pickerReferenceIndex == null) return;
|
|
2369
|
+
addReferenceAsset(pickerReferenceIndex, asset.id);
|
|
2370
|
+
}}
|
|
2371
|
+
>
|
|
2372
|
+
<div className="relative aspect-square overflow-hidden rounded-sm bg-muted">
|
|
2373
|
+
{src ? (
|
|
2374
|
+
<img
|
|
2375
|
+
src={src}
|
|
2376
|
+
alt={assetDisplayTitle(asset)}
|
|
2377
|
+
className="h-full w-full object-cover transition group-hover:scale-[1.02]"
|
|
2378
|
+
/>
|
|
2379
|
+
) : (
|
|
2380
|
+
<div className="flex h-full items-center justify-center px-2 text-center text-xs text-muted-foreground">
|
|
2381
|
+
{t("brandKitDetail.previewUnavailable")}
|
|
2382
|
+
</div>
|
|
2383
|
+
)}
|
|
2384
|
+
{alreadyAdded ? (
|
|
2385
|
+
<div className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-background/95 shadow-sm">
|
|
2386
|
+
<IconCheck className="h-4 w-4" />
|
|
2387
|
+
</div>
|
|
2388
|
+
) : null}
|
|
2389
|
+
</div>
|
|
2390
|
+
<div className="mt-1 truncate px-0.5 text-xs font-medium">
|
|
2391
|
+
{assetDisplayTitle(asset)}
|
|
2392
|
+
</div>
|
|
2393
|
+
</button>
|
|
2394
|
+
);
|
|
2395
|
+
})}
|
|
2396
|
+
</div>
|
|
2397
|
+
) : (
|
|
2398
|
+
<div className="flex min-h-32 items-center justify-center text-center text-sm text-muted-foreground">
|
|
2399
|
+
{t("brandKitDetail.noReferenceImages")}
|
|
2400
|
+
</div>
|
|
2401
|
+
)}
|
|
2402
|
+
</div>
|
|
2403
|
+
<div className="flex justify-end">
|
|
2404
|
+
<Button
|
|
2405
|
+
type="button"
|
|
2406
|
+
variant="outline"
|
|
2407
|
+
className="gap-2"
|
|
2408
|
+
disabled={referenceUploadDisabled}
|
|
2409
|
+
onClick={() => referenceUploadInputRef.current?.click()}
|
|
2410
|
+
>
|
|
2411
|
+
{referenceUploadPending ? (
|
|
2412
|
+
<Spinner className="h-4 w-4" />
|
|
2413
|
+
) : (
|
|
2414
|
+
<IconUpload className="h-4 w-4" />
|
|
2415
|
+
)}
|
|
2416
|
+
{referenceUploadPending
|
|
2417
|
+
? t("brandKitDetail.uploadingReferenceImage")
|
|
2418
|
+
: t("brandKitDetail.uploadReferenceImage")}
|
|
2419
|
+
</Button>
|
|
2420
|
+
</div>
|
|
2421
|
+
</div>
|
|
2422
|
+
</DialogContent>
|
|
2423
|
+
</Dialog>
|
|
1489
2424
|
</div>
|
|
1490
2425
|
);
|
|
1491
2426
|
}
|