@agent-native/core 0.77.24 → 0.78.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/template-analytics.md +5 -0
  4. package/corpus/core/docs/content/tracking.md +45 -0
  5. package/corpus/core/package.json +3 -1
  6. package/corpus/core/src/agent/types.ts +22 -0
  7. package/corpus/core/src/client/AgentPanel.tsx +34 -18
  8. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  9. package/corpus/core/src/client/agent-chat.ts +173 -0
  10. package/corpus/core/src/client/analytics.ts +141 -5
  11. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  12. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  13. package/corpus/core/src/client/composer/types.ts +22 -0
  14. package/corpus/core/src/client/index.ts +14 -0
  15. package/corpus/core/src/client/session-replay.ts +800 -0
  16. package/corpus/core/src/db/schema.ts +12 -0
  17. package/corpus/core/src/deploy/build.ts +75 -0
  18. package/corpus/core/src/private-blob/index.ts +18 -0
  19. package/corpus/core/src/private-blob/registry.ts +241 -0
  20. package/corpus/core/src/private-blob/types.ts +55 -0
  21. package/corpus/core/src/scripts/utils.ts +34 -6
  22. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  23. package/corpus/core/src/server/email.ts +5 -0
  24. package/corpus/core/src/server/index.ts +1 -0
  25. package/corpus/core/src/styles/agent-native.css +24 -0
  26. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
  27. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
  28. package/corpus/templates/analytics/AGENTS.md +32 -1
  29. package/corpus/templates/analytics/DEVELOPING.md +9 -1
  30. package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
  31. package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
  32. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
  33. package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
  34. package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
  35. package/corpus/templates/analytics/actions/navigate.ts +15 -5
  36. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  37. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
  38. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
  39. package/corpus/templates/analytics/actions/view-screen.ts +50 -2
  40. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
  41. package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
  44. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
  45. package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
  48. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
  53. package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
  54. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
  55. package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
  56. package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
  60. package/corpus/templates/analytics/package.json +5 -2
  61. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
  62. package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
  63. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
  64. package/corpus/templates/analytics/server/db/index.ts +12 -0
  65. package/corpus/templates/analytics/server/db/schema.ts +131 -0
  66. package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
  67. package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
  68. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
  69. package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
  73. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
  74. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
  75. package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
  76. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  77. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
  78. package/corpus/templates/analytics/server/plugins/db.ts +200 -0
  79. package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
  80. package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
  81. package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
  82. package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
  83. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
  84. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
  86. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
  87. package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
  88. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  89. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  90. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  91. package/corpus/templates/assets/AGENTS.md +18 -3
  92. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  93. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  94. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  95. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  96. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  97. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  98. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  99. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  100. package/corpus/templates/assets/actions/navigate.ts +1 -1
  101. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  102. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  103. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  104. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  105. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  106. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  107. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  108. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  109. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  110. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  111. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  112. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  113. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  114. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  115. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  116. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  117. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  118. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  119. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  120. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  121. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  122. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  123. package/corpus/templates/assets/server/db/index.ts +1 -1
  124. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  125. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  126. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  127. package/corpus/templates/assets/shared/api.ts +6 -3
  128. package/dist/agent/types.d.ts +21 -0
  129. package/dist/agent/types.d.ts.map +1 -1
  130. package/dist/agent/types.js.map +1 -1
  131. package/dist/client/AgentPanel.d.ts +6 -2
  132. package/dist/client/AgentPanel.d.ts.map +1 -1
  133. package/dist/client/AgentPanel.js +10 -4
  134. package/dist/client/AgentPanel.js.map +1 -1
  135. package/dist/client/AssistantChat.d.ts +6 -0
  136. package/dist/client/AssistantChat.d.ts.map +1 -1
  137. package/dist/client/AssistantChat.js +15 -3
  138. package/dist/client/AssistantChat.js.map +1 -1
  139. package/dist/client/agent-chat.d.ts +32 -0
  140. package/dist/client/agent-chat.d.ts.map +1 -1
  141. package/dist/client/agent-chat.js +113 -0
  142. package/dist/client/agent-chat.js.map +1 -1
  143. package/dist/client/analytics.d.ts +22 -3
  144. package/dist/client/analytics.d.ts.map +1 -1
  145. package/dist/client/analytics.js +96 -4
  146. package/dist/client/analytics.js.map +1 -1
  147. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  148. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  149. package/dist/client/composer/TiptapComposer.js +228 -56
  150. package/dist/client/composer/TiptapComposer.js.map +1 -1
  151. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  152. package/dist/client/composer/extensions/MentionReference.js +3 -0
  153. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  154. package/dist/client/composer/types.d.ts +21 -0
  155. package/dist/client/composer/types.d.ts.map +1 -1
  156. package/dist/client/composer/types.js.map +1 -1
  157. package/dist/client/index.d.ts +2 -2
  158. package/dist/client/index.d.ts.map +1 -1
  159. package/dist/client/index.js +2 -2
  160. package/dist/client/index.js.map +1 -1
  161. package/dist/client/session-replay.d.ts +41 -0
  162. package/dist/client/session-replay.d.ts.map +1 -0
  163. package/dist/client/session-replay.js +595 -0
  164. package/dist/client/session-replay.js.map +1 -0
  165. package/dist/collab/awareness.d.ts +2 -2
  166. package/dist/collab/awareness.d.ts.map +1 -1
  167. package/dist/collab/routes.d.ts +2 -2
  168. package/dist/db/schema.d.ts +3 -1
  169. package/dist/db/schema.d.ts.map +1 -1
  170. package/dist/db/schema.js +6 -2
  171. package/dist/db/schema.js.map +1 -1
  172. package/dist/deploy/build.d.ts +8 -0
  173. package/dist/deploy/build.d.ts.map +1 -1
  174. package/dist/deploy/build.js +66 -0
  175. package/dist/deploy/build.js.map +1 -1
  176. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +8 -8
  179. package/dist/private-blob/index.d.ts +3 -0
  180. package/dist/private-blob/index.d.ts.map +1 -0
  181. package/dist/private-blob/index.js +2 -0
  182. package/dist/private-blob/index.js.map +1 -0
  183. package/dist/private-blob/registry.d.ts +10 -0
  184. package/dist/private-blob/registry.d.ts.map +1 -0
  185. package/dist/private-blob/registry.js +152 -0
  186. package/dist/private-blob/registry.js.map +1 -0
  187. package/dist/private-blob/types.d.ts +51 -0
  188. package/dist/private-blob/types.d.ts.map +1 -0
  189. package/dist/private-blob/types.js +2 -0
  190. package/dist/private-blob/types.js.map +1 -0
  191. package/dist/resources/handlers.d.ts +3 -3
  192. package/dist/scripts/utils.d.ts +4 -4
  193. package/dist/scripts/utils.d.ts.map +1 -1
  194. package/dist/scripts/utils.js +30 -6
  195. package/dist/scripts/utils.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +5 -0
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  200. package/dist/server/email.d.ts +2 -0
  201. package/dist/server/email.d.ts.map +1 -1
  202. package/dist/server/email.js +3 -0
  203. package/dist/server/email.js.map +1 -1
  204. package/dist/server/index.d.ts +1 -1
  205. package/dist/server/index.d.ts.map +1 -1
  206. package/dist/server/index.js.map +1 -1
  207. package/dist/server/transcribe-voice.d.ts +1 -1
  208. package/dist/styles/agent-native.css +24 -0
  209. package/docs/content/template-analytics.md +5 -0
  210. package/docs/content/tracking.md +45 -0
  211. package/package.json +3 -1
  212. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +0 -209
@@ -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(/^\/brand-kits\/([^/]+)/);
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
- IconPalette,
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 === "/brand-kits"
498
- ? location.pathname === "/brand-kits" ||
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 brand-kit detail page (route /brand-kits/:id).
35
- // Keep the internal view key stable for the agent/MCP contract.
36
- const library = pathname.match(/^\/brand-kits\/([^/]+)/);
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
- libraryId: library[1],
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: "picker",
57
- mediaType:
58
- params.get("mediaType") === "video"
59
- ? "video"
60
- : params.get("mediaType") === "image"
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
- // The "libraries" view is the Brand Kits list (route /brand-kits).
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 `/brand-kits/${command.libraryId}${query ? `?${query}` : ""}`;
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 `/brand-kits/${command.libraryId}?tab=${encodeURIComponent(tab)}`;
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 "/brand-kits";
159
+ if (command.view === "libraries") return "/library";
134
160
  if (command.view === "extensions" && command.extensionId) {
135
161
  return `/extensions/${command.extensionId}`;
136
162
  }