@greatapps/greatagents-ui 0.3.4 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/greatagents-ui",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,62 +10,112 @@ import {
10
10
  DialogFooter,
11
11
  Button,
12
12
  Input,
13
-
14
13
  Label,
14
+ Switch,
15
15
  } from "@greatapps/greatauth-ui/ui";
16
16
  import { Loader2 } from "lucide-react";
17
17
  import { toast } from "sonner";
18
+ import { ImageCropUpload } from "@greatapps/greatauth-ui";
18
19
 
19
20
  interface AgentFormDialogProps {
20
21
  config: GagentsHookConfig;
21
22
  open: boolean;
22
23
  onOpenChange: (open: boolean) => void;
23
24
  agent?: Agent;
25
+ idAccount?: string | number | null;
26
+ }
27
+
28
+ interface FormState {
29
+ title: string;
30
+ photo: string;
31
+ active: boolean;
32
+ delayTyping: string;
33
+ waitingTime: string;
34
+ titleError: boolean;
35
+ }
36
+
37
+ function msToSeconds(ms: number | null | undefined): string {
38
+ if (ms == null || ms === 0) return "";
39
+ return String(Math.round(ms / 1000));
40
+ }
41
+
42
+ function secondsToMs(seconds: string): number | undefined {
43
+ const val = parseFloat(seconds);
44
+ if (isNaN(val) || val <= 0) return undefined;
45
+ return Math.round(val * 1000);
46
+ }
47
+
48
+ function agentToFormState(agent: Agent): FormState {
49
+ return {
50
+ title: agent.title,
51
+ photo: agent.photo || "",
52
+ active: agent.active,
53
+ delayTyping: msToSeconds(agent.delay_typing),
54
+ waitingTime: msToSeconds(agent.waiting_time),
55
+ titleError: false,
56
+ };
24
57
  }
25
58
 
59
+ const emptyFormState: FormState = {
60
+ title: "",
61
+ photo: "",
62
+ active: true,
63
+ delayTyping: "",
64
+ waitingTime: "",
65
+ titleError: false,
66
+ };
67
+
26
68
  export function AgentFormDialog({
27
69
  config,
28
70
  open,
29
71
  onOpenChange,
30
72
  agent,
73
+ idAccount,
31
74
  }: AgentFormDialogProps) {
32
75
  const isEditing = !!agent;
33
76
  const createAgent = useCreateAgent(config);
34
77
  const updateAgent = useUpdateAgent(config);
35
78
 
36
- const [title, setTitle] = useState("");
37
- const [photo, setPhoto] = useState("");
38
- const [delayTyping, setDelayTyping] = useState("");
39
- const [waitingTime, setWaitingTime] = useState("");
79
+ const [form, setForm] = useState<FormState>(emptyFormState);
40
80
 
41
81
  /* eslint-disable react-hooks/set-state-in-effect -- form state sync from props */
42
82
  useEffect(() => {
43
83
  if (agent) {
44
- setTitle(agent.title);
45
- setPhoto(agent.photo || "");
46
- setDelayTyping(agent.delay_typing != null ? String(agent.delay_typing) : "");
47
- setWaitingTime(agent.waiting_time != null ? String(agent.waiting_time) : "");
84
+ setForm(agentToFormState(agent));
48
85
  } else {
49
- setTitle("");
50
- setPhoto("");
51
- setDelayTyping("");
52
- setWaitingTime("");
86
+ setForm(emptyFormState);
53
87
  }
54
88
  }, [agent, open]);
55
89
  /* eslint-enable react-hooks/set-state-in-effect */
56
90
 
91
+ function updateField<K extends keyof FormState>(key: K, value: FormState[K]) {
92
+ setForm((prev) => ({ ...prev, [key]: value }));
93
+ }
94
+
57
95
  const isPending = createAgent.isPending || updateAgent.isPending;
58
96
 
59
97
  async function handleSubmit(e: React.FormEvent) {
60
98
  e.preventDefault();
61
- if (!title.trim()) return;
99
+
100
+ if (!form.title.trim()) {
101
+ updateField("titleError", true);
102
+ return;
103
+ }
62
104
 
63
105
  const body: Record<string, unknown> = {
64
- title: title.trim(),
106
+ title: form.title.trim(),
107
+ active: form.active,
65
108
  };
66
- if (photo.trim()) body.photo = photo.trim();
67
- if (delayTyping.trim()) body.delay_typing = Number(delayTyping);
68
- if (waitingTime.trim()) body.waiting_time = Number(waitingTime);
109
+ if (form.photo.trim()) body.photo = form.photo.trim();
110
+ else if (isEditing) body.photo = "";
111
+
112
+ const delayMs = secondsToMs(form.delayTyping);
113
+ if (delayMs !== undefined) body.delay_typing = delayMs;
114
+ else if (isEditing) body.delay_typing = 0;
115
+
116
+ const waitingMs = secondsToMs(form.waitingTime);
117
+ if (waitingMs !== undefined) body.waiting_time = waitingMs;
118
+ else if (isEditing) body.waiting_time = 0;
69
119
 
70
120
  try {
71
121
  if (isEditing) {
@@ -92,14 +142,15 @@ export function AgentFormDialog({
92
142
  </DialogTitle>
93
143
  </DialogHeader>
94
144
  <form onSubmit={handleSubmit} className="space-y-4">
95
- <div className="space-y-2">
96
- <Label htmlFor="agent-photo">Foto (URL)</Label>
97
- <Input
98
- id="agent-photo"
99
- name="photo"
100
- value={photo}
101
- onChange={(e) => setPhoto(e.target.value)}
102
- placeholder="https://exemplo.com/foto.jpg"
145
+ <div className="flex justify-center">
146
+ <ImageCropUpload
147
+ value={form.photo || null}
148
+ onChange={(url) => updateField("photo", url)}
149
+ onRemove={() => updateField("photo", "")}
150
+ entityType="agents"
151
+ entityId={agent?.id}
152
+ idAccount={typeof idAccount === "string" ? Number(idAccount) : (idAccount ?? Number(config.accountId) ?? 0)}
153
+ name={form.title || null}
103
154
  disabled={isPending}
104
155
  />
105
156
  </div>
@@ -108,39 +159,69 @@ export function AgentFormDialog({
108
159
  <Input
109
160
  id="agent-title"
110
161
  name="title"
111
- value={title}
112
- onChange={(e) => setTitle(e.target.value)}
162
+ value={form.title}
163
+ onChange={(e) => {
164
+ setForm((prev) => ({
165
+ ...prev,
166
+ title: e.target.value,
167
+ titleError: e.target.value.trim() ? false : prev.titleError,
168
+ }));
169
+ }}
113
170
  placeholder="Ex: Assistente de Agendamento"
114
171
  required
115
172
  disabled={isPending}
116
173
  />
174
+ {form.titleError && (
175
+ <p className="text-sm text-destructive">Nome é obrigatório</p>
176
+ )}
117
177
  </div>
178
+
179
+ <div className="flex items-center gap-3">
180
+ <Switch
181
+ id="agent-active"
182
+ checked={form.active}
183
+ onCheckedChange={(checked) => updateField("active", checked)}
184
+ disabled={isPending}
185
+ />
186
+ <Label htmlFor="agent-active" className="cursor-pointer">
187
+ {form.active ? "Ativo" : "Inativo"}
188
+ </Label>
189
+ </div>
190
+
118
191
  <div className="grid grid-cols-2 gap-4">
119
192
  <div className="space-y-2">
120
- <Label htmlFor="agent-delay">Delay de Digitação (ms)</Label>
193
+ <Label htmlFor="agent-delay">Delay de Digitação (s)</Label>
121
194
  <Input
122
195
  id="agent-delay"
123
196
  name="delay"
124
197
  type="number"
125
- value={delayTyping}
126
- onChange={(e) => setDelayTyping(e.target.value)}
198
+ value={form.delayTyping}
199
+ onChange={(e) => updateField("delayTyping", e.target.value)}
127
200
  placeholder="0"
128
201
  min="0"
202
+ step="0.5"
129
203
  disabled={isPending}
130
204
  />
205
+ <p className="text-xs text-muted-foreground">
206
+ Tempo de simulação de digitação
207
+ </p>
131
208
  </div>
132
209
  <div className="space-y-2">
133
- <Label htmlFor="agent-waiting">Tempo de Espera (ms)</Label>
210
+ <Label htmlFor="agent-waiting">Tempo de Espera (s)</Label>
134
211
  <Input
135
212
  id="agent-waiting"
136
213
  name="waiting"
137
214
  type="number"
138
- value={waitingTime}
139
- onChange={(e) => setWaitingTime(e.target.value)}
215
+ value={form.waitingTime}
216
+ onChange={(e) => updateField("waitingTime", e.target.value)}
140
217
  placeholder="0"
141
218
  min="0"
219
+ step="0.5"
142
220
  disabled={isPending}
143
221
  />
222
+ <p className="text-xs text-muted-foreground">
223
+ Espera por mensagens agrupadas
224
+ </p>
144
225
  </div>
145
226
  </div>
146
227
  <DialogFooter>
@@ -152,7 +233,7 @@ export function AgentFormDialog({
152
233
  >
153
234
  Cancelar
154
235
  </Button>
155
- <Button type="submit" disabled={isPending || !title.trim()}>
236
+ <Button type="submit" disabled={isPending || !form.title.trim()}>
156
237
  {isPending ? (
157
238
  <Loader2 aria-hidden="true" className="mr-2 h-4 w-4 animate-spin" />
158
239
  ) : null}
@@ -1,24 +1,46 @@
1
1
  import type { Agent } from "../../types";
2
2
  import type { GagentsHookConfig } from "../../hooks/types";
3
+ import type { IntegrationCardData } from "../../hooks/use-integrations";
4
+ import type { WizardIntegrationMeta } from "../capabilities/types";
5
+ import type { ConfigOption } from "../capabilities/wizard-steps/config-step";
3
6
  import { AgentToolsList } from "./agent-tools-list";
4
7
  import { AgentObjectivesList } from "./agent-objectives-list";
5
8
  import { AgentPromptEditor } from "./agent-prompt-editor";
6
9
  import { AgentConversationsPanel } from "../conversations/agent-conversations-panel";
10
+ import { AgentCapabilitiesPage } from "../../pages/agent-capabilities-page";
7
11
  import {
8
12
  Tabs,
9
13
  TabsList,
10
14
  TabsTrigger,
11
15
  TabsContent,
12
16
  } from "@greatapps/greatauth-ui/ui";
13
- import { Wrench, Target, FileText, MessageCircle } from "lucide-react";
17
+ import { Wrench, Target, FileText, MessageCircle, Blocks } from "lucide-react";
14
18
 
15
19
  interface AgentTabsProps {
16
20
  agent: Agent;
17
21
  config: GagentsHookConfig;
18
22
  renderChatLink?: (inboxId: number) => React.ReactNode;
23
+ /** Required for the Capacidades tab — gagents API URL for OAuth flows and advanced features. Falls back to config.baseUrl. */
24
+ gagentsApiUrl?: string;
25
+ /** Resolve wizard metadata for a given integration card. */
26
+ resolveWizardMeta?: (card: IntegrationCardData) => WizardIntegrationMeta;
27
+ /** Callback to load config options after OAuth completes. */
28
+ loadConfigOptions?: (credentialId: number) => Promise<ConfigOption[]>;
29
+ /** Called after wizard completes successfully. */
30
+ onWizardComplete?: () => void;
19
31
  }
20
32
 
21
- export function AgentTabs({ agent, config, renderChatLink }: AgentTabsProps) {
33
+ export function AgentTabs({
34
+ agent,
35
+ config,
36
+ renderChatLink,
37
+ gagentsApiUrl,
38
+ resolveWizardMeta,
39
+ loadConfigOptions,
40
+ onWizardComplete,
41
+ }: AgentTabsProps) {
42
+ const apiUrl = gagentsApiUrl || config.baseUrl;
43
+
22
44
  return (
23
45
  <Tabs defaultValue="prompt">
24
46
  <TabsList>
@@ -34,6 +56,10 @@ export function AgentTabs({ agent, config, renderChatLink }: AgentTabsProps) {
34
56
  <Wrench aria-hidden="true" className="h-3.5 w-3.5" />
35
57
  Ferramentas
36
58
  </TabsTrigger>
59
+ <TabsTrigger value="capacidades" className="flex items-center gap-1.5">
60
+ <Blocks aria-hidden="true" className="h-3.5 w-3.5" />
61
+ Capacidades
62
+ </TabsTrigger>
37
63
  <TabsTrigger value="conversas" className="flex items-center gap-1.5">
38
64
  <MessageCircle aria-hidden="true" className="h-3.5 w-3.5" />
39
65
  Conversas
@@ -52,6 +78,17 @@ export function AgentTabs({ agent, config, renderChatLink }: AgentTabsProps) {
52
78
  <AgentToolsList agent={agent} config={config} />
53
79
  </TabsContent>
54
80
 
81
+ <TabsContent value="capacidades" className="mt-4">
82
+ <AgentCapabilitiesPage
83
+ config={config}
84
+ agentId={agent.id}
85
+ gagentsApiUrl={apiUrl}
86
+ resolveWizardMeta={resolveWizardMeta}
87
+ loadConfigOptions={loadConfigOptions}
88
+ onWizardComplete={onWizardComplete}
89
+ />
90
+ </TabsContent>
91
+
55
92
  <TabsContent value="conversas" className="mt-4">
56
93
  <AgentConversationsPanel
57
94
  agent={agent}