@greatapps/greatagents-ui 0.1.0 → 0.2.1

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.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,18 +28,28 @@
28
28
  "lucide-react": "*",
29
29
  "date-fns": "*",
30
30
  "sonner": "*",
31
- "@greatapps/greatauth-ui": "*"
31
+ "@greatapps/greatauth-ui": "*",
32
+ "@dnd-kit/core": "*",
33
+ "@dnd-kit/modifiers": "*",
34
+ "@dnd-kit/sortable": "*",
35
+ "@dnd-kit/utilities": "*",
36
+ "radix-ui": "*"
32
37
  },
33
38
  "dependencies": {
34
39
  "clsx": "^2",
35
40
  "tailwind-merge": "^3"
36
41
  },
37
42
  "devDependencies": {
38
- "@greatapps/greatauth-ui": "0.3.7",
43
+ "@dnd-kit/core": "latest",
44
+ "@dnd-kit/modifiers": "latest",
45
+ "@dnd-kit/sortable": "latest",
46
+ "@dnd-kit/utilities": "latest",
47
+ "@greatapps/greatauth-ui": "^0.3.9",
39
48
  "@tanstack/react-query": "latest",
40
49
  "@tanstack/react-table": "latest",
41
50
  "@types/react": "latest",
42
51
  "@types/react-dom": "latest",
52
+ "radix-ui": "latest",
43
53
  "react": "^19",
44
54
  "react-dom": "^19",
45
55
  "tsup": "latest",
@@ -0,0 +1,218 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useUpdateAgent } from "../../hooks";
3
+ import type { Agent } from "../../types";
4
+ import type { GagentsHookConfig } from "../../hooks/types";
5
+ import {
6
+ Button,
7
+ Input,
8
+ Label,
9
+ Switch,
10
+ Dialog,
11
+ DialogContent,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogFooter,
15
+ } from "@greatapps/greatauth-ui/ui";
16
+ import { Loader2 } from "lucide-react";
17
+ import { toast } from "sonner";
18
+ import { ImageCropUpload } from "@greatapps/greatauth-ui";
19
+
20
+ interface AgentEditFormProps {
21
+ config: GagentsHookConfig;
22
+ agent: Agent;
23
+ idAccount: string | number | null;
24
+ open?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
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): 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
+ };
57
+ }
58
+
59
+ export function AgentEditForm({ config, agent, idAccount, open, onOpenChange }: AgentEditFormProps) {
60
+ const updateAgent = useUpdateAgent(config);
61
+ const [form, setForm] = useState<FormState>(() => agentToFormState(agent));
62
+
63
+ useEffect(() => {
64
+ setForm(agentToFormState(agent));
65
+ }, [agent]);
66
+
67
+ function updateField<K extends keyof FormState>(key: K, value: FormState[K]) {
68
+ setForm((prev) => ({ ...prev, [key]: value }));
69
+ }
70
+
71
+ async function handleSubmit(e: React.FormEvent) {
72
+ e.preventDefault();
73
+
74
+ if (!form.title.trim()) {
75
+ updateField("titleError", true);
76
+ return;
77
+ }
78
+
79
+ const body: Record<string, unknown> = {
80
+ title: form.title.trim(),
81
+ active: form.active,
82
+ };
83
+ if (form.photo.trim()) body.photo = form.photo.trim();
84
+ else body.photo = "";
85
+
86
+ const delayMs = secondsToMs(form.delayTyping);
87
+ if (delayMs !== undefined) body.delay_typing = delayMs;
88
+ else body.delay_typing = 0;
89
+
90
+ const waitingMs = secondsToMs(form.waitingTime);
91
+ if (waitingMs !== undefined) body.waiting_time = waitingMs;
92
+ else body.waiting_time = 0;
93
+
94
+ try {
95
+ await updateAgent.mutateAsync({ id: agent.id, body });
96
+ toast.success("Agente atualizado");
97
+ onOpenChange?.(false);
98
+ } catch {
99
+ toast.error("Erro ao atualizar agente");
100
+ }
101
+ }
102
+
103
+ const formContent = (
104
+ <form onSubmit={handleSubmit} className="space-y-4">
105
+ <div className="flex justify-center">
106
+ <ImageCropUpload
107
+ value={form.photo || null}
108
+ onChange={(url) => updateField("photo", url)}
109
+ onRemove={() => updateField("photo", "")}
110
+ entityType="agents"
111
+ entityId={agent.id}
112
+ idAccount={typeof idAccount === "string" ? Number(idAccount) : (idAccount ?? 0)}
113
+ name={form.title || null}
114
+ disabled={updateAgent.isPending}
115
+ />
116
+ </div>
117
+
118
+ <div className="space-y-2">
119
+ <Label htmlFor="edit-title">Nome do Agente *</Label>
120
+ <Input
121
+ id="edit-title"
122
+ value={form.title}
123
+ onChange={(e) => {
124
+ setForm((prev) => ({
125
+ ...prev,
126
+ title: e.target.value,
127
+ titleError: e.target.value.trim() ? false : prev.titleError,
128
+ }));
129
+ }}
130
+ placeholder="Ex: Assistente de Agendamento"
131
+ disabled={updateAgent.isPending}
132
+ />
133
+ {form.titleError && (
134
+ <p className="text-sm text-destructive">Nome é obrigatório</p>
135
+ )}
136
+ </div>
137
+
138
+ <div className="flex items-center gap-3">
139
+ <Switch
140
+ id="edit-active"
141
+ checked={form.active}
142
+ onCheckedChange={(checked) => updateField("active", checked)}
143
+ disabled={updateAgent.isPending}
144
+ />
145
+ <Label htmlFor="edit-active" className="cursor-pointer">
146
+ {form.active ? "Ativo" : "Inativo"}
147
+ </Label>
148
+ </div>
149
+
150
+ <div className="grid grid-cols-2 gap-4">
151
+ <div className="space-y-2">
152
+ <Label htmlFor="edit-delay">Delay de Digitação (s)</Label>
153
+ <Input
154
+ id="edit-delay"
155
+ type="number"
156
+ value={form.delayTyping}
157
+ onChange={(e) => updateField("delayTyping", e.target.value)}
158
+ placeholder="0"
159
+ min="0"
160
+ step="0.5"
161
+ disabled={updateAgent.isPending}
162
+ />
163
+ <p className="text-xs text-muted-foreground">
164
+ Tempo de simulação de digitação
165
+ </p>
166
+ </div>
167
+ <div className="space-y-2">
168
+ <Label htmlFor="edit-waiting">Tempo de Espera (s)</Label>
169
+ <Input
170
+ id="edit-waiting"
171
+ type="number"
172
+ value={form.waitingTime}
173
+ onChange={(e) => updateField("waitingTime", e.target.value)}
174
+ placeholder="0"
175
+ min="0"
176
+ step="0.5"
177
+ disabled={updateAgent.isPending}
178
+ />
179
+ <p className="text-xs text-muted-foreground">
180
+ Espera por mensagens agrupadas
181
+ </p>
182
+ </div>
183
+ </div>
184
+
185
+ <DialogFooter>
186
+ <Button
187
+ type="button"
188
+ variant="outline"
189
+ onClick={() => onOpenChange?.(false)}
190
+ disabled={updateAgent.isPending}
191
+ >
192
+ Cancelar
193
+ </Button>
194
+ <Button type="submit" disabled={updateAgent.isPending}>
195
+ {updateAgent.isPending && (
196
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
197
+ )}
198
+ Salvar
199
+ </Button>
200
+ </DialogFooter>
201
+ </form>
202
+ );
203
+
204
+ if (open !== undefined && onOpenChange) {
205
+ return (
206
+ <Dialog open={open} onOpenChange={onOpenChange}>
207
+ <DialogContent className="sm:max-w-md">
208
+ <DialogHeader>
209
+ <DialogTitle>Editar Agente</DialogTitle>
210
+ </DialogHeader>
211
+ {formContent}
212
+ </DialogContent>
213
+ </Dialog>
214
+ );
215
+ }
216
+
217
+ return <div className="max-w-lg pt-4">{formContent}</div>;
218
+ }
@@ -0,0 +1,177 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useCreateAgent, useUpdateAgent } from "../../hooks";
3
+ import type { Agent } from "../../types";
4
+ import type { GagentsHookConfig } from "../../hooks/types";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogFooter,
11
+ Button,
12
+ Input,
13
+ Textarea,
14
+ Label,
15
+ } from "@greatapps/greatauth-ui/ui";
16
+ import { Loader2 } from "lucide-react";
17
+ import { toast } from "sonner";
18
+
19
+ interface AgentFormDialogProps {
20
+ config: GagentsHookConfig;
21
+ open: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ agent?: Agent;
24
+ }
25
+
26
+ export function AgentFormDialog({
27
+ config,
28
+ open,
29
+ onOpenChange,
30
+ agent,
31
+ }: AgentFormDialogProps) {
32
+ const isEditing = !!agent;
33
+ const createAgent = useCreateAgent(config);
34
+ const updateAgent = useUpdateAgent(config);
35
+
36
+ const [title, setTitle] = useState("");
37
+ const [prompt, setPrompt] = useState("");
38
+ const [photo, setPhoto] = useState("");
39
+ const [delayTyping, setDelayTyping] = useState("");
40
+ const [waitingTime, setWaitingTime] = useState("");
41
+
42
+ /* eslint-disable react-hooks/set-state-in-effect -- form state sync from props */
43
+ useEffect(() => {
44
+ if (agent) {
45
+ setTitle(agent.title);
46
+ setPrompt(agent.prompt || "");
47
+ setPhoto(agent.photo || "");
48
+ setDelayTyping(agent.delay_typing != null ? String(agent.delay_typing) : "");
49
+ setWaitingTime(agent.waiting_time != null ? String(agent.waiting_time) : "");
50
+ } else {
51
+ setTitle("");
52
+ setPrompt("");
53
+ setPhoto("");
54
+ setDelayTyping("");
55
+ setWaitingTime("");
56
+ }
57
+ }, [agent, open]);
58
+ /* eslint-enable react-hooks/set-state-in-effect */
59
+
60
+ const isPending = createAgent.isPending || updateAgent.isPending;
61
+
62
+ async function handleSubmit(e: React.FormEvent) {
63
+ e.preventDefault();
64
+ if (!title.trim()) return;
65
+
66
+ const body: Record<string, unknown> = {
67
+ title: title.trim(),
68
+ };
69
+ if (prompt.trim()) body.prompt = prompt.trim();
70
+ if (photo.trim()) body.photo = photo.trim();
71
+ if (delayTyping.trim()) body.delay_typing = Number(delayTyping);
72
+ if (waitingTime.trim()) body.waiting_time = Number(waitingTime);
73
+
74
+ try {
75
+ if (isEditing) {
76
+ await updateAgent.mutateAsync({ id: agent.id, body });
77
+ toast.success("Agente atualizado");
78
+ } else {
79
+ await createAgent.mutateAsync(
80
+ body as { title: string; prompt?: string; photo?: string; delay_typing?: number; waiting_time?: number },
81
+ );
82
+ toast.success("Agente criado");
83
+ }
84
+ onOpenChange(false);
85
+ } catch {
86
+ toast.error(isEditing ? "Erro ao atualizar agente" : "Erro ao criar agente");
87
+ }
88
+ }
89
+
90
+ return (
91
+ <Dialog open={open} onOpenChange={onOpenChange}>
92
+ <DialogContent className="sm:max-w-lg">
93
+ <DialogHeader>
94
+ <DialogTitle>
95
+ {isEditing ? "Editar Agente" : "Novo Agente"}
96
+ </DialogTitle>
97
+ </DialogHeader>
98
+ <form onSubmit={handleSubmit} className="space-y-4">
99
+ <div className="space-y-2">
100
+ <Label htmlFor="agent-photo">Foto (URL)</Label>
101
+ <Input
102
+ id="agent-photo"
103
+ value={photo}
104
+ onChange={(e) => setPhoto(e.target.value)}
105
+ placeholder="https://exemplo.com/foto.jpg"
106
+ disabled={isPending}
107
+ />
108
+ </div>
109
+ <div className="space-y-2">
110
+ <Label htmlFor="agent-title">Nome do Agente *</Label>
111
+ <Input
112
+ id="agent-title"
113
+ value={title}
114
+ onChange={(e) => setTitle(e.target.value)}
115
+ placeholder="Ex: Assistente de Agendamento"
116
+ required
117
+ disabled={isPending}
118
+ />
119
+ </div>
120
+ <div className="space-y-2">
121
+ <Label htmlFor="agent-prompt">Prompt do Sistema</Label>
122
+ <Textarea
123
+ id="agent-prompt"
124
+ value={prompt}
125
+ onChange={(e) => setPrompt(e.target.value)}
126
+ placeholder="Instruções para o agente AI..."
127
+ rows={6}
128
+ disabled={isPending}
129
+ />
130
+ </div>
131
+ <div className="grid grid-cols-2 gap-4">
132
+ <div className="space-y-2">
133
+ <Label htmlFor="agent-delay">Delay de Digitação (ms)</Label>
134
+ <Input
135
+ id="agent-delay"
136
+ type="number"
137
+ value={delayTyping}
138
+ onChange={(e) => setDelayTyping(e.target.value)}
139
+ placeholder="0"
140
+ min="0"
141
+ disabled={isPending}
142
+ />
143
+ </div>
144
+ <div className="space-y-2">
145
+ <Label htmlFor="agent-waiting">Tempo de Espera (ms)</Label>
146
+ <Input
147
+ id="agent-waiting"
148
+ type="number"
149
+ value={waitingTime}
150
+ onChange={(e) => setWaitingTime(e.target.value)}
151
+ placeholder="0"
152
+ min="0"
153
+ disabled={isPending}
154
+ />
155
+ </div>
156
+ </div>
157
+ <DialogFooter>
158
+ <Button
159
+ type="button"
160
+ variant="outline"
161
+ onClick={() => onOpenChange(false)}
162
+ disabled={isPending}
163
+ >
164
+ Cancelar
165
+ </Button>
166
+ <Button type="submit" disabled={isPending || !title.trim()}>
167
+ {isPending ? (
168
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
169
+ ) : null}
170
+ {isEditing ? "Salvar" : "Criar"}
171
+ </Button>
172
+ </DialogFooter>
173
+ </form>
174
+ </DialogContent>
175
+ </Dialog>
176
+ );
177
+ }