@agent-native/core 0.99.1 → 0.99.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +12 -38
- package/corpus/core/src/audit/config.ts +0 -1
- package/corpus/core/src/chat-threads/store.ts +57 -41
- package/corpus/core/src/client/AssistantChat.tsx +1 -2
- package/corpus/core/src/client/chat/message-components.tsx +15 -3
- package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
- package/corpus/core/src/client/index.ts +7 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
- package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
- package/corpus/core/src/client/use-db-sync.ts +3 -15
- package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
- package/corpus/core/src/demo/browser-state.ts +47 -0
- package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
- package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
- package/corpus/core/src/server/action-discovery.ts +0 -1
- package/corpus/core/src/server/core-routes-plugin.ts +0 -30
- package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
- package/corpus/templates/analytics/AGENTS.md +4 -4
- package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
- package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
- package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
- package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/assets/app/routes/library.tsx +3 -4
- package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
- package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
- package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
- package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
- package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
- package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
- package/corpus/templates/design/app/pages/Index.tsx +6 -1
- package/corpus/templates/design/app/pages/Present.tsx +12 -6
- package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
- package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
- package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +13 -37
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/audit/config.d.ts.map +1 -1
- package/dist/audit/config.js +0 -1
- package/dist/audit/config.js.map +1 -1
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +47 -33
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +9 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +3 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +10 -2
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +2 -6
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +2 -59
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +1 -13
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +5 -52
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +3 -7
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-demo-mode-status.d.ts +3 -4
- package/dist/client/use-demo-mode-status.d.ts.map +1 -1
- package/dist/client/use-demo-mode-status.js +8 -23
- package/dist/client/use-demo-mode-status.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/browser-state.d.ts +10 -0
- package/dist/demo/browser-state.d.ts.map +1 -0
- package/dist/demo/browser-state.js +49 -0
- package/dist/demo/browser-state.js.map +1 -0
- package/dist/demo/fetch-interceptor.d.ts +2 -7
- package/dist/demo/fetch-interceptor.d.ts.map +1 -1
- package/dist/demo/fetch-interceptor.js +12 -56
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +2 -2
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +0 -1
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +0 -27
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
- package/corpus/core/src/demo/config.ts +0 -57
- package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
- package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
- package/dist/demo/actions/toggle-demo-mode.js +0 -21
- package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
- package/dist/demo/config.d.ts +0 -9
- package/dist/demo/config.d.ts.map +0 -1
- package/dist/demo/config.js +0 -58
- package/dist/demo/config.js.map +0 -1
|
@@ -18,7 +18,6 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
* reads go through `accessFilter` so an org-scoped key surfaces its issues to
|
|
19
19
|
* the whole org exactly like session recordings.
|
|
20
20
|
*/
|
|
21
|
-
import { appStateGet } from "@agent-native/core/application-state";
|
|
22
21
|
import { notifyWithDelivery } from "@agent-native/core/notifications";
|
|
23
22
|
import { recordChange } from "@agent-native/core/server";
|
|
24
23
|
import { accessFilter } from "@agent-native/core/sharing";
|
|
@@ -1099,56 +1098,6 @@ export interface ListErrorIssuesFilters {
|
|
|
1099
1098
|
limit?: number;
|
|
1100
1099
|
}
|
|
1101
1100
|
|
|
1102
|
-
export const ERROR_REPORTING_ANONYMOUS_EMAIL = "anonymous@builder.io";
|
|
1103
|
-
const ERROR_REPORTING_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
1104
|
-
|
|
1105
|
-
/**
|
|
1106
|
-
* Error reports contain emails in identity fields, messages, stacks, URLs,
|
|
1107
|
-
* tags, extra context, and breadcrumbs. Redact every string at this owning
|
|
1108
|
-
* read seam so demo mode never depends on browser interception timing.
|
|
1109
|
-
*/
|
|
1110
|
-
export function anonymizeErrorReportingEmails<T>(value: T): T {
|
|
1111
|
-
if (typeof value === "string") {
|
|
1112
|
-
return value.replace(
|
|
1113
|
-
ERROR_REPORTING_EMAIL_PATTERN,
|
|
1114
|
-
ERROR_REPORTING_ANONYMOUS_EMAIL,
|
|
1115
|
-
) as T;
|
|
1116
|
-
}
|
|
1117
|
-
if (Array.isArray(value)) {
|
|
1118
|
-
return value.map((item) => anonymizeErrorReportingEmails(item)) as T;
|
|
1119
|
-
}
|
|
1120
|
-
if (value && typeof value === "object") {
|
|
1121
|
-
if (value instanceof Date) return value;
|
|
1122
|
-
const output: Record<string, unknown> = {};
|
|
1123
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1124
|
-
output[key] = anonymizeErrorReportingEmails(entry);
|
|
1125
|
-
}
|
|
1126
|
-
return output as T;
|
|
1127
|
-
}
|
|
1128
|
-
return value;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
async function isErrorDemoModeEnabled(
|
|
1132
|
-
userEmail?: string | null,
|
|
1133
|
-
): Promise<boolean> {
|
|
1134
|
-
if (process.env.DEMO_MODE === "true") return true;
|
|
1135
|
-
if (!userEmail) return false;
|
|
1136
|
-
try {
|
|
1137
|
-
// Keep demo state tied to the caller whose issues are being read.
|
|
1138
|
-
const state = await appStateGet(userEmail, "demo-mode");
|
|
1139
|
-
return state?.enabled === true;
|
|
1140
|
-
} catch {
|
|
1141
|
-
return false;
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function anonymizeErrorReportingEmailsInDemoMode<T>(
|
|
1146
|
-
value: T,
|
|
1147
|
-
enabled: boolean,
|
|
1148
|
-
): T {
|
|
1149
|
-
return enabled ? anonymizeErrorReportingEmails(value) : value;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1101
|
export interface ErrorIssueSummary {
|
|
1153
1102
|
id: string;
|
|
1154
1103
|
fingerprint: string;
|
|
@@ -1221,7 +1170,6 @@ export async function listErrorIssues(
|
|
|
1221
1170
|
scope: ErrorReadScope,
|
|
1222
1171
|
filters: ListErrorIssuesFilters = {},
|
|
1223
1172
|
): Promise<ErrorIssueSummary[]> {
|
|
1224
|
-
const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
|
|
1225
1173
|
const db = getDb() as any;
|
|
1226
1174
|
const limit = Math.min(
|
|
1227
1175
|
MAX_ISSUE_LIMIT,
|
|
@@ -1312,7 +1260,7 @@ export async function listErrorIssues(
|
|
|
1312
1260
|
template: row.template ?? null,
|
|
1313
1261
|
sparkline: sparklines.get(row.id) ?? new Array(SPARKLINE_DAYS).fill(0),
|
|
1314
1262
|
}));
|
|
1315
|
-
return
|
|
1263
|
+
return issues;
|
|
1316
1264
|
}
|
|
1317
1265
|
|
|
1318
1266
|
export interface ErrorEventDetail {
|
|
@@ -1404,7 +1352,6 @@ export async function getErrorIssue(
|
|
|
1404
1352
|
issueId: string,
|
|
1405
1353
|
options: { eventsLimit?: number } = {},
|
|
1406
1354
|
): Promise<ErrorIssueDetail> {
|
|
1407
|
-
const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
|
|
1408
1355
|
const db = getDb() as any;
|
|
1409
1356
|
const [issueRow] = await db
|
|
1410
1357
|
.select()
|
|
@@ -1530,14 +1477,11 @@ export async function getErrorIssue(
|
|
|
1530
1477
|
sparkline: sparklines.get(issueId) ?? new Array(SPARKLINE_DAYS).fill(0),
|
|
1531
1478
|
};
|
|
1532
1479
|
|
|
1533
|
-
return
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
},
|
|
1539
|
-
await demoModeEnabled,
|
|
1540
|
-
);
|
|
1480
|
+
return {
|
|
1481
|
+
issue,
|
|
1482
|
+
events,
|
|
1483
|
+
sessions: Array.from(sessions.values()),
|
|
1484
|
+
};
|
|
1541
1485
|
}
|
|
1542
1486
|
|
|
1543
1487
|
export interface UpdateErrorIssueInput {
|
|
@@ -163,6 +163,7 @@ const DASHBOARD_WAU_BASE_RANGE_FILTER = dashboardLookbackTimeRangeFilter(
|
|
|
163
163
|
);
|
|
164
164
|
const DASHBOARD_EMAIL_FILTER =
|
|
165
165
|
"('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))";
|
|
166
|
+
const SESSION_STATUS_FILTER = `event_name = 'session status' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
|
|
166
167
|
const SIGNED_IN_ACTIVITY_KEY_SQL = USER_KEY_SQL;
|
|
167
168
|
const SIGNED_IN_ACTIVITY_FILTER = `event_name = 'session status' AND signed_in = 'true' AND ${SIGNED_IN_ACTIVITY_KEY_SQL} IS NOT NULL`;
|
|
168
169
|
const SIGNED_IN_PRODUCT_ACTIVITY_FILTER = `${SIGNED_IN_ACTIVITY_FILTER} AND ${PRODUCT_ACTIVITY_TEMPLATE_FILTER}`;
|
|
@@ -292,7 +293,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
292
293
|
width: 2,
|
|
293
294
|
windowed: false,
|
|
294
295
|
buildSql: fixed(
|
|
295
|
-
|
|
296
|
+
`SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20`,
|
|
296
297
|
),
|
|
297
298
|
config: {
|
|
298
299
|
xKey: "app",
|
|
@@ -310,7 +311,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
310
311
|
width: 1,
|
|
311
312
|
windowed: false,
|
|
312
313
|
buildSql: fixed(
|
|
313
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE
|
|
314
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY ${EVENT_DATE_SQL} ORDER BY date`,
|
|
314
315
|
),
|
|
315
316
|
config: { xKey: "date", yKey: "count", color: "#10b981" },
|
|
316
317
|
},
|
|
@@ -380,7 +381,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
380
381
|
width: 1,
|
|
381
382
|
windowed: false,
|
|
382
383
|
buildSql: fixed(
|
|
383
|
-
|
|
384
|
+
`SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in`,
|
|
384
385
|
),
|
|
385
386
|
config: {
|
|
386
387
|
xKey: "signed_in",
|
|
@@ -1007,14 +1008,28 @@ export function buildPanel(
|
|
|
1007
1008
|
overrides.width <= 6
|
|
1008
1009
|
? overrides.width
|
|
1009
1010
|
: metric.width;
|
|
1011
|
+
const title =
|
|
1012
|
+
overrides.title?.trim() ||
|
|
1013
|
+
(window === "all"
|
|
1014
|
+
? metric.title.replace(/\(\d+d\)$/i, "(all-time)")
|
|
1015
|
+
: metric.title);
|
|
1016
|
+
const config = { ...metric.config };
|
|
1017
|
+
if (metric.windowed) {
|
|
1018
|
+
config.timeScope = window === "all" ? "all-time" : "fixed-window";
|
|
1019
|
+
if (window === "all") {
|
|
1020
|
+
const description =
|
|
1021
|
+
typeof config.description === "string" ? config.description.trim() : "";
|
|
1022
|
+
config.description = `${description ? `${description} ` : ""}This panel is explicitly configured for an all-time window.`;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1010
1025
|
return {
|
|
1011
1026
|
id: overrides.id?.trim() || metric.key,
|
|
1012
|
-
title
|
|
1027
|
+
title,
|
|
1013
1028
|
chartType: overrides.chartType?.trim() || metric.chartType,
|
|
1014
1029
|
source: "first-party",
|
|
1015
1030
|
width,
|
|
1016
1031
|
sql: metric.buildSql(window),
|
|
1017
1032
|
// Clone so callers can't mutate the shared catalog config object.
|
|
1018
|
-
config
|
|
1033
|
+
config,
|
|
1019
1034
|
};
|
|
1020
1035
|
}
|
|
@@ -663,7 +663,7 @@ export function resolveSessionReplayAgentAccess(
|
|
|
663
663
|
resourceId: recordingId,
|
|
664
664
|
});
|
|
665
665
|
// Replay grants are always minted by an authenticated viewer. Fail closed
|
|
666
|
-
// for grants without that signed identity:
|
|
666
|
+
// for grants without that signed identity: access policy is viewer-scoped,
|
|
667
667
|
// so guessing from the ambient request could expose identities to agents.
|
|
668
668
|
if (!result.ok || !result.viewerEmail) return null;
|
|
669
669
|
return { viewerEmail: result.viewerEmail };
|
|
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
4
4
|
|
|
5
|
-
import { appStateGet } from "@agent-native/core/application-state";
|
|
6
5
|
import {
|
|
7
6
|
deletePrivateBlob,
|
|
8
7
|
putPrivateBlob,
|
|
@@ -258,8 +257,6 @@ const DEFAULT_REPLAY_MAX_REQUESTS_PER_MINUTE = 120;
|
|
|
258
257
|
const RETENTION_DELETE_BATCH_SIZE = 500;
|
|
259
258
|
const REPLAY_PRIVATE_BLOB_REF_KIND = "agent-native.session-replay.private-blob";
|
|
260
259
|
const REPLAY_PRIVATE_BLOB_REF_VERSION = 1;
|
|
261
|
-
const SESSION_DEMO_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
262
|
-
const SESSION_DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
|
|
263
260
|
let inlineReplayFallbackWarned = false;
|
|
264
261
|
|
|
265
262
|
function replayError(message: string, statusCode: number): Error {
|
|
@@ -297,89 +294,6 @@ function replayEmail(value: unknown): string | null {
|
|
|
297
294
|
return raw && raw.includes("@") ? raw : null;
|
|
298
295
|
}
|
|
299
296
|
|
|
300
|
-
function normalizedEmail(value: unknown): string | null {
|
|
301
|
-
return replayEmail(value)?.toLowerCase() ?? null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function isBuilderEmail(value: unknown): boolean {
|
|
305
|
-
return normalizedEmail(value)?.endsWith("@builder.io") === true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function isSessionDemoModeEnabled(
|
|
309
|
-
userEmail?: string | null,
|
|
310
|
-
): Promise<boolean> {
|
|
311
|
-
if (process.env.DEMO_MODE === "true") return true;
|
|
312
|
-
if (!userEmail) return false;
|
|
313
|
-
try {
|
|
314
|
-
// These reads also run from custom routes and actions, so use the
|
|
315
|
-
// authenticated scope explicitly instead of ambient request state.
|
|
316
|
-
const state = await appStateGet(userEmail, "demo-mode");
|
|
317
|
-
return state?.enabled === true;
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function demoBuilderSessionCondition() {
|
|
324
|
-
return or(
|
|
325
|
-
sql`lower(${schema.sessionRecordings.userId}) like ${"%@builder.io"}`,
|
|
326
|
-
sql`lower(${schema.sessionRecordings.userKey}) like ${"%@builder.io"}`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
class SessionDemoAnonymizer {
|
|
331
|
-
aliasFor(value: unknown): string | null {
|
|
332
|
-
const raw = replayString(value);
|
|
333
|
-
if (!raw || raw.match(SESSION_DEMO_EMAIL_PATTERN)?.[0] !== raw) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
return SESSION_DEMO_ANONYMOUS_EMAIL;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
summarize(row: any, role?: SessionReplayAccessRole): SessionRecordingSummary {
|
|
340
|
-
const summary = rowToSessionRecordingSummary(row, role);
|
|
341
|
-
return {
|
|
342
|
-
...summary,
|
|
343
|
-
userId: this.aliasFor(row.userId) ?? summary.userId,
|
|
344
|
-
userKey: this.aliasFor(row.userKey) ?? summary.userKey,
|
|
345
|
-
ownerEmail: this.aliasFor(summary.ownerEmail) ?? summary.ownerEmail,
|
|
346
|
-
metadata: anonymizeEmailValue(summary.metadata, this) as Record<
|
|
347
|
-
string,
|
|
348
|
-
unknown
|
|
349
|
-
>,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function anonymizeEmailValue(
|
|
355
|
-
value: unknown,
|
|
356
|
-
anonymizer: SessionDemoAnonymizer,
|
|
357
|
-
): unknown {
|
|
358
|
-
const alias = anonymizer.aliasFor(value);
|
|
359
|
-
if (alias) return alias;
|
|
360
|
-
if (Array.isArray(value)) {
|
|
361
|
-
return value.map((item) => anonymizeEmailValue(item, anonymizer));
|
|
362
|
-
}
|
|
363
|
-
if (typeof value === "string") {
|
|
364
|
-
return value.replace(SESSION_DEMO_EMAIL_PATTERN, (email) => {
|
|
365
|
-
return anonymizer.aliasFor(email) ?? email;
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
if (value && typeof value === "object") {
|
|
369
|
-
const out: Record<string, unknown> = {};
|
|
370
|
-
for (const [key, item] of Object.entries(value)) {
|
|
371
|
-
out[key] = anonymizeEmailValue(item, anonymizer);
|
|
372
|
-
}
|
|
373
|
-
return out;
|
|
374
|
-
}
|
|
375
|
-
return value;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function assertDemoVisibleSession(row: any): void {
|
|
379
|
-
if (isBuilderEmail(row.userId) || isBuilderEmail(row.userKey)) return;
|
|
380
|
-
throw replayError("Session recording not found", 404);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
297
|
function replayInteger(value: unknown): number | null {
|
|
384
298
|
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
385
299
|
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
@@ -1686,7 +1600,6 @@ export async function listSessionRecordings(
|
|
|
1686
1600
|
filters: SessionReplayListFilters = {},
|
|
1687
1601
|
): Promise<SessionRecordingSummary[]> {
|
|
1688
1602
|
const db = getDb() as any;
|
|
1689
|
-
const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
|
|
1690
1603
|
const limit = Math.min(
|
|
1691
1604
|
MAX_SESSION_RECORDINGS_LIMIT,
|
|
1692
1605
|
Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
|
|
@@ -1699,7 +1612,6 @@ export async function listSessionRecordings(
|
|
|
1699
1612
|
replayVisibleIdentityCondition(),
|
|
1700
1613
|
replayPlayableEventsCondition(),
|
|
1701
1614
|
];
|
|
1702
|
-
if (demoMode) conditions.push(demoBuilderSessionCondition());
|
|
1703
1615
|
if (filters.app)
|
|
1704
1616
|
conditions.push(eq(schema.sessionRecordings.app, filters.app));
|
|
1705
1617
|
if (filters.template) {
|
|
@@ -1753,22 +1665,13 @@ export async function listSessionRecordings(
|
|
|
1753
1665
|
.where(and(...conditions))
|
|
1754
1666
|
.orderBy(desc(schema.sessionRecordings.startedAt))
|
|
1755
1667
|
.limit(limit);
|
|
1756
|
-
|
|
1757
|
-
return rows.map((row: any) => rowToSessionRecordingSummary(row));
|
|
1758
|
-
}
|
|
1759
|
-
const anonymizer = new SessionDemoAnonymizer();
|
|
1760
|
-
return rows
|
|
1761
|
-
.filter(
|
|
1762
|
-
(row: any) => isBuilderEmail(row.userId) || isBuilderEmail(row.userKey),
|
|
1763
|
-
)
|
|
1764
|
-
.map((row: any) => anonymizer.summarize(row));
|
|
1668
|
+
return rows.map((row: any) => rowToSessionRecordingSummary(row));
|
|
1765
1669
|
}
|
|
1766
1670
|
|
|
1767
1671
|
export async function getSessionReplaySummary(
|
|
1768
1672
|
recordingId: string,
|
|
1769
1673
|
scope: SessionReplayScope,
|
|
1770
1674
|
): Promise<SessionRecordingSummary> {
|
|
1771
|
-
const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
|
|
1772
1675
|
const access = await resolveAccess("session-recording", recordingId, {
|
|
1773
1676
|
userEmail: scope.userEmail,
|
|
1774
1677
|
orgId: scope.orgId ?? undefined,
|
|
@@ -1777,22 +1680,15 @@ export async function getSessionReplaySummary(
|
|
|
1777
1680
|
if (!isVisibleSessionRecording(access.resource)) {
|
|
1778
1681
|
throw replayError("Session recording not found", 404);
|
|
1779
1682
|
}
|
|
1780
|
-
|
|
1781
|
-
return rowToSessionRecordingSummary(access.resource, access.role);
|
|
1782
|
-
}
|
|
1783
|
-
assertDemoVisibleSession(access.resource);
|
|
1784
|
-
return new SessionDemoAnonymizer().summarize(access.resource, access.role);
|
|
1683
|
+
return rowToSessionRecordingSummary(access.resource, access.role);
|
|
1785
1684
|
}
|
|
1786
1685
|
|
|
1787
1686
|
export async function getSessionReplayTokenizedSummary(
|
|
1788
1687
|
recordingId: string,
|
|
1789
1688
|
viewerEmail: string,
|
|
1790
1689
|
): Promise<SessionRecordingSummary> {
|
|
1791
|
-
// Tokenized reads often run without an authenticated request session.
|
|
1792
|
-
//
|
|
1793
|
-
// link minted while demo mode is enabled cannot reveal real identities when
|
|
1794
|
-
// it is opened in a signed-out browser or by an external agent.
|
|
1795
|
-
const demoMode = await isSessionDemoModeEnabled(viewerEmail);
|
|
1690
|
+
// Tokenized reads often run without an authenticated request session. The
|
|
1691
|
+
// viewer identity is still required by the signed, recording-scoped grant.
|
|
1796
1692
|
const db = getDb() as any;
|
|
1797
1693
|
// guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
|
|
1798
1694
|
const [row] = await db
|
|
@@ -1803,9 +1699,7 @@ export async function getSessionReplayTokenizedSummary(
|
|
|
1803
1699
|
if (!row || !isVisibleSessionRecording(row)) {
|
|
1804
1700
|
throw replayError("Session recording not found", 404);
|
|
1805
1701
|
}
|
|
1806
|
-
|
|
1807
|
-
assertDemoVisibleSession(row);
|
|
1808
|
-
return new SessionDemoAnonymizer().summarize(row, "viewer");
|
|
1702
|
+
return rowToSessionRecordingSummary(row, "viewer");
|
|
1809
1703
|
}
|
|
1810
1704
|
|
|
1811
1705
|
function parseInlineReplayEvents(inlineData: string): unknown[] {
|
|
@@ -286,6 +286,7 @@ export default createAgentChatPlugin({
|
|
|
286
286
|
analyticsSourceGuidanceOpening() +
|
|
287
287
|
"DASHBOARD CREATION RULE — You may create dashboards, analyses, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'create a new analysis', 'add a chart for...'). Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
|
|
288
288
|
"DASHBOARD MUTATION RULE — For dashboard edits, default to `mutate-dashboard` with the typed `dashboard.*` script API so the main payload is a string and avoids native-array serialization traps. It can move panels by id, edit titles/SQL/config, insert, duplicate, remove, and patch dashboard fields in one atomic save. The script API is constrained: no variables/imports/loops/functions, only JSON-compatible arguments on documented dashboard methods. Do not count shifting `/panels/<index>` positions for ordinary dashboard edits unless the user specifically asks for low-level JSON-pointer operations. " +
|
|
289
|
+
'FIRST-PARTY DASHBOARD TIME RULE — AI-generated `source: "first-party"` panels are dashboard-time-bound by default: set `config.timeScope` to `dashboard` and include a matching dashboard time predicate. `{{timeRange}}` requires a matching `filters` entry with `id: "timeRange"` and `type: "select"`; `{{<id>Start}}`/`{{<id>End}}` require a matching `type: "date-range"` filter with that id. Allowed `timeScope` values are `dashboard`, `fixed-window`, `cohort-history`, and `all-time`; use `all-time` only when the user requests full available history and put all-time, lifetime, or historical in the title or description. Server validation rejects unbound first-party SQL. ' +
|
|
289
290
|
"DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder`, `layout.firstPanelIds`, and `layout.groups[].rows[].rowNumber/panelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
|
|
290
291
|
'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. For visible placement requests like "second row" or "next to return rates", use row-aware placement such as `dashboard.insertPanel({...}).nextTo("retention-over-time")`, `.atRow(2)`, or `dashboard.panel("panel-a").moveNextTo("panel-b")`; these keep panels in the intended rendered row and expand/rebalance that row when needed. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.groups[].rows` as proof of visible row placement, not only flat `panelOrder`. ' +
|
|
291
292
|
"Use configured data sources and actions only. Call `data-source-status` when you need to know which providers are connected, and treat provider actions as unavailable for analysis if they return missing credentials, permission, syntax, quota, or network errors. " +
|
|
@@ -1339,13 +1339,12 @@ function AllAssetsBrowser() {
|
|
|
1339
1339
|
</div>
|
|
1340
1340
|
</button>
|
|
1341
1341
|
{(asset as any).libraryTitle ? (
|
|
1342
|
-
<
|
|
1343
|
-
|
|
1344
|
-
onClick={() => navigate(`/library/${asset.libraryId}`)}
|
|
1342
|
+
<Link
|
|
1343
|
+
to={`/library/${asset.libraryId}`}
|
|
1345
1344
|
className="absolute bottom-2 left-2 z-10 max-w-[calc(100%-1rem)] truncate rounded-full bg-background/95 px-2.5 py-1 text-[11px] font-medium shadow-sm transition hover:bg-background"
|
|
1346
1345
|
>
|
|
1347
1346
|
{(asset as any).libraryTitle}
|
|
1348
|
-
</
|
|
1347
|
+
</Link>
|
|
1349
1348
|
) : null}
|
|
1350
1349
|
<TooltipProvider>
|
|
1351
1350
|
<Tooltip>
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
} from "date-fns";
|
|
49
49
|
import { nanoid } from "nanoid";
|
|
50
50
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
51
|
-
import { useNavigate, useSearchParams } from "react-router";
|
|
51
|
+
import { Link, useNavigate, useSearchParams } from "react-router";
|
|
52
52
|
import { toast } from "sonner";
|
|
53
53
|
|
|
54
54
|
import { CloudUpgrade } from "@/components/CloudUpgrade";
|
|
@@ -915,12 +915,10 @@ export default function BookingLinksPage({
|
|
|
915
915
|
toast.error(t("common.clipboardUnavailable"));
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
const localPath = bookingUsername
|
|
918
|
+
function bookingPreviewPath(slug: string) {
|
|
919
|
+
return bookingUsername
|
|
921
920
|
? `/book/${bookingUsername}/${slug}`
|
|
922
921
|
: `/book/${slug}`;
|
|
923
|
-
window.open(localPath, "_blank", "noopener,noreferrer");
|
|
924
922
|
}
|
|
925
923
|
|
|
926
924
|
const handleSaveRef = useRef(handleSave);
|
|
@@ -952,14 +950,13 @@ export default function BookingLinksPage({
|
|
|
952
950
|
}
|
|
953
951
|
return {
|
|
954
952
|
left: (
|
|
955
|
-
<
|
|
956
|
-
|
|
957
|
-
onClick={() => navigate("/booking-links")}
|
|
953
|
+
<Link
|
|
954
|
+
to="/booking-links"
|
|
958
955
|
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
|
|
959
956
|
>
|
|
960
957
|
<IconChevronLeft className="h-4 w-4" />
|
|
961
958
|
{t("bookingLinks.back")}
|
|
962
|
-
</
|
|
959
|
+
</Link>
|
|
963
960
|
),
|
|
964
961
|
right: selectedLink ? (
|
|
965
962
|
<div className="flex items-center gap-1.5">
|
|
@@ -1008,14 +1005,19 @@ export default function BookingLinksPage({
|
|
|
1008
1005
|
<Tooltip>
|
|
1009
1006
|
<TooltipTrigger asChild>
|
|
1010
1007
|
<Button
|
|
1011
|
-
|
|
1008
|
+
asChild
|
|
1012
1009
|
variant="ghost"
|
|
1013
1010
|
size="icon"
|
|
1014
|
-
onClick={() => openPreview(draft.slug)}
|
|
1015
1011
|
className={cn("h-8 w-8", BRAND_ICON_LINK_CLASS)}
|
|
1016
1012
|
aria-label={t("bookingLinks.openBookingLink")}
|
|
1017
1013
|
>
|
|
1018
|
-
<
|
|
1014
|
+
<a
|
|
1015
|
+
href={bookingPreviewPath(draft.slug)}
|
|
1016
|
+
target="_blank"
|
|
1017
|
+
rel="noopener noreferrer"
|
|
1018
|
+
>
|
|
1019
|
+
<IconExternalLink className="h-4 w-4" />
|
|
1020
|
+
</a>
|
|
1019
1021
|
</Button>
|
|
1020
1022
|
</TooltipTrigger>
|
|
1021
1023
|
<TooltipContent>{t("bookingLinks.openLink")}</TooltipContent>
|
|
@@ -1300,14 +1302,19 @@ export default function BookingLinksPage({
|
|
|
1300
1302
|
<Tooltip>
|
|
1301
1303
|
<TooltipTrigger asChild>
|
|
1302
1304
|
<Button
|
|
1303
|
-
|
|
1305
|
+
asChild
|
|
1304
1306
|
variant="ghost"
|
|
1305
1307
|
size="icon"
|
|
1306
|
-
onClick={() => openPreview(draft.slug)}
|
|
1307
1308
|
className={cn("h-8 w-8", BRAND_ICON_LINK_CLASS)}
|
|
1308
1309
|
aria-label={t("bookingLinks.openBookingPageNewTab")}
|
|
1309
1310
|
>
|
|
1310
|
-
<
|
|
1311
|
+
<a
|
|
1312
|
+
href={bookingPreviewPath(draft.slug)}
|
|
1313
|
+
target="_blank"
|
|
1314
|
+
rel="noopener noreferrer"
|
|
1315
|
+
>
|
|
1316
|
+
<IconExternalLink className="h-4 w-4" />
|
|
1317
|
+
</a>
|
|
1311
1318
|
</Button>
|
|
1312
1319
|
</TooltipTrigger>
|
|
1313
1320
|
<TooltipContent>
|
|
@@ -1499,7 +1506,7 @@ export default function BookingLinksPage({
|
|
|
1499
1506
|
availability={availability ?? undefined}
|
|
1500
1507
|
bookingUrl={previewUrl}
|
|
1501
1508
|
onCopy={() => void copyPreviewUrl(draft.slug)}
|
|
1502
|
-
|
|
1509
|
+
openHref={bookingPreviewPath(draft.slug)}
|
|
1503
1510
|
onCollapse={() => setIsPreviewCollapsed(true)}
|
|
1504
1511
|
/>
|
|
1505
1512
|
)}
|
|
@@ -1597,9 +1604,8 @@ export default function BookingLinksPage({
|
|
|
1597
1604
|
>
|
|
1598
1605
|
<div className="flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:gap-4 sm:px-5">
|
|
1599
1606
|
{/* Info — clickable to edit */}
|
|
1600
|
-
<
|
|
1601
|
-
|
|
1602
|
-
onClick={() => navigate(`/booking-links/${link.id}`)}
|
|
1607
|
+
<Link
|
|
1608
|
+
to={`/booking-links/${link.id}`}
|
|
1603
1609
|
className="min-w-0 flex-1 text-left"
|
|
1604
1610
|
>
|
|
1605
1611
|
<div className="flex items-center gap-2">
|
|
@@ -1622,7 +1628,7 @@ export default function BookingLinksPage({
|
|
|
1622
1628
|
{availability.timezone}
|
|
1623
1629
|
</p>
|
|
1624
1630
|
)}
|
|
1625
|
-
</
|
|
1631
|
+
</Link>
|
|
1626
1632
|
|
|
1627
1633
|
{/* Actions */}
|
|
1628
1634
|
<div className="flex shrink-0 items-center gap-2">
|
|
@@ -1642,19 +1648,17 @@ export default function BookingLinksPage({
|
|
|
1642
1648
|
<IconLink className="h-3.5 w-3.5" />
|
|
1643
1649
|
{t("bookingLinks.copyLink")}
|
|
1644
1650
|
</button>
|
|
1645
|
-
<
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
openPreview(link.slug);
|
|
1650
|
-
}}
|
|
1651
|
+
<a
|
|
1652
|
+
href={bookingPreviewPath(link.slug)}
|
|
1653
|
+
target="_blank"
|
|
1654
|
+
rel="noopener noreferrer"
|
|
1651
1655
|
className={cn(
|
|
1652
1656
|
"flex h-9 w-9 items-center justify-center rounded-full border",
|
|
1653
1657
|
BRAND_PILL_LINK_CLASS,
|
|
1654
1658
|
)}
|
|
1655
1659
|
>
|
|
1656
1660
|
<IconExternalLink className="h-4 w-4" />
|
|
1657
|
-
</
|
|
1661
|
+
</a>
|
|
1658
1662
|
</>
|
|
1659
1663
|
)}
|
|
1660
1664
|
|
|
@@ -1669,12 +1673,10 @@ export default function BookingLinksPage({
|
|
|
1669
1673
|
</button>
|
|
1670
1674
|
</DropdownMenuTrigger>
|
|
1671
1675
|
<DropdownMenuContent align="end">
|
|
1672
|
-
<DropdownMenuItem
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
>
|
|
1677
|
-
{t("eventForm.edit")}
|
|
1676
|
+
<DropdownMenuItem asChild>
|
|
1677
|
+
<Link to={`/booking-links/${link.id}`}>
|
|
1678
|
+
{t("eventForm.edit")}
|
|
1679
|
+
</Link>
|
|
1678
1680
|
</DropdownMenuItem>
|
|
1679
1681
|
<DropdownMenuItem
|
|
1680
1682
|
onClick={() => {
|
|
@@ -1949,7 +1951,7 @@ function BookingPreview({
|
|
|
1949
1951
|
availability,
|
|
1950
1952
|
bookingUrl,
|
|
1951
1953
|
onCopy,
|
|
1952
|
-
|
|
1954
|
+
openHref,
|
|
1953
1955
|
onCollapse,
|
|
1954
1956
|
}: {
|
|
1955
1957
|
title: string;
|
|
@@ -1961,7 +1963,7 @@ function BookingPreview({
|
|
|
1961
1963
|
availability?: AvailabilityConfig;
|
|
1962
1964
|
bookingUrl?: string;
|
|
1963
1965
|
onCopy?: () => void;
|
|
1964
|
-
|
|
1966
|
+
openHref?: string;
|
|
1965
1967
|
onCollapse?: () => void;
|
|
1966
1968
|
}) {
|
|
1967
1969
|
const t = useT();
|
|
@@ -2138,19 +2140,20 @@ function BookingPreview({
|
|
|
2138
2140
|
<TooltipContent>{t("bookingLinks.copyLink")}</TooltipContent>
|
|
2139
2141
|
</Tooltip>
|
|
2140
2142
|
)}
|
|
2141
|
-
{
|
|
2143
|
+
{openHref && (
|
|
2142
2144
|
<Tooltip>
|
|
2143
2145
|
<TooltipTrigger asChild>
|
|
2144
|
-
<
|
|
2145
|
-
|
|
2146
|
-
|
|
2146
|
+
<a
|
|
2147
|
+
href={openHref}
|
|
2148
|
+
target="_blank"
|
|
2149
|
+
rel="noopener noreferrer"
|
|
2147
2150
|
className={cn(
|
|
2148
2151
|
"flex h-6 w-6 items-center justify-center rounded",
|
|
2149
2152
|
BRAND_ICON_LINK_CLASS,
|
|
2150
2153
|
)}
|
|
2151
2154
|
>
|
|
2152
2155
|
<IconExternalLink className="h-3.5 w-3.5" />
|
|
2153
|
-
</
|
|
2156
|
+
</a>
|
|
2154
2157
|
</TooltipTrigger>
|
|
2155
2158
|
<TooltipContent>
|
|
2156
2159
|
{t("bookingLinks.openInteractiveBookingLink")}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
10
10
|
import { format, parseISO } from "date-fns";
|
|
11
11
|
import { useState } from "react";
|
|
12
|
-
import { useParams, useNavigate } from "react-router";
|
|
12
|
+
import { Link, useParams, useNavigate } from "react-router";
|
|
13
13
|
import { toast } from "sonner";
|
|
14
14
|
|
|
15
15
|
import {
|
|
@@ -118,13 +118,11 @@ export function ManageBookingPage() {
|
|
|
118
118
|
{t("manageBooking.cancelledSuffix")}
|
|
119
119
|
</p>
|
|
120
120
|
{booking.slug && (
|
|
121
|
-
<Button
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<IconCalendarPlus className="h-4 w-4" />
|
|
127
|
-
{t("manageBooking.reschedule")}
|
|
121
|
+
<Button asChild variant="outline" className="mt-6 gap-2">
|
|
122
|
+
<Link to={`/book/${booking.slug}`}>
|
|
123
|
+
<IconCalendarPlus className="h-4 w-4" />
|
|
124
|
+
{t("manageBooking.reschedule")}
|
|
125
|
+
</Link>
|
|
128
126
|
</Button>
|
|
129
127
|
)}
|
|
130
128
|
</div>
|
|
@@ -58,13 +58,10 @@ export function SignInPromptDialog({
|
|
|
58
58
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|
59
59
|
{t("signInPrompt.notNow")}
|
|
60
60
|
</Button>
|
|
61
|
-
<Button
|
|
62
|
-
onClick={() =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}}
|
|
66
|
-
>
|
|
67
|
-
{t("signInPrompt.signIn")}
|
|
61
|
+
<Button asChild>
|
|
62
|
+
<a href={buildSignInHref(returnTo)} onClick={() => onSignIn?.()}>
|
|
63
|
+
{t("signInPrompt.signIn")}
|
|
64
|
+
</a>
|
|
68
65
|
</Button>
|
|
69
66
|
</DialogFooter>
|
|
70
67
|
</DialogContent>
|