@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.
@@ -0,0 +1,406 @@
1
+ import { useState } from "react";
2
+ import type { Agent, Objective } from "../../types";
3
+ import type { GagentsHookConfig } from "../../hooks/types";
4
+ import {
5
+ useObjectives,
6
+ useCreateObjective,
7
+ useUpdateObjective,
8
+ useDeleteObjective,
9
+ } from "../../hooks";
10
+ import {
11
+ Input,
12
+ Button,
13
+ Switch,
14
+ Skeleton,
15
+ Textarea,
16
+ Label,
17
+ Badge,
18
+ Dialog,
19
+ DialogContent,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ DialogFooter,
23
+ AlertDialog,
24
+ AlertDialogAction,
25
+ AlertDialogCancel,
26
+ AlertDialogContent,
27
+ AlertDialogDescription,
28
+ AlertDialogFooter,
29
+ AlertDialogHeader,
30
+ AlertDialogTitle,
31
+ } from "@greatapps/greatauth-ui/ui";
32
+ import {
33
+ Sortable,
34
+ SortableContent,
35
+ SortableItem,
36
+ SortableItemHandle,
37
+ SortableOverlay,
38
+ } from "../ui/sortable";
39
+ import { Trash2, Target, Pencil, Plus, GripVertical } from "lucide-react";
40
+ import { toast } from "sonner";
41
+
42
+ interface AgentObjectivesListProps {
43
+ agent: Agent;
44
+ config: GagentsHookConfig;
45
+ }
46
+
47
+ function slugify(text: string): string {
48
+ return text
49
+ .toLowerCase()
50
+ .normalize("NFD")
51
+ .replace(/[\u0300-\u036f]/g, "")
52
+ .replace(/[^a-z0-9]+/g, "-")
53
+ .replace(/^-+|-+$/g, "");
54
+ }
55
+
56
+ interface ObjectiveFormState {
57
+ title: string;
58
+ slug: string;
59
+ prompt: string;
60
+ }
61
+
62
+ const EMPTY_FORM: ObjectiveFormState = { title: "", slug: "", prompt: "" };
63
+
64
+ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps) {
65
+ const { data: objectivesData, isLoading } = useObjectives(config, agent.id);
66
+ const createMutation = useCreateObjective(config);
67
+ const updateMutation = useUpdateObjective(config);
68
+ const deleteMutation = useDeleteObjective(config);
69
+
70
+ const [formOpen, setFormOpen] = useState(false);
71
+ const [editTarget, setEditTarget] = useState<Objective | null>(null);
72
+ const [form, setForm] = useState<ObjectiveFormState>(EMPTY_FORM);
73
+ const [slugManual, setSlugManual] = useState(false);
74
+ const [removeTarget, setRemoveTarget] = useState<Objective | null>(null);
75
+
76
+ const objectives = objectivesData?.data || [];
77
+ const sortedObjectives = [...objectives].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
78
+
79
+ async function handleReorder(newItems: Objective[]) {
80
+ const updates = newItems
81
+ .map((item, index) => ({ ...item, order: index + 1 }))
82
+ .filter((item, index) => sortedObjectives[index]?.id !== item.id);
83
+
84
+ try {
85
+ for (const item of updates) {
86
+ await updateMutation.mutateAsync({
87
+ idAgent: agent.id,
88
+ id: item.id,
89
+ body: { order: item.order },
90
+ });
91
+ }
92
+ toast.success("Ordem atualizada");
93
+ } catch {
94
+ toast.error("Erro ao reordenar objetivos");
95
+ }
96
+ }
97
+
98
+ function openCreate() {
99
+ setEditTarget(null);
100
+ setForm(EMPTY_FORM);
101
+ setSlugManual(false);
102
+ setFormOpen(true);
103
+ }
104
+
105
+ function openEdit(objective: Objective) {
106
+ setEditTarget(objective);
107
+ setForm({
108
+ title: objective.title,
109
+ slug: objective.slug || "",
110
+ prompt: objective.prompt || "",
111
+ });
112
+ setSlugManual(true);
113
+ setFormOpen(true);
114
+ }
115
+
116
+ async function handleSubmit() {
117
+ if (!form.title.trim()) return;
118
+
119
+ const effectiveSlug = form.slug.trim() || slugify(form.title);
120
+ const nextOrder =
121
+ sortedObjectives.length > 0
122
+ ? Math.max(...sortedObjectives.map((o) => o.order)) + 1
123
+ : 1;
124
+
125
+ try {
126
+ if (editTarget) {
127
+ await updateMutation.mutateAsync({
128
+ idAgent: agent.id,
129
+ id: editTarget.id,
130
+ body: {
131
+ title: form.title.trim(),
132
+ slug: effectiveSlug,
133
+ prompt: form.prompt.trim() || null,
134
+ },
135
+ });
136
+ toast.success("Objetivo atualizado");
137
+ } else {
138
+ await createMutation.mutateAsync({
139
+ idAgent: agent.id,
140
+ body: {
141
+ title: form.title.trim(),
142
+ slug: effectiveSlug,
143
+ prompt: form.prompt.trim() || null,
144
+ order: nextOrder,
145
+ },
146
+ });
147
+ toast.success("Objetivo criado");
148
+ }
149
+ setFormOpen(false);
150
+ } catch (err) {
151
+ toast.error(
152
+ err instanceof Error
153
+ ? err.message
154
+ : editTarget
155
+ ? "Erro ao atualizar objetivo"
156
+ : "Erro ao criar objetivo",
157
+ );
158
+ }
159
+ }
160
+
161
+ async function handleToggleActive(objective: Objective, checked: boolean) {
162
+ try {
163
+ await updateMutation.mutateAsync({
164
+ idAgent: agent.id,
165
+ id: objective.id,
166
+ body: { active: checked },
167
+ });
168
+ toast.success(checked ? "Objetivo ativado" : "Objetivo desativado");
169
+ } catch (err) {
170
+ toast.error(
171
+ err instanceof Error ? err.message : "Erro ao alterar estado do objetivo",
172
+ );
173
+ }
174
+ }
175
+
176
+ async function handleRemove() {
177
+ if (!removeTarget) return;
178
+ try {
179
+ await deleteMutation.mutateAsync({
180
+ idAgent: agent.id,
181
+ id: removeTarget.id,
182
+ });
183
+ toast.success("Objetivo removido");
184
+ } catch (err) {
185
+ toast.error(
186
+ err instanceof Error ? err.message : "Erro ao remover o objetivo",
187
+ );
188
+ } finally {
189
+ setRemoveTarget(null);
190
+ }
191
+ }
192
+
193
+ if (isLoading) {
194
+ return (
195
+ <div className="space-y-3 p-4">
196
+ {Array.from({ length: 3 }).map((_, i) => (
197
+ <Skeleton key={i} className="h-14 w-full" />
198
+ ))}
199
+ </div>
200
+ );
201
+ }
202
+
203
+ return (
204
+ <div className="space-y-4 p-4">
205
+ <div className="flex items-center justify-between">
206
+ <div>
207
+ <h3 className="text-sm font-medium text-muted-foreground">
208
+ {sortedObjectives.length} objetivo{sortedObjectives.length !== 1 ? "s" : ""} definido{sortedObjectives.length !== 1 ? "s" : ""}
209
+ </h3>
210
+ <p className="text-xs text-muted-foreground">
211
+ Objetivos são modos de conversa que o agente ativa automaticamente conforme a intenção do utilizador.
212
+ </p>
213
+ </div>
214
+ <Button onClick={openCreate} size="sm">
215
+ <Plus className="mr-2 h-4 w-4" />
216
+ Novo Objetivo
217
+ </Button>
218
+ </div>
219
+
220
+ {sortedObjectives.length === 0 ? (
221
+ <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-8 text-center">
222
+ <Target className="mb-2 h-8 w-8 text-muted-foreground" />
223
+ <p className="text-sm text-muted-foreground">
224
+ Nenhum objetivo definido. Adicione objetivos para orientar o agente em diferentes contextos.
225
+ </p>
226
+ </div>
227
+ ) : (
228
+ <Sortable
229
+ value={sortedObjectives}
230
+ onValueChange={handleReorder}
231
+ getItemValue={(item) => item.id}
232
+ >
233
+ <SortableContent className="space-y-2">
234
+ {sortedObjectives.map((objective) => (
235
+ <SortableItem
236
+ key={objective.id}
237
+ value={objective.id}
238
+ className="flex items-center gap-3 rounded-lg border bg-card p-3"
239
+ >
240
+ <SortableItemHandle className="shrink-0 text-muted-foreground hover:text-foreground">
241
+ <GripVertical className="h-5 w-5" />
242
+ </SortableItemHandle>
243
+
244
+ <div className="flex flex-1 flex-col gap-1 min-w-0">
245
+ <div className="flex items-center gap-2">
246
+ <span className="truncate font-medium">
247
+ {objective.title}
248
+ </span>
249
+ {objective.slug && (
250
+ <Badge variant="secondary" className="shrink-0 text-xs font-mono">
251
+ {objective.slug}
252
+ </Badge>
253
+ )}
254
+ </div>
255
+ {objective.prompt && (
256
+ <p className="line-clamp-2 text-xs text-muted-foreground">
257
+ {objective.prompt}
258
+ </p>
259
+ )}
260
+ </div>
261
+
262
+ <Switch
263
+ checked={objective.active}
264
+ onCheckedChange={(checked) =>
265
+ handleToggleActive(objective, checked)
266
+ }
267
+ disabled={updateMutation.isPending}
268
+ />
269
+
270
+ <Button
271
+ variant="ghost"
272
+ size="icon"
273
+ className="shrink-0 text-muted-foreground hover:text-foreground"
274
+ onClick={() => openEdit(objective)}
275
+ >
276
+ <Pencil className="h-4 w-4" />
277
+ </Button>
278
+
279
+ <Button
280
+ variant="ghost"
281
+ size="icon"
282
+ className="shrink-0 text-muted-foreground hover:text-destructive"
283
+ onClick={() => setRemoveTarget(objective)}
284
+ >
285
+ <Trash2 className="h-4 w-4" />
286
+ </Button>
287
+ </SortableItem>
288
+ ))}
289
+ </SortableContent>
290
+ <SortableOverlay>
291
+ {({ value }) => {
292
+ const obj = sortedObjectives.find((o) => o.id === value);
293
+ return (
294
+ <div className="flex items-center gap-3 rounded-lg border bg-card p-3 shadow-lg">
295
+ <GripVertical className="h-5 w-5 text-muted-foreground" />
296
+ <span className="font-medium">{obj?.title}</span>
297
+ </div>
298
+ );
299
+ }}
300
+ </SortableOverlay>
301
+ </Sortable>
302
+ )}
303
+
304
+ {/* Create/Edit Dialog */}
305
+ <Dialog open={formOpen} onOpenChange={setFormOpen}>
306
+ <DialogContent className="sm:max-w-lg">
307
+ <DialogHeader>
308
+ <DialogTitle>
309
+ {editTarget ? "Editar Objetivo" : "Novo Objetivo"}
310
+ </DialogTitle>
311
+ </DialogHeader>
312
+ <div className="space-y-4">
313
+ <div className="space-y-2">
314
+ <Label>Título *</Label>
315
+ <Input
316
+ value={form.title}
317
+ onChange={(e) => {
318
+ const title = e.target.value;
319
+ setForm((f) => ({
320
+ ...f,
321
+ title,
322
+ ...(!slugManual ? { slug: slugify(title) } : {}),
323
+ }));
324
+ }}
325
+ placeholder="Ex: Agendar Consulta"
326
+ />
327
+ </div>
328
+
329
+ <div className="space-y-2">
330
+ <Label>Slug (identificador) *</Label>
331
+ <Input
332
+ value={form.slug}
333
+ onChange={(e) => {
334
+ setSlugManual(true);
335
+ setForm((f) => ({ ...f, slug: e.target.value }));
336
+ }}
337
+ placeholder="Ex: agendar-consulta"
338
+ className="font-mono"
339
+ />
340
+ <p className="text-xs text-muted-foreground">
341
+ Gerado automaticamente. Usado pelo agente para identificar o objetivo.
342
+ </p>
343
+ </div>
344
+
345
+ <div className="space-y-2">
346
+ <Label>Instruções do Objetivo</Label>
347
+ <Textarea
348
+ value={form.prompt}
349
+ onChange={(e) =>
350
+ setForm((f) => ({ ...f, prompt: e.target.value }))
351
+ }
352
+ placeholder="Instruções detalhadas que o agente seguirá quando este objetivo for ativado. Ex: passos para agendar consulta, perguntas a fazer, validações necessárias..."
353
+ rows={8}
354
+ />
355
+ <p className="text-xs text-muted-foreground">
356
+ Estas instruções são carregadas automaticamente quando o agente detecta que o utilizador precisa deste objetivo.
357
+ </p>
358
+ </div>
359
+ </div>
360
+ <DialogFooter>
361
+ <Button
362
+ variant="outline"
363
+ onClick={() => setFormOpen(false)}
364
+ >
365
+ Cancelar
366
+ </Button>
367
+ <Button
368
+ onClick={handleSubmit}
369
+ disabled={
370
+ !form.title.trim() ||
371
+ createMutation.isPending ||
372
+ updateMutation.isPending
373
+ }
374
+ >
375
+ {editTarget ? "Salvar" : "Criar"}
376
+ </Button>
377
+ </DialogFooter>
378
+ </DialogContent>
379
+ </Dialog>
380
+
381
+ {/* Delete confirmation */}
382
+ <AlertDialog
383
+ open={!!removeTarget}
384
+ onOpenChange={(open) => !open && setRemoveTarget(null)}
385
+ >
386
+ <AlertDialogContent>
387
+ <AlertDialogHeader>
388
+ <AlertDialogTitle>Remover objetivo?</AlertDialogTitle>
389
+ <AlertDialogDescription>
390
+ O objetivo será removido permanentemente.
391
+ </AlertDialogDescription>
392
+ </AlertDialogHeader>
393
+ <AlertDialogFooter>
394
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
395
+ <AlertDialogAction
396
+ onClick={handleRemove}
397
+ disabled={deleteMutation.isPending}
398
+ >
399
+ Remover
400
+ </AlertDialogAction>
401
+ </AlertDialogFooter>
402
+ </AlertDialogContent>
403
+ </AlertDialog>
404
+ </div>
405
+ );
406
+ }