@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.
Files changed (38) hide show
  1. package/dist/index.d.ts +204 -1
  2. package/dist/index.js +1797 -125
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/client/index.ts +14 -0
  6. package/src/components/agents/agent-edit-form.tsx +4 -1
  7. package/src/components/agents/agent-form-dialog.tsx +5 -1
  8. package/src/components/agents/agent-objectives-list.tsx +15 -6
  9. package/src/components/agents/agent-prompt-editor.tsx +9 -5
  10. package/src/components/agents/agent-tabs.tsx +4 -4
  11. package/src/components/agents/agent-tools-list.tsx +12 -5
  12. package/src/components/agents/agents-table.tsx +7 -2
  13. package/src/components/capabilities/advanced-tab.tsx +82 -0
  14. package/src/components/capabilities/capabilities-tab.tsx +475 -0
  15. package/src/components/capabilities/integration-card.tsx +162 -0
  16. package/src/components/capabilities/integration-wizard.tsx +537 -0
  17. package/src/components/capabilities/integrations-tab.tsx +61 -0
  18. package/src/components/capabilities/types.ts +48 -0
  19. package/src/components/capabilities/wizard-steps/config-step.tsx +117 -0
  20. package/src/components/capabilities/wizard-steps/confirm-step.tsx +123 -0
  21. package/src/components/capabilities/wizard-steps/credentials-step.tsx +205 -0
  22. package/src/components/capabilities/wizard-steps/info-step.tsx +78 -0
  23. package/src/components/conversations/agent-conversations-table.tsx +13 -2
  24. package/src/components/conversations/conversation-view.tsx +2 -2
  25. package/src/components/tools/tool-credentials-form.tsx +34 -14
  26. package/src/components/tools/tool-form-dialog.tsx +9 -5
  27. package/src/components/tools/tools-table.tsx +8 -3
  28. package/src/data/integrations-registry.ts +23 -0
  29. package/src/hooks/index.ts +10 -0
  30. package/src/hooks/use-capabilities.ts +50 -0
  31. package/src/hooks/use-integrations.ts +114 -0
  32. package/src/index.ts +34 -0
  33. package/src/pages/agent-capabilities-page.tsx +159 -0
  34. package/src/pages/agent-detail-page.tsx +1 -0
  35. package/src/pages/index.ts +2 -0
  36. package/src/pages/integrations-management-page.tsx +166 -0
  37. package/src/types/capabilities.ts +32 -0
  38. package/src/types/index.ts +10 -0
@@ -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
+ }
@@ -89,11 +89,21 @@ export function AgentConversationsTable({
89
89
  <TableRow
90
90
  key={conversation.id}
91
91
  className="cursor-pointer"
92
+ role="button"
93
+ tabIndex={0}
92
94
  onClick={() =>
93
95
  setSelectedId(
94
96
  selectedId === conversation.id ? null : conversation.id,
95
97
  )
96
98
  }
99
+ onKeyDown={(e) => {
100
+ if (e.key === "Enter" || e.key === " ") {
101
+ e.preventDefault();
102
+ setSelectedId(
103
+ selectedId === conversation.id ? null : conversation.id,
104
+ );
105
+ }
106
+ }}
97
107
  data-state={selectedId === conversation.id ? "selected" : undefined}
98
108
  >
99
109
  <TableCell className="font-mono text-xs">
@@ -111,10 +121,10 @@ export function AgentConversationsTable({
111
121
  <span className="text-xs text-muted-foreground">—</span>
112
122
  )}
113
123
  </TableCell>
114
- <TableCell className="text-right">
124
+ <TableCell className="text-right tabular-nums">
115
125
  {conversation.message_count ?? "—"}
116
126
  </TableCell>
117
- <TableCell className="text-right">
127
+ <TableCell className="text-right tabular-nums">
118
128
  {conversation.usage_tokens ?? "—"}
119
129
  </TableCell>
120
130
  <TableCell>
@@ -133,6 +143,7 @@ export function AgentConversationsTable({
133
143
  <a
134
144
  href={`/gchat/inbox/${conversation.id_external}`}
135
145
  title="Ver no Chat"
146
+ aria-label="Ver no Chat"
136
147
  onClick={(e) => e.stopPropagation()}
137
148
  className="inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
138
149
  >
@@ -45,7 +45,7 @@ export function ConversationView({
45
45
  <p className="text-sm text-muted-foreground">
46
46
  Conversa não encontrada.
47
47
  </p>
48
- <Button variant="ghost" size="icon" onClick={onClose}>
48
+ <Button variant="ghost" size="icon" aria-label="Fechar" onClick={onClose}>
49
49
  <X className="h-4 w-4" />
50
50
  </Button>
51
51
  </div>
@@ -58,7 +58,7 @@ export function ConversationView({
58
58
  <div className="flex items-center justify-between">
59
59
  <h3 className="font-semibold">Detalhes da conversa #{conversation.id}</h3>
60
60
  <div className="flex items-center gap-1">
61
- <Button variant="ghost" size="icon" onClick={onClose}>
61
+ <Button variant="ghost" size="icon" aria-label="Fechar" onClick={onClose}>
62
62
  <X className="h-4 w-4" />
63
63
  </Button>
64
64
  </div>