@agent-native/core 0.84.17 → 0.84.20
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 +19 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-store.ts +180 -13
- package/corpus/core/src/client/AssistantChat.tsx +49 -0
- package/corpus/core/src/client/active-run-state.ts +30 -0
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
- package/corpus/core/src/client/session-replay.ts +2 -2
- package/corpus/core/src/extensions/html-shell.ts +17 -3
- package/corpus/core/src/extensions/theme.ts +39 -0
- package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
- package/corpus/templates/analytics/changelog/2026-07-01-session-replay-recordings-upload-reliably-in-production.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +46 -5
- package/corpus/templates/calendar/app/components/booking/TimeSlotPicker.tsx +10 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +15 -6
- package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +22 -0
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +6 -2
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/calendar/app/i18n-data.ts +107 -0
- package/corpus/templates/calendar/app/lib/google-oauth-setup.ts +17 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +9 -1
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +40 -26
- package/corpus/templates/calendar/app/pages/Settings.tsx +3 -1
- package/corpus/templates/calendar/changelog/2026-07-01-booking-links-now-show-an-error-when-calendar-availability-c.md +6 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +78 -13
- package/corpus/templates/calendar/server/handlers/google-auth.ts +3 -2
- package/corpus/templates/calendar/server/lib/google-calendar.ts +16 -2
- package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -23
- package/corpus/templates/design/app/lib/design-system-preview.ts +69 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +71 -50
- package/corpus/templates/design/changelog/2026-07-01-design-systems-no-longer-crash-on-responsive-tokens.md +6 -0
- package/dist/agent/run-store.d.ts +19 -6
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +163 -13
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +36 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/active-run-state.d.ts +3 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +24 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +32 -3
- 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 +59 -2
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/session-replay.js +2 -2
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/extensions/html-shell.d.ts.map +1 -1
- package/dist/extensions/html-shell.js +17 -3
- package/dist/extensions/html-shell.js.map +1 -1
- package/dist/extensions/theme.d.ts +9 -0
- package/dist/extensions/theme.d.ts.map +1 -1
- package/dist/extensions/theme.js +38 -0
- package/dist/extensions/theme.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@agent-native/core/client";
|
|
6
6
|
import {
|
|
7
7
|
IconExternalLink,
|
|
8
|
+
IconAlertTriangle,
|
|
8
9
|
IconCheck,
|
|
9
10
|
IconCircle,
|
|
10
11
|
IconLoader2,
|
|
@@ -15,6 +16,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|
|
15
16
|
import { Button } from "@/components/ui/button";
|
|
16
17
|
import { Input } from "@/components/ui/input";
|
|
17
18
|
import { Label } from "@/components/ui/label";
|
|
19
|
+
import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
|
|
18
20
|
|
|
19
21
|
interface EnvKeyStatus {
|
|
20
22
|
key: string;
|
|
@@ -92,6 +94,26 @@ export function GoogleSetupWizard() {
|
|
|
92
94
|
const allConfigured =
|
|
93
95
|
envStatus.length > 0 && envStatus.every((k) => k.configured);
|
|
94
96
|
|
|
97
|
+
if (!shouldOfferGoogleOAuthSetup()) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="rounded-lg border border-amber-500/25 bg-amber-500/[0.07] p-4 text-start">
|
|
100
|
+
<div className="flex items-start gap-3">
|
|
101
|
+
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-amber-500/15 text-amber-300">
|
|
102
|
+
<IconAlertTriangle className="h-4 w-4" />
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<p className="text-sm font-medium text-foreground">
|
|
106
|
+
{t("googleConnect.managedCredentialsUnavailable")}
|
|
107
|
+
</p>
|
|
108
|
+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
109
|
+
{t("googleConnect.managedCredentialsUnavailableDescription")}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
async function handleJsonUpload(file: File) {
|
|
96
118
|
setSaving(true);
|
|
97
119
|
setError(null);
|
|
@@ -30,7 +30,9 @@ export function useAvailableSlots(
|
|
|
30
30
|
const res = await fetch(
|
|
31
31
|
appApiPath(`/api/bookings/available-slots?${params}`),
|
|
32
32
|
);
|
|
33
|
-
if (!res.ok)
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(await readErrorMessage(res, "Failed to fetch slots"));
|
|
35
|
+
}
|
|
34
36
|
const data = await res.json();
|
|
35
37
|
return Array.isArray(data) ? data : (data.slots ?? []);
|
|
36
38
|
},
|
|
@@ -57,7 +59,9 @@ export function useAvailableDays(
|
|
|
57
59
|
const res = await fetch(
|
|
58
60
|
appApiPath(`/api/bookings/available-slots?${params}`),
|
|
59
61
|
);
|
|
60
|
-
if (!res.ok)
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(await readErrorMessage(res, "Failed to fetch days"));
|
|
64
|
+
}
|
|
61
65
|
const data = await res.json();
|
|
62
66
|
return Array.isArray(data?.dates) ? data.dates : [];
|
|
63
67
|
},
|
|
@@ -375,6 +375,9 @@ const messages = {
|
|
|
375
375
|
googleWarningAfterUnsafe: "完成連線。",
|
|
376
376
|
accountConnectedElsewhere: "該帳戶已連線到另一個登入帳戶",
|
|
377
377
|
googleConnectionFailed: "Google 連線失敗",
|
|
378
|
+
managedCredentialsUnavailable: "Google Calendar 暫時無法使用",
|
|
379
|
+
managedCredentialsUnavailableDescription:
|
|
380
|
+
"此部署尚未設定 Calendar 的 Google 連線。請稍後再試。",
|
|
378
381
|
thatGoogleAccount: "那個 Google 帳戶",
|
|
379
382
|
signOutThenSignIn: "登出,然後使用 {{account}} 登入。",
|
|
380
383
|
signOut: "登出",
|
|
@@ -427,6 +430,7 @@ const messages = {
|
|
|
427
430
|
availabilityDescription: "設定你的預約可用時間。",
|
|
428
431
|
availabilitySaved: "已儲存可用性",
|
|
429
432
|
availabilitySaveFailed: "無法儲存可用性",
|
|
433
|
+
availabilityUnavailable: "目前無法檢查此主辦人的行事曆可用性。請稍後再試。",
|
|
430
434
|
availableTimes: "可用時間",
|
|
431
435
|
allCount: "全部({{count}})",
|
|
432
436
|
back: "返回",
|
|
@@ -398,6 +398,9 @@ const enUS = {
|
|
|
398
398
|
googleWarningAfterUnsafe: "to finish connecting.",
|
|
399
399
|
accountConnectedElsewhere: "This account is connected to another login",
|
|
400
400
|
googleConnectionFailed: "Google connection failed",
|
|
401
|
+
managedCredentialsUnavailable: "Google Calendar is unavailable",
|
|
402
|
+
managedCredentialsUnavailableDescription:
|
|
403
|
+
"Calendar's Google connection is not configured for this deployment. Please try again later.",
|
|
401
404
|
thatGoogleAccount: "that Google account",
|
|
402
405
|
signOutThenSignIn: "Sign out, then sign in with {{account}}.",
|
|
403
406
|
signOut: "Sign out",
|
|
@@ -452,6 +455,8 @@ const enUS = {
|
|
|
452
455
|
availabilityDescription: "Set your available hours for bookings.",
|
|
453
456
|
availabilitySaved: "Availability saved",
|
|
454
457
|
availabilitySaveFailed: "Failed to save availability",
|
|
458
|
+
availabilityUnavailable:
|
|
459
|
+
"This host's calendar availability can't be checked right now. Please try again later.",
|
|
455
460
|
availableTimes: "Available Times",
|
|
456
461
|
allCount: "All ({{count}})",
|
|
457
462
|
back: "Back",
|
|
@@ -9233,6 +9238,107 @@ const translatedCalendarRawBurnDown = {
|
|
|
9233
9238
|
},
|
|
9234
9239
|
} satisfies Partial<Record<LocaleCode, PartialMessages>>;
|
|
9235
9240
|
|
|
9241
|
+
const translatedCalendarAvailabilityFix = {
|
|
9242
|
+
"zh-CN": {
|
|
9243
|
+
googleConnect: {
|
|
9244
|
+
managedCredentialsUnavailable: "Google Calendar 暂不可用",
|
|
9245
|
+
managedCredentialsUnavailableDescription:
|
|
9246
|
+
"此部署尚未配置 Calendar 的 Google 连接。请稍后再试。",
|
|
9247
|
+
},
|
|
9248
|
+
bookingLinks: {
|
|
9249
|
+
availabilityUnavailable: "目前无法检查此主办人的日历可用性。请稍后再试。",
|
|
9250
|
+
},
|
|
9251
|
+
},
|
|
9252
|
+
"es-ES": {
|
|
9253
|
+
googleConnect: {
|
|
9254
|
+
managedCredentialsUnavailable: "Google Calendar no está disponible",
|
|
9255
|
+
managedCredentialsUnavailableDescription:
|
|
9256
|
+
"La conexión de Google de Calendar no está configurada para este despliegue. Inténtalo de nuevo más tarde.",
|
|
9257
|
+
},
|
|
9258
|
+
bookingLinks: {
|
|
9259
|
+
availabilityUnavailable:
|
|
9260
|
+
"No se puede comprobar ahora la disponibilidad del calendario de este anfitrión. Inténtalo de nuevo más tarde.",
|
|
9261
|
+
},
|
|
9262
|
+
},
|
|
9263
|
+
"fr-FR": {
|
|
9264
|
+
googleConnect: {
|
|
9265
|
+
managedCredentialsUnavailable: "Google Calendar est indisponible",
|
|
9266
|
+
managedCredentialsUnavailableDescription:
|
|
9267
|
+
"La connexion Google de Calendar n'est pas configurée pour ce déploiement. Réessayez plus tard.",
|
|
9268
|
+
},
|
|
9269
|
+
bookingLinks: {
|
|
9270
|
+
availabilityUnavailable:
|
|
9271
|
+
"La disponibilité du calendrier de cet hôte ne peut pas être vérifiée pour le moment. Réessayez plus tard.",
|
|
9272
|
+
},
|
|
9273
|
+
},
|
|
9274
|
+
"de-DE": {
|
|
9275
|
+
googleConnect: {
|
|
9276
|
+
managedCredentialsUnavailable: "Google Calendar ist nicht verfügbar",
|
|
9277
|
+
managedCredentialsUnavailableDescription:
|
|
9278
|
+
"Die Google-Verbindung von Calendar ist für diese Bereitstellung nicht konfiguriert. Bitte versuche es später erneut.",
|
|
9279
|
+
},
|
|
9280
|
+
bookingLinks: {
|
|
9281
|
+
availabilityUnavailable:
|
|
9282
|
+
"Die Kalenderverfügbarkeit dieses Gastgebers kann derzeit nicht geprüft werden. Bitte versuche es später erneut.",
|
|
9283
|
+
},
|
|
9284
|
+
},
|
|
9285
|
+
"ja-JP": {
|
|
9286
|
+
googleConnect: {
|
|
9287
|
+
managedCredentialsUnavailable: "Google Calendar を利用できません",
|
|
9288
|
+
managedCredentialsUnavailableDescription:
|
|
9289
|
+
"このデプロイでは Calendar の Google 接続が設定されていません。後でもう一度お試しください。",
|
|
9290
|
+
},
|
|
9291
|
+
bookingLinks: {
|
|
9292
|
+
availabilityUnavailable:
|
|
9293
|
+
"この主催者のカレンダーの空き状況を現在確認できません。後でもう一度お試しください。",
|
|
9294
|
+
},
|
|
9295
|
+
},
|
|
9296
|
+
"ko-KR": {
|
|
9297
|
+
googleConnect: {
|
|
9298
|
+
managedCredentialsUnavailable: "Google Calendar를 사용할 수 없습니다",
|
|
9299
|
+
managedCredentialsUnavailableDescription:
|
|
9300
|
+
"이 배포에는 Calendar의 Google 연결이 구성되어 있지 않습니다. 나중에 다시 시도하세요.",
|
|
9301
|
+
},
|
|
9302
|
+
bookingLinks: {
|
|
9303
|
+
availabilityUnavailable:
|
|
9304
|
+
"현재 이 주최자의 캘린더 가능 시간을 확인할 수 없습니다. 나중에 다시 시도하세요.",
|
|
9305
|
+
},
|
|
9306
|
+
},
|
|
9307
|
+
"pt-BR": {
|
|
9308
|
+
googleConnect: {
|
|
9309
|
+
managedCredentialsUnavailable: "Google Calendar indisponível",
|
|
9310
|
+
managedCredentialsUnavailableDescription:
|
|
9311
|
+
"A conexão do Calendar com o Google não está configurada para esta implantação. Tente novamente mais tarde.",
|
|
9312
|
+
},
|
|
9313
|
+
bookingLinks: {
|
|
9314
|
+
availabilityUnavailable:
|
|
9315
|
+
"Não é possível verificar a disponibilidade do calendário deste anfitrião agora. Tente novamente mais tarde.",
|
|
9316
|
+
},
|
|
9317
|
+
},
|
|
9318
|
+
"hi-IN": {
|
|
9319
|
+
googleConnect: {
|
|
9320
|
+
managedCredentialsUnavailable: "Google Calendar उपलब्ध नहीं है",
|
|
9321
|
+
managedCredentialsUnavailableDescription:
|
|
9322
|
+
"इस deployment के लिए Calendar का Google connection configured नहीं है। कृपया बाद में फिर कोशिश करें।",
|
|
9323
|
+
},
|
|
9324
|
+
bookingLinks: {
|
|
9325
|
+
availabilityUnavailable:
|
|
9326
|
+
"इस host की calendar availability अभी check नहीं की जा सकती। कृपया बाद में फिर कोशिश करें।",
|
|
9327
|
+
},
|
|
9328
|
+
},
|
|
9329
|
+
"ar-SA": {
|
|
9330
|
+
googleConnect: {
|
|
9331
|
+
managedCredentialsUnavailable: "Google Calendar غير متاح",
|
|
9332
|
+
managedCredentialsUnavailableDescription:
|
|
9333
|
+
"اتصال Google في Calendar غير مهيأ لهذا النشر. يُرجى المحاولة لاحقًا.",
|
|
9334
|
+
},
|
|
9335
|
+
bookingLinks: {
|
|
9336
|
+
availabilityUnavailable:
|
|
9337
|
+
"لا يمكن التحقق من توفر تقويم هذا المضيف الآن. يُرجى المحاولة لاحقًا.",
|
|
9338
|
+
},
|
|
9339
|
+
},
|
|
9340
|
+
} satisfies Partial<Record<LocaleCode, PartialMessages>>;
|
|
9341
|
+
|
|
9236
9342
|
function applyTranslatedCalendarOverrides(
|
|
9237
9343
|
translationSet: Partial<Record<LocaleCode, PartialMessages>>,
|
|
9238
9344
|
) {
|
|
@@ -9282,3 +9388,4 @@ applyTranslatedCalendarOverrides(translatedCalendarDebtTranslations);
|
|
|
9282
9388
|
applyTranslatedCalendarOverrides(translatedCalendarRemainingRaw);
|
|
9283
9389
|
applyTranslatedCalendarOverrides(translatedCalendarRawBurnDown);
|
|
9284
9390
|
applyTranslatedCalendarOverrides(translatedCalendarExactCleanup);
|
|
9391
|
+
applyTranslatedCalendarOverrides(translatedCalendarAvailabilityFix);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function localHostname(hostname: string): boolean {
|
|
2
|
+
return (
|
|
3
|
+
hostname === "localhost" ||
|
|
4
|
+
hostname === "127.0.0.1" ||
|
|
5
|
+
hostname === "::1" ||
|
|
6
|
+
hostname.endsWith(".localhost") ||
|
|
7
|
+
hostname.endsWith(".local")
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function shouldOfferGoogleOAuthSetup(): boolean {
|
|
12
|
+
const env = (import.meta.env ?? {}) as Record<string, unknown>;
|
|
13
|
+
if (env.DEV === true) return true;
|
|
14
|
+
if (env.VITE_ENABLE_GOOGLE_OAUTH_SETUP === "true") return true;
|
|
15
|
+
if (typeof window === "undefined") return false;
|
|
16
|
+
return localHostname(window.location.hostname.toLowerCase());
|
|
17
|
+
}
|
|
@@ -135,6 +135,7 @@ const BRAND_ICON_LINK_CLASS =
|
|
|
135
135
|
const BRAND_PILL_LINK_CLASS =
|
|
136
136
|
"border-[#00B5FF]/35 bg-[#00B5FF]/10 font-semibold text-[#00B5FF] hover:border-[#00B5FF]/55 hover:bg-[#00B5FF]/15 hover:text-[#33C4FF]";
|
|
137
137
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
138
|
+
const BOOKING_SLOT_STEP_MINUTES = 30;
|
|
138
139
|
|
|
139
140
|
type DraftLink = {
|
|
140
141
|
id?: string;
|
|
@@ -1993,7 +1994,14 @@ function BookingPreview({
|
|
|
1993
1994
|
const startMin = startH * 60 + startM;
|
|
1994
1995
|
const endMin = endH * 60 + endM;
|
|
1995
1996
|
const slots: string[] = [];
|
|
1996
|
-
|
|
1997
|
+
const firstStart =
|
|
1998
|
+
Math.ceil(startMin / BOOKING_SLOT_STEP_MINUTES) *
|
|
1999
|
+
BOOKING_SLOT_STEP_MINUTES;
|
|
2000
|
+
for (
|
|
2001
|
+
let m = firstStart;
|
|
2002
|
+
m + dur <= endMin;
|
|
2003
|
+
m += BOOKING_SLOT_STEP_MINUTES
|
|
2004
|
+
) {
|
|
1997
2005
|
const h = Math.floor(m / 60);
|
|
1998
2006
|
const mm = m % 60;
|
|
1999
2007
|
const ampm = h >= 12 ? "PM" : "AM";
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useT,
|
|
6
6
|
} from "@agent-native/core/client";
|
|
7
7
|
import type { Booking } from "@shared/api";
|
|
8
|
-
import { IconCalendar } from "@tabler/icons-react";
|
|
8
|
+
import { IconAlertTriangle, IconCalendar } from "@tabler/icons-react";
|
|
9
9
|
import {
|
|
10
10
|
addMinutes,
|
|
11
11
|
endOfMonth,
|
|
@@ -122,23 +122,26 @@ export default function BookingPage() {
|
|
|
122
122
|
availability?.slotDurationMinutes ??
|
|
123
123
|
settings?.defaultEventDuration ??
|
|
124
124
|
30;
|
|
125
|
-
const {
|
|
126
|
-
|
|
125
|
+
const {
|
|
126
|
+
data: slots = [],
|
|
127
|
+
isLoading: slotsLoading,
|
|
128
|
+
error: slotsError,
|
|
129
|
+
} = useAvailableSlots(dateStr, duration, slug);
|
|
130
|
+
const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
|
|
131
|
+
const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
|
|
132
|
+
const {
|
|
133
|
+
data: availableDates = [],
|
|
134
|
+
isLoading: availableDatesLoading,
|
|
135
|
+
error: availableDatesError,
|
|
136
|
+
} = useAvailableDays(
|
|
137
|
+
monthStart,
|
|
138
|
+
monthEnd,
|
|
127
139
|
duration,
|
|
128
140
|
slug,
|
|
141
|
+
step === "date" &&
|
|
142
|
+
!!availability &&
|
|
143
|
+
(!hasDurationChoice || selectedDuration !== null),
|
|
129
144
|
);
|
|
130
|
-
const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
|
|
131
|
-
const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
|
|
132
|
-
const { data: availableDates = [], isLoading: availableDatesLoading } =
|
|
133
|
-
useAvailableDays(
|
|
134
|
-
monthStart,
|
|
135
|
-
monthEnd,
|
|
136
|
-
duration,
|
|
137
|
-
slug,
|
|
138
|
-
step === "date" &&
|
|
139
|
-
!!availability &&
|
|
140
|
-
(!hasDurationChoice || selectedDuration !== null),
|
|
141
|
-
);
|
|
142
145
|
const createBooking = useCreateBooking();
|
|
143
146
|
const selectedSlotRange = selectedSlot
|
|
144
147
|
? {
|
|
@@ -236,6 +239,7 @@ export default function BookingPage() {
|
|
|
236
239
|
const pageTitle = bookingLink?.title || title;
|
|
237
240
|
const pageDescription = bookingLink?.description || description;
|
|
238
241
|
const requiredHostCount = (bookingLink?.hosts?.length ?? 0) + 1;
|
|
242
|
+
const availabilityErrorMessage = t("bookingLinks.availabilityUnavailable");
|
|
239
243
|
|
|
240
244
|
useEffect(() => {
|
|
241
245
|
if (hasDurationChoice && step === "date" && selectedDuration === null) {
|
|
@@ -404,17 +408,26 @@ export default function BookingPage() {
|
|
|
404
408
|
<h3 className="mb-4 text-sm font-medium text-center">
|
|
405
409
|
{t("bookingLinks.selectDate")}
|
|
406
410
|
</h3>
|
|
407
|
-
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
411
|
+
{availableDatesError ? (
|
|
412
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/[0.06] px-3 py-3 text-sm text-destructive">
|
|
413
|
+
<div className="flex items-start gap-2">
|
|
414
|
+
<IconAlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
|
415
|
+
<p>{availabilityErrorMessage}</p>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
) : (
|
|
419
|
+
<div className="flex justify-center">
|
|
420
|
+
<DatePicker
|
|
421
|
+
selectedDate={selectedDate}
|
|
422
|
+
onSelect={handleDateSelect}
|
|
423
|
+
availability={availability}
|
|
424
|
+
availableDates={availableDates}
|
|
425
|
+
availabilityLoading={availableDatesLoading}
|
|
426
|
+
viewMonth={viewMonth}
|
|
427
|
+
onViewMonthChange={setViewMonth}
|
|
428
|
+
/>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
418
431
|
</div>
|
|
419
432
|
)}
|
|
420
433
|
|
|
@@ -443,6 +456,7 @@ export default function BookingPage() {
|
|
|
443
456
|
selectedSlot={selectedSlot}
|
|
444
457
|
onSelect={handleSlotSelect}
|
|
445
458
|
loading={slotsLoading}
|
|
459
|
+
errorMessage={slotsError ? availabilityErrorMessage : undefined}
|
|
446
460
|
/>
|
|
447
461
|
</div>
|
|
448
462
|
)}
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
useDisconnectZoom,
|
|
48
48
|
useZoomStatus,
|
|
49
49
|
} from "@/hooks/use-zoom-auth";
|
|
50
|
+
import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
|
|
50
51
|
|
|
51
52
|
import changelog from "../../CHANGELOG.md?raw";
|
|
52
53
|
|
|
@@ -70,6 +71,7 @@ export default function Settings() {
|
|
|
70
71
|
const disconnectZoom = useDisconnectZoom();
|
|
71
72
|
const [wantAuthUrl, setWantAuthUrl] = useState(false);
|
|
72
73
|
const authUrl = useGoogleAuthUrl(wantAuthUrl);
|
|
74
|
+
const canOfferGoogleOAuthSetup = shouldOfferGoogleOAuthSetup();
|
|
73
75
|
|
|
74
76
|
const [timezone, setTimezone] = useState("");
|
|
75
77
|
const [bookingTitle, setBookingTitle] = useState("");
|
|
@@ -333,7 +335,7 @@ export default function Settings() {
|
|
|
333
335
|
</Card>
|
|
334
336
|
|
|
335
337
|
{/* Google Setup Wizard */}
|
|
336
|
-
{!googleStatus.data?.connected && (
|
|
338
|
+
{!googleStatus.data?.connected && canOfferGoogleOAuthSetup && (
|
|
337
339
|
<Card>
|
|
338
340
|
<CardHeader>
|
|
339
341
|
<CardTitle className="text-lg">
|
|
@@ -163,6 +163,7 @@ type ConflictItem = { start: string; end: string };
|
|
|
163
163
|
type ConflictResult = { items: ConflictItem[]; unavailableReason?: string };
|
|
164
164
|
type BookingLinkRow = typeof schema.bookingLinks.$inferSelect;
|
|
165
165
|
type ConflictDb = Pick<ReturnType<typeof getDb>, "select">;
|
|
166
|
+
const BOOKING_SLOT_STEP_MINUTES = 30;
|
|
166
167
|
|
|
167
168
|
type LocalDateTimeParts = {
|
|
168
169
|
year: number;
|
|
@@ -341,6 +342,27 @@ function dateEndIso(date: string, timezone: string): string {
|
|
|
341
342
|
).toISOString();
|
|
342
343
|
}
|
|
343
344
|
|
|
345
|
+
function formatAvailabilityUnavailableReason(email?: string): string {
|
|
346
|
+
return email
|
|
347
|
+
? `Calendar availability unavailable for ${email}`
|
|
348
|
+
: "Calendar availability unavailable";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function unavailableAvailabilityResponse(event: H3Event) {
|
|
352
|
+
setResponseStatus(event, 503);
|
|
353
|
+
return {
|
|
354
|
+
error:
|
|
355
|
+
"The host's calendar availability could not be checked. Please try again later.",
|
|
356
|
+
code: "calendar_availability_unavailable",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatLocalTime(totalMinutes: number): string {
|
|
361
|
+
const hour = Math.floor(totalMinutes / 60);
|
|
362
|
+
const minute = totalMinutes % 60;
|
|
363
|
+
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
344
366
|
async function resolveAvailabilityContext({
|
|
345
367
|
slug,
|
|
346
368
|
db = getDb(),
|
|
@@ -397,7 +419,7 @@ async function resolveAvailabilityContext({
|
|
|
397
419
|
};
|
|
398
420
|
}
|
|
399
421
|
|
|
400
|
-
async function getConflictItems({
|
|
422
|
+
export async function getConflictItems({
|
|
401
423
|
db = getDb(),
|
|
402
424
|
ownerEmail,
|
|
403
425
|
hostEmails,
|
|
@@ -424,7 +446,18 @@ async function getConflictItems({
|
|
|
424
446
|
);
|
|
425
447
|
const freeBusyResolvedHosts = new Set<string>();
|
|
426
448
|
|
|
427
|
-
|
|
449
|
+
const ownerConnected = ownerEmail
|
|
450
|
+
? await googleCalendar.isConnected(ownerEmail)
|
|
451
|
+
: false;
|
|
452
|
+
|
|
453
|
+
if (ownerEmail && !ownerConnected) {
|
|
454
|
+
return {
|
|
455
|
+
items: [],
|
|
456
|
+
unavailableReason: formatAvailabilityUnavailableReason(ownerEmail),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (ownerConnected) {
|
|
428
461
|
try {
|
|
429
462
|
if (requiredHosts.length > 0) {
|
|
430
463
|
const freeBusy = await googleCalendar.getFreeBusy(
|
|
@@ -434,6 +467,14 @@ async function getConflictItems({
|
|
|
434
467
|
ownerEmail,
|
|
435
468
|
timezone,
|
|
436
469
|
);
|
|
470
|
+
if (freeBusy.errors.length > 0) {
|
|
471
|
+
return {
|
|
472
|
+
items: [],
|
|
473
|
+
unavailableReason: formatAvailabilityUnavailableReason(
|
|
474
|
+
freeBusy.errors[0]?.email || ownerEmail,
|
|
475
|
+
),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
437
478
|
for (const [email, calendar] of Object.entries(freeBusy.calendars)) {
|
|
438
479
|
const normalizedEmail = email.toLowerCase();
|
|
439
480
|
if (!calendar.errors || calendar.errors.length === 0) {
|
|
@@ -448,11 +489,16 @@ async function getConflictItems({
|
|
|
448
489
|
}
|
|
449
490
|
}
|
|
450
491
|
|
|
451
|
-
const { events: googleEvents } =
|
|
452
|
-
rangeStartIso,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
492
|
+
const { events: googleEvents, errors: googleEventErrors } =
|
|
493
|
+
await googleCalendar.listEvents(rangeStartIso, rangeEndIso, ownerEmail);
|
|
494
|
+
if (googleEventErrors.length > 0) {
|
|
495
|
+
return {
|
|
496
|
+
items: [],
|
|
497
|
+
unavailableReason: formatAvailabilityUnavailableReason(
|
|
498
|
+
googleEventErrors[0]?.email || ownerEmail,
|
|
499
|
+
),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
456
502
|
conflictItems.push(
|
|
457
503
|
...googleEvents.filter(eventBlocksAvailability).map((event) => ({
|
|
458
504
|
start: event.start,
|
|
@@ -460,7 +506,10 @@ async function getConflictItems({
|
|
|
460
506
|
})),
|
|
461
507
|
);
|
|
462
508
|
} catch {
|
|
463
|
-
|
|
509
|
+
return {
|
|
510
|
+
items: [],
|
|
511
|
+
unavailableReason: formatAvailabilityUnavailableReason(ownerEmail),
|
|
512
|
+
};
|
|
464
513
|
}
|
|
465
514
|
}
|
|
466
515
|
|
|
@@ -501,7 +550,7 @@ async function getConflictItems({
|
|
|
501
550
|
return { items: conflictItems };
|
|
502
551
|
}
|
|
503
552
|
|
|
504
|
-
function generateAvailableSlotsForDate({
|
|
553
|
+
export function generateAvailableSlotsForDate({
|
|
505
554
|
date,
|
|
506
555
|
duration,
|
|
507
556
|
config,
|
|
@@ -571,7 +620,17 @@ function generateAvailableSlotsForDate({
|
|
|
571
620
|
const slotEnd = zonedTimeToUtc(date, scheduleSlot.end, timezone);
|
|
572
621
|
if (slotEnd <= slotStart) continue;
|
|
573
622
|
|
|
574
|
-
|
|
623
|
+
const scheduleStartMinutes = startHour * 60 + startMin;
|
|
624
|
+
const firstSlotStartMinutes =
|
|
625
|
+
Math.ceil(scheduleStartMinutes / BOOKING_SLOT_STEP_MINUTES) *
|
|
626
|
+
BOOKING_SLOT_STEP_MINUTES;
|
|
627
|
+
if (firstSlotStartMinutes >= 24 * 60) continue;
|
|
628
|
+
|
|
629
|
+
let current = zonedTimeToUtc(
|
|
630
|
+
date,
|
|
631
|
+
formatLocalTime(firstSlotStartMinutes),
|
|
632
|
+
timezone,
|
|
633
|
+
);
|
|
575
634
|
|
|
576
635
|
while (current.getTime() + slotDuration * 60 * 1000 <= slotEnd.getTime()) {
|
|
577
636
|
const candidateStart = new Date(current);
|
|
@@ -598,7 +657,9 @@ function generateAvailableSlotsForDate({
|
|
|
598
657
|
});
|
|
599
658
|
}
|
|
600
659
|
|
|
601
|
-
current = new Date(
|
|
660
|
+
current = new Date(
|
|
661
|
+
current.getTime() + BOOKING_SLOT_STEP_MINUTES * 60 * 1000,
|
|
662
|
+
);
|
|
602
663
|
}
|
|
603
664
|
}
|
|
604
665
|
|
|
@@ -1147,7 +1208,9 @@ export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
|
|
|
1147
1208
|
rangeEndIso: dateEndIso(rangeEnd, timezone),
|
|
1148
1209
|
timezone,
|
|
1149
1210
|
});
|
|
1150
|
-
if (conflictResult.unavailableReason)
|
|
1211
|
+
if (conflictResult.unavailableReason) {
|
|
1212
|
+
return unavailableAvailabilityResponse(event);
|
|
1213
|
+
}
|
|
1151
1214
|
const dates: string[] = [];
|
|
1152
1215
|
for (
|
|
1153
1216
|
let cursor = new Date(from!);
|
|
@@ -1177,7 +1240,9 @@ export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
|
|
|
1177
1240
|
rangeEndIso: dateEndIso(date, timezone),
|
|
1178
1241
|
timezone,
|
|
1179
1242
|
});
|
|
1180
|
-
if (conflictResult.unavailableReason)
|
|
1243
|
+
if (conflictResult.unavailableReason) {
|
|
1244
|
+
return unavailableAvailabilityResponse(event);
|
|
1245
|
+
}
|
|
1181
1246
|
const availableSlots = generateAvailableSlotsForDate({
|
|
1182
1247
|
date,
|
|
1183
1248
|
duration,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isElectron,
|
|
6
6
|
getAppUrl,
|
|
7
7
|
resolveGoogleSignInCredentials,
|
|
8
|
+
resolveGoogleProviderCredentials,
|
|
8
9
|
resolveOAuthRedirectUri,
|
|
9
10
|
encodeOAuthState,
|
|
10
11
|
decodeOAuthState,
|
|
@@ -74,8 +75,8 @@ async function resolveCalendarOAuthCredentials(event: H3Event) {
|
|
|
74
75
|
clientSecret: await resolveSecret("GOOGLE_CLIENT_SECRET"),
|
|
75
76
|
}),
|
|
76
77
|
);
|
|
77
|
-
if (
|
|
78
|
-
return
|
|
78
|
+
if (clientId && clientSecret) return { clientId, clientSecret };
|
|
79
|
+
return resolveGoogleProviderCredentials();
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function isCalendarConnectRequest(
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
getRequestOrgId,
|
|
11
11
|
resolveSecret,
|
|
12
12
|
runWithRequestContext,
|
|
13
|
+
resolveGoogleProviderCredentials,
|
|
14
|
+
resolveGoogleLegacyProviderCredentials,
|
|
13
15
|
} from "@agent-native/core/server";
|
|
14
16
|
|
|
15
17
|
import type {
|
|
@@ -95,8 +97,20 @@ async function readCredentialPair(
|
|
|
95
97
|
resolveSecret(clientIdKey),
|
|
96
98
|
resolveSecret(clientSecretKey),
|
|
97
99
|
]);
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
+
if (clientId && clientSecret) return { clientId, clientSecret };
|
|
101
|
+
if (
|
|
102
|
+
clientIdKey === "GOOGLE_CLIENT_ID" &&
|
|
103
|
+
clientSecretKey === "GOOGLE_CLIENT_SECRET"
|
|
104
|
+
) {
|
|
105
|
+
return resolveGoogleProviderCredentials();
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
clientIdKey === "GOOGLE_LEGACY_CLIENT_ID" &&
|
|
109
|
+
clientSecretKey === "GOOGLE_LEGACY_CLIENT_SECRET"
|
|
110
|
+
) {
|
|
111
|
+
return resolveGoogleLegacyProviderCredentials();
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
async function resolveGoogleProviderCredentialCandidates(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent-Native Clips",
|
|
4
4
|
"short_name": "Clips",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.9",
|
|
6
6
|
"description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
|
|
7
7
|
"minimum_chrome_version": "116",
|
|
8
8
|
"action": {
|