@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,450 @@
1
+ import {
2
+ useActionQuery,
3
+ useSendToAgentChat,
4
+ useT,
5
+ } from "@agent-native/core/client";
6
+ import {
7
+ IconArrowLeft,
8
+ IconClock,
9
+ IconExclamationCircle,
10
+ IconMessageCircle,
11
+ IconPlayerPause,
12
+ IconPlayerPlay,
13
+ IconPlayerSkipBack,
14
+ } from "@tabler/icons-react";
15
+ import { useEffect, useMemo, useRef, useState } from "react";
16
+ import { Link, useParams } from "react-router";
17
+
18
+ import { Badge } from "@/components/ui/badge";
19
+ import { Button } from "@/components/ui/button";
20
+ import {
21
+ Card,
22
+ CardContent,
23
+ CardDescription,
24
+ CardHeader,
25
+ CardTitle,
26
+ } from "@/components/ui/card";
27
+ import { Skeleton } from "@/components/ui/skeleton";
28
+ import {
29
+ Table,
30
+ TableBody,
31
+ TableCell,
32
+ TableHead,
33
+ TableHeader,
34
+ TableRow,
35
+ } from "@/components/ui/table";
36
+
37
+ type SessionRecordingSummary = {
38
+ id: string;
39
+ clientRecordingId: string;
40
+ sessionId: string;
41
+ userId: string | null;
42
+ anonymousId: string | null;
43
+ userKey: string | null;
44
+ startedAt: string;
45
+ endedAt: string | null;
46
+ durationMs: number | null;
47
+ chunkCount: number;
48
+ eventCount: number;
49
+ totalBytes: number;
50
+ pageCount: number;
51
+ errorCount: number;
52
+ rageClickCount: number;
53
+ privacyMode: string;
54
+ firstUrl: string | null;
55
+ lastUrl: string | null;
56
+ path: string | null;
57
+ hostname: string | null;
58
+ referrer: string | null;
59
+ app: string | null;
60
+ template: string | null;
61
+ status: "active" | "completed";
62
+ createdAt: string;
63
+ updatedAt: string;
64
+ lastIngestedAt: string | null;
65
+ };
66
+
67
+ type ReplayChunkEvents = {
68
+ seq: number;
69
+ checksum: string;
70
+ byteLength: number;
71
+ eventCount: number;
72
+ events: unknown[];
73
+ unavailable?: boolean;
74
+ };
75
+
76
+ type SessionReplayEventsResponse = {
77
+ recording: SessionRecordingSummary;
78
+ chunks: ReplayChunkEvents[];
79
+ eventCount: number;
80
+ truncated: boolean;
81
+ unavailableChunks: number;
82
+ };
83
+
84
+ type ReplayPlayerStatus = "idle" | "loading" | "ready" | "error";
85
+
86
+ export default function SessionDetailPage() {
87
+ const t = useT();
88
+ const { recordingId = "" } = useParams();
89
+ const { send, isGenerating, codeRequiredDialog } = useSendToAgentChat();
90
+ const { data, isLoading, error } =
91
+ useActionQuery<SessionReplayEventsResponse>(
92
+ "get-session-replay-events",
93
+ { recordingId, limit: 10000 },
94
+ { enabled: Boolean(recordingId), staleTime: 30_000 },
95
+ );
96
+ const recording = data?.recording;
97
+
98
+ return (
99
+ <div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6">
100
+ {codeRequiredDialog}
101
+ <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
102
+ <div className="min-w-0 space-y-3">
103
+ <Button variant="ghost" size="sm" asChild className="-ms-2 w-fit">
104
+ <Link to="/sessions">
105
+ <IconArrowLeft className="h-4 w-4" />
106
+ {t("sessions.backToSessions")}
107
+ </Link>
108
+ </Button>
109
+ <div className="space-y-2">
110
+ <div className="flex flex-wrap items-center gap-2">
111
+ <IconPlayerPlay className="h-5 w-5 text-primary" />
112
+ <h1 className="break-all font-mono text-xl font-semibold tracking-normal md:text-2xl">
113
+ {recording?.sessionId ?? recordingId}
114
+ </h1>
115
+ </div>
116
+ {recording ? (
117
+ <p className="max-w-3xl text-sm text-muted-foreground">
118
+ {recording.app ||
119
+ recording.template ||
120
+ t("sessions.unknownApp")}{" "}
121
+ · {formatDateTime(recording.startedAt)} -{" "}
122
+ {formatDateTime(
123
+ recording.endedAt ??
124
+ recording.lastIngestedAt ??
125
+ recording.startedAt,
126
+ )}
127
+ </p>
128
+ ) : null}
129
+ </div>
130
+ </div>
131
+ {recording ? (
132
+ <Button
133
+ type="button"
134
+ variant="outline"
135
+ disabled={isGenerating}
136
+ onClick={() =>
137
+ send({
138
+ type: "content",
139
+ submit: true,
140
+ message: `Summarize session replay recording ${recording.id}. Tell me what the user did, where they struggled, and whether errors or rage clicks stood out.`,
141
+ context: `Use get-session-replay-summary and get-session-replay-events with recordingId=${recording.id}. The visible page is /sessions/${recording.id}.`,
142
+ })
143
+ }
144
+ >
145
+ <IconMessageCircle className="h-4 w-4" />
146
+ {t("sessions.askAgent")}
147
+ </Button>
148
+ ) : null}
149
+ </div>
150
+
151
+ {error ? (
152
+ <Card>
153
+ <CardContent className="flex items-center gap-3 p-6 text-sm text-destructive">
154
+ <IconExclamationCircle className="h-5 w-5" />
155
+ {t("sessions.loadFailed", { message: error.message })}
156
+ </CardContent>
157
+ </Card>
158
+ ) : isLoading ? (
159
+ <DetailSkeleton />
160
+ ) : data && recording ? (
161
+ <>
162
+ <SessionStats recording={recording} />
163
+ <ReplayPlayer response={data} />
164
+ <ReplayChunksTable response={data} />
165
+ </>
166
+ ) : null}
167
+ </div>
168
+ );
169
+ }
170
+
171
+ function SessionStats({ recording }: { recording: SessionRecordingSummary }) {
172
+ const t = useT();
173
+ const stats = [
174
+ [t("sessions.started"), formatDateTime(recording.startedAt)],
175
+ [t("sessions.duration"), formatDuration(recording.durationMs)],
176
+ [t("sessions.events"), formatNumber(recording.eventCount)],
177
+ [t("sessions.pages"), formatNumber(recording.pageCount)],
178
+ [t("sessions.errors"), formatNumber(recording.errorCount)],
179
+ [t("sessions.visitor"), visitorLabel(recording, t)],
180
+ ];
181
+ return (
182
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
183
+ {stats.map(([label, value]) => (
184
+ <Card key={label}>
185
+ <CardContent className="p-4">
186
+ <p className="text-xs font-medium uppercase text-muted-foreground">
187
+ {label}
188
+ </p>
189
+ <p className="mt-1 truncate text-sm font-medium">{value}</p>
190
+ </CardContent>
191
+ </Card>
192
+ ))}
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function ReplayPlayer({ response }: { response: SessionReplayEventsResponse }) {
198
+ const t = useT();
199
+ const rootRef = useRef<HTMLDivElement>(null);
200
+ const replayerRef = useRef<any>(null);
201
+ const [status, setStatus] = useState<ReplayPlayerStatus>("idle");
202
+ const [error, setError] = useState<string | null>(null);
203
+ const events = useMemo(
204
+ () =>
205
+ response.chunks
206
+ .flatMap((chunk) => chunk.events)
207
+ .filter((event) => event && typeof event === "object")
208
+ .sort((a: any, b: any) => {
209
+ const at = Number(a.timestamp ?? 0);
210
+ const bt = Number(b.timestamp ?? 0);
211
+ return at - bt;
212
+ }),
213
+ [response.chunks],
214
+ );
215
+
216
+ useEffect(() => {
217
+ if (!rootRef.current) return;
218
+ let cancelled = false;
219
+ let localReplayer: any = null;
220
+
221
+ async function loadReplay() {
222
+ if (events.length === 0) {
223
+ throw new Error(t("sessions.noReplayEvents"));
224
+ }
225
+ setStatus("loading");
226
+ setError(null);
227
+ await import("@rrweb/replay/dist/style.css");
228
+ const { Replayer } = await import("@rrweb/replay");
229
+ if (cancelled || !rootRef.current) return;
230
+
231
+ rootRef.current.innerHTML = "";
232
+ localReplayer = new Replayer(events as any[], {
233
+ root: rootRef.current,
234
+ skipInactive: true,
235
+ showWarning: false,
236
+ showDebug: false,
237
+ triggerFocus: false,
238
+ });
239
+ replayerRef.current = localReplayer;
240
+ localReplayer.play();
241
+ setStatus("ready");
242
+ }
243
+
244
+ void loadReplay().catch((loadError: any) => {
245
+ if (cancelled) return;
246
+ setError(loadError?.message || String(loadError));
247
+ setStatus("error");
248
+ });
249
+
250
+ return () => {
251
+ cancelled = true;
252
+ try {
253
+ localReplayer?.destroy?.();
254
+ replayerRef.current?.destroy?.();
255
+ } catch {
256
+ // rrweb cleanup is best-effort across versions.
257
+ }
258
+ replayerRef.current = null;
259
+ };
260
+ }, [events, t]);
261
+
262
+ return (
263
+ <Card>
264
+ <CardHeader className="pb-4">
265
+ <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
266
+ <div>
267
+ <CardTitle className="flex items-center gap-2 text-base">
268
+ <IconPlayerPlay className="h-4 w-4" />
269
+ {t("sessions.replayPlayer")}
270
+ </CardTitle>
271
+ <CardDescription>
272
+ {t("sessions.chunkAndEventCount", {
273
+ chunks: String(response.chunks.length),
274
+ events: String(response.eventCount),
275
+ })}
276
+ {response.truncated ? ` ${t("sessions.truncated")}` : ""}
277
+ </CardDescription>
278
+ </div>
279
+ <div className="flex items-center gap-2">
280
+ <Button
281
+ type="button"
282
+ variant="outline"
283
+ size="sm"
284
+ disabled={status !== "ready"}
285
+ onClick={() => {
286
+ replayerRef.current?.pause?.();
287
+ replayerRef.current?.play?.(0);
288
+ }}
289
+ >
290
+ <IconPlayerSkipBack className="h-4 w-4" />
291
+ {t("sessions.restart")}
292
+ </Button>
293
+ <Button
294
+ type="button"
295
+ variant="outline"
296
+ size="icon"
297
+ aria-label={t("sessions.play")}
298
+ disabled={status !== "ready"}
299
+ onClick={() => replayerRef.current?.play?.()}
300
+ >
301
+ <IconPlayerPlay className="h-4 w-4" />
302
+ </Button>
303
+ <Button
304
+ type="button"
305
+ variant="outline"
306
+ size="icon"
307
+ aria-label={t("sessions.pause")}
308
+ disabled={status !== "ready"}
309
+ onClick={() => replayerRef.current?.pause?.()}
310
+ >
311
+ <IconPlayerPause className="h-4 w-4" />
312
+ </Button>
313
+ </div>
314
+ </div>
315
+ </CardHeader>
316
+ <CardContent>
317
+ <div className="space-y-3">
318
+ <div
319
+ ref={rootRef}
320
+ className="min-h-[420px] overflow-hidden rounded-md border bg-muted/20"
321
+ />
322
+ {status === "loading" ? (
323
+ <p className="text-sm text-muted-foreground">
324
+ {t("sessions.replayLoading")}
325
+ </p>
326
+ ) : null}
327
+ {status === "error" && error ? (
328
+ <p className="text-sm text-destructive">
329
+ {t("sessions.loadFailed", { message: error })}
330
+ </p>
331
+ ) : null}
332
+ {response.unavailableChunks > 0 ? (
333
+ <p className="text-sm text-muted-foreground">
334
+ {t("sessions.unavailableChunks", {
335
+ count: String(response.unavailableChunks),
336
+ })}
337
+ </p>
338
+ ) : null}
339
+ </div>
340
+ </CardContent>
341
+ </Card>
342
+ );
343
+ }
344
+
345
+ function ReplayChunksTable({
346
+ response,
347
+ }: {
348
+ response: SessionReplayEventsResponse;
349
+ }) {
350
+ const t = useT();
351
+ return (
352
+ <Card>
353
+ <CardHeader>
354
+ <CardTitle className="text-base">
355
+ {t("sessions.replayChunks")}
356
+ </CardTitle>
357
+ <CardDescription>
358
+ {t("sessions.replayChunksDescription")}
359
+ </CardDescription>
360
+ </CardHeader>
361
+ <CardContent className="p-0">
362
+ <div className="overflow-x-auto">
363
+ <Table>
364
+ <TableHeader>
365
+ <TableRow>
366
+ <TableHead>{t("sessions.sequence")}</TableHead>
367
+ <TableHead>{t("sessions.events")}</TableHead>
368
+ <TableHead>{t("sessions.size")}</TableHead>
369
+ <TableHead>{t("sessions.checksum")}</TableHead>
370
+ </TableRow>
371
+ </TableHeader>
372
+ <TableBody>
373
+ {response.chunks.map((chunk) => (
374
+ <TableRow key={chunk.seq}>
375
+ <TableCell className="font-mono text-xs">
376
+ {chunk.seq}
377
+ </TableCell>
378
+ <TableCell>{formatNumber(chunk.eventCount)}</TableCell>
379
+ <TableCell>{formatBytes(chunk.byteLength)}</TableCell>
380
+ <TableCell className="max-w-[360px] truncate font-mono text-xs">
381
+ {chunk.checksum}
382
+ </TableCell>
383
+ </TableRow>
384
+ ))}
385
+ </TableBody>
386
+ </Table>
387
+ </div>
388
+ </CardContent>
389
+ </Card>
390
+ );
391
+ }
392
+
393
+ function DetailSkeleton() {
394
+ return (
395
+ <div className="space-y-4">
396
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
397
+ {Array.from({ length: 6 }).map((_, index) => (
398
+ <Skeleton key={index} className="h-24 w-full" />
399
+ ))}
400
+ </div>
401
+ <Skeleton className="h-[520px] w-full" />
402
+ </div>
403
+ );
404
+ }
405
+
406
+ function visitorLabel(
407
+ recording: SessionRecordingSummary,
408
+ t: ReturnType<typeof useT>,
409
+ ): string {
410
+ return (
411
+ recording.userId ||
412
+ recording.userKey ||
413
+ recording.anonymousId ||
414
+ t("sessions.anonymous")
415
+ );
416
+ }
417
+
418
+ function formatDateTime(value: string): string {
419
+ const date = new Date(value);
420
+ if (!Number.isFinite(date.getTime())) return value;
421
+ return new Intl.DateTimeFormat(undefined, {
422
+ month: "short",
423
+ day: "numeric",
424
+ hour: "numeric",
425
+ minute: "2-digit",
426
+ }).format(date);
427
+ }
428
+
429
+ function formatDuration(ms: number | null): string {
430
+ if (!ms || !Number.isFinite(ms) || ms <= 0) return "0s";
431
+ const seconds = Math.round(ms / 1000);
432
+ if (seconds < 60) return `${seconds}s`;
433
+ const minutes = Math.floor(seconds / 60);
434
+ const remainingSeconds = seconds % 60;
435
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
436
+ const hours = Math.floor(minutes / 60);
437
+ const remainingMinutes = minutes % 60;
438
+ return `${hours}h ${remainingMinutes}m`;
439
+ }
440
+
441
+ function formatNumber(value: number): string {
442
+ return new Intl.NumberFormat().format(value);
443
+ }
444
+
445
+ function formatBytes(value: number): string {
446
+ if (!Number.isFinite(value) || value <= 0) return "0 B";
447
+ if (value < 1024) return `${value} B`;
448
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
449
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`;
450
+ }