@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.
@@ -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
+ }
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+
3
+ import { Loader2 } from "lucide-react";
4
+ import {
5
+ Label,
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "@greatapps/greatauth-ui/ui";
12
+ import type { IntegrationDefinition } from "../../../data/integrations-registry";
13
+
14
+ export interface ConfigOption {
15
+ id: string;
16
+ label: string;
17
+ description?: string;
18
+ }
19
+
20
+ interface ConfigStepProps {
21
+ integration: IntegrationDefinition;
22
+ /** Available options loaded from backend (e.g. calendars) */
23
+ options: ConfigOption[];
24
+ isLoading: boolean;
25
+ selectedValue: string;
26
+ onValueChange: (value: string) => void;
27
+ /** Custom label for the select dropdown */
28
+ selectLabel?: string;
29
+ /** Custom placeholder */
30
+ selectPlaceholder?: string;
31
+ }
32
+
33
+ export function ConfigStep({
34
+ integration,
35
+ options,
36
+ isLoading,
37
+ selectedValue,
38
+ onValueChange,
39
+ selectLabel,
40
+ selectPlaceholder,
41
+ }: ConfigStepProps) {
42
+ const label = selectLabel || getDefaultLabel(integration.slug);
43
+ const placeholder =
44
+ selectPlaceholder || getDefaultPlaceholder(integration.slug);
45
+
46
+ return (
47
+ <div className="space-y-6">
48
+ <div className="space-y-2">
49
+ <h3 className="text-lg font-semibold">Configuração</h3>
50
+ <p className="text-sm text-muted-foreground">
51
+ Configure as opções específicas da integração.
52
+ </p>
53
+ </div>
54
+
55
+ {isLoading ? (
56
+ <div className="flex flex-col items-center gap-3 py-8">
57
+ <Loader2
58
+ aria-hidden="true"
59
+ className="h-6 w-6 animate-spin text-muted-foreground"
60
+ />
61
+ <p className="text-sm text-muted-foreground">
62
+ Carregando opções...
63
+ </p>
64
+ </div>
65
+ ) : options.length === 0 ? (
66
+ <div className="rounded-lg border border-dashed p-6 text-center">
67
+ <p className="text-sm text-muted-foreground">
68
+ Nenhuma opção disponível. A configuração padrão será usada.
69
+ </p>
70
+ </div>
71
+ ) : (
72
+ <div className="space-y-2">
73
+ <Label htmlFor="integration-config-select">{label}</Label>
74
+ <Select value={selectedValue} onValueChange={onValueChange}>
75
+ <SelectTrigger id="integration-config-select">
76
+ <SelectValue placeholder={placeholder} />
77
+ </SelectTrigger>
78
+ <SelectContent>
79
+ {options.map((opt) => (
80
+ <SelectItem key={opt.id} value={opt.id}>
81
+ <span>{opt.label}</span>
82
+ {opt.description && (
83
+ <span className="ml-2 text-xs text-muted-foreground">
84
+ ({opt.description})
85
+ </span>
86
+ )}
87
+ </SelectItem>
88
+ ))}
89
+ </SelectContent>
90
+ </Select>
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Helpers
99
+ // ---------------------------------------------------------------------------
100
+
101
+ function getDefaultLabel(slug: string): string {
102
+ switch (slug) {
103
+ case "google_calendar":
104
+ return "Calendário";
105
+ default:
106
+ return "Opção";
107
+ }
108
+ }
109
+
110
+ function getDefaultPlaceholder(slug: string): string {
111
+ switch (slug) {
112
+ case "google_calendar":
113
+ return "Selecione o calendário...";
114
+ default:
115
+ return "Selecione uma opção...";
116
+ }
117
+ }
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import { CheckCircle2 } from "lucide-react";
4
+ import { Label } from "@greatapps/greatauth-ui/ui";
5
+ import type { IntegrationDefinition } from "../../../data/integrations-registry";
6
+ import type { OAuthResult } from "../types";
7
+ import type { ConfigOption } from "./config-step";
8
+
9
+ interface ConfirmStepProps {
10
+ integration: IntegrationDefinition;
11
+ oauthResult: OAuthResult | null;
12
+ selectedConfigOption: ConfigOption | null;
13
+ enableOnComplete: boolean;
14
+ onEnableChange: (enabled: boolean) => void;
15
+ }
16
+
17
+ export function ConfirmStep({
18
+ integration,
19
+ oauthResult,
20
+ selectedConfigOption,
21
+ enableOnComplete,
22
+ onEnableChange,
23
+ }: ConfirmStepProps) {
24
+ return (
25
+ <div className="space-y-6">
26
+ <div className="space-y-2">
27
+ <h3 className="text-lg font-semibold">Confirmação</h3>
28
+ <p className="text-sm text-muted-foreground">
29
+ Revise as configurações antes de concluir.
30
+ </p>
31
+ </div>
32
+
33
+ {/* Summary */}
34
+ <div className="space-y-3 rounded-lg border p-4">
35
+ <SummaryRow label="Integração" value={integration.name} />
36
+
37
+ {oauthResult?.email && (
38
+ <SummaryRow label="Conta conectada" value={oauthResult.email} />
39
+ )}
40
+
41
+ {selectedConfigOption && (
42
+ <SummaryRow
43
+ label={getConfigLabel(integration.slug)}
44
+ value={selectedConfigOption.label}
45
+ />
46
+ )}
47
+
48
+ <SummaryRow
49
+ label="Tipo de autenticação"
50
+ value={integration.authType === "oauth2" ? "OAuth 2.0" : "API Key"}
51
+ />
52
+ </div>
53
+
54
+ {/* Enable toggle */}
55
+ <div className="flex items-center justify-between rounded-lg border p-4">
56
+ <div className="space-y-0.5">
57
+ <Label htmlFor="enable-on-complete" className="text-sm font-medium">
58
+ Ativar imediatamente
59
+ </Label>
60
+ <p className="text-xs text-muted-foreground">
61
+ A integração ficará ativa assim que concluir o assistente.
62
+ </p>
63
+ </div>
64
+ <button
65
+ id="enable-on-complete"
66
+ role="switch"
67
+ type="button"
68
+ aria-checked={enableOnComplete}
69
+ onClick={() => onEnableChange(!enableOnComplete)}
70
+ className={`
71
+ relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent
72
+ transition-colors duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2
73
+ focus-visible:ring-ring focus-visible:ring-offset-2
74
+ ${enableOnComplete ? "bg-primary" : "bg-muted"}
75
+ `}
76
+ >
77
+ <span
78
+ aria-hidden="true"
79
+ className={`
80
+ pointer-events-none inline-block h-5 w-5 transform rounded-full bg-background shadow-lg
81
+ ring-0 transition duration-200 ease-in-out
82
+ ${enableOnComplete ? "translate-x-5" : "translate-x-0"}
83
+ `}
84
+ />
85
+ </button>
86
+ </div>
87
+
88
+ {/* Success indicator */}
89
+ <div className="flex items-center gap-2 rounded-md bg-green-50 p-3 dark:bg-green-950/20">
90
+ <CheckCircle2
91
+ aria-hidden="true"
92
+ className="h-4 w-4 shrink-0 text-green-600"
93
+ />
94
+ <p className="text-xs text-green-700 dark:text-green-400">
95
+ Tudo pronto! Clique em &quot;Concluir&quot; para finalizar a
96
+ configuração.
97
+ </p>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Helpers
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function SummaryRow({ label, value }: { label: string; value: string }) {
108
+ return (
109
+ <div className="flex items-center justify-between text-sm">
110
+ <span className="text-muted-foreground">{label}</span>
111
+ <span className="font-medium">{value}</span>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function getConfigLabel(slug: string): string {
117
+ switch (slug) {
118
+ case "google_calendar":
119
+ return "Calendário";
120
+ default:
121
+ return "Configuração";
122
+ }
123
+ }
@@ -0,0 +1,205 @@
1
+ 'use client';
2
+
3
+ import { CheckCircle2, Loader2, AlertCircle, Shield } from "lucide-react";
4
+ import { Button, Input, Label } from "@greatapps/greatauth-ui/ui";
5
+ import type { IntegrationDefinition } from "../../../data/integrations-registry";
6
+ import type { WizardIntegrationMeta, OAuthStatus, OAuthResult } from "../types";
7
+
8
+ interface CredentialsStepProps {
9
+ integration: IntegrationDefinition;
10
+ meta: WizardIntegrationMeta;
11
+ oauthStatus: OAuthStatus;
12
+ oauthResult: OAuthResult | null;
13
+ apiKey: string;
14
+ onApiKeyChange: (value: string) => void;
15
+ onStartOAuth: () => void;
16
+ isReconnect?: boolean;
17
+ }
18
+
19
+ export function CredentialsStep({
20
+ integration,
21
+ meta,
22
+ oauthStatus,
23
+ oauthResult,
24
+ apiKey,
25
+ onApiKeyChange,
26
+ onStartOAuth,
27
+ isReconnect = false,
28
+ }: CredentialsStepProps) {
29
+ if (integration.authType === "oauth2") {
30
+ return (
31
+ <OAuthCredentials
32
+ integration={integration}
33
+ meta={meta}
34
+ oauthStatus={oauthStatus}
35
+ oauthResult={oauthResult}
36
+ onStartOAuth={onStartOAuth}
37
+ isReconnect={isReconnect}
38
+ />
39
+ );
40
+ }
41
+
42
+ return <ApiKeyCredentials apiKey={apiKey} onApiKeyChange={onApiKeyChange} />;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // OAuth flow UI
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function OAuthCredentials({
50
+ integration,
51
+ meta,
52
+ oauthStatus,
53
+ oauthResult,
54
+ onStartOAuth,
55
+ isReconnect,
56
+ }: {
57
+ integration: IntegrationDefinition;
58
+ meta: WizardIntegrationMeta;
59
+ oauthStatus: OAuthStatus;
60
+ oauthResult: OAuthResult | null;
61
+ onStartOAuth: () => void;
62
+ isReconnect: boolean;
63
+ }) {
64
+ const providerLabel = meta.providerLabel || integration.name;
65
+
66
+ return (
67
+ <div className="space-y-6">
68
+ <div className="space-y-2">
69
+ <h3 className="text-lg font-semibold">Autenticação</h3>
70
+ <p className="text-sm text-muted-foreground">
71
+ {isReconnect
72
+ ? `Reconecte sua conta ${providerLabel} para renovar a autorização.`
73
+ : `Conecte sua conta ${providerLabel} para permitir o acesso.`}
74
+ </p>
75
+ </div>
76
+
77
+ {/* OAuth status area */}
78
+ <div className="flex flex-col items-center gap-4 rounded-lg border p-6">
79
+ {oauthStatus === "idle" && (
80
+ <Button onClick={onStartOAuth} size="lg" className="gap-2">
81
+ {meta.icon}
82
+ {isReconnect
83
+ ? `Reconectar com ${providerLabel}`
84
+ : `Conectar com ${providerLabel}`}
85
+ </Button>
86
+ )}
87
+
88
+ {oauthStatus === "waiting" && (
89
+ <div className="flex flex-col items-center gap-3 text-center">
90
+ <Loader2
91
+ aria-hidden="true"
92
+ className="h-8 w-8 animate-spin text-muted-foreground"
93
+ />
94
+ <div>
95
+ <p className="text-sm font-medium">Aguardando autorização...</p>
96
+ <p className="text-xs text-muted-foreground">
97
+ Complete o login na janela que foi aberta.
98
+ </p>
99
+ </div>
100
+ </div>
101
+ )}
102
+
103
+ {oauthStatus === "success" && oauthResult && (
104
+ <div className="flex flex-col items-center gap-3 text-center">
105
+ <CheckCircle2
106
+ aria-hidden="true"
107
+ className="h-8 w-8 text-green-600"
108
+ />
109
+ <div>
110
+ <p className="text-sm font-medium text-green-700">
111
+ Conectado com sucesso!
112
+ </p>
113
+ {oauthResult.email && (
114
+ <p className="text-xs text-muted-foreground">
115
+ {oauthResult.email}
116
+ </p>
117
+ )}
118
+ </div>
119
+ </div>
120
+ )}
121
+
122
+ {oauthStatus === "error" && (
123
+ <div className="flex flex-col items-center gap-3 text-center">
124
+ <AlertCircle
125
+ aria-hidden="true"
126
+ className="h-8 w-8 text-destructive"
127
+ />
128
+ <div>
129
+ <p className="text-sm font-medium text-destructive">
130
+ Falha na conexão
131
+ </p>
132
+ {oauthResult?.error && (
133
+ <p className="text-xs text-muted-foreground">
134
+ {oauthResult.error}
135
+ </p>
136
+ )}
137
+ </div>
138
+ <Button variant="outline" onClick={onStartOAuth} size="sm">
139
+ Tentar novamente
140
+ </Button>
141
+ </div>
142
+ )}
143
+ </div>
144
+
145
+ {/* Security note */}
146
+ <div className="flex items-start gap-2 rounded-md bg-muted/50 p-3">
147
+ <Shield
148
+ aria-hidden="true"
149
+ className="mt-0.5 h-4 w-4 shrink-0 text-green-600"
150
+ />
151
+ <p className="text-xs text-muted-foreground">
152
+ Seus dados estão seguros. Usamos OAuth 2.0 para autenticação — nunca
153
+ armazenamos sua senha. Você pode revogar o acesso a qualquer momento
154
+ nas configurações da sua conta {providerLabel}.
155
+ </p>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // API Key flow UI
163
+ // ---------------------------------------------------------------------------
164
+
165
+ function ApiKeyCredentials({
166
+ apiKey,
167
+ onApiKeyChange,
168
+ }: {
169
+ apiKey: string;
170
+ onApiKeyChange: (value: string) => void;
171
+ }) {
172
+ return (
173
+ <div className="space-y-6">
174
+ <div className="space-y-2">
175
+ <h3 className="text-lg font-semibold">Autenticação</h3>
176
+ <p className="text-sm text-muted-foreground">
177
+ Insira a chave de API para conectar a integração.
178
+ </p>
179
+ </div>
180
+
181
+ <div className="space-y-2">
182
+ <Label htmlFor="integration-api-key">Chave de API</Label>
183
+ <Input
184
+ id="integration-api-key"
185
+ type="password"
186
+ autoComplete="off"
187
+ placeholder="Insira sua chave de API..."
188
+ value={apiKey}
189
+ onChange={(e) => onApiKeyChange(e.target.value)}
190
+ />
191
+ </div>
192
+
193
+ <div className="flex items-start gap-2 rounded-md bg-muted/50 p-3">
194
+ <Shield
195
+ aria-hidden="true"
196
+ className="mt-0.5 h-4 w-4 shrink-0 text-green-600"
197
+ />
198
+ <p className="text-xs text-muted-foreground">
199
+ Sua chave de API é armazenada de forma segura e encriptada. Nunca é
200
+ exposta no frontend.
201
+ </p>
202
+ </div>
203
+ </div>
204
+ );
205
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { Check, Info } from "lucide-react";
4
+ import type { IntegrationDefinition } from "../../../data/integrations-registry";
5
+ import type { WizardIntegrationMeta } from "../types";
6
+
7
+ interface InfoStepProps {
8
+ integration: IntegrationDefinition;
9
+ meta: WizardIntegrationMeta;
10
+ }
11
+
12
+ export function InfoStep({ integration, meta }: InfoStepProps) {
13
+ return (
14
+ <div className="space-y-6">
15
+ {/* Header */}
16
+ <div className="flex items-start gap-4">
17
+ {meta.icon && (
18
+ <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-muted">
19
+ {meta.icon}
20
+ </div>
21
+ )}
22
+ <div className="space-y-1">
23
+ <h3 className="text-lg font-semibold">{integration.name}</h3>
24
+ <p className="text-sm text-muted-foreground">
25
+ {integration.description}
26
+ </p>
27
+ </div>
28
+ </div>
29
+
30
+ {/* Capabilities */}
31
+ {meta.capabilities.length > 0 && (
32
+ <div className="space-y-3">
33
+ <h4 className="text-sm font-medium">O que esta integração permite:</h4>
34
+ <ul className="space-y-2">
35
+ {meta.capabilities.map((cap, i) => (
36
+ <li key={i} className="flex items-start gap-2 text-sm">
37
+ <Check
38
+ aria-hidden="true"
39
+ className="mt-0.5 h-4 w-4 shrink-0 text-green-600"
40
+ />
41
+ <div>
42
+ <span className="font-medium">{cap.label}</span>
43
+ {cap.description && (
44
+ <span className="text-muted-foreground">
45
+ {" "}
46
+ — {cap.description}
47
+ </span>
48
+ )}
49
+ </div>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </div>
54
+ )}
55
+
56
+ {/* Requirements */}
57
+ {meta.requirements.length > 0 && (
58
+ <div className="space-y-3">
59
+ <h4 className="text-sm font-medium">Requisitos:</h4>
60
+ <ul className="space-y-2">
61
+ {meta.requirements.map((req, i) => (
62
+ <li
63
+ key={i}
64
+ className="flex items-start gap-2 text-sm text-muted-foreground"
65
+ >
66
+ <Info
67
+ aria-hidden="true"
68
+ className="mt-0.5 h-4 w-4 shrink-0 text-blue-500"
69
+ />
70
+ <span>{req}</span>
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ </div>
75
+ )}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,23 @@
1
+ export type IntegrationAuthType = "oauth2" | "api_key" | "none";
2
+ export type IntegrationStatus = "available" | "coming_soon";
3
+
4
+ export interface IntegrationDefinition {
5
+ slug: string;
6
+ name: string;
7
+ description: string;
8
+ icon: string; // Lucide icon name
9
+ authType: IntegrationAuthType;
10
+ status: IntegrationStatus;
11
+ }
12
+
13
+ export const INTEGRATIONS_REGISTRY: IntegrationDefinition[] = [
14
+ {
15
+ slug: "google_calendar",
16
+ name: "Google Agenda",
17
+ description: "Sincronize agendamentos com o Google Calendar",
18
+ icon: "CalendarSync",
19
+ authType: "oauth2",
20
+ status: "available",
21
+ },
22
+ // Future integrations — add entries here without code changes
23
+ ];
@@ -28,3 +28,13 @@ export {
28
28
 
29
29
  // Contacts
30
30
  export { useGagentsContacts } from "./use-contacts";
31
+
32
+ // Capabilities
33
+ export { useCapabilities, useAgentCapabilities, useUpdateAgentCapabilities } from "./use-capabilities";
34
+
35
+ // Integrations
36
+ export {
37
+ useIntegrationState,
38
+ type IntegrationCardState,
39
+ type IntegrationCardData,
40
+ } from "./use-integrations";