lytx 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useChat } from "@ai-sdk/react";
|
|
5
|
+
import { DefaultChatTransport } from "ai";
|
|
6
|
+
import { ResponsiveBar } from "@nivo/bar";
|
|
7
|
+
import { ResponsiveLine } from "@nivo/line";
|
|
8
|
+
import { ResponsivePie } from "@nivo/pie";
|
|
9
|
+
import { Button } from "@/app/components/ui/Button";
|
|
10
|
+
import { AuthContext } from "@/app/providers/AuthProvider";
|
|
11
|
+
import { useTheme } from "@/app/providers/ThemeProvider";
|
|
12
|
+
import { createChartTheme } from "@/app/utils/chartThemes";
|
|
13
|
+
|
|
14
|
+
type AskAiWorkspaceProps = {
|
|
15
|
+
initialAiConfigured: boolean;
|
|
16
|
+
initialAiModel: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AiChartPoint = {
|
|
20
|
+
x: string;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AiNivoChartOutput = {
|
|
25
|
+
kind: "nivo-chart";
|
|
26
|
+
chartType: "bar" | "line" | "pie";
|
|
27
|
+
title: string;
|
|
28
|
+
metricType?: string;
|
|
29
|
+
siteId: number;
|
|
30
|
+
dateRange?: {
|
|
31
|
+
start?: string;
|
|
32
|
+
end?: string;
|
|
33
|
+
};
|
|
34
|
+
points: AiChartPoint[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const truncateAxisLabel = (value: unknown, max = 20) => {
|
|
38
|
+
const label = String(value ?? "").trim();
|
|
39
|
+
if (label.length <= max) return label;
|
|
40
|
+
return `${label.slice(0, max - 3)}...`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function isAiNivoChartOutput(value: unknown): value is AiNivoChartOutput {
|
|
44
|
+
if (!value || typeof value !== "object") return false;
|
|
45
|
+
const candidate = value as Partial<AiNivoChartOutput>;
|
|
46
|
+
const chartType = candidate.chartType;
|
|
47
|
+
return candidate.kind === "nivo-chart"
|
|
48
|
+
&& (chartType === "bar" || chartType === "line" || chartType === "pie")
|
|
49
|
+
&& Array.isArray(candidate.points);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getMessageText(parts: Array<unknown>) {
|
|
53
|
+
return parts
|
|
54
|
+
.map((part) => {
|
|
55
|
+
if (!part || typeof part !== "object") return "";
|
|
56
|
+
const candidate = part as { type?: string; text?: string };
|
|
57
|
+
return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
|
|
58
|
+
})
|
|
59
|
+
.join("");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getChartOutput(parts: Array<unknown>): AiNivoChartOutput | null {
|
|
63
|
+
for (const part of parts) {
|
|
64
|
+
if (!part || typeof part !== "object") continue;
|
|
65
|
+
const candidate = part as { state?: string; output?: unknown };
|
|
66
|
+
if (candidate.state !== "output-available") continue;
|
|
67
|
+
if (isAiNivoChartOutput(candidate.output)) {
|
|
68
|
+
return candidate.output;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getChartFallbackSummary(chart: AiNivoChartOutput) {
|
|
76
|
+
const points = chart.points
|
|
77
|
+
.map((point) => ({ x: String(point.x ?? "Unknown"), y: Number(point.y) || 0 }))
|
|
78
|
+
.filter((point) => point.x.length > 0)
|
|
79
|
+
.toSorted((a, b) => b.y - a.y);
|
|
80
|
+
|
|
81
|
+
if (points.length === 0) {
|
|
82
|
+
return `Here is your ${chart.chartType} chart for ${chart.title}. There is no data in the selected range.`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const top = points.slice(0, 3).map((point) => `${point.x} (${point.y})`).join(", ");
|
|
86
|
+
return `Here is your ${chart.chartType} chart for ${chart.title}. Top values: ${top}.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const starterPrompts = [
|
|
90
|
+
"Show a bar chart of top pages in the last 14 days",
|
|
91
|
+
"Make a bar chart of top referrers for the last 7 days",
|
|
92
|
+
"Create a line chart of daily event volume for the last 30 days",
|
|
93
|
+
"Plot a line chart of hourly events for the last 24 hours",
|
|
94
|
+
"Show a pie chart of device types for this week",
|
|
95
|
+
"Give me a pie chart of country distribution for today",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function AskAiChartPanel({
|
|
99
|
+
chart,
|
|
100
|
+
chartTheme,
|
|
101
|
+
legendTextColor,
|
|
102
|
+
}: {
|
|
103
|
+
chart: AiNivoChartOutput;
|
|
104
|
+
chartTheme: ReturnType<typeof createChartTheme>;
|
|
105
|
+
legendTextColor: string;
|
|
106
|
+
}) {
|
|
107
|
+
const points = chart.points
|
|
108
|
+
.map((point) => ({ x: String(point.x ?? "Unknown"), y: Number(point.y) || 0 }))
|
|
109
|
+
.filter((point) => point.x.length > 0);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="w-full rounded-lg border border-(--theme-border-primary) bg-(--theme-bg-primary) p-3">
|
|
113
|
+
<div className="mb-2 text-sm font-medium text-(--theme-text-primary)">{chart.title}</div>
|
|
114
|
+
<div style={{ height: 260 }}>
|
|
115
|
+
{points.length === 0 ? (
|
|
116
|
+
<div className="flex h-full items-center justify-center text-sm text-(--theme-text-secondary)">
|
|
117
|
+
No chart data for this range.
|
|
118
|
+
</div>
|
|
119
|
+
) : chart.chartType === "line" ? (
|
|
120
|
+
<ResponsiveLine
|
|
121
|
+
data={[
|
|
122
|
+
{
|
|
123
|
+
id: chart.metricType || chart.title,
|
|
124
|
+
data: points.map((point) => ({ x: point.x, y: point.y })),
|
|
125
|
+
},
|
|
126
|
+
]}
|
|
127
|
+
margin={{ top: 20, right: 24, bottom: 44, left: 56 }}
|
|
128
|
+
xScale={{ type: "point" }}
|
|
129
|
+
yScale={{ type: "linear", min: 0, max: "auto", stacked: false, reverse: false }}
|
|
130
|
+
pointSize={7}
|
|
131
|
+
pointBorderWidth={2}
|
|
132
|
+
enableArea
|
|
133
|
+
areaOpacity={0.2}
|
|
134
|
+
useMesh
|
|
135
|
+
colors={["#f59e0b"]}
|
|
136
|
+
theme={chartTheme}
|
|
137
|
+
/>
|
|
138
|
+
) : chart.chartType === "pie" ? (
|
|
139
|
+
<ResponsivePie
|
|
140
|
+
data={points.map((point) => ({ id: point.x, label: point.x, value: point.y }))}
|
|
141
|
+
margin={{ top: 20, right: 24, bottom: 44, left: 24 }}
|
|
142
|
+
innerRadius={0.5}
|
|
143
|
+
padAngle={0.7}
|
|
144
|
+
cornerRadius={3}
|
|
145
|
+
activeOuterRadiusOffset={8}
|
|
146
|
+
colors={["#f59e0b", "#f97316", "#fb923c", "#fdba74", "#fcd34d", "#fbbf24"]}
|
|
147
|
+
theme={chartTheme}
|
|
148
|
+
legends={[
|
|
149
|
+
{
|
|
150
|
+
anchor: "bottom",
|
|
151
|
+
direction: "row",
|
|
152
|
+
justify: false,
|
|
153
|
+
translateY: 36,
|
|
154
|
+
itemWidth: 90,
|
|
155
|
+
itemHeight: 18,
|
|
156
|
+
itemsSpacing: 4,
|
|
157
|
+
symbolSize: 12,
|
|
158
|
+
symbolShape: "circle",
|
|
159
|
+
itemTextColor: legendTextColor,
|
|
160
|
+
},
|
|
161
|
+
]}
|
|
162
|
+
/>
|
|
163
|
+
) : (
|
|
164
|
+
<ResponsiveBar
|
|
165
|
+
data={points.map((point) => ({ x: point.x, y: point.y }))}
|
|
166
|
+
keys={["y"]}
|
|
167
|
+
indexBy="x"
|
|
168
|
+
margin={{ top: 20, right: 24, bottom: 56, left: 72 }}
|
|
169
|
+
padding={0.3}
|
|
170
|
+
colors={["#f59e0b"]}
|
|
171
|
+
valueScale={{ type: "linear" }}
|
|
172
|
+
indexScale={{ type: "band", round: true }}
|
|
173
|
+
axisBottom={{
|
|
174
|
+
tickRotation: -20,
|
|
175
|
+
tickPadding: 14,
|
|
176
|
+
format: (value) => truncateAxisLabel(value),
|
|
177
|
+
}}
|
|
178
|
+
theme={chartTheme}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function AskAiWorkspace({ initialAiConfigured, initialAiModel }: AskAiWorkspaceProps) {
|
|
187
|
+
const { current_site } = useContext(AuthContext) || { current_site: null };
|
|
188
|
+
const { theme } = useTheme();
|
|
189
|
+
const chartTheme = useMemo(() => createChartTheme(theme === "dark"), [theme]);
|
|
190
|
+
const legendTextColor = theme === "dark" ? "#ffffff" : "#4b5563";
|
|
191
|
+
const chatTransport = useMemo(
|
|
192
|
+
() =>
|
|
193
|
+
new DefaultChatTransport({
|
|
194
|
+
api: "/api/ai/chat",
|
|
195
|
+
body: {
|
|
196
|
+
site_id: current_site?.id ?? null,
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
[current_site?.id],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
203
|
+
const aiConfigured = initialAiConfigured;
|
|
204
|
+
|
|
205
|
+
const {
|
|
206
|
+
messages,
|
|
207
|
+
status,
|
|
208
|
+
error,
|
|
209
|
+
sendMessage,
|
|
210
|
+
setMessages,
|
|
211
|
+
clearError,
|
|
212
|
+
} = useChat({
|
|
213
|
+
transport: chatTransport,
|
|
214
|
+
experimental_throttle: 24,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const isBusy = status === "submitted" || status === "streaming";
|
|
218
|
+
const canSend = aiConfigured && !isBusy;
|
|
219
|
+
const [draft, setDraft] = useState("");
|
|
220
|
+
const draftValue = typeof draft === "string" ? draft : "";
|
|
221
|
+
const modelLabel = aiConfigured
|
|
222
|
+
? (initialAiModel || "Configured model")
|
|
223
|
+
: "Model not configured";
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const container = messagesContainerRef.current;
|
|
227
|
+
if (!container) return;
|
|
228
|
+
|
|
229
|
+
const frame = window.requestAnimationFrame(() => {
|
|
230
|
+
container.scrollTo({ top: container.scrollHeight, behavior: "auto" });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return () => window.cancelAnimationFrame(frame);
|
|
234
|
+
}, [messages, status, error]);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<section className="w-full min-h-[calc(100dvh-9rem)]">
|
|
238
|
+
<div className="flex h-[calc(100dvh-11rem)] min-h-[560px] flex-col rounded-xl border border-(--theme-border-primary) bg-(--theme-card-bg) p-4 sm:p-6">
|
|
239
|
+
<div className="flex items-center justify-between gap-3">
|
|
240
|
+
<div>
|
|
241
|
+
<h2 className="text-xl font-semibold text-(--theme-text-primary)">Ask AI</h2>
|
|
242
|
+
<p className="mt-1 text-sm text-(--theme-text-secondary)">
|
|
243
|
+
Quick conversational help for analytics questions and report ideas.
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex items-center gap-2">
|
|
247
|
+
<span className="hidden sm:inline-flex rounded-full border border-(--theme-border-primary) bg-(--theme-bg-secondary) px-2.5 py-1 text-xs text-(--theme-text-secondary)">
|
|
248
|
+
{modelLabel}
|
|
249
|
+
</span>
|
|
250
|
+
<Button
|
|
251
|
+
variant="secondary"
|
|
252
|
+
size="sm"
|
|
253
|
+
onClick={() => {
|
|
254
|
+
setMessages([]);
|
|
255
|
+
clearError();
|
|
256
|
+
}}
|
|
257
|
+
disabled={messages.length === 0 && !error}
|
|
258
|
+
>
|
|
259
|
+
Clear
|
|
260
|
+
</Button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{!aiConfigured ? (
|
|
265
|
+
<div className="mt-4 rounded-lg border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-red-400">
|
|
266
|
+
AI is not configured on the server yet.
|
|
267
|
+
</div>
|
|
268
|
+
) : null}
|
|
269
|
+
|
|
270
|
+
<div className="mt-4 flex min-h-0 flex-1 flex-col rounded-lg border border-(--theme-border-primary) bg-(--theme-bg-secondary) p-3">
|
|
271
|
+
<div ref={messagesContainerRef} className="min-h-0 flex-1 overflow-y-auto space-y-3 pr-1">
|
|
272
|
+
{messages.length === 0 ? (
|
|
273
|
+
<div className="space-y-3">
|
|
274
|
+
<p className="text-sm text-(--theme-text-secondary)">
|
|
275
|
+
Ask a question or use a starter prompt below.
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
) : (
|
|
279
|
+
messages.map((message) => {
|
|
280
|
+
const isUser = message.role === "user";
|
|
281
|
+
const text = getMessageText(message.parts as Array<unknown>);
|
|
282
|
+
const chart = getChartOutput(message.parts as Array<unknown>);
|
|
283
|
+
|
|
284
|
+
if (text.trim().length === 0 && !chart) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (isUser) {
|
|
289
|
+
return (
|
|
290
|
+
<div
|
|
291
|
+
key={message.id}
|
|
292
|
+
className="ml-auto max-w-[92%] rounded-lg bg-(--theme-text-primary) px-3 py-2 text-sm whitespace-pre-wrap text-(--theme-bg-primary)"
|
|
293
|
+
>
|
|
294
|
+
{text}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div key={message.id} className="mr-auto w-full space-y-2">
|
|
301
|
+
{text.trim().length > 0 ? (
|
|
302
|
+
<div className="max-w-[92%] rounded-lg border border-(--theme-border-primary) bg-(--theme-bg-primary) px-3 py-2 text-sm whitespace-pre-wrap text-(--theme-text-primary)">
|
|
303
|
+
{text}
|
|
304
|
+
</div>
|
|
305
|
+
) : chart ? (
|
|
306
|
+
<div className="max-w-[92%] rounded-lg border border-(--theme-border-primary) bg-(--theme-bg-primary) px-3 py-2 text-sm whitespace-pre-wrap text-(--theme-text-primary)">
|
|
307
|
+
{getChartFallbackSummary(chart)}
|
|
308
|
+
</div>
|
|
309
|
+
) : null}
|
|
310
|
+
{chart ? (
|
|
311
|
+
<AskAiChartPanel
|
|
312
|
+
chart={chart}
|
|
313
|
+
chartTheme={chartTheme}
|
|
314
|
+
legendTextColor={legendTextColor}
|
|
315
|
+
/>
|
|
316
|
+
) : null}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
})
|
|
320
|
+
)}
|
|
321
|
+
{error ? <p className="text-sm text-red-400">{error.message}</p> : null}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{messages.length === 0 ? (
|
|
325
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
326
|
+
{starterPrompts.map((prompt) => (
|
|
327
|
+
<button
|
|
328
|
+
key={prompt}
|
|
329
|
+
type="button"
|
|
330
|
+
className="rounded-full border border-(--theme-border-primary) bg-(--theme-bg-primary) px-3 py-1.5 text-xs text-(--theme-text-primary) hover:bg-(--theme-bg-tertiary)"
|
|
331
|
+
onClick={() => setDraft(prompt)}
|
|
332
|
+
>
|
|
333
|
+
{prompt}
|
|
334
|
+
</button>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
) : null}
|
|
338
|
+
|
|
339
|
+
<form
|
|
340
|
+
className="mt-3 flex items-center gap-2"
|
|
341
|
+
onSubmit={(event) => {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
if (!canSend) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const nextMessage = draftValue.trim();
|
|
347
|
+
if (!nextMessage) return;
|
|
348
|
+
clearError();
|
|
349
|
+
void sendMessage({ text: nextMessage });
|
|
350
|
+
setDraft("");
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
<input
|
|
354
|
+
value={draftValue}
|
|
355
|
+
onChange={(event) => {
|
|
356
|
+
if (error) clearError();
|
|
357
|
+
setDraft(event.target.value);
|
|
358
|
+
}}
|
|
359
|
+
placeholder={aiConfigured ? "Ask about your data..." : "AI not configured"}
|
|
360
|
+
disabled={!aiConfigured || isBusy}
|
|
361
|
+
className="flex-1 rounded-md border border-(--theme-input-border) bg-(--theme-input-bg) px-3 py-2 text-sm text-(--theme-text-primary) focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
|
|
362
|
+
/>
|
|
363
|
+
<Button type="submit" variant="primary" disabled={!canSend || draftValue.trim().length === 0}>
|
|
364
|
+
{isBusy ? "..." : "Send"}
|
|
365
|
+
</Button>
|
|
366
|
+
</form>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</section>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
type ReportTemplateCard = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const reportTemplateCards: ReportTemplateCard[] = [
|
|
10
|
+
{
|
|
11
|
+
id: "ecomm-tracker",
|
|
12
|
+
title: "Ecomm Tracker",
|
|
13
|
+
description: "Monitor storefront traffic, product views, cart starts, and checkout conversion in one report.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "marketing-leads",
|
|
17
|
+
title: "Marketing leads",
|
|
18
|
+
description: "Track landing-page engagement, lead-form submissions, and channel quality for campaign optimization.",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
type CreateReportStarterProps = {
|
|
23
|
+
onStartCustomReport: () => void;
|
|
24
|
+
onStartTemplate: (templateId: string) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function CreateReportStarter({ onStartCustomReport, onStartTemplate }: CreateReportStarterProps) {
|
|
28
|
+
return (
|
|
29
|
+
<section className="max-w-5xl mx-auto space-y-6">
|
|
30
|
+
<div className="space-y-2">
|
|
31
|
+
<h2 className="text-2xl sm:text-3xl font-semibold text-(--theme-text-primary)">
|
|
32
|
+
Start a report
|
|
33
|
+
</h2>
|
|
34
|
+
<p className="text-(--theme-text-secondary)">
|
|
35
|
+
Build a custom report from scratch or start from a saved template.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={onStartCustomReport}
|
|
43
|
+
className="group rounded-xl border border-dashed border-(--theme-border-primary) bg-(--theme-bg-secondary) hover:bg-(--theme-bg-tertiary) p-5 text-left transition-colors focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
|
|
44
|
+
>
|
|
45
|
+
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-(--theme-border-primary) text-(--theme-text-primary) mb-4">
|
|
46
|
+
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
47
|
+
<path d="M12 5v14" />
|
|
48
|
+
<path d="M5 12h14" />
|
|
49
|
+
</svg>
|
|
50
|
+
</span>
|
|
51
|
+
<p className="text-base font-semibold text-(--theme-text-primary)">Build custom report</p>
|
|
52
|
+
<p className="mt-1 text-sm text-(--theme-text-secondary)">
|
|
53
|
+
Start with a blank canvas and add the metrics and dimensions you need.
|
|
54
|
+
</p>
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
{reportTemplateCards.map((template) => (
|
|
58
|
+
<button
|
|
59
|
+
key={template.id}
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => onStartTemplate(template.id)}
|
|
62
|
+
className="rounded-xl border border-(--theme-border-primary) bg-(--theme-card-bg) hover:bg-(--theme-bg-secondary) p-5 text-left transition-colors focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
|
|
63
|
+
>
|
|
64
|
+
<p className="text-base font-semibold text-(--theme-text-primary)">{template.title}</p>
|
|
65
|
+
<p className="mt-1 text-sm text-(--theme-text-secondary)">{template.description}</p>
|
|
66
|
+
<span className="mt-4 inline-flex items-center text-xs font-medium text-(--theme-text-secondary)">
|
|
67
|
+
Use template
|
|
68
|
+
</span>
|
|
69
|
+
</button>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import type { DashboardFilters } from "@/app/components/charts/ChartComponents";
|
|
5
|
+
|
|
6
|
+
type DashboardRouteFiltersContextValue = {
|
|
7
|
+
filters: DashboardFilters;
|
|
8
|
+
timezone: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const DashboardRouteFiltersContext =
|
|
12
|
+
createContext<DashboardRouteFiltersContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
export const useDashboardRouteFilters = () => useContext(DashboardRouteFiltersContext);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useMemo } from "react";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { SiteSelector } from "@components/SiteSelector";
|
|
7
|
+
import { AuthContext } from "@/app/providers/AuthProvider";
|
|
8
|
+
import { CurrentVisitors } from "@/app/components/charts/ChartComponents";
|
|
9
|
+
import {
|
|
10
|
+
ReportBuilderMenu,
|
|
11
|
+
type ReportBuilderMenuActiveId,
|
|
12
|
+
type ReportBuilderMenuItem,
|
|
13
|
+
} from "@/app/components/ui/ReportBuilderMenu";
|
|
14
|
+
import { getDashboardReportBuilderMenuItems } from "@/app/components/reports/reportBuilderMenuItems";
|
|
15
|
+
import type { CustomReportRecord } from "@/app/components/reports/custom/types";
|
|
16
|
+
|
|
17
|
+
export type ToolbarSiteOption = {
|
|
18
|
+
site_id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
tag_id: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type DashboardToolbarProps = {
|
|
24
|
+
activeReportBuilderItemId?: ReportBuilderMenuActiveId;
|
|
25
|
+
reportBuilderEnabled?: boolean;
|
|
26
|
+
askAiEnabled?: boolean;
|
|
27
|
+
controls?: ReactNode;
|
|
28
|
+
footer?: ReactNode;
|
|
29
|
+
initialSites?: ToolbarSiteOption[];
|
|
30
|
+
initialSiteId?: number | null;
|
|
31
|
+
initialReportSwitcherItems?: ReportBuilderMenuItem[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function DashboardToolbar({
|
|
35
|
+
activeReportBuilderItemId = "create-report",
|
|
36
|
+
reportBuilderEnabled = false,
|
|
37
|
+
askAiEnabled = true,
|
|
38
|
+
controls,
|
|
39
|
+
footer,
|
|
40
|
+
initialSites = [],
|
|
41
|
+
initialSiteId = null,
|
|
42
|
+
initialReportSwitcherItems = [],
|
|
43
|
+
}: DashboardToolbarProps) {
|
|
44
|
+
const { data: session, current_site } = useContext(AuthContext) || { data: null, current_site: null };
|
|
45
|
+
const activeCustomReportUuid =
|
|
46
|
+
activeReportBuilderItemId.startsWith("custom-report:")
|
|
47
|
+
? activeReportBuilderItemId.slice("custom-report:".length)
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
const fallbackSiteId = session?.userSites?.[0]?.site_id ?? initialSiteId ?? initialSites[0]?.site_id;
|
|
51
|
+
const currentSiteId = current_site?.id ?? fallbackSiteId;
|
|
52
|
+
|
|
53
|
+
const { data: customReportsData } = useQuery<{ reports?: CustomReportRecord[] }>({
|
|
54
|
+
queryKey: ["dashboard-toolbar-custom-reports", currentSiteId],
|
|
55
|
+
enabled: reportBuilderEnabled && !!currentSiteId,
|
|
56
|
+
queryFn: async () => {
|
|
57
|
+
const response = await fetch(`/api/reports/custom?site_id=${currentSiteId}`);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return { reports: [] };
|
|
60
|
+
}
|
|
61
|
+
return (await response.json()) as { reports?: CustomReportRecord[] };
|
|
62
|
+
},
|
|
63
|
+
staleTime: 0,
|
|
64
|
+
gcTime: 0,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const reportSwitcherItems = useMemo<ReportBuilderMenuItem[]>(() => {
|
|
68
|
+
const reports = customReportsData?.reports ?? [];
|
|
69
|
+
if (reports.length === 0 && initialReportSwitcherItems.length > 0) {
|
|
70
|
+
return initialReportSwitcherItems;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return reports.slice(0, 10).map((report) => ({
|
|
74
|
+
id: `custom-report:${report.uuid}`,
|
|
75
|
+
label: report.name || "Untitled custom report",
|
|
76
|
+
href: `/dashboard/reports/custom/${report.uuid}`,
|
|
77
|
+
}));
|
|
78
|
+
}, [customReportsData?.reports, initialReportSwitcherItems]);
|
|
79
|
+
|
|
80
|
+
const hasActiveCustomReportItem = activeCustomReportUuid
|
|
81
|
+
? reportSwitcherItems.some((item) => item.id === `custom-report:${activeCustomReportUuid}`)
|
|
82
|
+
: false;
|
|
83
|
+
|
|
84
|
+
const { data: activeCustomReportData } = useQuery<{ report?: CustomReportRecord | null }>({
|
|
85
|
+
queryKey: ["dashboard-toolbar-active-custom-report", activeCustomReportUuid],
|
|
86
|
+
enabled: reportBuilderEnabled && Boolean(activeCustomReportUuid) && !hasActiveCustomReportItem,
|
|
87
|
+
queryFn: async () => {
|
|
88
|
+
const response = await fetch(`/api/reports/custom/${activeCustomReportUuid}`);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
return { report: null };
|
|
91
|
+
}
|
|
92
|
+
return (await response.json()) as { report?: CustomReportRecord | null };
|
|
93
|
+
},
|
|
94
|
+
staleTime: 0,
|
|
95
|
+
gcTime: 0,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const resolvedReportSwitcherItems = useMemo<ReportBuilderMenuItem[]>(() => {
|
|
99
|
+
if (!activeCustomReportUuid || hasActiveCustomReportItem) {
|
|
100
|
+
return reportSwitcherItems;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
id: `custom-report:${activeCustomReportUuid}`,
|
|
106
|
+
label: activeCustomReportData?.report?.name || "Custom report",
|
|
107
|
+
href: `/dashboard/reports/custom/${activeCustomReportUuid}`,
|
|
108
|
+
},
|
|
109
|
+
...reportSwitcherItems,
|
|
110
|
+
];
|
|
111
|
+
}, [
|
|
112
|
+
activeCustomReportData?.report?.name,
|
|
113
|
+
activeCustomReportUuid,
|
|
114
|
+
hasActiveCustomReportItem,
|
|
115
|
+
reportSwitcherItems,
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const reportBuilderMenuItems = useMemo<ReportBuilderMenuItem[]>(
|
|
119
|
+
() => [...getDashboardReportBuilderMenuItems({ askAiEnabled }), ...resolvedReportSwitcherItems],
|
|
120
|
+
[askAiEnabled, resolvedReportSwitcherItems],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="sticky top-0 z-40 bg-(--theme-bg-primary) border-t border-b border-(--theme-border-primary) px-3 py-2 sm:px-6 sm:py-3 lg:px-8 shadow-[0_6px_14px_rgba(0,0,0,0.12)]">
|
|
125
|
+
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
|
126
|
+
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
|
|
127
|
+
<SiteSelector initialSites={initialSites} initialSiteId={initialSiteId} />
|
|
128
|
+
{currentSiteId ? (
|
|
129
|
+
<div className="hidden sm:flex">
|
|
130
|
+
<CurrentVisitors siteId={currentSiteId} />
|
|
131
|
+
</div>
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex items-center gap-2 relative">
|
|
135
|
+
{reportBuilderEnabled ? (
|
|
136
|
+
<ReportBuilderMenu
|
|
137
|
+
items={reportBuilderMenuItems}
|
|
138
|
+
activeItemId={activeReportBuilderItemId}
|
|
139
|
+
/>
|
|
140
|
+
) : null}
|
|
141
|
+
{controls}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{currentSiteId ? (
|
|
146
|
+
<div className="flex sm:hidden items-center mt-1.5">
|
|
147
|
+
<CurrentVisitors siteId={currentSiteId} />
|
|
148
|
+
</div>
|
|
149
|
+
) : null}
|
|
150
|
+
|
|
151
|
+
{footer}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LayoutProps } from "rwsdk/router";
|
|
2
|
+
import type { ReportBuilderMenuActiveId } from "@/app/components/ui/ReportBuilderMenu";
|
|
3
|
+
import { DashboardWorkspaceShell } from "@/app/components/reports/DashboardWorkspaceShell";
|
|
4
|
+
import { isAskAiEnabled, isReportBuilderEnabled } from "@/lib/featureFlags";
|
|
5
|
+
import type { ToolbarSiteOption } from "@/app/components/reports/DashboardToolbar";
|
|
6
|
+
|
|
7
|
+
type LayoutContextLike = {
|
|
8
|
+
session?: {
|
|
9
|
+
last_site_id?: number | null;
|
|
10
|
+
};
|
|
11
|
+
sites?: Array<{
|
|
12
|
+
site_id: number;
|
|
13
|
+
name: string | null;
|
|
14
|
+
tag_id: string;
|
|
15
|
+
}> | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const resolveActiveReportBuilderItem = (pathname: string): ReportBuilderMenuActiveId => {
|
|
19
|
+
const customReportMarker = "/dashboard/reports/custom/";
|
|
20
|
+
if (pathname.includes(customReportMarker)) {
|
|
21
|
+
const reportUuid = decodeURIComponent(pathname.slice(pathname.indexOf(customReportMarker) + customReportMarker.length));
|
|
22
|
+
if (reportUuid && reportUuid !== "new" && !reportUuid.includes("/")) {
|
|
23
|
+
return `custom-report:${reportUuid}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (pathname.includes("/dashboard/reports/create-reference")) return "create-reference";
|
|
28
|
+
if (pathname.includes("/dashboard/reports/ask-ai")) return "ask-ai";
|
|
29
|
+
if (pathname.includes("/dashboard/reports/create-dashboard")) return "create-dashboard";
|
|
30
|
+
if (pathname.includes("/dashboard/reports/create-notification")) return "create-notification";
|
|
31
|
+
return "create-report";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function DashboardWorkspaceLayout({ children, requestInfo }: LayoutProps) {
|
|
35
|
+
const pathname = requestInfo ? new URL(requestInfo.request.url).pathname : "/dashboard/reports/create-report";
|
|
36
|
+
const activeReportBuilderItemId = resolveActiveReportBuilderItem(pathname);
|
|
37
|
+
const reportBuilderEnabled = isReportBuilderEnabled();
|
|
38
|
+
const askAiEnabled = reportBuilderEnabled && isAskAiEnabled();
|
|
39
|
+
const ctx = requestInfo?.ctx as LayoutContextLike | undefined;
|
|
40
|
+
|
|
41
|
+
const initialSites: ToolbarSiteOption[] = (ctx?.sites ?? []).map((site) => ({
|
|
42
|
+
site_id: site.site_id,
|
|
43
|
+
name: site.name || `Site ${site.site_id}`,
|
|
44
|
+
tag_id: site.tag_id,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const preferredSiteId = ctx?.session?.last_site_id ?? null;
|
|
48
|
+
const initialSiteId = initialSites.some((site) => site.site_id === preferredSiteId)
|
|
49
|
+
? preferredSiteId
|
|
50
|
+
: (initialSites[0]?.site_id ?? null);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<DashboardWorkspaceShell
|
|
54
|
+
activeReportBuilderItemId={activeReportBuilderItemId}
|
|
55
|
+
reportBuilderEnabled={reportBuilderEnabled}
|
|
56
|
+
askAiEnabled={askAiEnabled}
|
|
57
|
+
initialSites={initialSites}
|
|
58
|
+
initialSiteId={initialSiteId}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</DashboardWorkspaceShell>
|
|
62
|
+
);
|
|
63
|
+
}
|