@agent-native/toolkit 0.201.0 → 0.201.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-native.eject.json +1 -0
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +21 -13
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +15 -1
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +30 -4
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +11 -7
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +9 -4
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/FileStorageSetupPopover.d.ts.map +1 -1
- package/dist/app/chat/FileStorageSetupPopover.js +3 -2
- package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +11 -5
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +11 -5
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +11 -5
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +11 -5
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +11 -5
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +11 -5
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +11 -5
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +11 -5
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +11 -5
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +11 -5
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +11 -5
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +11 -5
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/onboarding/BuilderIncludedServices.d.ts +23 -0
- package/dist/app/onboarding/BuilderIncludedServices.d.ts.map +1 -0
- package/dist/app/onboarding/BuilderIncludedServices.js +67 -0
- package/dist/app/onboarding/BuilderIncludedServices.js.map +1 -0
- package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.js +10 -29
- package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.js +55 -21
- package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/app/settings/BuilderConnectPopover.d.ts +1 -2
- package/dist/app/settings/BuilderConnectPopover.d.ts.map +1 -1
- package/dist/app/settings/BuilderConnectPopover.js +35 -21
- package/dist/app/settings/BuilderConnectPopover.js.map +1 -1
- package/dist/app/settings/deferred-builder-connect-popover.d.ts.map +1 -1
- package/dist/app/settings/deferred-builder-connect-popover.js +28 -1
- package/dist/app/settings/deferred-builder-connect-popover.js.map +1 -1
- package/dist/app/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/app/settings/useBuilderStatus.js +8 -1
- package/dist/app/settings/useBuilderStatus.js.map +1 -1
- package/dist/app/styles/tokens/agent-kit.css +23 -11
- package/dist/composer/MessageQueueDrawer.d.ts +5 -0
- package/dist/composer/MessageQueueDrawer.d.ts.map +1 -1
- package/dist/composer/MessageQueueDrawer.js +11 -5
- package/dist/composer/MessageQueueDrawer.js.map +1 -1
- package/package.json +4 -3
- package/src/app/agentkit/react/components.interaction.spec.tsx +13 -5
- package/src/app/agentkit/react/components.tsx +24 -14
- package/src/app/agentkit/react/context.tsx +53 -6
- package/src/app/agentkit/react/styles.css +11 -7
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +35 -16
- package/src/app/chat/AgentKitAssistantChat.tsx +13 -4
- package/src/app/chat/FileStorageSetupPopover.spec.tsx +129 -0
- package/src/app/chat/FileStorageSetupPopover.tsx +10 -3
- package/src/app/i18n/catalogs/ar-SA.ts +14 -5
- package/src/app/i18n/catalogs/de-DE.ts +15 -5
- package/src/app/i18n/catalogs/en-US.ts +14 -5
- package/src/app/i18n/catalogs/es-ES.ts +14 -5
- package/src/app/i18n/catalogs/fr-FR.ts +15 -5
- package/src/app/i18n/catalogs/hi-IN.ts +14 -5
- package/src/app/i18n/catalogs/ja-JP.ts +14 -5
- package/src/app/i18n/catalogs/ko-KR.ts +14 -5
- package/src/app/i18n/catalogs/pt-BR.ts +15 -5
- package/src/app/i18n/catalogs/zh-CN.ts +13 -5
- package/src/app/i18n/catalogs/zh-TW.ts +13 -5
- package/src/app/onboarding/BuilderIncludedServices.tsx +263 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +23 -8
- package/src/app/onboarding/FirstRunOnboarding.tsx +22 -112
- package/src/app/resources/McpConnectionSuggestion.render.spec.tsx +54 -2
- package/src/app/resources/McpConnectionSuggestion.tsx +73 -25
- package/src/app/settings/BuilderConnectPopover.spec.tsx +175 -6
- package/src/app/settings/BuilderConnectPopover.tsx +87 -67
- package/src/app/settings/deferred-builder-connect-popover.tsx +46 -0
- package/src/app/settings/model/ModelSettingsPage.spec.tsx +8 -24
- package/src/app/settings/useBuilderStatus.spec.tsx +5 -0
- package/src/app/settings/useBuilderStatus.ts +8 -0
- package/src/app/styles/agent-native.spec.ts +5 -8
- package/src/app/styles/tokens/agent-kit.css +23 -11
- package/src/composer/MessageQueueDrawer.spec.tsx +20 -7
- package/src/composer/MessageQueueDrawer.tsx +47 -21
|
@@ -44,32 +44,65 @@ export interface McpConnectionSuggestionProps {
|
|
|
44
44
|
|
|
45
45
|
const DISMISSED_MCP_SUGGESTIONS_STORAGE_KEY =
|
|
46
46
|
"agent-native:mcp-connection-suggestions-dismissed";
|
|
47
|
+
const MCP_SUGGESTION_DISMISSAL_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
47
48
|
|
|
48
|
-
function
|
|
49
|
-
|
|
49
|
+
function readDismissedMcpSuggestionTimestamps(
|
|
50
|
+
now = Date.now(),
|
|
51
|
+
): Record<string, number> {
|
|
52
|
+
if (typeof window === "undefined") return {};
|
|
50
53
|
try {
|
|
51
54
|
const raw = window.localStorage.getItem(
|
|
52
55
|
DISMISSED_MCP_SUGGESTIONS_STORAGE_KEY,
|
|
53
56
|
);
|
|
54
|
-
if (!raw) return
|
|
57
|
+
if (!raw) return {};
|
|
55
58
|
const parsed: unknown = JSON.parse(raw);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
if (Array.isArray(parsed)) {
|
|
60
|
+
const timestamps = Object.fromEntries(
|
|
61
|
+
parsed
|
|
62
|
+
.filter((id): id is string => typeof id === "string")
|
|
63
|
+
.map((id) => [id, now]),
|
|
64
|
+
) as Record<string, number>;
|
|
65
|
+
try {
|
|
66
|
+
// Legacy IDs have no timestamp, so start their cooldown at migration.
|
|
67
|
+
window.localStorage.setItem(
|
|
68
|
+
DISMISSED_MCP_SUGGESTIONS_STORAGE_KEY,
|
|
69
|
+
JSON.stringify(timestamps),
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
// coercion-ok: unavailable storage cannot persist the migration.
|
|
73
|
+
}
|
|
74
|
+
return timestamps;
|
|
75
|
+
}
|
|
76
|
+
if (!parsed || typeof parsed !== "object") {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
const timestamps: Record<string, number> = {};
|
|
80
|
+
for (const [id, dismissedAt] of Object.entries(
|
|
81
|
+
parsed as Record<string, unknown>,
|
|
82
|
+
)) {
|
|
83
|
+
if (
|
|
84
|
+
typeof dismissedAt === "number" &&
|
|
85
|
+
Number.isFinite(dismissedAt) &&
|
|
86
|
+
dismissedAt <= now &&
|
|
87
|
+
now - dismissedAt < MCP_SUGGESTION_DISMISSAL_WINDOW_MS
|
|
88
|
+
) {
|
|
89
|
+
timestamps[id] = dismissedAt;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return timestamps;
|
|
59
93
|
} catch {
|
|
60
94
|
// coercion-ok: unavailable browser storage means no persisted dismissals.
|
|
61
|
-
return
|
|
95
|
+
return {};
|
|
62
96
|
}
|
|
63
97
|
}
|
|
64
98
|
|
|
65
|
-
function rememberMcpSuggestionDismissal(id: string): void {
|
|
99
|
+
function rememberMcpSuggestionDismissal(id: string, dismissedAt: number): void {
|
|
66
100
|
if (typeof window === "undefined") return;
|
|
67
101
|
try {
|
|
68
|
-
const
|
|
69
|
-
if (ids.includes(id)) return;
|
|
102
|
+
const timestamps = readDismissedMcpSuggestionTimestamps(dismissedAt);
|
|
70
103
|
window.localStorage.setItem(
|
|
71
104
|
DISMISSED_MCP_SUGGESTIONS_STORAGE_KEY,
|
|
72
|
-
JSON.stringify(
|
|
105
|
+
JSON.stringify({ ...timestamps, [id]: dismissedAt }),
|
|
73
106
|
);
|
|
74
107
|
} catch {
|
|
75
108
|
// coercion-ok: unavailable browser storage only skips persistence.
|
|
@@ -180,8 +213,8 @@ export function McpConnectionSuggestion({
|
|
|
180
213
|
const [quickConnectIntegrationId, setQuickConnectIntegrationId] = useState<
|
|
181
214
|
string | null
|
|
182
215
|
>(null);
|
|
183
|
-
const [
|
|
184
|
-
|
|
216
|
+
const [dismissedAtById, setDismissedAtById] = useState(
|
|
217
|
+
readDismissedMcpSuggestionTimestamps,
|
|
185
218
|
);
|
|
186
219
|
const [connecting, setConnecting] = useState(false);
|
|
187
220
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -230,11 +263,15 @@ export function McpConnectionSuggestion({
|
|
|
230
263
|
(mcpServersQuery.data?.role === "owner" ||
|
|
231
264
|
mcpServersQuery.data?.role === "admin"),
|
|
232
265
|
);
|
|
266
|
+
const dismissedAt = integration ? dismissedAtById[integration.id] : undefined;
|
|
267
|
+
const dismissed =
|
|
268
|
+
dismissedAt !== undefined &&
|
|
269
|
+
Date.now() - dismissedAt < MCP_SUGGESTION_DISMISSAL_WINDOW_MS;
|
|
233
270
|
const shouldSuggest =
|
|
234
271
|
mcpServersQuery.isSuccess &&
|
|
235
272
|
integration &&
|
|
236
273
|
!connected &&
|
|
237
|
-
!
|
|
274
|
+
!dismissed &&
|
|
238
275
|
(Boolean(integrationId) ||
|
|
239
276
|
variant === "composer" ||
|
|
240
277
|
isMcpConnectionFailureText(text) ||
|
|
@@ -249,6 +286,18 @@ export function McpConnectionSuggestion({
|
|
|
249
286
|
setConnecting(false);
|
|
250
287
|
}, [integration?.id, integration?.logoUrl, variant]);
|
|
251
288
|
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (!integration || dismissedAt === undefined) return;
|
|
291
|
+
const timeout = window.setTimeout(
|
|
292
|
+
() => setDismissedAtById(readDismissedMcpSuggestionTimestamps()),
|
|
293
|
+
Math.max(
|
|
294
|
+
0,
|
|
295
|
+
dismissedAt + MCP_SUGGESTION_DISMISSAL_WINDOW_MS - Date.now(),
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
return () => window.clearTimeout(timeout);
|
|
299
|
+
}, [dismissedAt, integration?.id]);
|
|
300
|
+
|
|
252
301
|
if (!shouldSuggest) return null;
|
|
253
302
|
|
|
254
303
|
const connect = async () => {
|
|
@@ -314,12 +363,12 @@ export function McpConnectionSuggestion({
|
|
|
314
363
|
if (connecting) return;
|
|
315
364
|
setError(null);
|
|
316
365
|
setConnecting(true);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
);
|
|
322
|
-
rememberMcpSuggestionDismissal(integration.id);
|
|
366
|
+
const dismissedAt = Date.now();
|
|
367
|
+
setDismissedAtById((current) => ({
|
|
368
|
+
...current,
|
|
369
|
+
[integration.id]: dismissedAt,
|
|
370
|
+
}));
|
|
371
|
+
rememberMcpSuggestionDismissal(integration.id, dismissedAt);
|
|
323
372
|
void Promise.resolve(onDismiss?.())
|
|
324
373
|
.catch((cause: unknown) => {
|
|
325
374
|
setError(
|
|
@@ -372,11 +421,10 @@ export function McpConnectionSuggestion({
|
|
|
372
421
|
: undefined
|
|
373
422
|
}
|
|
374
423
|
onCreated={() => {
|
|
375
|
-
|
|
376
|
-
current
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
424
|
+
setDismissedAtById((current) => ({
|
|
425
|
+
...current,
|
|
426
|
+
[integration.id]: Date.now(),
|
|
427
|
+
}));
|
|
380
428
|
saveMcpConnectionResume(variant === "response" ? contextText : text);
|
|
381
429
|
notifyMcpConnectionComplete();
|
|
382
430
|
onConnected?.();
|
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
+
import { TooltipProvider } from "@agent-native/toolkit/ui/tooltip";
|
|
3
4
|
import React, { act } from "react";
|
|
4
5
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
7
|
|
|
7
8
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
8
|
-
useT:
|
|
9
|
-
options
|
|
9
|
+
useT:
|
|
10
|
+
() => (_key: string, options?: { defaultValue?: string; count?: number }) =>
|
|
11
|
+
(options?.defaultValue ?? _key).replace(
|
|
12
|
+
"{{count}}",
|
|
13
|
+
String(options?.count ?? ""),
|
|
14
|
+
),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
|
|
18
|
+
useOnboarding: () => ({
|
|
19
|
+
loading: false,
|
|
20
|
+
error: null,
|
|
21
|
+
profile: {
|
|
22
|
+
capabilities: [
|
|
23
|
+
{
|
|
24
|
+
id: "llm",
|
|
25
|
+
label: "AI model",
|
|
26
|
+
required: true,
|
|
27
|
+
builderIncluded: true,
|
|
28
|
+
service: "model",
|
|
29
|
+
keySummary: "Connect your own AI model",
|
|
30
|
+
why: "The agent uses a language model.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "design-system-intelligence",
|
|
34
|
+
label: "Design system intelligence",
|
|
35
|
+
required: false,
|
|
36
|
+
builderIncluded: true,
|
|
37
|
+
service: "design-system-intelligence",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "background-agents",
|
|
41
|
+
label: "Background agents",
|
|
42
|
+
required: false,
|
|
43
|
+
builderIncluded: true,
|
|
44
|
+
service: "background-agents",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
10
49
|
}));
|
|
11
50
|
|
|
12
51
|
import { BuilderConnectPopover } from "./BuilderConnectPopover.js";
|
|
52
|
+
import { DeferredBuilderConnectPopover } from "./deferred-builder-connect-popover.js";
|
|
13
53
|
|
|
14
54
|
let container: HTMLDivElement;
|
|
15
55
|
let root: Root;
|
|
@@ -46,7 +86,7 @@ function click(element: HTMLElement) {
|
|
|
46
86
|
}
|
|
47
87
|
|
|
48
88
|
function render(node: React.ReactElement) {
|
|
49
|
-
act(() => root.render(node));
|
|
89
|
+
act(() => root.render(React.createElement(TooltipProvider, null, node)));
|
|
50
90
|
}
|
|
51
91
|
|
|
52
92
|
function trigger() {
|
|
@@ -57,6 +97,23 @@ function trigger() {
|
|
|
57
97
|
}
|
|
58
98
|
|
|
59
99
|
describe("BuilderConnectPopover before the status read resolves", () => {
|
|
100
|
+
it("starts the OAuth path from the original trigger click", () => {
|
|
101
|
+
const flow = {
|
|
102
|
+
connecting: false,
|
|
103
|
+
start: vi.fn(),
|
|
104
|
+
statusResolved: true,
|
|
105
|
+
agentNativeProvisioningEnabled: false,
|
|
106
|
+
accountExists: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
React.createElement(DeferredBuilderConnectPopover, { flow }, trigger()),
|
|
111
|
+
);
|
|
112
|
+
click(connectButton());
|
|
113
|
+
|
|
114
|
+
expect(flow.start).toHaveBeenCalledWith({ provisionAccount: false });
|
|
115
|
+
});
|
|
116
|
+
|
|
60
117
|
it("uses existing-account sign-in when provisioning is unavailable", () => {
|
|
61
118
|
const onConnect = vi.fn();
|
|
62
119
|
const flow = {
|
|
@@ -182,6 +239,120 @@ describe("BuilderConnectPopover before the status read resolves", () => {
|
|
|
182
239
|
expect(onConnect).toHaveBeenCalledWith(true);
|
|
183
240
|
});
|
|
184
241
|
|
|
242
|
+
it("shows one sign-in action when Builder reports an existing account", () => {
|
|
243
|
+
const onConnect = vi.fn();
|
|
244
|
+
const flow = {
|
|
245
|
+
connecting: false,
|
|
246
|
+
start: vi.fn(),
|
|
247
|
+
statusResolved: true,
|
|
248
|
+
agentNativeProvisioningEnabled: true,
|
|
249
|
+
accountExists: true,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
render(
|
|
253
|
+
React.createElement(
|
|
254
|
+
BuilderConnectPopover,
|
|
255
|
+
{
|
|
256
|
+
flow,
|
|
257
|
+
onConnect,
|
|
258
|
+
contentTestId: "consent",
|
|
259
|
+
primaryTestId: "create",
|
|
260
|
+
secondaryTestId: "sign-in",
|
|
261
|
+
},
|
|
262
|
+
trigger(),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
expect(document.querySelector("[data-testid='consent']")).toBeNull();
|
|
266
|
+
click(connectButton());
|
|
267
|
+
|
|
268
|
+
const consent = document.querySelector("[data-testid='consent']");
|
|
269
|
+
expect(consent?.textContent).toContain("builderActivateTitle");
|
|
270
|
+
expect(consent?.querySelector("p[role='status']")?.textContent).toBe(
|
|
271
|
+
"You already have a Builder.io account",
|
|
272
|
+
);
|
|
273
|
+
expect(consent?.textContent).toContain("Included free");
|
|
274
|
+
expect(consent?.textContent).not.toContain(
|
|
275
|
+
"Included free with a Builder.io account",
|
|
276
|
+
);
|
|
277
|
+
expect(consent?.textContent).toContain("60 monthly Agent Credits");
|
|
278
|
+
expect(consent?.textContent).toContain("+ 2 more services");
|
|
279
|
+
const services = consent?.querySelector<HTMLElement>(
|
|
280
|
+
"[data-testid='builder-included-services']",
|
|
281
|
+
);
|
|
282
|
+
const servicesToggle = services?.querySelector<HTMLButtonElement>(
|
|
283
|
+
"button[aria-expanded]",
|
|
284
|
+
);
|
|
285
|
+
expect(servicesToggle?.getAttribute("aria-expanded")).toBe("false");
|
|
286
|
+
expect(
|
|
287
|
+
Array.from(
|
|
288
|
+
servicesToggle?.querySelectorAll(":scope > span > span") ?? [],
|
|
289
|
+
).map((line) => line.textContent),
|
|
290
|
+
).toEqual([
|
|
291
|
+
"Included free",
|
|
292
|
+
"60 monthly Agent Credits",
|
|
293
|
+
"+ 2 more services",
|
|
294
|
+
]);
|
|
295
|
+
expect(
|
|
296
|
+
consent?.querySelector<HTMLButtonElement>("[data-testid='create']")
|
|
297
|
+
?.textContent,
|
|
298
|
+
).toBe("agentChat.onboarding.builderSignInWithAccount");
|
|
299
|
+
expect(
|
|
300
|
+
consent?.querySelector<HTMLButtonElement>("[data-testid='sign-in']"),
|
|
301
|
+
).toBeNull();
|
|
302
|
+
expect(
|
|
303
|
+
consent?.querySelector<HTMLButtonElement>("[data-testid='create'] svg"),
|
|
304
|
+
).toBeNull();
|
|
305
|
+
expect(consent?.textContent).not.toContain("agentChat.auth.logIn");
|
|
306
|
+
|
|
307
|
+
click(servicesToggle!);
|
|
308
|
+
expect(servicesToggle?.getAttribute("aria-expanded")).toBe("true");
|
|
309
|
+
click(consent?.querySelector("[data-testid='create']") as HTMLElement);
|
|
310
|
+
expect(onConnect).toHaveBeenCalledWith(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("reopens the same account choices when activation finds an existing account", () => {
|
|
314
|
+
const flow = {
|
|
315
|
+
connecting: false,
|
|
316
|
+
start: vi.fn(),
|
|
317
|
+
statusResolved: true,
|
|
318
|
+
agentNativeProvisioningEnabled: true,
|
|
319
|
+
accountExists: false,
|
|
320
|
+
};
|
|
321
|
+
const props = {
|
|
322
|
+
flow,
|
|
323
|
+
contentTestId: "consent",
|
|
324
|
+
primaryTestId: "create",
|
|
325
|
+
secondaryTestId: "sign-in",
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
render(React.createElement(BuilderConnectPopover, props, trigger()));
|
|
329
|
+
click(connectButton());
|
|
330
|
+
click(
|
|
331
|
+
document.querySelector(
|
|
332
|
+
"[data-testid='consent'] [data-testid='create']",
|
|
333
|
+
) as HTMLElement,
|
|
334
|
+
);
|
|
335
|
+
expect(flow.start).toHaveBeenCalledWith({ provisionAccount: true });
|
|
336
|
+
|
|
337
|
+
render(
|
|
338
|
+
React.createElement(
|
|
339
|
+
BuilderConnectPopover,
|
|
340
|
+
{ ...props, flow: { ...flow, accountExists: true } },
|
|
341
|
+
trigger(),
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const consent = document.querySelector("[data-testid='consent']");
|
|
346
|
+
expect(consent?.querySelector("p[role='status']")?.textContent).toBe(
|
|
347
|
+
"You already have a Builder.io account",
|
|
348
|
+
);
|
|
349
|
+
expect(consent?.querySelector("[data-testid='create']")).not.toBeNull();
|
|
350
|
+
expect(consent?.querySelector("[data-testid='sign-in']")).toBeNull();
|
|
351
|
+
|
|
352
|
+
click(consent?.querySelector("[data-testid='create']") as HTMLElement);
|
|
353
|
+
expect(flow.start).toHaveBeenLastCalledWith({ provisionAccount: false });
|
|
354
|
+
});
|
|
355
|
+
|
|
185
356
|
it("stacks the create and sign-in buttons together with the terms below them", () => {
|
|
186
357
|
const flow = {
|
|
187
358
|
connecting: false,
|
|
@@ -225,9 +396,7 @@ describe("BuilderConnectPopover before the status read resolves", () => {
|
|
|
225
396
|
p.querySelector("a"),
|
|
226
397
|
);
|
|
227
398
|
expect(create && signIn && terms).toBeTruthy();
|
|
228
|
-
expect(consent?.textContent).toContain(
|
|
229
|
-
"Included free with a Builder.io account",
|
|
230
|
-
);
|
|
399
|
+
expect(consent?.textContent).toContain("Included free");
|
|
231
400
|
expect(consent?.textContent).toContain("60 monthly Agent Credits");
|
|
232
401
|
expect(signIn?.parentElement).toBe(create?.parentElement);
|
|
233
402
|
expect(create!.compareDocumentPosition(signIn!)).toBe(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { useOnboarding } from "@agent-native/core/client/onboarding/use-onboarding";
|
|
2
3
|
import { Button } from "@agent-native/toolkit/ui/button";
|
|
3
4
|
import {
|
|
4
5
|
Popover,
|
|
@@ -6,9 +7,12 @@ import {
|
|
|
6
7
|
PopoverTrigger,
|
|
7
8
|
} from "@agent-native/toolkit/ui/popover";
|
|
8
9
|
import { Spinner } from "@agent-native/toolkit/ui/spinner";
|
|
9
|
-
import { IconArrowRight } from "@tabler/icons-react";
|
|
10
10
|
import React, { useEffect, useRef, useState } from "react";
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
BuilderIncludedBenefitsDisclosure,
|
|
14
|
+
getBuilderIncludedCapabilities,
|
|
15
|
+
} from "../onboarding/BuilderIncludedServices.js";
|
|
12
16
|
import type { BuilderConnectFlow } from "./useBuilderStatus.js";
|
|
13
17
|
|
|
14
18
|
type BuilderConnectTrigger = React.ReactElement<{
|
|
@@ -19,13 +23,12 @@ type BuilderConnectTrigger = React.ReactElement<{
|
|
|
19
23
|
|
|
20
24
|
export interface BuilderConnectPopoverProps {
|
|
21
25
|
flow: Pick<BuilderConnectFlow, "connecting" | "start"> & {
|
|
26
|
+
accountExists?: boolean;
|
|
22
27
|
cancel?: BuilderConnectFlow["cancel"];
|
|
23
28
|
agentNativeProvisioningEnabled?: boolean;
|
|
24
|
-
accountExists?: boolean;
|
|
25
29
|
retry?: () => boolean | void;
|
|
26
30
|
statusResolved?: boolean;
|
|
27
31
|
statusReadSettledCount?: number;
|
|
28
|
-
canConnect?: BuilderConnectFlow["canConnect"];
|
|
29
32
|
provisionAccount?: boolean;
|
|
30
33
|
};
|
|
31
34
|
children: BuilderConnectTrigger;
|
|
@@ -49,30 +52,31 @@ export function BuilderConnectPopover({
|
|
|
49
52
|
}: BuilderConnectPopoverProps) {
|
|
50
53
|
const t = useT();
|
|
51
54
|
const [open, setOpen] = useState(false);
|
|
55
|
+
const provisioningAttemptRef = useRef(false);
|
|
52
56
|
const capabilityResolved = flow.statusResolved === true;
|
|
53
57
|
const showPopover =
|
|
54
58
|
(defaultProvisionAccount && !capabilityResolved) ||
|
|
55
59
|
(capabilityResolved && flow.agentNativeProvisioningEnabled === true);
|
|
56
|
-
const accountExists = capabilityResolved && flow.accountExists;
|
|
57
|
-
const initiatedByThisTriggerRef = useRef(false);
|
|
58
60
|
const [queuedClick, setQueuedClick] = useState<{ settledAt: number } | null>(
|
|
59
61
|
null,
|
|
60
62
|
);
|
|
61
63
|
const settledCount = flow.statusReadSettledCount ?? 0;
|
|
62
64
|
|
|
63
65
|
useEffect(() => {
|
|
64
|
-
if (accountExists &&
|
|
65
|
-
|
|
66
|
+
if (flow.accountExists && provisioningAttemptRef.current) {
|
|
67
|
+
provisioningAttemptRef.current = false;
|
|
66
68
|
setOpen(true);
|
|
69
|
+
} else if (!flow.connecting) {
|
|
70
|
+
provisioningAttemptRef.current = false;
|
|
67
71
|
}
|
|
68
|
-
}, [accountExists]);
|
|
72
|
+
}, [flow.accountExists, flow.connecting]);
|
|
69
73
|
|
|
70
74
|
const start = (provisionAccount?: boolean) => {
|
|
71
75
|
const shouldProvision =
|
|
72
76
|
provisionAccount ??
|
|
73
77
|
(flow.agentNativeProvisioningEnabled === true &&
|
|
74
78
|
(defaultProvisionAccount || flow.provisionAccount === true));
|
|
75
|
-
|
|
79
|
+
if (shouldProvision) provisioningAttemptRef.current = true;
|
|
76
80
|
setOpen(false);
|
|
77
81
|
if (onConnect) {
|
|
78
82
|
onConnect(shouldProvision);
|
|
@@ -139,11 +143,11 @@ export function BuilderConnectPopover({
|
|
|
139
143
|
if (!showPopover) {
|
|
140
144
|
return cancelAction ? (
|
|
141
145
|
<span className="inline-flex max-w-full items-center gap-2">
|
|
142
|
-
{trigger}
|
|
146
|
+
<span className="contents">{trigger}</span>
|
|
143
147
|
{cancelAction}
|
|
144
148
|
</span>
|
|
145
149
|
) : (
|
|
146
|
-
trigger
|
|
150
|
+
<span className="contents">{trigger}</span>
|
|
147
151
|
);
|
|
148
152
|
}
|
|
149
153
|
|
|
@@ -151,60 +155,50 @@ export function BuilderConnectPopover({
|
|
|
151
155
|
<Popover open={open} onOpenChange={setOpen}>
|
|
152
156
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
153
157
|
<PopoverContent
|
|
154
|
-
align="
|
|
155
|
-
side="
|
|
156
|
-
sideOffset={
|
|
158
|
+
align="center"
|
|
159
|
+
side="right"
|
|
160
|
+
sideOffset={8}
|
|
157
161
|
aria-labelledby="builder-connect-popover-title"
|
|
158
162
|
data-testid={contentTestId}
|
|
159
|
-
className="z-[330] w-80 max-w-[calc(100vw-2rem)] p-3 text-left"
|
|
163
|
+
className="z-[330] max-h-[min(640px,calc(100dvh-2rem),var(--radix-popover-content-available-height))] w-80 max-w-[calc(100vw-2rem)] overflow-y-auto p-3 text-left"
|
|
160
164
|
>
|
|
161
165
|
<div className="space-y-2.5">
|
|
162
166
|
<h2
|
|
163
167
|
id="builder-connect-popover-title"
|
|
164
168
|
className="text-sm font-semibold text-foreground"
|
|
165
169
|
>
|
|
166
|
-
{
|
|
167
|
-
? t("agentChat.onboarding.builderAccountExistsTitle")
|
|
168
|
-
: t("agentChat.onboarding.builderActivateTitle")}
|
|
170
|
+
{t("agentChat.onboarding.builderActivateTitle")}
|
|
169
171
|
</h2>
|
|
170
|
-
<p
|
|
171
|
-
{accountExists
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
<p
|
|
173
|
+
role={flow.accountExists ? "status" : undefined}
|
|
174
|
+
className="text-xs leading-5 text-muted-foreground"
|
|
175
|
+
>
|
|
176
|
+
{flow.accountExists
|
|
177
|
+
? t("agentChat.onboarding.builderAccountExistsTitle", {
|
|
178
|
+
defaultValue: "You already have a Builder.io account",
|
|
179
|
+
})
|
|
180
|
+
: t("agentChat.onboarding.builderActivationDescription", {
|
|
181
|
+
defaultValue:
|
|
182
|
+
"Create or connect a Builder.io account in one click to get free credits.",
|
|
183
|
+
})}
|
|
176
184
|
</p>
|
|
177
|
-
<
|
|
178
|
-
<p className="text-[13px] font-semibold text-foreground">
|
|
179
|
-
{t("agentChat.onboarding.builderIncludedFreeWithAccount", {
|
|
180
|
-
defaultValue: "Included free with a Builder.io account",
|
|
181
|
-
})}
|
|
182
|
-
</p>
|
|
183
|
-
<p className="text-xs font-medium text-emerald-700 dark:text-emerald-400">
|
|
184
|
-
{t("agentChat.onboarding.builderMonthlyCredits", {
|
|
185
|
-
defaultValue: "60 monthly Agent Credits",
|
|
186
|
-
})}
|
|
187
|
-
</p>
|
|
188
|
-
</div>
|
|
185
|
+
<BuilderConnectIncludedServices />
|
|
189
186
|
<div className="flex flex-col gap-2">
|
|
190
187
|
<Button
|
|
191
188
|
type="button"
|
|
192
189
|
data-testid={primaryTestId}
|
|
193
190
|
className="w-full"
|
|
194
|
-
onClick={() => start(accountExists
|
|
191
|
+
onClick={() => start(!flow.accountExists)}
|
|
195
192
|
disabled={flow.connecting}
|
|
196
193
|
>
|
|
197
194
|
{flow.connecting ? <Spinner aria-hidden /> : null}
|
|
198
|
-
{accountExists
|
|
199
|
-
? t("agentChat.
|
|
195
|
+
{flow.accountExists
|
|
196
|
+
? t("agentChat.onboarding.builderSignInWithAccount")
|
|
200
197
|
: flow.connecting
|
|
201
198
|
? t("agentChat.onboarding.builderActivating")
|
|
202
199
|
: t("agentChat.onboarding.builderCreateAndActivate")}
|
|
203
|
-
{!accountExists && !flow.connecting ? (
|
|
204
|
-
<IconArrowRight aria-hidden />
|
|
205
|
-
) : null}
|
|
206
200
|
</Button>
|
|
207
|
-
{!accountExists
|
|
201
|
+
{!flow.accountExists ? (
|
|
208
202
|
<Button
|
|
209
203
|
type="button"
|
|
210
204
|
variant="secondary"
|
|
@@ -215,31 +209,29 @@ export function BuilderConnectPopover({
|
|
|
215
209
|
>
|
|
216
210
|
{t("agentChat.onboarding.builderExistingAccount")}
|
|
217
211
|
</Button>
|
|
218
|
-
)}
|
|
212
|
+
) : null}
|
|
219
213
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
</p>
|
|
242
|
-
)}
|
|
214
|
+
<p className="text-[11px] leading-4 text-muted-foreground">
|
|
215
|
+
{t("agentChat.onboarding.builderConsentPrefix")}{" "}
|
|
216
|
+
<a
|
|
217
|
+
href="https://www.builder.io/legal/terms"
|
|
218
|
+
target="_blank"
|
|
219
|
+
rel="noreferrer"
|
|
220
|
+
className="underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
221
|
+
>
|
|
222
|
+
{t("agentChat.onboarding.builderTerms")}
|
|
223
|
+
</a>{" "}
|
|
224
|
+
{t("agentChat.onboarding.builderConsentAnd")}{" "}
|
|
225
|
+
<a
|
|
226
|
+
href="https://www.builder.io/legal/privacy"
|
|
227
|
+
target="_blank"
|
|
228
|
+
rel="noreferrer"
|
|
229
|
+
className="underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
230
|
+
>
|
|
231
|
+
{t("agentChat.onboarding.builderPrivacy")}
|
|
232
|
+
</a>
|
|
233
|
+
.
|
|
234
|
+
</p>
|
|
243
235
|
</div>
|
|
244
236
|
</PopoverContent>
|
|
245
237
|
</Popover>
|
|
@@ -254,3 +246,31 @@ export function BuilderConnectPopover({
|
|
|
254
246
|
popover
|
|
255
247
|
);
|
|
256
248
|
}
|
|
249
|
+
|
|
250
|
+
function BuilderConnectIncludedServices() {
|
|
251
|
+
const t = useT();
|
|
252
|
+
const { profile, loading, error } = useOnboarding();
|
|
253
|
+
const capabilities = profile
|
|
254
|
+
? getBuilderIncludedCapabilities(profile.capabilities)
|
|
255
|
+
: [];
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<BuilderIncludedBenefitsDisclosure
|
|
259
|
+
capabilities={capabilities}
|
|
260
|
+
includedLabel={t("agentChat.onboarding.builderIncludedFree", {
|
|
261
|
+
defaultValue: "Included free",
|
|
262
|
+
})}
|
|
263
|
+
creditsLabel={t("agentChat.onboarding.builderMonthlyCredits", {
|
|
264
|
+
defaultValue: "60 monthly Agent Credits",
|
|
265
|
+
})}
|
|
266
|
+
loading={loading}
|
|
267
|
+
loadingLabel={t("agentChat.common.loading")}
|
|
268
|
+
error={
|
|
269
|
+
error || (!loading && !profile)
|
|
270
|
+
? t("agentChat.common.chunkLoadFailed")
|
|
271
|
+
: null
|
|
272
|
+
}
|
|
273
|
+
testId="builder-included-services"
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
}
|