@agent-native/core 0.77.4 → 0.77.7
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 +31 -0
- package/corpus/core/package.json +3 -3
- package/corpus/core/src/agent/production-agent.ts +35 -0
- package/corpus/core/src/agent/run-store.ts +2 -0
- package/corpus/core/src/agent/thread-data-builder.ts +21 -2
- package/corpus/core/src/cli/create.ts +35 -1
- package/corpus/core/src/cli/skills.ts +8 -0
- package/corpus/core/src/client/AgentPanel.tsx +57 -25
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
- package/corpus/core/src/client/analytics.ts +24 -2
- package/corpus/core/src/client/changelog/Changelog.tsx +22 -10
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +405 -35
- package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
- package/corpus/core/src/client/use-chat-threads.ts +104 -55
- package/corpus/core/src/server/analytics.ts +27 -3
- package/corpus/core/src/sharing/access.ts +35 -13
- package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
- package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
- package/corpus/core/src/sharing/registry.ts +9 -0
- package/corpus/core/src/vite/client.ts +3 -0
- package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
- package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
- package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
- package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
- package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
- package/corpus/templates/analytics/netlify.toml +1 -0
- package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
- package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
- package/corpus/templates/assets/netlify.toml +1 -0
- package/corpus/templates/brain/netlify.toml +1 -0
- package/corpus/templates/calendar/app/i18n-data.ts +12 -0
- package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
- package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
- package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
- package/corpus/templates/calendar/netlify.toml +1 -0
- package/corpus/templates/chat/netlify.toml +1 -0
- package/corpus/templates/clips/AGENTS.md +9 -7
- package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
- package/corpus/templates/clips/actions/archive-recording.ts +5 -2
- package/corpus/templates/clips/actions/clear-edits.ts +5 -2
- package/corpus/templates/clips/actions/create-folder.ts +7 -2
- package/corpus/templates/clips/actions/delete-folder.ts +7 -4
- package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
- package/corpus/templates/clips/actions/list-notifications.ts +9 -5
- package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
- package/corpus/templates/clips/actions/list-recordings.ts +4 -1
- package/corpus/templates/clips/actions/move-recording.ts +5 -1
- package/corpus/templates/clips/actions/rename-folder.ts +6 -3
- package/corpus/templates/clips/actions/request-transcript.ts +5 -2
- package/corpus/templates/clips/actions/restore-recording.ts +5 -2
- package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
- package/corpus/templates/clips/actions/set-chapters.ts +5 -2
- package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
- package/corpus/templates/clips/actions/split-recording.ts +5 -2
- package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
- package/corpus/templates/clips/actions/trash-recording.ts +5 -2
- package/corpus/templates/clips/actions/undo-edit.ts +5 -2
- package/corpus/templates/clips/actions/view-screen.ts +2 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
- package/corpus/templates/clips/app/i18n/ar-SA.ts +14 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +14 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +14 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +14 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +14 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +14 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +14 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +14 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +14 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +14 -0
- package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
- package/corpus/templates/clips/app/root.tsx +2 -2
- package/corpus/templates/clips/app/routes/_app.extensions.$id.tsx +4 -1
- package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +4 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +12 -5
- package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-localized-clips-settings-changelog-and-extension-sidebar-lab.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +3 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
- package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +4 -2
- package/corpus/templates/clips/desktop/src/styles.css +15 -1
- package/corpus/templates/clips/netlify.toml +1 -0
- package/corpus/templates/clips/server/db/index.ts +1 -0
- package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
- package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
- package/corpus/templates/clips/server/lib/recordings.ts +18 -2
- package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
- package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
- package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
- package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
- package/corpus/templates/content/netlify.toml +1 -0
- package/corpus/templates/design/netlify.toml +1 -0
- package/corpus/templates/dispatch/netlify.toml +1 -0
- package/corpus/templates/forms/netlify.toml +1 -0
- package/corpus/templates/macros/netlify.toml +1 -0
- package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
- package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
- package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
- package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
- package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
- package/corpus/templates/mail/netlify.toml +1 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
- package/corpus/templates/plan/netlify.toml +1 -0
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/videos/netlify.toml +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +31 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +2 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +23 -2
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +28 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +8 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +8 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +29 -15
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +4 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +22 -2
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +7 -2
- package/dist/client/changelog/Changelog.d.ts.map +1 -1
- package/dist/client/changelog/Changelog.js +9 -7
- package/dist/client/changelog/Changelog.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +339 -11
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +23 -13
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +4 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +49 -4
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/analytics.d.ts +4 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +19 -3
- package/dist/server/analytics.js.map +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +26 -12
- package/dist/sharing/access.js.map +1 -1
- package/dist/sharing/actions/share-resource.js +20 -9
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/actions/unshare-resource.js +13 -2
- package/dist/sharing/actions/unshare-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +9 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +3 -3
|
@@ -60,8 +60,8 @@ export function NewAnalysisDialog() {
|
|
|
60
60
|
const status = statusData as DataSourceStatusResponse | undefined;
|
|
61
61
|
const envStatus = credentialRowsFromStatus(status);
|
|
62
62
|
const configuredSources = useMemo(
|
|
63
|
-
() => getConfiguredDataSources(envStatus),
|
|
64
|
-
[envStatus],
|
|
63
|
+
() => getConfiguredDataSources(envStatus, status),
|
|
64
|
+
[envStatus, status],
|
|
65
65
|
);
|
|
66
66
|
const configuredSourceNames = configuredSources.map((source) => source.name);
|
|
67
67
|
const statusMessage = status?.message || status?.error;
|
|
@@ -79,6 +79,10 @@ const sharedConnectionLabels: Record<SharedConnectionStatusKind, string> = {
|
|
|
79
79
|
local_credentials: "Local credentials",
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
function normalizeCredentialKey(key: string): string {
|
|
83
|
+
return key.trim().toUpperCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
export function credentialRowsFromStatus(
|
|
83
87
|
data: DataSourceStatusResponse | EnvKeyStatus[] | undefined,
|
|
84
88
|
): EnvKeyStatus[] {
|
|
@@ -90,7 +94,9 @@ export function getOptionalCredentialKeys(source: DataSource): Set<string> {
|
|
|
90
94
|
return new Set(
|
|
91
95
|
source.walkthroughSteps
|
|
92
96
|
.filter((step) => step.optional)
|
|
93
|
-
.map((step) =>
|
|
97
|
+
.map((step) =>
|
|
98
|
+
step.inputKey ? normalizeCredentialKey(step.inputKey) : undefined,
|
|
99
|
+
)
|
|
94
100
|
.filter((k): k is string => Boolean(k)),
|
|
95
101
|
);
|
|
96
102
|
}
|
|
@@ -99,11 +105,21 @@ export function isSourceConfigured(
|
|
|
99
105
|
source: DataSource,
|
|
100
106
|
envStatus: EnvKeyStatus[],
|
|
101
107
|
): boolean {
|
|
102
|
-
const statusMap = new Map(
|
|
108
|
+
const statusMap = new Map(
|
|
109
|
+
envStatus.map((s) => [normalizeCredentialKey(s.key), s.configured]),
|
|
110
|
+
);
|
|
103
111
|
const optionalKeys = getOptionalCredentialKeys(source);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const requiredKeys = source.envKeys.filter(
|
|
113
|
+
(key) => !optionalKeys.has(normalizeCredentialKey(key)),
|
|
114
|
+
);
|
|
115
|
+
if (source.credentialRequirementMode === "any") {
|
|
116
|
+
return requiredKeys.some(
|
|
117
|
+
(key) => statusMap.get(normalizeCredentialKey(key)) === true,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return requiredKeys.every(
|
|
121
|
+
(key) => statusMap.get(normalizeCredentialKey(key)) === true,
|
|
122
|
+
);
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
export function getWorkspaceProviderIdForSource(
|
|
@@ -132,6 +148,14 @@ export function getWorkspaceConnectionForSource(
|
|
|
132
148
|
);
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
export function getProviderStatusForSource(
|
|
152
|
+
source: DataSource,
|
|
153
|
+
data: DataSourceStatusResponse | undefined,
|
|
154
|
+
): DataSourceProviderStatus | undefined {
|
|
155
|
+
const providerId = getWorkspaceProviderIdForSource(source) ?? source.id;
|
|
156
|
+
return data?.providers?.find((provider) => provider.provider === providerId);
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
export function getSharedConnectionStatus(
|
|
136
160
|
source: DataSource,
|
|
137
161
|
data: DataSourceStatusResponse | undefined,
|
|
@@ -178,12 +202,29 @@ export function isSourceReady(
|
|
|
178
202
|
): boolean {
|
|
179
203
|
return (
|
|
180
204
|
isSourceConfigured(source, envStatus) ||
|
|
205
|
+
getProviderStatusForSource(source, data)?.configured === true ||
|
|
181
206
|
getSharedConnectionStatus(source, data, envStatus)?.kind === "ready"
|
|
182
207
|
);
|
|
183
208
|
}
|
|
184
209
|
|
|
210
|
+
export function isSourceLocallyConfigured(
|
|
211
|
+
source: DataSource,
|
|
212
|
+
data: DataSourceStatusResponse | undefined,
|
|
213
|
+
envStatus: EnvKeyStatus[],
|
|
214
|
+
): boolean {
|
|
215
|
+
if (isSourceConfigured(source, envStatus)) return true;
|
|
216
|
+
const providerStatus = getProviderStatusForSource(source, data);
|
|
217
|
+
if (!providerStatus?.configured) return false;
|
|
218
|
+
return providerStatus.configuredKeys.length > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
185
221
|
export function getConfiguredDataSources(
|
|
186
222
|
envStatus: EnvKeyStatus[],
|
|
223
|
+
data?: DataSourceStatusResponse,
|
|
187
224
|
): DataSource[] {
|
|
188
|
-
return dataSources.filter((source) =>
|
|
225
|
+
return dataSources.filter((source) =>
|
|
226
|
+
data
|
|
227
|
+
? isSourceReady(source, data, envStatus)
|
|
228
|
+
: isSourceConfigured(source, envStatus),
|
|
229
|
+
);
|
|
189
230
|
}
|
|
@@ -52,6 +52,7 @@ export interface DataSource {
|
|
|
52
52
|
category: DataSourceCategory;
|
|
53
53
|
icon: ComponentType<Record<string, unknown>>;
|
|
54
54
|
envKeys: string[];
|
|
55
|
+
credentialRequirementMode?: "all" | "any";
|
|
55
56
|
walkthroughSteps: WalkthroughStep[];
|
|
56
57
|
docsUrl: string;
|
|
57
58
|
}
|
|
@@ -380,7 +381,8 @@ export const dataSources: DataSource[] = [
|
|
|
380
381
|
description: "CRM deals, contacts, companies, tickets, and pipelines",
|
|
381
382
|
category: "crm",
|
|
382
383
|
icon: IconShoppingCart,
|
|
383
|
-
envKeys: ["HUBSPOT_ACCESS_TOKEN"],
|
|
384
|
+
envKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
|
|
385
|
+
credentialRequirementMode: "any",
|
|
384
386
|
docsUrl: "https://developers.hubspot.com/docs/api/overview",
|
|
385
387
|
walkthroughSteps: [
|
|
386
388
|
{
|
|
@@ -391,10 +393,19 @@ export const dataSources: DataSource[] = [
|
|
|
391
393
|
linkText: "Private Apps",
|
|
392
394
|
},
|
|
393
395
|
{
|
|
394
|
-
title: "Enter your
|
|
396
|
+
title: "Enter your Private App Token",
|
|
395
397
|
description: "Copy the access token from your private app.",
|
|
398
|
+
inputKey: "HUBSPOT_PRIVATE_APP_TOKEN",
|
|
399
|
+
inputLabel: "Private App Token",
|
|
400
|
+
inputPlaceholder: "pat-na1-...",
|
|
401
|
+
inputType: "password",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
title: "Legacy access token",
|
|
405
|
+
description:
|
|
406
|
+
"Only use this if you already have an older HubSpot access token saved under the legacy key.",
|
|
396
407
|
inputKey: "HUBSPOT_ACCESS_TOKEN",
|
|
397
|
-
inputLabel: "Access Token",
|
|
408
|
+
inputLabel: "Legacy Access Token",
|
|
398
409
|
inputPlaceholder: "pat-na1-...",
|
|
399
410
|
inputType: "password",
|
|
400
411
|
},
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
getOptionalCredentialKeys,
|
|
67
67
|
getSharedConnectionStatus,
|
|
68
68
|
isSourceReady,
|
|
69
|
-
|
|
69
|
+
isSourceLocallyConfigured,
|
|
70
70
|
credentialRowsFromStatus,
|
|
71
71
|
type DataSourceStatusResponse,
|
|
72
72
|
type EnvKeyStatus,
|
|
@@ -674,6 +674,13 @@ function ConnectedView({
|
|
|
674
674
|
),
|
|
675
675
|
);
|
|
676
676
|
const optionalKeys = getOptionalCredentialKeys(source);
|
|
677
|
+
const isAnyCredentialMode = source.credentialRequirementMode === "any";
|
|
678
|
+
const configuredCredentialKeys = new Set(
|
|
679
|
+
envStatus.filter((s) => s.configured).map((s) => s.key),
|
|
680
|
+
);
|
|
681
|
+
const hasAlternativeCredential =
|
|
682
|
+
isAnyCredentialMode &&
|
|
683
|
+
source.envKeys.some((key) => configuredCredentialKeys.has(key));
|
|
677
684
|
|
|
678
685
|
const handleDisconnect = () => {
|
|
679
686
|
if (sharedCredentialKeys.length > 0) {
|
|
@@ -852,8 +859,7 @@ function ConnectedView({
|
|
|
852
859
|
<div className="flex items-start justify-between gap-4">
|
|
853
860
|
<div className="flex-1 space-y-2">
|
|
854
861
|
{source.envKeys.map((key) => {
|
|
855
|
-
const configured =
|
|
856
|
-
envStatus.find((s) => s.key === key)?.configured ?? false;
|
|
862
|
+
const configured = configuredCredentialKeys.has(key);
|
|
857
863
|
const optional = optionalKeys.has(key);
|
|
858
864
|
return (
|
|
859
865
|
<div
|
|
@@ -873,6 +879,11 @@ function ConnectedView({
|
|
|
873
879
|
<IconCircle className="h-3 w-3" />
|
|
874
880
|
{t("dataSources.optional")}
|
|
875
881
|
</span>
|
|
882
|
+
) : hasAlternativeCredential ? (
|
|
883
|
+
<span className="flex items-center gap-1 whitespace-nowrap text-muted-foreground">
|
|
884
|
+
<IconCircle className="h-3 w-3" />
|
|
885
|
+
Alternative
|
|
886
|
+
</span>
|
|
876
887
|
) : (
|
|
877
888
|
<span className="flex items-center gap-1 whitespace-nowrap text-rose-400">
|
|
878
889
|
<IconAlertCircle className="h-3 w-3" />
|
|
@@ -1643,7 +1654,11 @@ export default function DataSources() {
|
|
|
1643
1654
|
<DataSourceCard
|
|
1644
1655
|
key={source.id}
|
|
1645
1656
|
source={source}
|
|
1646
|
-
locallyConfigured={
|
|
1657
|
+
locallyConfigured={isSourceLocallyConfigured(
|
|
1658
|
+
source,
|
|
1659
|
+
statusData,
|
|
1660
|
+
envStatus,
|
|
1661
|
+
)}
|
|
1647
1662
|
ready={isSourceReady(source, statusData, envStatus)}
|
|
1648
1663
|
sharedConnectionStatus={getSharedConnectionStatus(
|
|
1649
1664
|
source,
|
|
@@ -1676,7 +1691,11 @@ export default function DataSources() {
|
|
|
1676
1691
|
<DataSourceCard
|
|
1677
1692
|
key={source.id}
|
|
1678
1693
|
source={source}
|
|
1679
|
-
locallyConfigured={
|
|
1694
|
+
locallyConfigured={isSourceLocallyConfigured(
|
|
1695
|
+
source,
|
|
1696
|
+
statusData,
|
|
1697
|
+
envStatus,
|
|
1698
|
+
)}
|
|
1680
1699
|
ready={isSourceReady(source, statusData, envStatus)}
|
|
1681
1700
|
sharedConnectionStatus={getSharedConnectionStatus(
|
|
1682
1701
|
source,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useSendToAgentChat, useT } from "@agent-native/core/client";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "@
|
|
2
|
+
PromptComposer,
|
|
3
|
+
useSendToAgentChat,
|
|
4
|
+
useT,
|
|
5
|
+
} from "@agent-native/core/client";
|
|
6
|
+
import { IconAlertTriangle, IconAlignLeft } from "@tabler/icons-react";
|
|
7
7
|
import { useEffect, useState, type ReactElement, type ReactNode } from "react";
|
|
8
8
|
import { toast } from "sonner";
|
|
9
9
|
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
SelectValue,
|
|
31
31
|
} from "@/components/ui/select";
|
|
32
32
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
33
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
34
33
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
35
34
|
import { canFormatPanelSql, formatPanelSql } from "@/lib/format-sql";
|
|
36
35
|
|
|
@@ -182,7 +181,6 @@ function PanelEditorContent({
|
|
|
182
181
|
const [form, setForm] = useState<PanelFormValues>(() => panelToForm(panel));
|
|
183
182
|
const [saving, setSaving] = useState(false);
|
|
184
183
|
const [error, setError] = useState<string | null>(null);
|
|
185
|
-
const [prompt, setPrompt] = useState("");
|
|
186
184
|
const [tab, setTab] = useState<"describe" | "manual">("describe");
|
|
187
185
|
const { send, isGenerating } = useSendToAgentChat();
|
|
188
186
|
|
|
@@ -192,7 +190,6 @@ function PanelEditorContent({
|
|
|
192
190
|
setForm(panelToForm(panel));
|
|
193
191
|
setError(null);
|
|
194
192
|
setSaving(false);
|
|
195
|
-
setPrompt("");
|
|
196
193
|
// Editing an existing panel always goes straight to the manual form.
|
|
197
194
|
setTab(panel ? "manual" : "describe");
|
|
198
195
|
}
|
|
@@ -200,7 +197,6 @@ function PanelEditorContent({
|
|
|
200
197
|
|
|
201
198
|
const isEdit = !!panel;
|
|
202
199
|
const canSave = form.title.trim().length > 0 && form.sql.trim().length > 0;
|
|
203
|
-
const canGenerate = prompt.trim().length > 0 && !isGenerating;
|
|
204
200
|
const canFormat = canFormatPanelSql(form.source);
|
|
205
201
|
|
|
206
202
|
const handleSubmit = async () => {
|
|
@@ -219,13 +215,14 @@ function PanelEditorContent({
|
|
|
219
215
|
}
|
|
220
216
|
};
|
|
221
217
|
|
|
222
|
-
const handleDescribe = () => {
|
|
223
|
-
|
|
218
|
+
const handleDescribe = (text: string) => {
|
|
219
|
+
const trimmed = text.trim();
|
|
220
|
+
if (!trimmed || isGenerating) return;
|
|
224
221
|
const titlesLine = existingPanelTitles.length
|
|
225
222
|
? `Existing panels on this dashboard: ${existingPanelTitles.join(", ")}.`
|
|
226
223
|
: "This dashboard has no panels yet.";
|
|
227
224
|
send({
|
|
228
|
-
message:
|
|
225
|
+
message: trimmed,
|
|
229
226
|
context:
|
|
230
227
|
`The user wants to add a new panel to SQL dashboard "${dashboardId}". ${titlesLine} ` +
|
|
231
228
|
`REAL_DATA_REQUIRED: before saving or answering, run at least one real data-source query action for this panel; \`data-source-status\`, \`list-data-dictionary\`, \`update-dashboard\`, and dry-run validation do not count as data queries. ` +
|
|
@@ -246,7 +243,6 @@ function PanelEditorContent({
|
|
|
246
243
|
`After the panel saves, call \`refresh-screen\` so the UI picks up the change.`,
|
|
247
244
|
submit: true,
|
|
248
245
|
});
|
|
249
|
-
setPrompt("");
|
|
250
246
|
onOpenChange(false);
|
|
251
247
|
};
|
|
252
248
|
|
|
@@ -441,28 +437,14 @@ function PanelEditorContent({
|
|
|
441
437
|
|
|
442
438
|
<TabsContent value="describe" className="mt-4">
|
|
443
439
|
<div className="grid gap-3">
|
|
444
|
-
<Label
|
|
445
|
-
<
|
|
446
|
-
id="panel-prompt"
|
|
447
|
-
value={prompt}
|
|
448
|
-
onChange={(e) => setPrompt(e.target.value)}
|
|
449
|
-
placeholder={t("panelEditor.promptPlaceholder")}
|
|
450
|
-
className="min-h-[140px] resize-y text-sm"
|
|
440
|
+
<Label>{t("panelEditor.whatToChart")}</Label>
|
|
441
|
+
<PromptComposer
|
|
451
442
|
autoFocus
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
}}
|
|
443
|
+
disabled={isGenerating}
|
|
444
|
+
placeholder={t("panelEditor.promptPlaceholder")}
|
|
445
|
+
draftScope="analytics:add-panel"
|
|
446
|
+
onSubmit={handleDescribe}
|
|
458
447
|
/>
|
|
459
|
-
<p className="text-xs text-muted-foreground">
|
|
460
|
-
{t("panelEditor.describeHelp", {
|
|
461
|
-
shortcut: `${
|
|
462
|
-
/Mac|iPhone|iPad/.test(navigator.userAgent) ? "⌘" : "Ctrl"
|
|
463
|
-
}+Enter`,
|
|
464
|
-
})}
|
|
465
|
-
</p>
|
|
466
448
|
</div>
|
|
467
449
|
<EditorFooter className="mt-4">
|
|
468
450
|
<Button
|
|
@@ -473,16 +455,6 @@ function PanelEditorContent({
|
|
|
473
455
|
>
|
|
474
456
|
{t("panelEditor.cancel")}
|
|
475
457
|
</Button>
|
|
476
|
-
<Button size="sm" onClick={handleDescribe} disabled={!canGenerate}>
|
|
477
|
-
{isGenerating ? (
|
|
478
|
-
<>
|
|
479
|
-
<IconLoader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
|
480
|
-
{t("panelEditor.generating")}
|
|
481
|
-
</>
|
|
482
|
-
) : (
|
|
483
|
-
t("panelEditor.generate")
|
|
484
|
-
)}
|
|
485
|
-
</Button>
|
|
486
458
|
</EditorFooter>
|
|
487
459
|
</TabsContent>
|
|
488
460
|
|
|
@@ -8,6 +8,7 @@ export interface CredentialProviderConfig {
|
|
|
8
8
|
provider: string;
|
|
9
9
|
label: string;
|
|
10
10
|
requiredKeys: string[];
|
|
11
|
+
requiredMode?: "all" | "any";
|
|
11
12
|
optionalKeys?: string[];
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -217,7 +218,8 @@ export const credentialProviderConfigs: CredentialProviderConfig[] = [
|
|
|
217
218
|
{
|
|
218
219
|
provider: "hubspot",
|
|
219
220
|
label: "HubSpot",
|
|
220
|
-
requiredKeys: ["HUBSPOT_ACCESS_TOKEN"],
|
|
221
|
+
requiredKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
|
|
222
|
+
requiredMode: "any",
|
|
221
223
|
},
|
|
222
224
|
{
|
|
223
225
|
provider: "gong",
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
resolveCredential,
|
|
6
6
|
withRequestContextFromEvent,
|
|
7
7
|
} from "../../lib/credentials";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
HUBSPOT_ANALYTICS_CREDENTIAL_KEYS,
|
|
10
|
+
resolveAnalyticsGongCredentials,
|
|
11
|
+
resolveAnalyticsProviderCredential,
|
|
12
|
+
} from "../../lib/provider-credentials";
|
|
9
13
|
|
|
10
14
|
export default defineEventHandler(async (event) => {
|
|
11
15
|
const body = await readBody(event);
|
|
@@ -91,8 +95,13 @@ export default defineEventHandler(async (event) => {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
case "hubspot": {
|
|
94
|
-
const
|
|
95
|
-
|
|
98
|
+
const credential = await resolveAnalyticsProviderCredential({
|
|
99
|
+
provider: "hubspot",
|
|
100
|
+
keys: HUBSPOT_ANALYTICS_CREDENTIAL_KEYS,
|
|
101
|
+
ctx,
|
|
102
|
+
});
|
|
103
|
+
const token = credential?.value;
|
|
104
|
+
if (!token) return { ok: false, error: "Missing HubSpot token" };
|
|
96
105
|
const res = await fetch(
|
|
97
106
|
"https://api.hubapi.com/crm/v3/objects/contacts?limit=1",
|
|
98
107
|
{
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
DevDatabaseLink,
|
|
3
3
|
FeedbackButton,
|
|
4
4
|
appPath,
|
|
5
|
+
focusAgentChat,
|
|
5
6
|
navigateWithAgentChatViewTransition,
|
|
6
7
|
useActionQuery,
|
|
7
8
|
useChatThreads,
|
|
@@ -507,14 +508,16 @@ export function Sidebar() {
|
|
|
507
508
|
onClick={(event) => {
|
|
508
509
|
if (
|
|
509
510
|
item.href === "/" &&
|
|
510
|
-
!isCreateRoute &&
|
|
511
511
|
!event.metaKey &&
|
|
512
512
|
!event.ctrlKey &&
|
|
513
513
|
!event.shiftKey &&
|
|
514
514
|
!event.altKey
|
|
515
515
|
) {
|
|
516
516
|
event.preventDefault();
|
|
517
|
-
|
|
517
|
+
focusAgentChat();
|
|
518
|
+
if (!isCreateRoute || location.pathname !== "/") {
|
|
519
|
+
navigateWithAgentChatViewTransition(navigate, "/");
|
|
520
|
+
}
|
|
518
521
|
}
|
|
519
522
|
}}
|
|
520
523
|
className={cn(
|
|
@@ -5,6 +5,7 @@ const enUS = {
|
|
|
5
5
|
connect: "Connect",
|
|
6
6
|
connected: "Connected",
|
|
7
7
|
connecting: "Connecting...",
|
|
8
|
+
clipboardUnavailable: "Clipboard access is unavailable",
|
|
8
9
|
disconnect: "Disconnect",
|
|
9
10
|
notConnected: "Not connected",
|
|
10
11
|
saving: "Saving...",
|
|
@@ -3626,6 +3627,7 @@ export const messagesByLocale = {
|
|
|
3626
3627
|
connect: "连接",
|
|
3627
3628
|
connected: "已连接",
|
|
3628
3629
|
connecting: "正在连接...",
|
|
3630
|
+
clipboardUnavailable: "无法访问剪贴板",
|
|
3629
3631
|
disconnect: "断开连接",
|
|
3630
3632
|
notConnected: "未连接",
|
|
3631
3633
|
saving: "正在保存...",
|
|
@@ -3681,6 +3683,7 @@ export const messagesByLocale = {
|
|
|
3681
3683
|
connect: "Conectar",
|
|
3682
3684
|
connected: "Conectado",
|
|
3683
3685
|
connecting: "Conectando...",
|
|
3686
|
+
clipboardUnavailable: "El acceso al portapapeles no está disponible",
|
|
3684
3687
|
disconnect: "Desconectar",
|
|
3685
3688
|
notConnected: "No conectado",
|
|
3686
3689
|
saving: "Guardando...",
|
|
@@ -3737,6 +3740,7 @@ export const messagesByLocale = {
|
|
|
3737
3740
|
connect: "Connecter",
|
|
3738
3741
|
connected: "Connecté",
|
|
3739
3742
|
connecting: "Connexion...",
|
|
3743
|
+
clipboardUnavailable: "L’accès au presse-papiers n’est pas disponible",
|
|
3740
3744
|
disconnect: "Déconnecter",
|
|
3741
3745
|
notConnected: "Non connecté",
|
|
3742
3746
|
saving: "Enregistrement...",
|
|
@@ -3794,6 +3798,8 @@ export const messagesByLocale = {
|
|
|
3794
3798
|
connect: "Verbinden",
|
|
3795
3799
|
connected: "Verbunden",
|
|
3796
3800
|
connecting: "Verbinden...",
|
|
3801
|
+
clipboardUnavailable:
|
|
3802
|
+
"Zugriff auf die Zwischenablage ist nicht verfügbar",
|
|
3797
3803
|
disconnect: "Trennen",
|
|
3798
3804
|
notConnected: "Nicht verbunden",
|
|
3799
3805
|
saving: "Speichern...",
|
|
@@ -3851,6 +3857,7 @@ export const messagesByLocale = {
|
|
|
3851
3857
|
connect: "接続",
|
|
3852
3858
|
connected: "接続済み",
|
|
3853
3859
|
connecting: "接続中...",
|
|
3860
|
+
clipboardUnavailable: "クリップボードへのアクセスは利用できません",
|
|
3854
3861
|
disconnect: "切断",
|
|
3855
3862
|
notConnected: "未接続",
|
|
3856
3863
|
saving: "保存中...",
|
|
@@ -3906,6 +3913,7 @@ export const messagesByLocale = {
|
|
|
3906
3913
|
connect: "연결",
|
|
3907
3914
|
connected: "연결됨",
|
|
3908
3915
|
connecting: "연결 중...",
|
|
3916
|
+
clipboardUnavailable: "클립보드 접근을 사용할 수 없습니다",
|
|
3909
3917
|
disconnect: "연결 해제",
|
|
3910
3918
|
notConnected: "연결되지 않음",
|
|
3911
3919
|
saving: "저장 중...",
|
|
@@ -3961,6 +3969,8 @@ export const messagesByLocale = {
|
|
|
3961
3969
|
connect: "Conectar",
|
|
3962
3970
|
connected: "Conectado",
|
|
3963
3971
|
connecting: "Conectando...",
|
|
3972
|
+
clipboardUnavailable:
|
|
3973
|
+
"O acesso à área de transferência não está disponível",
|
|
3964
3974
|
disconnect: "Desconectar",
|
|
3965
3975
|
notConnected: "Não conectado",
|
|
3966
3976
|
saving: "Salvando...",
|
|
@@ -4017,6 +4027,7 @@ export const messagesByLocale = {
|
|
|
4017
4027
|
connect: "कनेक्ट करें",
|
|
4018
4028
|
connected: "कनेक्टेड",
|
|
4019
4029
|
connecting: "कनेक्ट कर रहे हैं...",
|
|
4030
|
+
clipboardUnavailable: "क्लिपबोर्ड एक्सेस उपलब्ध नहीं है",
|
|
4020
4031
|
disconnect: "डिस्कनेक्ट करें",
|
|
4021
4032
|
notConnected: "कनेक्टेड नहीं",
|
|
4022
4033
|
saving: "सहेज रहे हैं...",
|
|
@@ -4071,6 +4082,7 @@ export const messagesByLocale = {
|
|
|
4071
4082
|
connect: "اتصال",
|
|
4072
4083
|
connected: "متصل",
|
|
4073
4084
|
connecting: "جارٍ الاتصال...",
|
|
4085
|
+
clipboardUnavailable: "الوصول إلى الحافظة غير متاح",
|
|
4074
4086
|
disconnect: "قطع الاتصال",
|
|
4075
4087
|
notConnected: "غير متصل",
|
|
4076
4088
|
saving: "جارٍ الحفظ...",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type ElectronClipboardApi = {
|
|
2
|
+
clipboard?: {
|
|
3
|
+
writeText?: (text: string) => boolean | Promise<boolean>;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function getElectronClipboard(): ElectronClipboardApi["clipboard"] | null {
|
|
8
|
+
const api = (
|
|
9
|
+
globalThis as typeof globalThis & {
|
|
10
|
+
electronAPI?: ElectronClipboardApi;
|
|
11
|
+
}
|
|
12
|
+
).electronAPI;
|
|
13
|
+
return api?.clipboard ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function copyWithExecCommand(text: string): boolean {
|
|
17
|
+
if (typeof document === "undefined" || !document.body) return false;
|
|
18
|
+
const textarea = document.createElement("textarea");
|
|
19
|
+
textarea.value = text;
|
|
20
|
+
textarea.setAttribute("readonly", "");
|
|
21
|
+
textarea.style.position = "fixed";
|
|
22
|
+
textarea.style.left = "-9999px";
|
|
23
|
+
textarea.style.top = "0";
|
|
24
|
+
textarea.style.opacity = "0";
|
|
25
|
+
document.body.appendChild(textarea);
|
|
26
|
+
textarea.select();
|
|
27
|
+
textarea.setSelectionRange(0, text.length);
|
|
28
|
+
try {
|
|
29
|
+
return document.execCommand("copy");
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
} finally {
|
|
33
|
+
document.body.removeChild(textarea);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
38
|
+
const electronClipboard = getElectronClipboard();
|
|
39
|
+
if (electronClipboard?.writeText) {
|
|
40
|
+
try {
|
|
41
|
+
const copied = await electronClipboard.writeText(text);
|
|
42
|
+
if (copied !== false) return true;
|
|
43
|
+
} catch {
|
|
44
|
+
// Fall through to browser clipboard options.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
49
|
+
try {
|
|
50
|
+
await navigator.clipboard.writeText(text);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
// Electron/web embeds can deny clipboard-write even after a click.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return copyWithExecCommand(text);
|
|
58
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
useUpdateAvailability,
|
|
22
22
|
} from "@/hooks/use-availability";
|
|
23
23
|
import { useDbStatus } from "@/hooks/use-db-status";
|
|
24
|
+
import { copyTextToClipboard } from "@/lib/clipboard";
|
|
24
25
|
|
|
25
26
|
type DayName = keyof AvailabilityConfig["weeklySchedule"];
|
|
26
27
|
|
|
@@ -64,6 +65,19 @@ export default function AvailabilitySettings() {
|
|
|
64
65
|
const { isLocal } = useDbStatus();
|
|
65
66
|
const [showCloudUpgrade, setShowCloudUpgrade] = useState(false);
|
|
66
67
|
|
|
68
|
+
async function handleCopyBookingLink() {
|
|
69
|
+
if (isLocal) {
|
|
70
|
+
setShowCloudUpgrade(true);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const url = `${window.location.origin}/book/${bookingSlug}`;
|
|
74
|
+
if (await copyTextToClipboard(url)) {
|
|
75
|
+
toast.success(t("bookingLinks.bookingLinkCopied"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
toast.error(t("common.clipboardUnavailable"));
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
useEffect(() => {
|
|
68
82
|
if (availability) {
|
|
69
83
|
setSchedule(availability.weeklySchedule);
|
|
@@ -283,15 +297,7 @@ export default function AvailabilitySettings() {
|
|
|
283
297
|
<Button
|
|
284
298
|
variant="outline"
|
|
285
299
|
size="sm"
|
|
286
|
-
onClick={() =>
|
|
287
|
-
if (isLocal) {
|
|
288
|
-
setShowCloudUpgrade(true);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const url = `${window.location.origin}/book/${bookingSlug}`;
|
|
292
|
-
navigator.clipboard.writeText(url);
|
|
293
|
-
toast.success(t("bookingLinks.bookingLinkCopied"));
|
|
294
|
-
}}
|
|
300
|
+
onClick={() => void handleCopyBookingLink()}
|
|
295
301
|
>
|
|
296
302
|
{t("bookingLinks.copyBookingLink")}
|
|
297
303
|
</Button>
|
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
} from "@/hooks/use-booking-links";
|
|
113
113
|
import { useGoogleAuthStatus } from "@/hooks/use-google-auth";
|
|
114
114
|
import { useZoomStatus, useConnectZoom } from "@/hooks/use-zoom-auth";
|
|
115
|
+
import { copyTextToClipboard } from "@/lib/clipboard";
|
|
115
116
|
import { cn } from "@/lib/utils";
|
|
116
117
|
|
|
117
118
|
import BookingsList from "./BookingsList";
|
|
@@ -899,8 +900,11 @@ export default function BookingLinksPage({
|
|
|
899
900
|
}
|
|
900
901
|
|
|
901
902
|
async function copyPreviewUrl(slug: string) {
|
|
902
|
-
await
|
|
903
|
-
|
|
903
|
+
if (await copyTextToClipboard(getBookingUrl(slug))) {
|
|
904
|
+
toast.success(t("bookingLinks.bookingLinkCopied"));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
toast.error(t("common.clipboardUnavailable"));
|
|
904
908
|
}
|
|
905
909
|
|
|
906
910
|
function openPreview(slug: string) {
|