@agent-native/core 0.78.0 → 0.78.2
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 +1 -1
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +269 -237
- package/corpus/core/src/agent/types.ts +22 -0
- package/corpus/core/src/client/AgentPanel.tsx +8 -0
- package/corpus/core/src/client/AssistantChat.tsx +40 -0
- package/corpus/core/src/client/agent-chat.ts +173 -0
- 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 +5 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
- package/corpus/core/src/server/request-context.ts +8 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +13 -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/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +50 -29
- package/dist/agent/production-agent.js.map +1 -1
- 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 +5 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- 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/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 +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +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 +1 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readClientAppState,
|
|
3
|
+
sendToAgentChat,
|
|
4
|
+
useActionMutation,
|
|
5
|
+
useActionQuery,
|
|
6
|
+
useT,
|
|
7
|
+
} from "@agent-native/core/client";
|
|
8
|
+
import { IconMessageCircle, IconPhoto, IconTrash } from "@tabler/icons-react";
|
|
9
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import { toast } from "sonner";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AlertDialog,
|
|
15
|
+
AlertDialogAction,
|
|
16
|
+
AlertDialogCancel,
|
|
17
|
+
AlertDialogContent,
|
|
18
|
+
AlertDialogDescription,
|
|
19
|
+
AlertDialogFooter,
|
|
20
|
+
AlertDialogHeader,
|
|
21
|
+
AlertDialogTitle,
|
|
22
|
+
} from "@/components/ui/alert-dialog";
|
|
23
|
+
import { Badge } from "@/components/ui/badge";
|
|
24
|
+
import { Button } from "@/components/ui/button";
|
|
25
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
26
|
+
import { assetPreviewSources } from "@/lib/asset-preview-sources";
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
AssetVariantState,
|
|
30
|
+
ImageLibrarySummary,
|
|
31
|
+
} from "../../../shared/api";
|
|
32
|
+
|
|
33
|
+
type LibraryListResult = {
|
|
34
|
+
libraries?: ImageLibrarySummary[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function variantStateKey(threadId: string | null) {
|
|
38
|
+
return threadId ? `asset-variants:${threadId}` : "asset-variants";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function slotTime(slot: AssetVariantState["slots"][number]): number {
|
|
42
|
+
const raw = slot.createdAt ?? slot.updatedAt ?? "";
|
|
43
|
+
const time = Date.parse(raw);
|
|
44
|
+
return Number.isNaN(time) ? 0 : time;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stalePendingRunId(
|
|
48
|
+
slot: AssetVariantState["slots"][number],
|
|
49
|
+
): string | null {
|
|
50
|
+
if (slot.status !== "pending") return null;
|
|
51
|
+
if (!slot.runId) return null;
|
|
52
|
+
const timestamp = slotTime(slot);
|
|
53
|
+
if (!timestamp) return null;
|
|
54
|
+
return Date.now() - timestamp >= 2 * 60 * 1000 ? slot.runId : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function GenerationResults({ threadId }: { threadId: string | null }) {
|
|
58
|
+
const t = useT();
|
|
59
|
+
const queryClient = useQueryClient();
|
|
60
|
+
const [clearAllOpen, setClearAllOpen] = useState(false);
|
|
61
|
+
const stateKey = variantStateKey(threadId);
|
|
62
|
+
const stateQueryKey = useMemo(() => ["app-state", stateKey], [stateKey]);
|
|
63
|
+
const { data: variants } = useQuery({
|
|
64
|
+
queryKey: stateQueryKey,
|
|
65
|
+
queryFn: async ({ signal }) => {
|
|
66
|
+
return readClientAppState<AssetVariantState>(stateKey, { signal });
|
|
67
|
+
},
|
|
68
|
+
refetchInterval: 1000,
|
|
69
|
+
});
|
|
70
|
+
const { data: librariesData } = useActionQuery("list-libraries", {
|
|
71
|
+
compact: true,
|
|
72
|
+
} as any) as { data?: LibraryListResult };
|
|
73
|
+
const saveGenerated = useActionMutation("save-generated-image");
|
|
74
|
+
const dismissSlot = useActionMutation("dismiss-variant-slots");
|
|
75
|
+
const refreshGeneration = useActionMutation("refresh-generation-run");
|
|
76
|
+
const refreshingRunIds = useRef<Set<string>>(new Set());
|
|
77
|
+
const libraryTitle = useMemo(() => {
|
|
78
|
+
if (!variants?.libraryId) return null;
|
|
79
|
+
return (
|
|
80
|
+
librariesData?.libraries?.find((item) => item.id === variants.libraryId)
|
|
81
|
+
?.title ?? null
|
|
82
|
+
);
|
|
83
|
+
}, [librariesData?.libraries, variants?.libraryId]);
|
|
84
|
+
const slots = useMemo(
|
|
85
|
+
() =>
|
|
86
|
+
(variants?.slots ?? [])
|
|
87
|
+
.slice()
|
|
88
|
+
.sort(
|
|
89
|
+
(left, right) =>
|
|
90
|
+
slotTime(right) - slotTime(left) ||
|
|
91
|
+
right.slotId.localeCompare(left.slotId),
|
|
92
|
+
),
|
|
93
|
+
[variants?.slots],
|
|
94
|
+
);
|
|
95
|
+
const belongsToThread = Boolean(
|
|
96
|
+
variants &&
|
|
97
|
+
(threadId ? variants.threadId === threadId : !variants.threadId),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!belongsToThread) return;
|
|
102
|
+
if (!slots.length || refreshGeneration.isPending) return;
|
|
103
|
+
const runId = slots
|
|
104
|
+
.map(stalePendingRunId)
|
|
105
|
+
.find((id): id is string =>
|
|
106
|
+
Boolean(id && !refreshingRunIds.current.has(id)),
|
|
107
|
+
);
|
|
108
|
+
if (!runId) return;
|
|
109
|
+
refreshingRunIds.current.add(runId);
|
|
110
|
+
refreshGeneration.mutate(
|
|
111
|
+
{ runId },
|
|
112
|
+
{
|
|
113
|
+
onSettled: () => {
|
|
114
|
+
window.setTimeout(() => {
|
|
115
|
+
refreshingRunIds.current.delete(runId);
|
|
116
|
+
}, 30_000);
|
|
117
|
+
void queryClient.invalidateQueries({
|
|
118
|
+
queryKey: stateQueryKey,
|
|
119
|
+
refetchType: "active",
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}, [belongsToThread, queryClient, refreshGeneration, slots, stateQueryKey]);
|
|
125
|
+
|
|
126
|
+
if (!belongsToThread || !variants) return null;
|
|
127
|
+
if (slots.length === 0) return null;
|
|
128
|
+
|
|
129
|
+
const pendingCount = slots.filter((slot) => slot.status === "pending").length;
|
|
130
|
+
const readyCount = slots.filter((slot) => slot.status === "ready").length;
|
|
131
|
+
const failedCount = slots.filter((slot) => slot.status === "failed").length;
|
|
132
|
+
const statusSummary = [
|
|
133
|
+
pendingCount > 0
|
|
134
|
+
? t("library.generatingCount", { count: pendingCount })
|
|
135
|
+
: null,
|
|
136
|
+
readyCount > 0 ? t("library.readyCount", { count: readyCount }) : null,
|
|
137
|
+
failedCount > 0 ? t("library.failedCount", { count: failedCount }) : null,
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(" / ");
|
|
141
|
+
|
|
142
|
+
function clearAllCandidates() {
|
|
143
|
+
dismissSlot.mutate(
|
|
144
|
+
{ threadId, scope: "all" },
|
|
145
|
+
{
|
|
146
|
+
onSuccess: () => {
|
|
147
|
+
setClearAllOpen(false);
|
|
148
|
+
void queryClient.invalidateQueries({
|
|
149
|
+
queryKey: stateQueryKey,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
onError: (error) =>
|
|
153
|
+
toast.error(error.message || t("library.couldNotClearCandidates")),
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<AlertDialog open={clearAllOpen} onOpenChange={setClearAllOpen}>
|
|
161
|
+
<AlertDialogContent>
|
|
162
|
+
<AlertDialogHeader>
|
|
163
|
+
<AlertDialogTitle>
|
|
164
|
+
{t("library.clearGeneratedCandidatesTitle")}
|
|
165
|
+
</AlertDialogTitle>
|
|
166
|
+
<AlertDialogDescription>
|
|
167
|
+
{t("library.clearGeneratedCandidatesDescription")}
|
|
168
|
+
</AlertDialogDescription>
|
|
169
|
+
</AlertDialogHeader>
|
|
170
|
+
<AlertDialogFooter>
|
|
171
|
+
<AlertDialogCancel disabled={dismissSlot.isPending}>
|
|
172
|
+
{t("library.cancel")}
|
|
173
|
+
</AlertDialogCancel>
|
|
174
|
+
<AlertDialogAction
|
|
175
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
176
|
+
disabled={dismissSlot.isPending}
|
|
177
|
+
onClick={(event) => {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
clearAllCandidates();
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{dismissSlot.isPending ? (
|
|
183
|
+
<>
|
|
184
|
+
<Spinner className="h-4 w-4" />
|
|
185
|
+
{t("library.clearing")}
|
|
186
|
+
</>
|
|
187
|
+
) : (
|
|
188
|
+
t("library.clearCandidates")
|
|
189
|
+
)}
|
|
190
|
+
</AlertDialogAction>
|
|
191
|
+
</AlertDialogFooter>
|
|
192
|
+
</AlertDialogContent>
|
|
193
|
+
</AlertDialog>
|
|
194
|
+
|
|
195
|
+
<section className="mx-auto mb-4 w-full max-w-[760px] px-4">
|
|
196
|
+
<div className="overflow-hidden rounded-xl border border-border bg-card shadow-sm">
|
|
197
|
+
<div className="flex items-start justify-between gap-3 border-b border-border/80 px-3 py-3">
|
|
198
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
199
|
+
<div className="flex size-7 shrink-0 items-center justify-center rounded-md border border-border bg-muted/50 text-muted-foreground">
|
|
200
|
+
{pendingCount > 0 ? (
|
|
201
|
+
<Spinner className="h-3.5 w-3.5" />
|
|
202
|
+
) : (
|
|
203
|
+
<IconPhoto className="h-4 w-4" />
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
<div className="min-w-0">
|
|
207
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
208
|
+
<h2 className="truncate text-sm font-semibold">
|
|
209
|
+
{t("library.generatedCandidatesTitle")}
|
|
210
|
+
</h2>
|
|
211
|
+
<Badge variant="secondary" className="shrink-0">
|
|
212
|
+
{statusSummary}
|
|
213
|
+
</Badge>
|
|
214
|
+
</div>
|
|
215
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
216
|
+
{libraryTitle || t("library.noBrandKit")} / {variants.prompt}
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<Button
|
|
221
|
+
variant="ghost"
|
|
222
|
+
size="sm"
|
|
223
|
+
className="h-7 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-destructive"
|
|
224
|
+
disabled={dismissSlot.isPending}
|
|
225
|
+
onClick={() => setClearAllOpen(true)}
|
|
226
|
+
>
|
|
227
|
+
<IconTrash className="h-3.5 w-3.5" />
|
|
228
|
+
{t("library.clear")}
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div className="max-h-[min(640px,52vh)] overflow-y-auto p-3">
|
|
233
|
+
<div
|
|
234
|
+
className={
|
|
235
|
+
slots.length === 1
|
|
236
|
+
? "grid grid-cols-1 gap-3"
|
|
237
|
+
: "grid grid-cols-1 gap-3 sm:grid-cols-2"
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
{slots.map((slot, index) => (
|
|
241
|
+
<GenerationResultItem
|
|
242
|
+
key={slot.slotId}
|
|
243
|
+
slot={slot}
|
|
244
|
+
index={index}
|
|
245
|
+
prompt={variants.prompt}
|
|
246
|
+
libraryTitle={libraryTitle}
|
|
247
|
+
isSaving={saveGenerated.isPending}
|
|
248
|
+
isDismissing={dismissSlot.isPending}
|
|
249
|
+
onSave={() => {
|
|
250
|
+
if (!slot.assetId && !slot.slotId) return;
|
|
251
|
+
saveGenerated.mutate(
|
|
252
|
+
{
|
|
253
|
+
...(slot.assetId ? { assetId: slot.assetId } : {}),
|
|
254
|
+
...(slot.slotId ? { slotId: slot.slotId } : {}),
|
|
255
|
+
threadId,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
onSuccess: () => {
|
|
259
|
+
toast.success(t("library.savedGeneratedAsset"));
|
|
260
|
+
void queryClient.invalidateQueries({
|
|
261
|
+
queryKey: stateQueryKey,
|
|
262
|
+
});
|
|
263
|
+
void queryClient.invalidateQueries({
|
|
264
|
+
queryKey: ["action", "get-library"],
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
onError: (error) =>
|
|
268
|
+
toast.error(
|
|
269
|
+
error.message || t("library.couldNotSaveCandidate"),
|
|
270
|
+
),
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}}
|
|
274
|
+
onDismiss={() => {
|
|
275
|
+
dismissSlot.mutate(
|
|
276
|
+
{ slotId: slot.slotId, threadId },
|
|
277
|
+
{
|
|
278
|
+
onSuccess: () => {
|
|
279
|
+
void queryClient.invalidateQueries({
|
|
280
|
+
queryKey: stateQueryKey,
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
onError: (error) =>
|
|
284
|
+
toast.error(
|
|
285
|
+
error.message ||
|
|
286
|
+
t("library.couldNotDismissCandidate"),
|
|
287
|
+
),
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</section>
|
|
297
|
+
</>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function GenerationResultItem({
|
|
302
|
+
slot,
|
|
303
|
+
index,
|
|
304
|
+
prompt,
|
|
305
|
+
libraryTitle,
|
|
306
|
+
isSaving,
|
|
307
|
+
isDismissing,
|
|
308
|
+
onSave,
|
|
309
|
+
onDismiss,
|
|
310
|
+
}: {
|
|
311
|
+
slot: AssetVariantState["slots"][number];
|
|
312
|
+
index: number;
|
|
313
|
+
prompt: string;
|
|
314
|
+
libraryTitle: string | null;
|
|
315
|
+
isSaving: boolean;
|
|
316
|
+
isDismissing: boolean;
|
|
317
|
+
onSave: () => void;
|
|
318
|
+
onDismiss: () => void;
|
|
319
|
+
}) {
|
|
320
|
+
const ready = slot.status === "ready" && Boolean(slot.assetId);
|
|
321
|
+
const t = useT();
|
|
322
|
+
return (
|
|
323
|
+
<article className="overflow-hidden rounded-lg border border-border/80 bg-background/70">
|
|
324
|
+
<div className="relative aspect-[16/10] overflow-hidden bg-muted/40">
|
|
325
|
+
<GenerationSlotPreview slot={slot} />
|
|
326
|
+
<div className="absolute left-2 top-2 flex items-center gap-1 rounded-full border border-border/70 bg-background/90 px-2 py-1 text-[11px] font-medium shadow-sm">
|
|
327
|
+
{slot.status === "pending" ? <Spinner className="h-3 w-3" /> : null}
|
|
328
|
+
<span>
|
|
329
|
+
{slot.status === "pending"
|
|
330
|
+
? t("library.generating")
|
|
331
|
+
: slot.status === "ready"
|
|
332
|
+
? t("library.candidateWithNumber", { number: index + 1 })
|
|
333
|
+
: t("library.failed")}
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="space-y-2 p-2.5">
|
|
338
|
+
<div className="min-w-0 text-xs">
|
|
339
|
+
<div className="truncate font-medium">
|
|
340
|
+
{slot.status === "ready"
|
|
341
|
+
? t("library.readyToSave")
|
|
342
|
+
: slot.status === "pending"
|
|
343
|
+
? t("library.stillRendering")
|
|
344
|
+
: t("library.generationFailed")}
|
|
345
|
+
</div>
|
|
346
|
+
<div className="mt-0.5 truncate text-muted-foreground">
|
|
347
|
+
{libraryTitle || t("library.noBrandKit")}
|
|
348
|
+
{prompt ? ` / ${prompt}` : null}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
352
|
+
<Button
|
|
353
|
+
size="sm"
|
|
354
|
+
className="h-7 px-2 text-xs"
|
|
355
|
+
disabled={!ready || isSaving}
|
|
356
|
+
onClick={onSave}
|
|
357
|
+
>
|
|
358
|
+
{isSaving ? <Spinner className="h-3 w-3" /> : t("library.save")}
|
|
359
|
+
</Button>
|
|
360
|
+
<Button
|
|
361
|
+
variant="outline"
|
|
362
|
+
size="sm"
|
|
363
|
+
className="h-7 gap-1 px-2 text-xs"
|
|
364
|
+
disabled={!slot.assetId}
|
|
365
|
+
onClick={() =>
|
|
366
|
+
sendToAgentChat({
|
|
367
|
+
message: `Refine generated asset ${slot.assetId}: `,
|
|
368
|
+
context: [
|
|
369
|
+
"## Assets candidate",
|
|
370
|
+
`Asset ID: ${slot.assetId}`,
|
|
371
|
+
`Run ID: ${slot.runId ?? "unknown"}`,
|
|
372
|
+
`Prompt: ${prompt}`,
|
|
373
|
+
libraryTitle ? `Brand kit: ${libraryTitle}` : "",
|
|
374
|
+
"Use refine-image with assetId when the user describes the change.",
|
|
375
|
+
]
|
|
376
|
+
.filter(Boolean)
|
|
377
|
+
.join("\n"),
|
|
378
|
+
submit: false,
|
|
379
|
+
openSidebar: true,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
>
|
|
383
|
+
<IconMessageCircle className="h-3.5 w-3.5" />
|
|
384
|
+
{t("library.refine")}
|
|
385
|
+
</Button>
|
|
386
|
+
<Button
|
|
387
|
+
variant="ghost"
|
|
388
|
+
size="icon"
|
|
389
|
+
className="h-7 w-7 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
|
390
|
+
disabled={isDismissing}
|
|
391
|
+
onClick={onDismiss}
|
|
392
|
+
aria-label={t("library.deleteCandidate")}
|
|
393
|
+
>
|
|
394
|
+
<IconTrash className="h-3.5 w-3.5" />
|
|
395
|
+
</Button>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</article>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function GenerationSlotPreview({
|
|
403
|
+
slot,
|
|
404
|
+
}: {
|
|
405
|
+
slot: AssetVariantState["slots"][number];
|
|
406
|
+
}) {
|
|
407
|
+
const t = useT();
|
|
408
|
+
const sources = assetPreviewSources(slot, "thumbnail");
|
|
409
|
+
const src = sources[0];
|
|
410
|
+
if (src) {
|
|
411
|
+
return <img src={src} alt="" className="h-full w-full object-contain" />;
|
|
412
|
+
}
|
|
413
|
+
if (slot.status === "failed") {
|
|
414
|
+
return (
|
|
415
|
+
<div className="flex h-full w-full items-center justify-center p-2 text-center text-[11px] text-destructive">
|
|
416
|
+
{slot.error || t("library.failed")}
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return (
|
|
421
|
+
<div className="flex h-full w-full items-center justify-center bg-muted">
|
|
422
|
+
<IconPhoto className="h-6 w-6 animate-pulse text-muted-foreground" />
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
@@ -35,7 +35,9 @@ function StaticTitle({ pathname }: { pathname: string }) {
|
|
|
35
35
|
|
|
36
36
|
function ResolvedTitle() {
|
|
37
37
|
const location = useLocation();
|
|
38
|
-
const libraryMatch = location.pathname.match(
|
|
38
|
+
const libraryMatch = location.pathname.match(
|
|
39
|
+
/^\/(?:library|brand-kits)\/([^/]+)/,
|
|
40
|
+
);
|
|
39
41
|
if (libraryMatch) {
|
|
40
42
|
return <LibraryTitle id={libraryMatch[1]} />;
|
|
41
43
|
}
|
|
@@ -13,6 +13,7 @@ import { IconMenu2 } from "@tabler/icons-react";
|
|
|
13
13
|
import { useState, useEffect } from "react";
|
|
14
14
|
import { useLocation, useNavigate } from "react-router";
|
|
15
15
|
|
|
16
|
+
import { GenerationResults } from "@/components/generation/GenerationResults";
|
|
16
17
|
import { useNavigationState } from "@/hooks/use-navigation-state";
|
|
17
18
|
import { ASSETS_CHAT_STORAGE_KEY } from "@/lib/chat";
|
|
18
19
|
import { cn } from "@/lib/utils";
|
|
@@ -59,6 +60,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
59
60
|
const isPicker = location.pathname === "/library";
|
|
60
61
|
const hideHeader =
|
|
61
62
|
location.pathname === "/library" ||
|
|
63
|
+
location.pathname.startsWith("/library/") ||
|
|
62
64
|
location.pathname === "/extensions" ||
|
|
63
65
|
location.pathname.startsWith("/extensions/");
|
|
64
66
|
const chromeless =
|
|
@@ -138,6 +140,9 @@ export function Layout({ children }: LayoutProps) {
|
|
|
138
140
|
t("chat.suggestionProductVideo"),
|
|
139
141
|
t("chat.suggestionReferenceStyle"),
|
|
140
142
|
]}
|
|
143
|
+
threadFooterSlot={({ threadId }) => (
|
|
144
|
+
<GenerationResults threadId={threadId} />
|
|
145
|
+
)}
|
|
141
146
|
>
|
|
142
147
|
{appFrame}
|
|
143
148
|
</AgentSidebar>
|
|
@@ -16,10 +16,9 @@ import {
|
|
|
16
16
|
IconClipboardList,
|
|
17
17
|
IconDots,
|
|
18
18
|
IconEdit,
|
|
19
|
-
IconLayoutGrid,
|
|
20
19
|
IconLayoutSidebarLeftCollapse,
|
|
21
20
|
IconLayoutSidebarLeftExpand,
|
|
22
|
-
|
|
21
|
+
IconLayoutGrid,
|
|
23
22
|
IconPhotoPlus,
|
|
24
23
|
IconPin,
|
|
25
24
|
IconPlus,
|
|
@@ -48,7 +47,6 @@ import { cn } from "@/lib/utils";
|
|
|
48
47
|
const baseNavItems = [
|
|
49
48
|
{ icon: IconPhotoPlus, labelKey: "navigation.create", href: "/" },
|
|
50
49
|
{ icon: IconLayoutGrid, labelKey: "navigation.library", href: "/library" },
|
|
51
|
-
{ icon: IconPalette, labelKey: "navigation.brandKits", href: "/brand-kits" },
|
|
52
50
|
{ icon: IconSettings, labelKey: "navigation.settings", href: "/settings" },
|
|
53
51
|
];
|
|
54
52
|
|
|
@@ -494,10 +492,10 @@ export function Sidebar() {
|
|
|
494
492
|
const isActive =
|
|
495
493
|
item.href === "/"
|
|
496
494
|
? isCreateRoute
|
|
497
|
-
: item.href === "/
|
|
498
|
-
? location.pathname === "/
|
|
499
|
-
location.pathname.startsWith("/brand-kits/") ||
|
|
495
|
+
: item.href === "/library"
|
|
496
|
+
? location.pathname === "/library" ||
|
|
500
497
|
location.pathname.startsWith("/library/") ||
|
|
498
|
+
location.pathname.startsWith("/brand-kits/") ||
|
|
501
499
|
location.pathname.startsWith("/image/") ||
|
|
502
500
|
location.pathname.startsWith("/asset/")
|
|
503
501
|
: location.pathname.startsWith(item.href);
|
|
@@ -22,6 +22,13 @@ function optionalLibraryTab(params: URLSearchParams) {
|
|
|
22
22
|
: undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function isPickerRequest(params: URLSearchParams) {
|
|
26
|
+
return (
|
|
27
|
+
params.get("__an_picker") === "1" ||
|
|
28
|
+
params.get("__an_mcp_chat_bridge") === "1"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
function navigationFromPath(pathname: string, search = "") {
|
|
26
33
|
const params = new URLSearchParams(search);
|
|
27
34
|
const chat = pathname.match(/^\/chat\/([^/]+)/);
|
|
@@ -31,13 +38,15 @@ function navigationFromPath(pathname: string, search = "") {
|
|
|
31
38
|
threadId: decodePathParam(chat[1]),
|
|
32
39
|
};
|
|
33
40
|
}
|
|
34
|
-
// The "library" view is the
|
|
35
|
-
//
|
|
36
|
-
|
|
41
|
+
// The "library" view is the unified Library workspace. Keep the internal
|
|
42
|
+
// detail key stable for agent/MCP callers that already navigate by brand-kit
|
|
43
|
+
// id, while the URL is now /library/:id.
|
|
44
|
+
const library = pathname.match(/^\/(?:library|brand-kits)\/([^/]+)/);
|
|
37
45
|
if (library) {
|
|
38
46
|
return {
|
|
39
47
|
view: "library",
|
|
40
|
-
|
|
48
|
+
selection: decodePathParam(library[1]),
|
|
49
|
+
libraryId: decodePathParam(library[1]),
|
|
41
50
|
activeTab: optionalLibraryTab(params),
|
|
42
51
|
};
|
|
43
52
|
}
|
|
@@ -50,24 +59,40 @@ function navigationFromPath(pathname: string, search = "") {
|
|
|
50
59
|
view: "create",
|
|
51
60
|
};
|
|
52
61
|
}
|
|
53
|
-
// The "picker" view is the image Library browser (route /library).
|
|
54
62
|
if (pathname === "/library") {
|
|
63
|
+
if (isPickerRequest(params)) {
|
|
64
|
+
return {
|
|
65
|
+
view: "picker",
|
|
66
|
+
mediaType:
|
|
67
|
+
params.get("mediaType") === "video"
|
|
68
|
+
? "video"
|
|
69
|
+
: params.get("mediaType") === "image"
|
|
70
|
+
? "image"
|
|
71
|
+
: undefined,
|
|
72
|
+
libraryId: optionalParam(params, "libraryId"),
|
|
73
|
+
query: optionalParam(params, "q"),
|
|
74
|
+
prompt: optionalParam(params, "prompt"),
|
|
75
|
+
aspectRatio: optionalParam(params, "aspectRatio"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const queryLibraryId = optionalParam(params, "libraryId");
|
|
79
|
+
if (queryLibraryId) {
|
|
80
|
+
return {
|
|
81
|
+
view: "library",
|
|
82
|
+
selection: queryLibraryId,
|
|
83
|
+
libraryId: queryLibraryId,
|
|
84
|
+
activeTab: optionalLibraryTab(params),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
55
87
|
return {
|
|
56
|
-
view: "
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? "image"
|
|
62
|
-
: undefined,
|
|
63
|
-
libraryId: optionalParam(params, "libraryId"),
|
|
64
|
-
query: optionalParam(params, "q"),
|
|
65
|
-
prompt: optionalParam(params, "prompt"),
|
|
66
|
-
aspectRatio: optionalParam(params, "aspectRatio"),
|
|
88
|
+
view: "library",
|
|
89
|
+
selection: "all",
|
|
90
|
+
tab: optionalParam(params, "tab"),
|
|
91
|
+
scope: optionalParam(params, "scope"),
|
|
92
|
+
search: optionalParam(params, "q"),
|
|
67
93
|
};
|
|
68
94
|
}
|
|
69
|
-
|
|
70
|
-
if (pathname === "/brand-kits") return { view: "libraries" };
|
|
95
|
+
if (pathname === "/brand-kits") return { view: "library", selection: "all" };
|
|
71
96
|
if (pathname === "/extensions") return { view: "extensions" };
|
|
72
97
|
const extension = pathname.match(/^\/extensions\/([^/]+)/);
|
|
73
98
|
if (extension) return { view: "extensions", extensionId: extension[1] };
|
|
@@ -85,7 +110,7 @@ function pathFromCommand(command: any): string | null {
|
|
|
85
110
|
params.set("tab", command.activeTab);
|
|
86
111
|
}
|
|
87
112
|
const query = params.toString();
|
|
88
|
-
return `/
|
|
113
|
+
return `/library/${command.libraryId}${query ? `?${query}` : ""}`;
|
|
89
114
|
}
|
|
90
115
|
if (
|
|
91
116
|
(command.view === "asset" || command.view === "image") &&
|
|
@@ -100,7 +125,7 @@ function pathFromCommand(command: any): string | null {
|
|
|
100
125
|
) {
|
|
101
126
|
const tab =
|
|
102
127
|
typeof command.activeTab === "string" ? command.activeTab : "runs";
|
|
103
|
-
return `/
|
|
128
|
+
return `/library/${command.libraryId}?tab=${encodeURIComponent(tab)}`;
|
|
104
129
|
}
|
|
105
130
|
if (command.view === "audit") return "/audit";
|
|
106
131
|
if (command.view === "settings") return "/settings";
|
|
@@ -112,6 +137,7 @@ function pathFromCommand(command: any): string | null {
|
|
|
112
137
|
}
|
|
113
138
|
if (command.view === "picker") {
|
|
114
139
|
const params = new URLSearchParams();
|
|
140
|
+
params.set("__an_picker", "1");
|
|
115
141
|
if (command.mediaType === "image" || command.mediaType === "video") {
|
|
116
142
|
params.set("mediaType", command.mediaType);
|
|
117
143
|
}
|
|
@@ -130,7 +156,7 @@ function pathFromCommand(command: any): string | null {
|
|
|
130
156
|
const query = params.toString();
|
|
131
157
|
return query ? `/library?${query}` : "/library";
|
|
132
158
|
}
|
|
133
|
-
if (command.view === "libraries") return "/
|
|
159
|
+
if (command.view === "libraries") return "/library";
|
|
134
160
|
if (command.view === "extensions" && command.extensionId) {
|
|
135
161
|
return `/extensions/${command.extensionId}`;
|
|
136
162
|
}
|