@devicai/ui 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/dist/cjs/api/client.js +26 -2
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/api/types.js.map +1 -1
- package/dist/cjs/components/AICommandBar/AICommandBar.js +2 -1
- package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
- package/dist/cjs/components/AICommandBar/useAICommandBar.js +3 -2
- package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js +2 -1
- package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +3 -2
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js +2 -1
- package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +3 -2
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +3 -2
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +4 -4
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +10 -4
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js +60 -0
- package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -0
- package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js +61 -7
- package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
- package/dist/cjs/hooks/useDevicChat.js +14 -5
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/hooks/usePolling.js +50 -1
- package/dist/cjs/hooks/usePolling.js.map +1 -1
- package/dist/cjs/index.js +3 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/provider/DevicProvider.js +3 -1
- package/dist/cjs/provider/DevicProvider.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/esm/api/client.d.ts +31 -3
- package/dist/esm/api/client.js +26 -2
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/api/types.d.ts +57 -0
- package/dist/esm/api/types.js.map +1 -1
- package/dist/esm/components/AICommandBar/AICommandBar.js +2 -1
- package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
- package/dist/esm/components/AICommandBar/AICommandBar.types.d.ts +7 -0
- package/dist/esm/components/AICommandBar/useAICommandBar.d.ts +2 -0
- package/dist/esm/components/AICommandBar/useAICommandBar.js +4 -3
- package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/esm/components/AIElementWrapper/AIElementWrapper.js +2 -1
- package/dist/esm/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
- package/dist/esm/components/AIElementWrapper/AIElementWrapper.types.d.ts +7 -0
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.d.ts +2 -0
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +4 -3
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/esm/components/AIGenerationButton/AIGenerationButton.js +2 -1
- package/dist/esm/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
- package/dist/esm/components/AIGenerationButton/AIGenerationButton.types.d.ts +7 -0
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.d.ts +2 -0
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +4 -3
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +3 -2
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +15 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +1 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.js +4 -4
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.d.ts +7 -1
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +10 -4
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.d.ts +29 -0
- package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js +58 -0
- package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -0
- package/dist/esm/components/IntegrationsModal/IntegrationsModal.js +61 -7
- package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
- package/dist/esm/hooks/index.d.ts +1 -1
- package/dist/esm/hooks/useDevicChat.d.ts +3 -1
- package/dist/esm/hooks/useDevicChat.js +15 -6
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/hooks/usePolling.d.ts +17 -0
- package/dist/esm/hooks/usePolling.js +48 -2
- package/dist/esm/hooks/usePolling.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/provider/DevicProvider.d.ts +1 -1
- package/dist/esm/provider/DevicProvider.js +3 -1
- package/dist/esm/provider/DevicProvider.js.map +1 -1
- package/dist/esm/provider/types.d.ts +22 -0
- package/dist/esm/styles.css +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var React = require('react');
|
|
5
5
|
var reactDom = require('react-dom');
|
|
6
6
|
var theme = require('../theme.js');
|
|
7
|
+
var ConnectFieldsForm = require('./ConnectFieldsForm.js');
|
|
7
8
|
var IntegrationLogo = require('./IntegrationLogo.js');
|
|
8
9
|
var useIntegrations = require('./useIntegrations.js');
|
|
9
10
|
|
|
@@ -80,6 +81,10 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
|
|
|
80
81
|
const [busyApp, setBusyApp] = React.useState(null);
|
|
81
82
|
/** Authorization URL surfaced as a link when the popup was blocked. */
|
|
82
83
|
const [blockedUrl, setBlockedUrl] = React.useState(null);
|
|
84
|
+
/** The app whose credentials are being asked for, with what the server asked
|
|
85
|
+
* and every way it can be connected. Rendered inside that app's own card,
|
|
86
|
+
* where the user pressed Connect. */
|
|
87
|
+
const [setupPrompt, setSetupPrompt] = React.useState(null);
|
|
83
88
|
const [query, setQuery] = React.useState("");
|
|
84
89
|
const visible = React.useMemo(() => integrations.filter((i) => matches(i, query)), [integrations, query]);
|
|
85
90
|
// Report the listing without making the caller's identity part of the
|
|
@@ -98,6 +103,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
|
|
|
98
103
|
if (isOpen && !wasOpenRef.current) {
|
|
99
104
|
setBlockedUrl(null);
|
|
100
105
|
setActionError(null);
|
|
106
|
+
setSetupPrompt(null);
|
|
101
107
|
setQuery("");
|
|
102
108
|
// The very first open of an uncontrolled modal is already covered by the
|
|
103
109
|
// hook switching on; asking again here would double every first open.
|
|
@@ -161,19 +167,37 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
|
|
|
161
167
|
if (pollRef.current !== null)
|
|
162
168
|
window.clearInterval(pollRef.current);
|
|
163
169
|
}, []);
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Connects an app, asking for credentials only if it needs them.
|
|
172
|
+
*
|
|
173
|
+
* Optimistic: the request goes out first, and the form appears only when the
|
|
174
|
+
* server answers that something is missing. Most apps take an API key rather
|
|
175
|
+
* than an OAuth round trip, so this is the path that used to fail outright.
|
|
176
|
+
*/
|
|
177
|
+
const handleConnect = async (integration, values) => {
|
|
178
|
+
if (!client || (busyApp && busyApp !== integration.app))
|
|
166
179
|
return;
|
|
167
180
|
setActionError(null);
|
|
168
181
|
setBlockedUrl(null);
|
|
169
182
|
setBusyApp(integration.app);
|
|
170
183
|
const nonce = newNonce();
|
|
171
184
|
const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;
|
|
172
|
-
// Opened empty inside the click, navigated once the URL is known.
|
|
185
|
+
// Opened empty inside the click, navigated once the URL is known. Both
|
|
186
|
+
// entry points are gestures — the card's button and the form's submit —
|
|
187
|
+
// so this holds on the retry too.
|
|
173
188
|
const popup = window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
|
|
174
189
|
try {
|
|
175
|
-
const { authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo });
|
|
190
|
+
const { connected, authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo, ...values });
|
|
191
|
+
// Nothing to authorise: the key the user typed is the account. Close the
|
|
192
|
+
// window that was opened in case it was needed and re-read the listing.
|
|
193
|
+
if (connected || !authorizationUrl) {
|
|
194
|
+
popup?.close();
|
|
195
|
+
setSetupPrompt(null);
|
|
196
|
+
finishConnect();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
176
199
|
pendingRef.current = { app: integration.app, returnTo };
|
|
200
|
+
setSetupPrompt(null);
|
|
177
201
|
if (popup && !popup.closed) {
|
|
178
202
|
popupRef.current = popup;
|
|
179
203
|
popup.location.href = authorizationUrl;
|
|
@@ -195,8 +219,38 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
|
|
|
195
219
|
catch (err) {
|
|
196
220
|
popup?.close();
|
|
197
221
|
pendingRef.current = null;
|
|
198
|
-
setActionError(err instanceof Error ? err.message : String(err));
|
|
199
222
|
setBusyApp(null);
|
|
223
|
+
const setup = err?.setupRequired;
|
|
224
|
+
if (setup) {
|
|
225
|
+
await openSetupForm(integration, setup);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Opens the credentials form for an app that needs one.
|
|
233
|
+
*
|
|
234
|
+
* `stage: "app"` is not the end user's to fix — it means the developer who
|
|
235
|
+
* embedded this widget has not registered an application with the provider
|
|
236
|
+
* yet. Asking a stranger for someone else's client secret would be both
|
|
237
|
+
* useless and a good way to teach them to hand credentials to a form, so it
|
|
238
|
+
* is reported as unavailable instead.
|
|
239
|
+
*/
|
|
240
|
+
const openSetupForm = async (integration, setup) => {
|
|
241
|
+
if (setup.stage === "app") {
|
|
242
|
+
setActionError(`${integration.name} is not available yet — it still needs to be set up ` +
|
|
243
|
+
`by the app's provider.`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const { schemes } = await client.getIntegrationAuth(integration.app, scope);
|
|
248
|
+
if (!schemes.length)
|
|
249
|
+
throw new Error(setup.message);
|
|
250
|
+
setSetupPrompt({ app: integration.app, setup, schemes });
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
200
254
|
}
|
|
201
255
|
};
|
|
202
256
|
const handleDisconnect = async (app, account) => {
|
|
@@ -227,7 +281,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
|
|
|
227
281
|
}, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : visible.length === 0 ? (jsxRuntime.jsxs("div", { className: "devic-int-empty", children: ["No apps match \u201C", query.trim(), "\u201D."] })) : (jsxRuntime.jsx("div", { className: "devic-int-grid", children: visible.map((integration) => {
|
|
228
282
|
const cardState = stateOf(integration);
|
|
229
283
|
const busy = busyApp === integration.app;
|
|
230
|
-
return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("span", { className: "devic-int-state", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsxRuntime.jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
|
|
284
|
+
return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("span", { className: "devic-int-state", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsxRuntime.jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), setupPrompt?.app === integration.app ? (jsxRuntime.jsx(ConnectFieldsForm.ConnectFieldsForm, { integration: integration, schemes: setupPrompt.schemes, initialScheme: setupPrompt.setup.authScheme, onlyFields: setupPrompt.setup.fields, submitting: busy, onCancel: () => setSetupPrompt(null), onSubmit: (values) => handleConnect(integration, values) })) : (jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
|
|
231
285
|
? ""
|
|
232
286
|
: " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, title: cardState.key === "connected"
|
|
233
287
|
? "Sign in with a different account. The one connected now is replaced."
|
|
@@ -240,7 +294,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
|
|
|
240
294
|
: // Not "Add account": one account per app is all
|
|
241
295
|
// the assistant can use, and connecting again
|
|
242
296
|
// retires the previous one.
|
|
243
|
-
"Switch account" }), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-unlink", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, title: "Disconnect this account", "aria-label": `Disconnect ${integration.name}`, children: "\u00D7" })] }, account.id))) }))] }, integration.app));
|
|
297
|
+
"Switch account" })), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-unlink", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, title: "Disconnect this account", "aria-label": `Disconnect ${integration.name}`, children: "\u00D7" })] }, account.id))) }))] }, integration.app));
|
|
244
298
|
}) }))] }), jsxRuntime.jsxs("div", { className: "devic-int-footer", children: [jsxRuntime.jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
|
|
245
299
|
}
|
|
246
300
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntegrationsModal.js","sources":["../../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { Integration, IntegrationAccount } from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport { useIntegrations, type IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Search field placeholder. @default \"Search connected apps\" */\n searchPlaceholder?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n /**\n * Colours and font. Same names as the drawer's style options, and the drawer\n * passes its own down — a dialog opening in the default light palette over a\n * themed application is the one thing this must not do.\n */\n theme?: DevicTheme;\n /**\n * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to\n * load it to decide whether its button exists, and passing it down is what\n * keeps opening the modal from asking for the very same thing again.\n */\n state?: IntegrationsState;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\nfunction SearchIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\nfunction matches(integration: Integration, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n integration.name.toLowerCase().includes(q) ||\n integration.app.toLowerCase().includes(q) ||\n (integration.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n searchPlaceholder = \"Search connected apps\",\n onChange,\n theme,\n state,\n}: IntegrationsModalProps): JSX.Element | null {\n // Hooks cannot be skipped, so the fallback is always built and only fetches\n // when nobody handed a listing down.\n const own = useIntegrations({\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled: isOpen && !state,\n });\n const { integrations, loading, error: loadError, refresh, client, scope } =\n state ?? own;\n\n /** Errors from connecting or disconnecting, kept apart from load failures. */\n const [actionError, setActionError] = useState<string | null>(null);\n const error = actionError ?? loadError;\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Report the listing without making the caller's identity part of the\n // dependency: an inline arrow would fire this on every render.\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n useEffect(() => {\n if (integrations.length) onChangeRef.current?.(integrations);\n }, [integrations]);\n\n // Reopening starts clean, and re-reads: accounts may have been connected or\n // revoked elsewhere since the last look.\n const wasOpenRef = useRef(false);\n const openedBeforeRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n setActionError(null);\n setQuery(\"\");\n // The very first open of an uncontrolled modal is already covered by the\n // hook switching on; asking again here would double every first open.\n if (state || openedBeforeRef.current) void refresh();\n openedBeforeRef.current = true;\n }\n wasOpenRef.current = isOpen;\n // `state.refresh` is stable per scope; re-running on every render of the\n // owner is exactly what this must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const handleConnect = async (integration: Integration) => {\n if (!client || busyApp) return;\n setActionError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo }\n );\n pendingRef.current = { app: integration.app, returnTo };\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setActionError(err instanceof Error ? err.message : String(err));\n setBusyApp(null);\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setActionError(null);\n try {\n await client.disconnectIntegration(account.id, scope);\n await refresh(true);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n // The variables go on the overlay, not on the modal: the backdrop is part\n // of the dialog, and a portal inherits nothing from the drawer that opened\n // it.\n <div\n className=\"devic-int-overlay\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onClose}\n >\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-search\">\n <SearchIcon />\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n autoComplete=\"off\"\n />\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">No apps available here yet.</div>\n ) : visible.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps match “{query.trim()}”.\n </div>\n ) : (\n <div className=\"devic-int-grid\">\n {visible.map((integration) => {\n const cardState = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n <IntegrationLogo integration={integration} />\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={integration.name}>\n {integration.name}\n </div>\n\n {integration.description && (\n <div\n className=\"devic-int-description\"\n title={integration.description}\n >\n {integration.description}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\"\n ? \"\"\n : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch account\"}\n </button>\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-unlink\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n title=\"Disconnect this account\"\n aria-label={`Disconnect ${integration.name}`}\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx","theme","useIntegrations","useState","useMemo","useRef","useEffect","useCallback","createPortal","themeVars","isDarkTheme","IntegrationLogo"],"mappings":";;;;;;;;;AA0DA,SAAS,QAAQ,GAAA;AACf,IAAA,QACEA,eAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EAAA,aAAA,EACV,MAAM,EAAA,QAAA,EAAA,CAElBC,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,WAAW,EAAA,CAAG,EACtBA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,QAAQ,EAAA,CAAG,EACnBA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,SAAS,EAAA,CAAG,EACpBA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,2CAA2C,EAAA,CAAG,CAAA,EAAA,CAClD;AAEV;AAEA,SAAS,UAAU,GAAA;IACjB,QACED,eAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EAAA,aAAA,EACV,MAAM,EAAA,QAAA,EAAA,CAElBC,cAAA,CAAA,QAAA,EAAA,EAAQ,EAAE,EAAC,IAAI,EAAC,EAAE,EAAC,IAAI,EAAC,CAAC,EAAC,GAAG,EAAA,CAAG,EAChCA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,gBAAgB,EAAA,CAAG,CAAA,EAAA,CACvB;AAEV;AAEA;AACA,SAAS,QAAQ,GAAA;AACf,IAAA,MAAM,CAAC,GAAG,OAAO,MAAM,KAAK,WAAW,GAAG,MAAM,GAAG,SAAS;IAC5D,IAAI,CAAC,EAAE,UAAU;AAAE,QAAA,OAAO,CAAC,CAAC,UAAU,EAAE;IACxC,OAAO,CAAA,EAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA,CAAA,EAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA,CAAE;AAC5E;AAEA;AACA,SAAS,OAAO,CAAC,WAAwB,EAAA;IAIvC,IAAI,WAAW,CAAC,SAAS;QAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE;AAC1E,IAAA,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,EAAE;QACtD,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAC1D;IACA,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,EAAE;AACxD;AAEA,SAAS,YAAY,CAAC,OAA2B,EAAA;IAC/C,IAAI,CAAC,OAAO,CAAC,WAAW;AAAE,QAAA,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;IAC7D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;IAC1C,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;AAAE,QAAA,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;AACrE,IAAA,OAAO,aAAa,IAAI,CAAC,kBAAkB,EAAE,EAAE;AACjD;AAEA,SAAS,OAAO,CAAC,WAAwB,EAAE,KAAa,EAAA;IACtD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;AACpC,IAAA,IAAI,CAAC,CAAC;AAAE,QAAA,OAAO,IAAI;IACnB,QACE,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC1C,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACzC,QAAA,CAAC,WAAW,CAAC,WAAW,IAAI,EAAE,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE7D;AAEA;;;;;;;;;;;;;;;AAeG;AACG,SAAU,iBAAiB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,WAAW,EACX,QAAQ,EACR,WAAW,EACX,MAAM,EACN,OAAO,EACP,KAAK,GAAG,gBAAgB,EACxB,iBAAiB,GAAG,uBAAuB,EAC3C,QAAQ,SACRC,OAAK,EACL,KAAK,GACkB,EAAA;;;IAGvB,MAAM,GAAG,GAAGC,+BAAe,CAAC;QAC1B,WAAW;QACX,QAAQ;QACR,WAAW;QACX,MAAM;QACN,OAAO;AACP,QAAA,OAAO,EAAE,MAAM,IAAI,CAAC,KAAK;AAC1B,KAAA,CAAC;AACF,IAAA,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GACvE,KAAK,IAAI,GAAG;;IAGd,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAGC,cAAQ,CAAgB,IAAI,CAAC;AACnE,IAAA,MAAM,KAAK,GAAG,WAAW,IAAI,SAAS;;IAEtC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAGA,cAAQ,CAAgB,IAAI,CAAC;;IAE3D,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAGA,cAAQ,CAC1C,IAAI,CACL;IACD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAGA,cAAQ,CAAC,EAAE,CAAC;AAEtC,IAAA,MAAM,OAAO,GAAGC,aAAO,CACrB,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EACnD,CAAC,YAAY,EAAE,KAAK,CAAC,CACtB;;;AAID,IAAA,MAAM,WAAW,GAAGC,YAAM,CAAC,QAAQ,CAAC;AACpC,IAAA,WAAW,CAAC,OAAO,GAAG,QAAQ;IAC9BC,eAAS,CAAC,MAAK;QACb,IAAI,YAAY,CAAC,MAAM;AAAE,YAAA,WAAW,CAAC,OAAO,GAAG,YAAY,CAAC;AAC9D,IAAA,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;;;AAIlB,IAAA,MAAM,UAAU,GAAGD,YAAM,CAAC,KAAK,CAAC;AAChC,IAAA,MAAM,eAAe,GAAGA,YAAM,CAAC,KAAK,CAAC;IACrCC,eAAS,CAAC,MAAK;AACb,QAAA,IAAI,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;YACjC,aAAa,CAAC,IAAI,CAAC;YACnB,cAAc,CAAC,IAAI,CAAC;YACpB,QAAQ,CAAC,EAAE,CAAC;;;AAGZ,YAAA,IAAI,KAAK,IAAI,eAAe,CAAC,OAAO;gBAAE,KAAK,OAAO,EAAE;AACpD,YAAA,eAAe,CAAC,OAAO,GAAG,IAAI;QAChC;AACA,QAAA,UAAU,CAAC,OAAO,GAAG,MAAM;;;;AAI7B,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;;IAGZA,eAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,MAAM;YAAE;AACb,QAAA,MAAM,KAAK,GAAG,CAAC,CAAgB,KAAI;AACjC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;AAAE,gBAAA,OAAO,EAAE;AACnC,QAAA,CAAC;AACD,QAAA,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC;QAC3C,OAAO,MAAM,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,CAAC;AAC7D,IAAA,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;;AAGrB,IAAA,MAAM,UAAU,GAAGD,YAAM,CAA2C,IAAI,CAAC;AACzE,IAAA,MAAM,QAAQ,GAAGA,YAAM,CAAgB,IAAI,CAAC;AAC5C,IAAA,MAAM,OAAO,GAAGA,YAAM,CAAgB,IAAI,CAAC;AAE3C,IAAA,MAAM,aAAa,GAAGE,iBAAW,CAAC,MAAK;AACrC,QAAA,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE;AAC5B,YAAA,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC;AACrC,YAAA,OAAO,CAAC,OAAO,GAAG,IAAI;QACxB;AACA,QAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,QAAA,QAAQ,CAAC,OAAO,GAAG,IAAI;QACvB,UAAU,CAAC,IAAI,CAAC;AAChB,QAAA,KAAK,OAAO,CAAC,IAAI,CAAC;AACpB,IAAA,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;;;;IAKbD,eAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,MAAM;YAAE;AACb,QAAA,MAAM,SAAS,GAAG,CAAC,KAAmB,KAAI;AACxC,YAAA,MAAM,IAAI,GAAG,KAAK,CAAC,IAAmC;AACtD,YAAA,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO;gBAAE;AACtC,YAAA,IAAI,IAAI,CAAC,IAAI,KAAK,uBAAuB;gBAAE;AAC3C,YAAA,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO;YAClC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ;gBAAE;AACpD,YAAA,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE;AACzB,YAAA,aAAa,EAAE;AACjB,QAAA,CAAC;AACD,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC;QAC7C,OAAO,MAAM,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC;AAC/D,IAAA,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;;AAG3B,IAAAA,eAAS,CACP,MAAM,MAAK;AACT,QAAA,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;AAAE,YAAA,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC;IACrE,CAAC,EACD,EAAE,CACH;AAED,IAAA,MAAM,aAAa,GAAG,OAAO,WAAwB,KAAI;QACvD,IAAI,CAAC,MAAM,IAAI,OAAO;YAAE;QACxB,cAAc,CAAC,IAAI,CAAC;QACpB,aAAa,CAAC,IAAI,CAAC;AACnB,QAAA,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC;AAE3B,QAAA,MAAM,KAAK,GAAG,QAAQ,EAAE;QACxB,MAAM,QAAQ,GAAG,CAAA,EAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAA,cAAA,EAAiB,KAAK,CAAA,CAAE;;AAElE,QAAA,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CACvB,EAAE,EACF,aAAa,EACb,4CAA4C,CAC7C;AAED,QAAA,IAAI;YACF,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAC1D,WAAW,CAAC,GAAG,EACf,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,CACvB;AACD,YAAA,UAAU,CAAC,OAAO,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE;AACvD,YAAA,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;AAC1B,gBAAA,QAAQ,CAAC,OAAO,GAAG,KAAK;AACxB,gBAAA,KAAK,CAAC,QAAQ,CAAC,IAAI,GAAG,gBAAgB;;;;;gBAKtC,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,MAAK;oBACxC,IAAI,KAAK,CAAC,MAAM;AAAE,wBAAA,aAAa,EAAE;gBACnC,CAAC,EAAE,GAAG,CAAC;YACT;iBAAO;;AAEL,gBAAA,aAAa,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC;gBAC9D,UAAU,CAAC,IAAI,CAAC;YAClB;QACF;QAAE,OAAO,GAAG,EAAE;YACZ,KAAK,EAAE,KAAK,EAAE;AACd,YAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,YAAA,cAAc,CAAC,GAAG,YAAY,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,UAAU,CAAC,IAAI,CAAC;QAClB;AACF,IAAA,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,GAAW,EAAE,OAA2B,KAAI;QAC1E,IAAI,CAAC,MAAM,IAAI,OAAO;YAAE;QACxB,UAAU,CAAC,GAAG,CAAC;QACf,cAAc,CAAC,IAAI,CAAC;AACpB,QAAA,IAAI;YACF,MAAM,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC;AACrD,YAAA,MAAM,OAAO,CAAC,IAAI,CAAC;QACrB;QAAE,OAAO,GAAG,EAAE;AACZ,YAAA,cAAc,CAAC,GAAG,YAAY,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAClE;gBAAU;YACR,UAAU,CAAC,IAAI,CAAC;QAClB;AACF,IAAA,CAAC;AAED,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,IAAI;AAExB,IAAA,OAAOE,qBAAY;;;;AAIjB,IAAAR,cAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAC,mBAAmB,EAC7B,KAAK,EAAES,eAAS,CAACR,OAAK,CAAC,eACZS,iBAAW,CAACT,OAAK,CAAC,EAC7B,OAAO,EAAE,OAAO,EAAA,QAAA,EAEhBF,eAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAC,iBAAiB,EAC3B,IAAI,EAAC,QAAQ,EAAA,YAAA,EACF,MAAM,EAAA,YAAA,EACL,KAAK,EACjB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,eAAe,EAAE,EAAA,QAAA,EAAA,CAEnCA,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAC/BA,eAAA,CAAA,IAAA,EAAA,EAAI,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,CAC7BC,cAAA,CAAC,QAAQ,EAAA,EAAA,CAAG,EACX,KAAK,CAAA,EAAA,CACH,EACLA,cAAA,CAAA,QAAA,EAAA,EACE,SAAS,EAAC,iBAAiB,EAC3B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAC,QAAQ,EAAA,YAAA,EACF,OAAO,EAAA,QAAA,EAAA,QAAA,EAAA,CAGX,CAAA,EAAA,CACL,EAEND,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAC/BC,cAAA,CAAC,UAAU,KAAG,EACdA,cAAA,CAAA,OAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAE,iBAAiB,EAAA,YAAA,EAClB,iBAAiB,EAC7B,YAAY,EAAC,KAAK,EAAA,CAClB,CAAA,EAAA,CACE,EAEND,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,gBAAgB,aAC5B,KAAK,IAAIC,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAE,KAAK,EAAA,CAAO,EAEvD,UAAU,KACTD,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAAA,kCAAA,EACE,GAAG,EACpCC,sBACE,IAAI,EAAE,UAAU,CAAC,GAAG,EACpB,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,OAAO,EAAE,MAAK;AACZ,wCAAA,UAAU,CAAC,OAAO,GAAG,IAAI;wCACzB,aAAa,CAAC,IAAI,CAAC;AACrB,oCAAA,CAAC,4CAGC,EAAC,GAAG,EAAA,wCAAA,CAAA,EAAA,CAEJ,CACP,EAEA,OAAO,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IACnCA,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,mBAAmB,mCAAoB,IACpD,YAAY,CAAC,MAAM,KAAK,CAAC,IAC3BA,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,6BAAA,EAAA,CAAkC,IAChE,OAAO,CAAC,MAAM,KAAK,CAAC,IACtBD,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,CAAA,sBAAA,EACd,KAAK,CAAC,IAAI,EAAE,EAAA,SAAA,CAAA,EAAA,CACxB,KAENC,wBAAK,SAAS,EAAC,gBAAgB,EAAA,QAAA,EAC5B,OAAO,CAAC,GAAG,CAAC,CAAC,WAAW,KAAI;AAC3B,gCAAA,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;AACtC,gCAAA,MAAM,IAAI,GAAG,OAAO,KAAK,WAAW,CAAC,GAAG;gCACxC,QACED,yBAEE,SAAS,EAAC,gBAAgB,EAAA,YAAA,EACd,SAAS,CAAC,GAAG,EAAA,QAAA,EAAA,CAEzBA,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,qBAAqB,EAAA,QAAA,EAAA,CAClCC,cAAA,CAACW,+BAAe,EAAA,EAAC,WAAW,EAAE,WAAW,EAAA,CAAI,EAC7CZ,eAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,CAC/BC,yBACE,SAAS,EAAC,eAAe,EAAA,SAAA,EAChB,SAAS,CAAC,GAAG,KAAK,WAAW,cAC5B,SAAS,CAAC,GAAG,KAAK,cAAc,EAAA,aAAA,EAC9B,MAAM,EAAA,CAClB,EACD,SAAS,CAAC,KAAK,CAAA,EAAA,CACX,IACH,EAENA,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,gBAAgB,EAAC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAA,QAAA,EACpD,WAAW,CAAC,IAAI,EAAA,CACb,EAEL,WAAW,CAAC,WAAW,KACtBA,cAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAC,uBAAuB,EACjC,KAAK,EAAE,WAAW,CAAC,WAAW,YAE7B,WAAW,CAAC,WAAW,EAAA,CACpB,CACP,EAEDA,2BACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,oCACT,SAAS,CAAC,GAAG,KAAK;AAChB,kDAAE;AACF,kDAAE,wBACN,CAAA,CAAE,EACF,OAAO,EAAE,MAAM,aAAa,CAAC,WAAW,CAAC,EACzC,QAAQ,EAAE,IAAI,IAAI,CAAC,CAAC,OAAO,EAC3B,KAAK,EACH,SAAS,CAAC,GAAG,KAAK;AAChB,kDAAE;kDACA,SAAS,EAAA,QAAA,EAGd;AACC,kDAAE;AACF,kDAAE,SAAS,CAAC,GAAG,KAAK;AAClB,sDAAE;AACF,sDAAE,SAAS,CAAC,GAAG,KAAK;AAClB,0DAAE;AACF;;;AAGE,4DAAA,gBAAgB,EAAA,CACjB,EAER,WAAW,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,KAC9BA,cAAA,CAAA,IAAA,EAAA,EAAI,SAAS,EAAC,oBAAoB,EAAA,QAAA,EAC/B,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,MAChCD,eAAA,CAAA,IAAA,EAAA,EAAqB,SAAS,EAAC,mBAAmB,EAAA,QAAA,EAAA,CAChDC,cAAA,CAAA,MAAA,EAAA,EACE,SAAS,EAAC,eAAe,EAAA,SAAA,EAChB,CAAC,OAAO,CAAC,cAAc,EAAA,aAAA,EACpB,MAAM,EAAA,CAClB,EACFD,eAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAC,yBAAyB,EAAA,QAAA,EAAA,CACtC,YAAY,CAAC,OAAO,CAAC,EACrB,OAAO,CAAC,cAAc,KACrBA,eAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAC,wBAAwB,EAAA,QAAA,EAAA,CACrC,GAAG,EAAA,2BAAA,CAAA,EAAA,CAEC,CACR,CAAA,EAAA,CACI,EACPC,cAAA,CAAA,QAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,kBAAkB,EAC5B,OAAO,EAAE,MACP,gBAAgB,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAE5C,QAAQ,EAAE,CAAC,CAAC,OAAO,EACnB,KAAK,EAAC,yBAAyB,EAAA,YAAA,EACnB,CAAA,WAAA,EAAc,WAAW,CAAC,IAAI,CAAA,CAAE,EAAA,QAAA,EAAA,QAAA,EAAA,CAGrC,CAAA,EAAA,EA1BF,OAAO,CAAC,EAAE,CA2Bd,CACN,CAAC,EAAA,CACC,CACN,CAAA,EAAA,EA1FI,WAAW,CAAC,GAAG,CA2FhB;4BAEV,CAAC,CAAC,GACE,CACP,CAAA,EAAA,CACG,EAEND,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAC/BC,cAAA,CAAA,MAAA,EAAA,EAAA,QAAA,EAAA,yDAAA,EAAA,CAAoE,EACpEA,cAAA,CAAA,QAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,mCAAmC,EAC7C,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EACjC,QAAQ,EAAE,OAAO,IAAI,CAAC,CAAC,OAAO,EAAA,QAAA,EAAA,SAAA,EAAA,CAGvB,IACL,CAAA,EAAA,CACF,EAAA,CACF,EACN,QAAQ,CAAC,IAAI,CACd;AACH;;;;"}
|
|
1
|
+
{"version":3,"file":"IntegrationsModal.js","sources":["../../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { DevicApiError } from \"../../api/client\";\nimport type {\n Integration,\n IntegrationAccount,\n IntegrationAuthScheme,\n IntegrationSetupRequired,\n} from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { ConnectFieldsForm } from \"./ConnectFieldsForm\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport { useIntegrations, type IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Search field placeholder. @default \"Search connected apps\" */\n searchPlaceholder?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n /**\n * Colours and font. Same names as the drawer's style options, and the drawer\n * passes its own down — a dialog opening in the default light palette over a\n * themed application is the one thing this must not do.\n */\n theme?: DevicTheme;\n /**\n * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to\n * load it to decide whether its button exists, and passing it down is what\n * keeps opening the modal from asking for the very same thing again.\n */\n state?: IntegrationsState;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\nfunction SearchIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\nfunction matches(integration: Integration, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n integration.name.toLowerCase().includes(q) ||\n integration.app.toLowerCase().includes(q) ||\n (integration.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n searchPlaceholder = \"Search connected apps\",\n onChange,\n theme,\n state,\n}: IntegrationsModalProps): JSX.Element | null {\n // Hooks cannot be skipped, so the fallback is always built and only fetches\n // when nobody handed a listing down.\n const own = useIntegrations({\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled: isOpen && !state,\n });\n const { integrations, loading, error: loadError, refresh, client, scope } =\n state ?? own;\n\n /** Errors from connecting or disconnecting, kept apart from load failures. */\n const [actionError, setActionError] = useState<string | null>(null);\n const error = actionError ?? loadError;\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n /** The app whose credentials are being asked for, with what the server asked\n * and every way it can be connected. Rendered inside that app's own card,\n * where the user pressed Connect. */\n const [setupPrompt, setSetupPrompt] = useState<{\n app: string;\n setup: IntegrationSetupRequired;\n schemes: IntegrationAuthScheme[];\n } | null>(null);\n const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Report the listing without making the caller's identity part of the\n // dependency: an inline arrow would fire this on every render.\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n useEffect(() => {\n if (integrations.length) onChangeRef.current?.(integrations);\n }, [integrations]);\n\n // Reopening starts clean, and re-reads: accounts may have been connected or\n // revoked elsewhere since the last look.\n const wasOpenRef = useRef(false);\n const openedBeforeRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n setActionError(null);\n setSetupPrompt(null);\n setQuery(\"\");\n // The very first open of an uncontrolled modal is already covered by the\n // hook switching on; asking again here would double every first open.\n if (state || openedBeforeRef.current) void refresh();\n openedBeforeRef.current = true;\n }\n wasOpenRef.current = isOpen;\n // `state.refresh` is stable per scope; re-running on every render of the\n // owner is exactly what this must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n /**\n * Connects an app, asking for credentials only if it needs them.\n *\n * Optimistic: the request goes out first, and the form appears only when the\n * server answers that something is missing. Most apps take an API key rather\n * than an OAuth round trip, so this is the path that used to fail outright.\n */\n const handleConnect = async (\n integration: Integration,\n values?: { authScheme: string; accountFields: Record<string, string> }\n ) => {\n if (!client || (busyApp && busyApp !== integration.app)) return;\n setActionError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known. Both\n // entry points are gestures — the card's button and the form's submit —\n // so this holds on the retry too.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { connected, authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo, ...values }\n );\n\n // Nothing to authorise: the key the user typed is the account. Close the\n // window that was opened in case it was needed and re-read the listing.\n if (connected || !authorizationUrl) {\n popup?.close();\n setSetupPrompt(null);\n finishConnect();\n return;\n }\n\n pendingRef.current = { app: integration.app, returnTo };\n setSetupPrompt(null);\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setBusyApp(null);\n const setup = (err as DevicApiError)?.setupRequired;\n if (setup) {\n await openSetupForm(integration, setup);\n return;\n }\n setActionError(err instanceof Error ? err.message : String(err));\n }\n };\n\n /**\n * Opens the credentials form for an app that needs one.\n *\n * `stage: \"app\"` is not the end user's to fix — it means the developer who\n * embedded this widget has not registered an application with the provider\n * yet. Asking a stranger for someone else's client secret would be both\n * useless and a good way to teach them to hand credentials to a form, so it\n * is reported as unavailable instead.\n */\n const openSetupForm = async (\n integration: Integration,\n setup: IntegrationSetupRequired\n ) => {\n if (setup.stage === \"app\") {\n setActionError(\n `${integration.name} is not available yet — it still needs to be set up ` +\n `by the app's provider.`\n );\n return;\n }\n try {\n const { schemes } = await client!.getIntegrationAuth(\n integration.app,\n scope\n );\n if (!schemes.length) throw new Error(setup.message);\n setSetupPrompt({ app: integration.app, setup, schemes });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setActionError(null);\n try {\n await client.disconnectIntegration(account.id, scope);\n await refresh(true);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n // The variables go on the overlay, not on the modal: the backdrop is part\n // of the dialog, and a portal inherits nothing from the drawer that opened\n // it.\n <div\n className=\"devic-int-overlay\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onClose}\n >\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-search\">\n <SearchIcon />\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n autoComplete=\"off\"\n />\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">No apps available here yet.</div>\n ) : visible.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps match “{query.trim()}”.\n </div>\n ) : (\n <div className=\"devic-int-grid\">\n {visible.map((integration) => {\n const cardState = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n <IntegrationLogo integration={integration} />\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={integration.name}>\n {integration.name}\n </div>\n\n {integration.description && (\n <div\n className=\"devic-int-description\"\n title={integration.description}\n >\n {integration.description}\n </div>\n )}\n\n {/* The form replaces this app's button rather than opening\n a dialog of its own: it belongs to the card the user\n pressed, and the modal is already a dialog. */}\n {setupPrompt?.app === integration.app ? (\n <ConnectFieldsForm\n integration={integration}\n schemes={setupPrompt.schemes}\n initialScheme={setupPrompt.setup.authScheme}\n onlyFields={setupPrompt.setup.fields}\n submitting={busy}\n onCancel={() => setSetupPrompt(null)}\n onSubmit={(values) => handleConnect(integration, values)}\n />\n ) : (\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\"\n ? \"\"\n : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch account\"}\n </button>\n )}\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-unlink\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n title=\"Disconnect this account\"\n aria-label={`Disconnect ${integration.name}`}\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx","theme","useIntegrations","useState","useMemo","useRef","useEffect","useCallback","createPortal","themeVars","isDarkTheme","IntegrationLogo","ConnectFieldsForm"],"mappings":"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"}
|
|
@@ -8,6 +8,12 @@ var logger = require('../utils/logger.js');
|
|
|
8
8
|
var usePolling = require('./usePolling.js');
|
|
9
9
|
var useModelInterface = require('./useModelInterface.js');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Cadence for the handoff watch, which only waits for the parent thread to
|
|
13
|
+
* leave `handed_off` and so does not need the main conversation's rhythm.
|
|
14
|
+
* A configured `pollingInterval` still wins over it.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_HANDOFF_POLL_INTERVAL_MS = 5000;
|
|
11
17
|
/**
|
|
12
18
|
* Main hook for managing chat with a Devic assistant
|
|
13
19
|
*
|
|
@@ -31,7 +37,7 @@ var useModelInterface = require('./useModelInterface.js');
|
|
|
31
37
|
* ```
|
|
32
38
|
*/
|
|
33
39
|
function useDevicChat(options) {
|
|
34
|
-
const { assistantId, chatUid: initialChatUid, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, enabledTools, modelInterfaceTools = [], pollingInterval
|
|
40
|
+
const { assistantId, chatUid: initialChatUid, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, enabledTools, modelInterfaceTools = [], pollingInterval: propsPollingInterval, onMessageSent, onMessageReceived, onToolCall, onError, onChatCreated, onFileUpload, debug: propsDebug, } = options;
|
|
35
41
|
// Get context (may be null if not wrapped in provider)
|
|
36
42
|
const context = DevicContext.useOptionalDevicContext();
|
|
37
43
|
// Resolve configuration
|
|
@@ -48,6 +54,8 @@ function useDevicChat(options) {
|
|
|
48
54
|
};
|
|
49
55
|
// Provider tags + hook tags, deduped. Per-message tags are merged at send time.
|
|
50
56
|
const resolvedTags = React.useMemo(() => Array.from(new Set([...(context?.tags ?? []), ...(tags ?? [])])), [context?.tags, tags]);
|
|
57
|
+
const pollingInterval = usePolling.resolvePollingInterval(propsPollingInterval, context?.pollingInterval);
|
|
58
|
+
const handoffPollingInterval = usePolling.resolvePollingInterval(propsPollingInterval, context?.pollingInterval, DEFAULT_HANDOFF_POLL_INTERVAL_MS);
|
|
51
59
|
const debug = propsDebug ?? context?.debug ?? false;
|
|
52
60
|
const log = React.useMemo(() => logger.createLogger(debug), [debug]);
|
|
53
61
|
const logRef = React.useRef(log);
|
|
@@ -523,8 +531,9 @@ function useDevicChat(options) {
|
|
|
523
531
|
setIsLoading(false);
|
|
524
532
|
}
|
|
525
533
|
}, [assistantId, resolvedTenantId, resumeFromRealtimeStatus, mergeRecalledMemories]);
|
|
526
|
-
// Handoff polling: while handedOff is true, poll the realtime endpoint every
|
|
527
|
-
// to detect when the parent thread is no
|
|
534
|
+
// Handoff polling: while handedOff is true, poll the realtime endpoint every
|
|
535
|
+
// 5s (or the configured cadence) to detect when the parent thread is no
|
|
536
|
+
// longer in handed_off state.
|
|
528
537
|
React.useEffect(() => {
|
|
529
538
|
if (!handedOff || !chatUid || !clientRef.current)
|
|
530
539
|
return;
|
|
@@ -546,14 +555,14 @@ function useDevicChat(options) {
|
|
|
546
555
|
}
|
|
547
556
|
catch { }
|
|
548
557
|
};
|
|
549
|
-
handoffPollRef.current = setInterval(pollHandoff,
|
|
558
|
+
handoffPollRef.current = setInterval(pollHandoff, handoffPollingInterval);
|
|
550
559
|
return () => {
|
|
551
560
|
if (handoffPollRef.current) {
|
|
552
561
|
clearInterval(handoffPollRef.current);
|
|
553
562
|
handoffPollRef.current = null;
|
|
554
563
|
}
|
|
555
564
|
};
|
|
556
|
-
}, [handedOff, chatUid, assistantId]);
|
|
565
|
+
}, [handedOff, chatUid, assistantId, handoffPollingInterval]);
|
|
557
566
|
// Called by HandoffSubagentWidget when the subthread reaches a terminal state
|
|
558
567
|
const onHandoffCompleted = React.useCallback(() => {
|
|
559
568
|
logRef.current.log('[useDevicChat] onHandoffCompleted called');
|