@greatapps/greatagents-ui 0.3.2 → 0.3.4
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/dist/index.d.ts +204 -1
- package/dist/index.js +1797 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +14 -0
- package/src/components/agents/agent-edit-form.tsx +4 -1
- package/src/components/agents/agent-form-dialog.tsx +5 -1
- package/src/components/agents/agent-objectives-list.tsx +15 -6
- package/src/components/agents/agent-prompt-editor.tsx +9 -5
- package/src/components/agents/agent-tabs.tsx +4 -4
- package/src/components/agents/agent-tools-list.tsx +12 -5
- package/src/components/agents/agents-table.tsx +7 -2
- package/src/components/capabilities/advanced-tab.tsx +82 -0
- package/src/components/capabilities/capabilities-tab.tsx +475 -0
- package/src/components/capabilities/integration-card.tsx +162 -0
- package/src/components/capabilities/integration-wizard.tsx +537 -0
- package/src/components/capabilities/integrations-tab.tsx +61 -0
- package/src/components/capabilities/types.ts +48 -0
- package/src/components/capabilities/wizard-steps/config-step.tsx +117 -0
- package/src/components/capabilities/wizard-steps/confirm-step.tsx +123 -0
- package/src/components/capabilities/wizard-steps/credentials-step.tsx +205 -0
- package/src/components/capabilities/wizard-steps/info-step.tsx +78 -0
- package/src/components/conversations/agent-conversations-table.tsx +13 -2
- package/src/components/conversations/conversation-view.tsx +2 -2
- package/src/components/tools/tool-credentials-form.tsx +34 -14
- package/src/components/tools/tool-form-dialog.tsx +9 -5
- package/src/components/tools/tools-table.tsx +8 -3
- package/src/data/integrations-registry.ts +23 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-capabilities.ts +50 -0
- package/src/hooks/use-integrations.ts +114 -0
- package/src/index.ts +34 -0
- package/src/pages/agent-capabilities-page.tsx +159 -0
- package/src/pages/agent-detail-page.tsx +1 -0
- package/src/pages/index.ts +2 -0
- package/src/pages/integrations-management-page.tsx +166 -0
- package/src/types/capabilities.ts +32 -0
- package/src/types/index.ts +10 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
Button,
|
|
11
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
12
|
+
import { Loader2, ChevronLeft, ChevronRight, Check } from "lucide-react";
|
|
13
|
+
import { toast } from "sonner";
|
|
14
|
+
|
|
15
|
+
import type { GagentsHookConfig } from "../../hooks/types";
|
|
16
|
+
import type { IntegrationDefinition } from "../../data/integrations-registry";
|
|
17
|
+
import type {
|
|
18
|
+
WizardIntegrationMeta,
|
|
19
|
+
OAuthResult,
|
|
20
|
+
OAuthStatus,
|
|
21
|
+
WizardStep,
|
|
22
|
+
} from "./types";
|
|
23
|
+
import { InfoStep } from "./wizard-steps/info-step";
|
|
24
|
+
import { CredentialsStep } from "./wizard-steps/credentials-step";
|
|
25
|
+
import { ConfigStep, type ConfigOption } from "./wizard-steps/config-step";
|
|
26
|
+
import { ConfirmStep } from "./wizard-steps/confirm-step";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Constants
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const STEPS: WizardStep[] = ["info", "credentials", "config", "confirm"];
|
|
33
|
+
|
|
34
|
+
const STEP_LABELS: Record<WizardStep, string> = {
|
|
35
|
+
info: "Informação",
|
|
36
|
+
credentials: "Credenciais",
|
|
37
|
+
config: "Configuração",
|
|
38
|
+
confirm: "Confirmação",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Props
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export interface IntegrationWizardProps {
|
|
46
|
+
open: boolean;
|
|
47
|
+
onOpenChange: (open: boolean) => void;
|
|
48
|
+
/** Base integration definition from registry */
|
|
49
|
+
integration: IntegrationDefinition;
|
|
50
|
+
/** Wizard-specific metadata (capabilities, requirements, etc.) */
|
|
51
|
+
meta: WizardIntegrationMeta;
|
|
52
|
+
agentId: string | number;
|
|
53
|
+
config: GagentsHookConfig;
|
|
54
|
+
onComplete: () => void;
|
|
55
|
+
/** gagents-r3-api base URL (used to build OAuth URLs) */
|
|
56
|
+
gagentsApiUrl: string;
|
|
57
|
+
/**
|
|
58
|
+
* Existing credential ID -- when set, the wizard opens in edit mode
|
|
59
|
+
* (step 2 shows "Reconectar" and step 3 is pre-filled).
|
|
60
|
+
*/
|
|
61
|
+
existingCredentialId?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Callback to load config options after OAuth completes.
|
|
64
|
+
* e.g. load Google Calendar list. Returns ConfigOption[].
|
|
65
|
+
*/
|
|
66
|
+
loadConfigOptions?: (credentialId: number) => Promise<ConfigOption[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Existing config value to pre-fill the config step in edit/reconnect mode.
|
|
69
|
+
*/
|
|
70
|
+
existingConfigValue?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Component
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export function IntegrationWizard({
|
|
78
|
+
open,
|
|
79
|
+
onOpenChange,
|
|
80
|
+
integration,
|
|
81
|
+
meta,
|
|
82
|
+
agentId: _agentId,
|
|
83
|
+
config,
|
|
84
|
+
onComplete,
|
|
85
|
+
gagentsApiUrl,
|
|
86
|
+
existingCredentialId,
|
|
87
|
+
loadConfigOptions,
|
|
88
|
+
existingConfigValue,
|
|
89
|
+
}: IntegrationWizardProps) {
|
|
90
|
+
const isReconnect = !!existingCredentialId;
|
|
91
|
+
|
|
92
|
+
// Step navigation
|
|
93
|
+
const [currentStep, setCurrentStep] = useState<WizardStep>("info");
|
|
94
|
+
const currentIndex = STEPS.indexOf(currentStep);
|
|
95
|
+
|
|
96
|
+
// OAuth state
|
|
97
|
+
const [oauthStatus, setOauthStatus] = useState<OAuthStatus>("idle");
|
|
98
|
+
const [oauthResult, setOauthResult] = useState<OAuthResult | null>(null);
|
|
99
|
+
const popupRef = useRef<Window | null>(null);
|
|
100
|
+
const popupPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
101
|
+
|
|
102
|
+
// API Key state
|
|
103
|
+
const [apiKey, setApiKey] = useState("");
|
|
104
|
+
|
|
105
|
+
// Config state
|
|
106
|
+
const [configOptions, setConfigOptions] = useState<ConfigOption[]>([]);
|
|
107
|
+
const [configLoading, setConfigLoading] = useState(false);
|
|
108
|
+
const [selectedConfigValue, setSelectedConfigValue] = useState("");
|
|
109
|
+
|
|
110
|
+
// Confirm state
|
|
111
|
+
const [enableOnComplete, setEnableOnComplete] = useState(true);
|
|
112
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
113
|
+
|
|
114
|
+
// Clean up popup poll interval on unmount
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
return () => {
|
|
117
|
+
if (popupPollRef.current) {
|
|
118
|
+
clearInterval(popupPollRef.current);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
// Reset state when dialog opens/closes
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (open) {
|
|
126
|
+
setCurrentStep("info");
|
|
127
|
+
setOauthStatus("idle");
|
|
128
|
+
setOauthResult(null);
|
|
129
|
+
setApiKey("");
|
|
130
|
+
setConfigOptions([]);
|
|
131
|
+
setConfigLoading(false);
|
|
132
|
+
setSelectedConfigValue(existingConfigValue ?? "");
|
|
133
|
+
setEnableOnComplete(true);
|
|
134
|
+
setIsSubmitting(false);
|
|
135
|
+
} else {
|
|
136
|
+
// Close any lingering popup
|
|
137
|
+
if (popupRef.current && !popupRef.current.closed) {
|
|
138
|
+
popupRef.current.close();
|
|
139
|
+
}
|
|
140
|
+
// Clear popup poll interval
|
|
141
|
+
if (popupPollRef.current) {
|
|
142
|
+
clearInterval(popupPollRef.current);
|
|
143
|
+
popupPollRef.current = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, [open]);
|
|
147
|
+
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
// OAuth popup message listener
|
|
150
|
+
// -----------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
const handleOAuthMessage = useCallback(
|
|
153
|
+
(event: MessageEvent) => {
|
|
154
|
+
// Validate origin against gagents API URL
|
|
155
|
+
try {
|
|
156
|
+
if (event.origin !== new URL(gagentsApiUrl).origin) return;
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Only accept messages from our popup -- check for known data shape
|
|
162
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
163
|
+
const msg = event.data as {
|
|
164
|
+
type?: string;
|
|
165
|
+
success?: boolean;
|
|
166
|
+
email?: string;
|
|
167
|
+
error?: string;
|
|
168
|
+
credentialId?: number;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (msg.type !== "oauth-callback") return;
|
|
172
|
+
|
|
173
|
+
if (msg.success) {
|
|
174
|
+
setOauthStatus("success");
|
|
175
|
+
setOauthResult({
|
|
176
|
+
success: true,
|
|
177
|
+
email: msg.email,
|
|
178
|
+
credentialId: msg.credentialId,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Auto-load config options if available
|
|
182
|
+
const credId = msg.credentialId || existingCredentialId;
|
|
183
|
+
if (credId && loadConfigOptions && meta.hasConfigStep) {
|
|
184
|
+
setConfigLoading(true);
|
|
185
|
+
loadConfigOptions(credId)
|
|
186
|
+
.then((opts) => {
|
|
187
|
+
setConfigOptions(opts);
|
|
188
|
+
if (opts.length === 1) {
|
|
189
|
+
setSelectedConfigValue(opts[0].id);
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
.catch(() => setConfigOptions([]))
|
|
193
|
+
.finally(() => setConfigLoading(false));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Auto-advance to next step after a brief delay
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
setCurrentStep(meta.hasConfigStep ? "config" : "confirm");
|
|
199
|
+
}, 1200);
|
|
200
|
+
} else {
|
|
201
|
+
setOauthStatus("error");
|
|
202
|
+
setOauthResult({
|
|
203
|
+
success: false,
|
|
204
|
+
error: msg.error || "Falha na autorização",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
[gagentsApiUrl, existingCredentialId, meta.hasConfigStep, loadConfigOptions],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!open) return;
|
|
213
|
+
window.addEventListener("message", handleOAuthMessage);
|
|
214
|
+
return () => window.removeEventListener("message", handleOAuthMessage);
|
|
215
|
+
}, [open, handleOAuthMessage]);
|
|
216
|
+
|
|
217
|
+
// -----------------------------------------------------------------------
|
|
218
|
+
// OAuth start
|
|
219
|
+
// -----------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
function startOAuth() {
|
|
222
|
+
const { language = "pt-br", idWl = 1, accountId } = config;
|
|
223
|
+
|
|
224
|
+
// Build OAuth authorize URL -- the backend already handles the full flow
|
|
225
|
+
const redirectUri = `${window.location.origin}/oauth/callback`;
|
|
226
|
+
const url = new URL(
|
|
227
|
+
`${gagentsApiUrl}/v1/${language}/${idWl}/accounts/${accountId}/oauth/authorize/${integration.slug}`,
|
|
228
|
+
);
|
|
229
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
230
|
+
|
|
231
|
+
setOauthStatus("waiting");
|
|
232
|
+
|
|
233
|
+
// Open popup
|
|
234
|
+
const popup = window.open(
|
|
235
|
+
url.toString(),
|
|
236
|
+
"oauth-popup",
|
|
237
|
+
"width=500,height=600,scrollbars=yes,resizable=yes",
|
|
238
|
+
);
|
|
239
|
+
popupRef.current = popup;
|
|
240
|
+
|
|
241
|
+
// Poll for popup closed without completing
|
|
242
|
+
if (popup) {
|
|
243
|
+
// Clear any previous poll interval
|
|
244
|
+
if (popupPollRef.current) {
|
|
245
|
+
clearInterval(popupPollRef.current);
|
|
246
|
+
}
|
|
247
|
+
popupPollRef.current = setInterval(() => {
|
|
248
|
+
if (popup.closed) {
|
|
249
|
+
if (popupPollRef.current) {
|
|
250
|
+
clearInterval(popupPollRef.current);
|
|
251
|
+
popupPollRef.current = null;
|
|
252
|
+
}
|
|
253
|
+
// Only set error if we're still waiting (no success message received)
|
|
254
|
+
setOauthStatus((prev) =>
|
|
255
|
+
prev === "waiting" ? "error" : prev,
|
|
256
|
+
);
|
|
257
|
+
setOauthResult((prev) =>
|
|
258
|
+
prev === null
|
|
259
|
+
? { success: false, error: "Janela fechada antes de concluir" }
|
|
260
|
+
: prev,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}, 500);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// -----------------------------------------------------------------------
|
|
268
|
+
// Navigation
|
|
269
|
+
// -----------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
function canAdvance(): boolean {
|
|
272
|
+
switch (currentStep) {
|
|
273
|
+
case "info":
|
|
274
|
+
return true;
|
|
275
|
+
case "credentials":
|
|
276
|
+
if (integration.authType === "oauth2") {
|
|
277
|
+
return oauthStatus === "success";
|
|
278
|
+
}
|
|
279
|
+
return apiKey.trim().length > 0;
|
|
280
|
+
case "config":
|
|
281
|
+
// Config step is optional -- can always advance
|
|
282
|
+
return true;
|
|
283
|
+
case "confirm":
|
|
284
|
+
return true;
|
|
285
|
+
default:
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function goNext() {
|
|
291
|
+
if (!canAdvance()) return;
|
|
292
|
+
|
|
293
|
+
// Skip config step if not needed
|
|
294
|
+
if (currentStep === "credentials" && !meta.hasConfigStep) {
|
|
295
|
+
setCurrentStep("confirm");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const nextIndex = currentIndex + 1;
|
|
300
|
+
if (nextIndex < STEPS.length) {
|
|
301
|
+
setCurrentStep(STEPS[nextIndex]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function goPrev() {
|
|
306
|
+
// Skip config step backwards if not needed
|
|
307
|
+
if (currentStep === "confirm" && !meta.hasConfigStep) {
|
|
308
|
+
setCurrentStep("credentials");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const prevIndex = currentIndex - 1;
|
|
313
|
+
if (prevIndex >= 0) {
|
|
314
|
+
setCurrentStep(STEPS[prevIndex]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// -----------------------------------------------------------------------
|
|
319
|
+
// Complete
|
|
320
|
+
// -----------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
async function handleComplete() {
|
|
323
|
+
setIsSubmitting(true);
|
|
324
|
+
try {
|
|
325
|
+
// The actual credential creation happened during OAuth callback
|
|
326
|
+
// or will happen via API key submission. Here we just finalize.
|
|
327
|
+
//
|
|
328
|
+
// For OAuth, the credential was already created by the backend callback.
|
|
329
|
+
// The parent component handles any additional linking (agent_tool creation)
|
|
330
|
+
// via the onComplete callback.
|
|
331
|
+
|
|
332
|
+
onComplete();
|
|
333
|
+
onOpenChange(false);
|
|
334
|
+
toast.success(
|
|
335
|
+
`${integration.name} ${isReconnect ? "reconectado" : "configurado"} com sucesso!`,
|
|
336
|
+
);
|
|
337
|
+
} catch {
|
|
338
|
+
toast.error("Erro ao finalizar configuração");
|
|
339
|
+
} finally {
|
|
340
|
+
setIsSubmitting(false);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// -----------------------------------------------------------------------
|
|
345
|
+
// Render
|
|
346
|
+
// -----------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
const selectedConfigOption =
|
|
349
|
+
configOptions.find((o) => o.id === selectedConfigValue) || null;
|
|
350
|
+
|
|
351
|
+
const isLastStep = currentStep === "confirm";
|
|
352
|
+
|
|
353
|
+
// Effective steps (may skip config)
|
|
354
|
+
const effectiveSteps = meta.hasConfigStep
|
|
355
|
+
? STEPS
|
|
356
|
+
: STEPS.filter((s) => s !== "config");
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
360
|
+
<DialogContent className="sm:max-w-lg">
|
|
361
|
+
<DialogHeader>
|
|
362
|
+
<DialogTitle>{integration.name}</DialogTitle>
|
|
363
|
+
</DialogHeader>
|
|
364
|
+
|
|
365
|
+
{/* Step indicator */}
|
|
366
|
+
<StepIndicator steps={effectiveSteps} currentStep={currentStep} />
|
|
367
|
+
|
|
368
|
+
{/* Step content */}
|
|
369
|
+
<div className="min-h-[280px] py-2">
|
|
370
|
+
{currentStep === "info" && (
|
|
371
|
+
<InfoStep
|
|
372
|
+
integration={integration}
|
|
373
|
+
meta={meta}
|
|
374
|
+
/>
|
|
375
|
+
)}
|
|
376
|
+
{currentStep === "credentials" && (
|
|
377
|
+
<CredentialsStep
|
|
378
|
+
integration={integration}
|
|
379
|
+
meta={meta}
|
|
380
|
+
oauthStatus={oauthStatus}
|
|
381
|
+
oauthResult={oauthResult}
|
|
382
|
+
apiKey={apiKey}
|
|
383
|
+
onApiKeyChange={setApiKey}
|
|
384
|
+
onStartOAuth={startOAuth}
|
|
385
|
+
isReconnect={isReconnect}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
{currentStep === "config" && (
|
|
389
|
+
<ConfigStep
|
|
390
|
+
integration={integration}
|
|
391
|
+
options={configOptions}
|
|
392
|
+
isLoading={configLoading}
|
|
393
|
+
selectedValue={selectedConfigValue}
|
|
394
|
+
onValueChange={setSelectedConfigValue}
|
|
395
|
+
/>
|
|
396
|
+
)}
|
|
397
|
+
{currentStep === "confirm" && (
|
|
398
|
+
<ConfirmStep
|
|
399
|
+
integration={integration}
|
|
400
|
+
oauthResult={oauthResult}
|
|
401
|
+
selectedConfigOption={selectedConfigOption}
|
|
402
|
+
enableOnComplete={enableOnComplete}
|
|
403
|
+
onEnableChange={setEnableOnComplete}
|
|
404
|
+
/>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{/* Footer */}
|
|
409
|
+
<DialogFooter className="flex-row justify-between sm:justify-between">
|
|
410
|
+
<div>
|
|
411
|
+
{currentStep === "info" ? (
|
|
412
|
+
<Button
|
|
413
|
+
type="button"
|
|
414
|
+
variant="outline"
|
|
415
|
+
onClick={() => onOpenChange(false)}
|
|
416
|
+
>
|
|
417
|
+
Cancelar
|
|
418
|
+
</Button>
|
|
419
|
+
) : (
|
|
420
|
+
<Button
|
|
421
|
+
type="button"
|
|
422
|
+
variant="outline"
|
|
423
|
+
onClick={goPrev}
|
|
424
|
+
className="gap-1"
|
|
425
|
+
>
|
|
426
|
+
<ChevronLeft aria-hidden="true" className="h-4 w-4" />
|
|
427
|
+
Voltar
|
|
428
|
+
</Button>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
<div>
|
|
432
|
+
{isLastStep ? (
|
|
433
|
+
<Button
|
|
434
|
+
type="button"
|
|
435
|
+
onClick={handleComplete}
|
|
436
|
+
disabled={isSubmitting}
|
|
437
|
+
className="gap-1"
|
|
438
|
+
>
|
|
439
|
+
{isSubmitting ? (
|
|
440
|
+
<Loader2
|
|
441
|
+
aria-hidden="true"
|
|
442
|
+
className="h-4 w-4 animate-spin"
|
|
443
|
+
/>
|
|
444
|
+
) : (
|
|
445
|
+
<Check aria-hidden="true" className="h-4 w-4" />
|
|
446
|
+
)}
|
|
447
|
+
Concluir
|
|
448
|
+
</Button>
|
|
449
|
+
) : (
|
|
450
|
+
<Button
|
|
451
|
+
type="button"
|
|
452
|
+
onClick={goNext}
|
|
453
|
+
disabled={!canAdvance()}
|
|
454
|
+
className="gap-1"
|
|
455
|
+
>
|
|
456
|
+
Continuar
|
|
457
|
+
<ChevronRight aria-hidden="true" className="h-4 w-4" />
|
|
458
|
+
</Button>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
</DialogFooter>
|
|
462
|
+
</DialogContent>
|
|
463
|
+
</Dialog>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Step Indicator
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
function StepIndicator({
|
|
472
|
+
steps,
|
|
473
|
+
currentStep,
|
|
474
|
+
}: {
|
|
475
|
+
steps: WizardStep[];
|
|
476
|
+
currentStep: WizardStep;
|
|
477
|
+
}) {
|
|
478
|
+
const currentIndex = steps.indexOf(currentStep);
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<div
|
|
482
|
+
className="flex items-center justify-center gap-1 py-2"
|
|
483
|
+
role="list"
|
|
484
|
+
aria-label="Passos do assistente"
|
|
485
|
+
>
|
|
486
|
+
{steps.map((step, i) => {
|
|
487
|
+
const isCompleted = i < currentIndex;
|
|
488
|
+
const isCurrent = step === currentStep;
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div key={step} className="flex items-center" role="listitem">
|
|
492
|
+
{/* Circle */}
|
|
493
|
+
<div
|
|
494
|
+
className={`
|
|
495
|
+
flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium
|
|
496
|
+
transition-colors duration-200
|
|
497
|
+
${
|
|
498
|
+
isCurrent
|
|
499
|
+
? "bg-primary text-primary-foreground"
|
|
500
|
+
: isCompleted
|
|
501
|
+
? "bg-green-600 text-white"
|
|
502
|
+
: "bg-muted text-muted-foreground"
|
|
503
|
+
}
|
|
504
|
+
`}
|
|
505
|
+
aria-current={isCurrent ? "step" : undefined}
|
|
506
|
+
aria-label={`${STEP_LABELS[step]}${isCompleted ? " (concluído)" : isCurrent ? " (atual)" : ""}`}
|
|
507
|
+
>
|
|
508
|
+
{isCompleted ? (
|
|
509
|
+
<Check aria-hidden="true" className="h-3.5 w-3.5" />
|
|
510
|
+
) : (
|
|
511
|
+
i + 1
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
{/* Label */}
|
|
515
|
+
<span
|
|
516
|
+
className={`
|
|
517
|
+
ml-1.5 hidden text-xs sm:inline
|
|
518
|
+
${isCurrent ? "font-medium text-foreground" : "text-muted-foreground"}
|
|
519
|
+
`}
|
|
520
|
+
>
|
|
521
|
+
{STEP_LABELS[step]}
|
|
522
|
+
</span>
|
|
523
|
+
{/* Connector line */}
|
|
524
|
+
{i < steps.length - 1 && (
|
|
525
|
+
<div
|
|
526
|
+
className={`
|
|
527
|
+
mx-2 h-px w-6
|
|
528
|
+
${i < currentIndex ? "bg-green-600" : "bg-border"}
|
|
529
|
+
`}
|
|
530
|
+
/>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
})}
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { GagentsHookConfig } from "../../hooks/types";
|
|
4
|
+
import { useIntegrationState, type IntegrationCardData } from "../../hooks/use-integrations";
|
|
5
|
+
import { IntegrationCard } from "./integration-card";
|
|
6
|
+
import { Plug, Loader2 } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Props
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface IntegrationsTabProps {
|
|
13
|
+
config: GagentsHookConfig;
|
|
14
|
+
agentId: number | null;
|
|
15
|
+
/** Called when user clicks a card action (connect / configure / reconnect).
|
|
16
|
+
* The consuming app wires this to the wizard (Story 18.9). */
|
|
17
|
+
onConnect: (card: IntegrationCardData) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Component
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export function IntegrationsTab({
|
|
25
|
+
config,
|
|
26
|
+
agentId,
|
|
27
|
+
onConnect,
|
|
28
|
+
}: IntegrationsTabProps) {
|
|
29
|
+
const { cards, isLoading } = useIntegrationState(config, agentId);
|
|
30
|
+
|
|
31
|
+
// Loading state
|
|
32
|
+
if (isLoading) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-center justify-center py-16">
|
|
35
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Empty state
|
|
41
|
+
if (cards.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-col items-center justify-center gap-3 py-16 text-muted-foreground">
|
|
44
|
+
<Plug className="h-10 w-10" />
|
|
45
|
+
<p className="text-sm">Nenhuma integração disponível</p>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
52
|
+
{cards.map((card) => (
|
|
53
|
+
<IntegrationCard
|
|
54
|
+
key={card.definition.slug}
|
|
55
|
+
card={card}
|
|
56
|
+
onConnect={onConnect}
|
|
57
|
+
/>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Integration Wizard flow.
|
|
3
|
+
*
|
|
4
|
+
* The wizard uses the base IntegrationDefinition from the integrations registry
|
|
5
|
+
* and extends it with wizard-specific metadata via WizardIntegrationMeta.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { IntegrationDefinition } from "../../data/integrations-registry";
|
|
9
|
+
|
|
10
|
+
// Re-export for convenience
|
|
11
|
+
export type { IntegrationDefinition };
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Wizard-specific metadata (passed alongside the registry definition)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface IntegrationCapability {
|
|
18
|
+
label: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WizardIntegrationMeta {
|
|
23
|
+
/** Icon as React node for the wizard header */
|
|
24
|
+
icon?: React.ReactNode;
|
|
25
|
+
/** Provider label for OAuth button (e.g. "Google") */
|
|
26
|
+
providerLabel?: string;
|
|
27
|
+
/** What this integration can do */
|
|
28
|
+
capabilities: IntegrationCapability[];
|
|
29
|
+
/** Required permissions / prerequisites */
|
|
30
|
+
requirements: string[];
|
|
31
|
+
/** Whether this integration has a config step */
|
|
32
|
+
hasConfigStep: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Wizard state types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export type WizardStep = "info" | "credentials" | "config" | "confirm";
|
|
40
|
+
|
|
41
|
+
export type OAuthStatus = "idle" | "waiting" | "success" | "error";
|
|
42
|
+
|
|
43
|
+
export interface OAuthResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
email?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
credentialId?: number;
|
|
48
|
+
}
|