@greatapps/greatagents-ui 0.3.3 → 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 +1675 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +14 -0
- 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/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/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,162 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { IntegrationCardData, IntegrationCardState } from "../../hooks/use-integrations";
|
|
4
|
+
import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from "@greatapps/greatauth-ui/ui";
|
|
5
|
+
import {
|
|
6
|
+
CalendarSync,
|
|
7
|
+
Plug,
|
|
8
|
+
Settings,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Users,
|
|
11
|
+
Clock,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import type { LucideIcon } from "lucide-react";
|
|
14
|
+
import { cn } from "../../lib";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Icon mapping — extend as new integrations are added
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const ICON_MAP: Record<string, LucideIcon> = {
|
|
21
|
+
CalendarSync,
|
|
22
|
+
Plug,
|
|
23
|
+
Settings,
|
|
24
|
+
RefreshCw,
|
|
25
|
+
Users,
|
|
26
|
+
Clock,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveIcon(name: string): LucideIcon {
|
|
30
|
+
return ICON_MAP[name] ?? Plug;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Badge helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
interface BadgeVariant {
|
|
38
|
+
label: string;
|
|
39
|
+
className: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STATE_BADGES: Record<IntegrationCardState, BadgeVariant> = {
|
|
43
|
+
available: {
|
|
44
|
+
label: "Disponível",
|
|
45
|
+
className: "bg-muted text-muted-foreground",
|
|
46
|
+
},
|
|
47
|
+
connected: {
|
|
48
|
+
label: "Conectado",
|
|
49
|
+
className: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400",
|
|
50
|
+
},
|
|
51
|
+
expired: {
|
|
52
|
+
label: "Expirado",
|
|
53
|
+
className: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
|
|
54
|
+
},
|
|
55
|
+
coming_soon: {
|
|
56
|
+
label: "Em breve",
|
|
57
|
+
className: "bg-muted text-muted-foreground",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function getActionLabel(state: IntegrationCardState): string {
|
|
62
|
+
switch (state) {
|
|
63
|
+
case "available":
|
|
64
|
+
return "Conectar";
|
|
65
|
+
case "connected":
|
|
66
|
+
return "Configurar";
|
|
67
|
+
case "expired":
|
|
68
|
+
return "Reconectar";
|
|
69
|
+
default:
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Component
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export interface IntegrationCardProps {
|
|
79
|
+
card: IntegrationCardData;
|
|
80
|
+
onConnect: (card: IntegrationCardData) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
|
|
84
|
+
const { definition, state, sharedByAgentsCount } = card;
|
|
85
|
+
const Icon = resolveIcon(definition.icon);
|
|
86
|
+
const badge = STATE_BADGES[state];
|
|
87
|
+
const actionLabel = getActionLabel(state);
|
|
88
|
+
const isComingSoon = state === "coming_soon";
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
"group relative flex flex-col gap-3 rounded-xl border bg-card p-5 transition-shadow",
|
|
94
|
+
isComingSoon
|
|
95
|
+
? "opacity-60 cursor-default"
|
|
96
|
+
: "hover:shadow-md cursor-pointer",
|
|
97
|
+
)}
|
|
98
|
+
role="button"
|
|
99
|
+
tabIndex={isComingSoon ? -1 : 0}
|
|
100
|
+
aria-label={`${definition.name} — ${badge.label}`}
|
|
101
|
+
aria-disabled={isComingSoon}
|
|
102
|
+
onClick={() => !isComingSoon && onConnect(card)}
|
|
103
|
+
onKeyDown={(e) => {
|
|
104
|
+
if (!isComingSoon && (e.key === "Enter" || e.key === " ")) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
onConnect(card);
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{/* Header row */}
|
|
111
|
+
<div className="flex items-start justify-between gap-2">
|
|
112
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
113
|
+
<Icon className="h-5 w-5" />
|
|
114
|
+
</div>
|
|
115
|
+
<Badge variant="outline" className={cn("text-xs", badge.className)}>
|
|
116
|
+
{badge.label}
|
|
117
|
+
</Badge>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Name + description */}
|
|
121
|
+
<div className="space-y-1">
|
|
122
|
+
<h3 className="text-sm font-semibold leading-tight">{definition.name}</h3>
|
|
123
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
124
|
+
{definition.description}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Footer */}
|
|
129
|
+
<div className="mt-auto flex items-center justify-between gap-2 pt-1">
|
|
130
|
+
{sharedByAgentsCount > 0 ? (
|
|
131
|
+
<Tooltip>
|
|
132
|
+
<TooltipTrigger asChild>
|
|
133
|
+
<span className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
|
|
134
|
+
<Users className="h-3.5 w-3.5" />
|
|
135
|
+
Compartilhada
|
|
136
|
+
</span>
|
|
137
|
+
</TooltipTrigger>
|
|
138
|
+
<TooltipContent>
|
|
139
|
+
Esta credencial está disponível para todos os agentes da conta
|
|
140
|
+
</TooltipContent>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
) : (
|
|
143
|
+
<span />
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{!isComingSoon && (
|
|
147
|
+
<Button
|
|
148
|
+
variant={state === "expired" ? "destructive" : "outline"}
|
|
149
|
+
size="sm"
|
|
150
|
+
className="text-xs"
|
|
151
|
+
onClick={(e) => {
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
onConnect(card);
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{actionLabel}
|
|
157
|
+
</Button>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -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
|
+
}
|