@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,437 @@
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ useSession,
5
+ useT,
6
+ } from "@agent-native/core/client";
7
+ import {
8
+ IconFilter,
9
+ IconMail,
10
+ IconPencil,
11
+ IconPlus,
12
+ IconSend,
13
+ IconTrash,
14
+ } from "@tabler/icons-react";
15
+ import { useEffect, useMemo, useRef, useState } from "react";
16
+ import { toast } from "sonner";
17
+
18
+ import { Button } from "@/components/ui/button";
19
+ import {
20
+ Dialog,
21
+ DialogContent,
22
+ DialogDescription,
23
+ DialogFooter,
24
+ DialogHeader,
25
+ DialogTitle,
26
+ } from "@/components/ui/dialog";
27
+ import { Input } from "@/components/ui/input";
28
+ import { Label } from "@/components/ui/label";
29
+ import { Switch } from "@/components/ui/switch";
30
+ import { Textarea } from "@/components/ui/textarea";
31
+
32
+ import {
33
+ normalizeReportFilterSnapshot,
34
+ reportFilterSnapshotKey,
35
+ savedReportFiltersForEdit,
36
+ } from "./report-filters";
37
+
38
+ type DashboardReportSubscription = {
39
+ id: string;
40
+ dashboardId: string;
41
+ name: string;
42
+ recipients: string[];
43
+ filters: Record<string, string>;
44
+ timeOfDay: string;
45
+ timezone: string;
46
+ enabled: boolean;
47
+ nextRunAt: string | null;
48
+ lastRunAt: string | null;
49
+ lastStatus: "success" | "error" | "running" | null;
50
+ lastError: string | null;
51
+ };
52
+
53
+ interface EmailReportDialogProps {
54
+ open: boolean;
55
+ onOpenChange: (open: boolean) => void;
56
+ dashboardId: string;
57
+ dashboardName: string;
58
+ filters: Record<string, string>;
59
+ }
60
+
61
+ function localTimezone(): string {
62
+ try {
63
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
64
+ } catch {
65
+ return "UTC";
66
+ }
67
+ }
68
+
69
+ function splitRecipients(value: string): string[] {
70
+ return value
71
+ .split(/[\n,]+/)
72
+ .map((item) => item.trim())
73
+ .filter(Boolean);
74
+ }
75
+
76
+ function formatDate(value: string | null, fallback: string): string {
77
+ if (!value) return fallback;
78
+ const date = new Date(value);
79
+ if (Number.isNaN(date.getTime())) return fallback;
80
+ return date.toLocaleString(undefined, {
81
+ month: "short",
82
+ day: "numeric",
83
+ hour: "numeric",
84
+ minute: "2-digit",
85
+ });
86
+ }
87
+
88
+ export function EmailReportDialog({
89
+ open,
90
+ onOpenChange,
91
+ dashboardId,
92
+ dashboardName,
93
+ filters,
94
+ }: EmailReportDialogProps) {
95
+ const t = useT();
96
+ const { session } = useSession();
97
+ const initialized = useRef(false);
98
+ const defaultEmail = session?.email ?? "";
99
+ const defaultName = `${dashboardName} daily email`;
100
+
101
+ const [selectedId, setSelectedId] = useState<string | undefined>();
102
+ const [name, setName] = useState(defaultName);
103
+ const [recipientsText, setRecipientsText] = useState(defaultEmail);
104
+ const [reportFilters, setReportFilters] = useState<Record<string, string>>(
105
+ () => normalizeReportFilterSnapshot(filters),
106
+ );
107
+ const [timeOfDay, setTimeOfDay] = useState("09:00");
108
+ const [timezone, setTimezone] = useState(localTimezone());
109
+ const [enabled, setEnabled] = useState(true);
110
+
111
+ const { data, isLoading, refetch } = useActionQuery<
112
+ DashboardReportSubscription[]
113
+ >(
114
+ "list-dashboard-report-subscriptions",
115
+ dashboardId ? { dashboardId } : undefined,
116
+ { enabled: open && !!dashboardId },
117
+ );
118
+ const saveSubscription = useActionMutation(
119
+ "save-dashboard-report-subscription",
120
+ );
121
+ const deleteSubscription = useActionMutation(
122
+ "delete-dashboard-report-subscription",
123
+ { method: "DELETE" },
124
+ );
125
+ const sendNow = useActionMutation("send-dashboard-report-now");
126
+
127
+ const subscriptions = useMemo(
128
+ () => (Array.isArray(data) ? data : []),
129
+ [data],
130
+ );
131
+ const selectedSubscription = subscriptions.find(
132
+ (sub) => sub.id === selectedId,
133
+ );
134
+ const currentFilterSnapshot = useMemo(
135
+ () => normalizeReportFilterSnapshot(filters),
136
+ [filters],
137
+ );
138
+ const currentFilterSnapshotKey = useMemo(
139
+ () => reportFilterSnapshotKey(currentFilterSnapshot),
140
+ [currentFilterSnapshot],
141
+ );
142
+ const reportFilterKey = useMemo(
143
+ () => reportFilterSnapshotKey(reportFilters),
144
+ [reportFilters],
145
+ );
146
+ const reportFilterCount = Object.keys(reportFilters).length;
147
+ const reportFiltersMatchCurrent =
148
+ reportFilterKey === currentFilterSnapshotKey;
149
+
150
+ const loadSubscription = (sub: DashboardReportSubscription) => {
151
+ setSelectedId(sub.id);
152
+ setName(sub.name);
153
+ setRecipientsText(sub.recipients.join("\n"));
154
+ setReportFilters(savedReportFiltersForEdit(sub.filters));
155
+ setTimeOfDay(sub.timeOfDay);
156
+ setTimezone(sub.timezone);
157
+ setEnabled(sub.enabled);
158
+ };
159
+
160
+ const resetForm = () => {
161
+ setSelectedId(undefined);
162
+ setName(defaultName);
163
+ setRecipientsText(defaultEmail);
164
+ setReportFilters(currentFilterSnapshot);
165
+ setTimeOfDay("09:00");
166
+ setTimezone(localTimezone());
167
+ setEnabled(true);
168
+ };
169
+
170
+ useEffect(() => {
171
+ if (!open) {
172
+ initialized.current = false;
173
+ return;
174
+ }
175
+ if (initialized.current || isLoading) return;
176
+ if (subscriptions[0]) {
177
+ loadSubscription(subscriptions[0]);
178
+ } else {
179
+ resetForm();
180
+ }
181
+ initialized.current = true;
182
+ }, [open, isLoading, subscriptions, defaultEmail, defaultName]);
183
+
184
+ const handleSave = async () => {
185
+ const recipients = splitRecipients(recipientsText);
186
+ if (recipients.length === 0) {
187
+ toast.error(t("sqlDashboard.reportRecipientsRequired"));
188
+ return;
189
+ }
190
+ try {
191
+ const saved = await saveSubscription.mutateAsync({
192
+ id: selectedId,
193
+ dashboardId,
194
+ name: name.trim() || defaultName,
195
+ recipients,
196
+ filters: reportFilters,
197
+ timeOfDay,
198
+ timezone,
199
+ enabled,
200
+ });
201
+ loadSubscription(saved as DashboardReportSubscription);
202
+ await refetch();
203
+ toast.success(t("sqlDashboard.reportSubscriptionSaved"));
204
+ } catch (err: any) {
205
+ toast.error(
206
+ t("sqlDashboard.reportSubscriptionSaveFailed", {
207
+ message: err?.message ?? String(err),
208
+ }),
209
+ );
210
+ }
211
+ };
212
+
213
+ const handleDelete = async () => {
214
+ if (!selectedId) return;
215
+ try {
216
+ await deleteSubscription.mutateAsync({ id: selectedId });
217
+ resetForm();
218
+ await refetch();
219
+ toast.success(t("sqlDashboard.reportSubscriptionDeleted"));
220
+ } catch (err: any) {
221
+ toast.error(
222
+ t("sqlDashboard.reportSubscriptionDeleteFailed", {
223
+ message: err?.message ?? String(err),
224
+ }),
225
+ );
226
+ }
227
+ };
228
+
229
+ const handleSendNow = async () => {
230
+ if (!selectedId) return;
231
+ try {
232
+ await sendNow.mutateAsync({ id: selectedId });
233
+ await refetch();
234
+ toast.success(t("sqlDashboard.reportSent"));
235
+ } catch (err: any) {
236
+ toast.error(
237
+ t("sqlDashboard.reportSendFailed", {
238
+ message: err?.message ?? String(err),
239
+ }),
240
+ );
241
+ }
242
+ };
243
+
244
+ return (
245
+ <Dialog open={open} onOpenChange={onOpenChange}>
246
+ <DialogContent className="sm:max-w-[560px] max-h-[90vh] overflow-y-auto">
247
+ <DialogHeader>
248
+ <DialogTitle>{t("sqlDashboard.emailReports")}</DialogTitle>
249
+ <DialogDescription>
250
+ {t("sqlDashboard.emailReportsDescription", {
251
+ name: dashboardName,
252
+ })}
253
+ </DialogDescription>
254
+ </DialogHeader>
255
+
256
+ <div className="space-y-4">
257
+ <div className="space-y-2">
258
+ <div className="flex items-center justify-between gap-3">
259
+ <Label>{t("sqlDashboard.existingReportSubscriptions")}</Label>
260
+ <Button variant="outline" size="sm" onClick={resetForm}>
261
+ <IconPlus className="mr-1.5 h-3.5 w-3.5" />
262
+ {t("sqlDashboard.newSubscription")}
263
+ </Button>
264
+ </div>
265
+ {subscriptions.length === 0 ? (
266
+ <div className="rounded-md border border-dashed px-3 py-4 text-sm text-muted-foreground">
267
+ {t("sqlDashboard.noReportSubscriptions")}
268
+ </div>
269
+ ) : (
270
+ <div className="space-y-2">
271
+ {subscriptions.map((sub) => (
272
+ <button
273
+ key={sub.id}
274
+ type="button"
275
+ className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2 text-left transition-colors ${
276
+ sub.id === selectedId
277
+ ? "border-primary bg-primary/5"
278
+ : "hover:bg-muted/60"
279
+ }`}
280
+ onClick={() => loadSubscription(sub)}
281
+ >
282
+ <span className="min-w-0">
283
+ <span className="block truncate text-sm font-medium">
284
+ {sub.name}
285
+ </span>
286
+ <span className="block truncate text-xs text-muted-foreground">
287
+ {sub.recipients.join(", ")}
288
+ </span>
289
+ </span>
290
+ <span className="shrink-0 text-xs text-muted-foreground">
291
+ {formatDate(sub.nextRunAt, t("sqlDashboard.never"))}
292
+ </span>
293
+ </button>
294
+ ))}
295
+ </div>
296
+ )}
297
+ </div>
298
+
299
+ <div className="grid gap-4 sm:grid-cols-2">
300
+ <div className="space-y-2 sm:col-span-2">
301
+ <Label htmlFor="dashboard-report-name">
302
+ {t("sqlDashboard.reportName")}
303
+ </Label>
304
+ <Input
305
+ id="dashboard-report-name"
306
+ value={name}
307
+ onChange={(event) => setName(event.target.value)}
308
+ />
309
+ </div>
310
+
311
+ <div className="space-y-2 sm:col-span-2">
312
+ <Label htmlFor="dashboard-report-recipients">
313
+ {t("sqlDashboard.reportRecipients")}
314
+ </Label>
315
+ <Textarea
316
+ id="dashboard-report-recipients"
317
+ value={recipientsText}
318
+ onChange={(event) => setRecipientsText(event.target.value)}
319
+ placeholder={t("sqlDashboard.reportRecipientsPlaceholder")}
320
+ className="min-h-[76px]"
321
+ />
322
+ </div>
323
+
324
+ <div className="space-y-2">
325
+ <Label htmlFor="dashboard-report-time">
326
+ {t("sqlDashboard.reportSendTime")}
327
+ </Label>
328
+ <Input
329
+ id="dashboard-report-time"
330
+ type="time"
331
+ value={timeOfDay}
332
+ onChange={(event) => setTimeOfDay(event.target.value)}
333
+ />
334
+ </div>
335
+
336
+ <div className="space-y-2">
337
+ <Label htmlFor="dashboard-report-timezone">
338
+ {t("sqlDashboard.reportTimezone")}
339
+ </Label>
340
+ <Input
341
+ id="dashboard-report-timezone"
342
+ value={timezone}
343
+ onChange={(event) => setTimezone(event.target.value)}
344
+ />
345
+ </div>
346
+
347
+ <div className="flex items-center justify-between gap-3 rounded-md border px-3 py-2 sm:col-span-2">
348
+ <div className="min-w-0">
349
+ <Label>{t("sqlDashboard.reportEnabled")}</Label>
350
+ <p className="text-xs text-muted-foreground">
351
+ {t("sqlDashboard.reportFilterSnapshot", {
352
+ count: reportFilterCount,
353
+ })}
354
+ </p>
355
+ </div>
356
+ <div className="flex shrink-0 items-center gap-2">
357
+ <Button
358
+ type="button"
359
+ variant="outline"
360
+ size="sm"
361
+ onClick={() => setReportFilters(currentFilterSnapshot)}
362
+ disabled={reportFiltersMatchCurrent}
363
+ >
364
+ <IconFilter className="mr-1.5 h-3.5 w-3.5" />
365
+ {t("sqlDashboard.reportUseCurrentFilters")}
366
+ </Button>
367
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
368
+ </div>
369
+ </div>
370
+
371
+ {selectedSubscription ? (
372
+ <div className="grid gap-2 text-xs text-muted-foreground sm:col-span-2 sm:grid-cols-2">
373
+ <div>
374
+ {t("sqlDashboard.reportLastRun")}:{" "}
375
+ {formatDate(
376
+ selectedSubscription.lastRunAt,
377
+ t("sqlDashboard.never"),
378
+ )}
379
+ </div>
380
+ <div>
381
+ {t("sqlDashboard.reportNextRun")}:{" "}
382
+ {formatDate(
383
+ selectedSubscription.nextRunAt,
384
+ t("sqlDashboard.never"),
385
+ )}
386
+ </div>
387
+ </div>
388
+ ) : null}
389
+ </div>
390
+ </div>
391
+
392
+ <DialogFooter className="gap-2 sm:gap-0">
393
+ {selectedId ? (
394
+ <Button
395
+ variant="outline"
396
+ size="sm"
397
+ onClick={handleDelete}
398
+ disabled={deleteSubscription.isPending}
399
+ >
400
+ <IconTrash className="mr-1.5 h-3.5 w-3.5" />
401
+ {t("sidebar.delete")}
402
+ </Button>
403
+ ) : null}
404
+ {selectedId ? (
405
+ <Button
406
+ variant="outline"
407
+ size="sm"
408
+ onClick={handleSendNow}
409
+ disabled={sendNow.isPending}
410
+ >
411
+ <IconSend className="mr-1.5 h-3.5 w-3.5" />
412
+ {sendNow.isPending
413
+ ? t("sqlDashboard.reportSending")
414
+ : t("sqlDashboard.reportSendNow")}
415
+ </Button>
416
+ ) : null}
417
+ <Button
418
+ size="sm"
419
+ onClick={handleSave}
420
+ disabled={saveSubscription.isPending}
421
+ >
422
+ {selectedId ? (
423
+ <IconPencil className="mr-1.5 h-3.5 w-3.5" />
424
+ ) : (
425
+ <IconMail className="mr-1.5 h-3.5 w-3.5" />
426
+ )}
427
+ {saveSubscription.isPending
428
+ ? t("sqlDashboard.saving")
429
+ : selectedId
430
+ ? t("sqlDashboard.updateSubscription")
431
+ : t("sqlDashboard.saveSubscription")}
432
+ </Button>
433
+ </DialogFooter>
434
+ </DialogContent>
435
+ </Dialog>
436
+ );
437
+ }
@@ -1,5 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
- import { useSortable } from "@dnd-kit/sortable";
2
+ import { useDraggable } from "@dnd-kit/core";
3
3
  import { CSS } from "@dnd-kit/utilities";
4
4
  import {
5
5
  IconGripVertical,
@@ -58,6 +58,7 @@ interface SqlChartCardProps {
58
58
  * validation failure so the popover can stay open and surface the error. */
59
59
  onSaveSql?: (sql: string) => Promise<void>;
60
60
  editable?: boolean;
61
+ eagerLoad?: boolean;
61
62
  }
62
63
 
63
64
  export function SqlChartCard({
@@ -67,23 +68,18 @@ export function SqlChartCard({
67
68
  onEdit,
68
69
  onSaveSql,
69
70
  editable = true,
71
+ eagerLoad = false,
70
72
  }: SqlChartCardProps) {
71
73
  const t = useT();
72
74
  const queryClient = useQueryClient();
73
- const {
74
- attributes,
75
- listeners,
76
- setNodeRef,
77
- transform,
78
- transition,
79
- isDragging,
80
- } = useSortable({ id: panel.id, disabled: !editable });
75
+ const { attributes, listeners, setNodeRef, transform, isDragging } =
76
+ useDraggable({ id: panel.id, disabled: !editable });
81
77
 
82
78
  const [confirmOpen, setConfirmOpen] = useState(false);
83
79
  const [expanded, setExpanded] = useState(false);
84
80
  const [exportCsv, setExportCsv] = useState<(() => void) | null>(null);
85
81
  const [shouldLoadData, setShouldLoadData] = useState(
86
- panel.chartType === "section",
82
+ eagerLoad || panel.chartType === "section",
87
83
  );
88
84
  const cardRef = useRef<HTMLDivElement | null>(null);
89
85
 
@@ -112,6 +108,10 @@ export function SqlChartCard({
112
108
  }, [panel.id, panel.source, panel.sql, queryClient, resolvedSql]);
113
109
 
114
110
  useEffect(() => {
111
+ if (eagerLoad) {
112
+ setShouldLoadData(true);
113
+ return;
114
+ }
115
115
  if (panel.chartType === "section") {
116
116
  setShouldLoadData(true);
117
117
  return;
@@ -138,7 +138,7 @@ export function SqlChartCard({
138
138
  );
139
139
  observer.observe(node);
140
140
  return () => observer.disconnect();
141
- }, [panel.chartType, panel.id]);
141
+ }, [eagerLoad, panel.chartType, panel.id]);
142
142
 
143
143
  useEffect(() => {
144
144
  setExportCsv(null);
@@ -146,7 +146,6 @@ export function SqlChartCard({
146
146
 
147
147
  const style = {
148
148
  transform: CSS.Transform.toString(transform),
149
- transition,
150
149
  zIndex: isDragging ? 50 : undefined,
151
150
  opacity: isDragging ? 0.7 : 1,
152
151
  };
@@ -212,6 +212,41 @@ export function readDropSlot(value: unknown): DashboardDropSlot | null {
212
212
  return null;
213
213
  }
214
214
 
215
+ function panelIsInRenderedRow(
216
+ groups: DashboardPanelGroup[],
217
+ panelId: string,
218
+ ): boolean {
219
+ return groups.some((group) =>
220
+ group.rows.some((row) => row.panels.some((panel) => panel.id === panelId)),
221
+ );
222
+ }
223
+
224
+ export function isDropSlotAvailable(
225
+ groups: DashboardPanelGroup[],
226
+ panelId: string,
227
+ slot: DashboardDropSlot,
228
+ ): boolean {
229
+ if (!panelIsInRenderedRow(groups, panelId)) return false;
230
+
231
+ const group = groups.find((item) => item.key === slot.groupKey);
232
+ if (!group) return false;
233
+
234
+ if (slot.type === "row") {
235
+ return slot.rowIndex >= 0 && slot.rowIndex <= group.rows.length;
236
+ }
237
+
238
+ const row = group.rows[slot.rowIndex];
239
+ if (!row) return false;
240
+ if (slot.columnIndex < 0 || slot.columnIndex > row.panels.length) {
241
+ return false;
242
+ }
243
+
244
+ const rowContainsPanel = row.panels.some((panel) => panel.id === panelId);
245
+ if (rowContainsPanel) return row.panels.length > 1;
246
+
247
+ return row.panels.length < group.columns;
248
+ }
249
+
215
250
  export function movePanelToDropSlot(
216
251
  panels: SqlPanel[],
217
252
  panelId: string,
@@ -270,6 +305,14 @@ export function movePanelToDropSlot(
270
305
  });
271
306
  } else {
272
307
  let rowIndex = slot.rowIndex;
308
+ if (
309
+ sourceGroupKey === slot.groupKey &&
310
+ sourceRowWasSingle &&
311
+ sourceRowIndex === rowIndex
312
+ ) {
313
+ return panels;
314
+ }
315
+
273
316
  if (
274
317
  sourceGroupKey === slot.groupKey &&
275
318
  sourceRowWasSingle &&