@greatapps/greatagents-ui 0.1.0 → 0.2.0
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/dist/index.d.ts +152 -1
- package/dist/index.js +3154 -0
- package/dist/index.js.map +1 -1
- package/package.json +13 -3
- package/src/components/agents/agent-edit-form.tsx +218 -0
- package/src/components/agents/agent-form-dialog.tsx +177 -0
- package/src/components/agents/agent-objectives-list.tsx +406 -0
- package/src/components/agents/agent-prompt-editor.tsx +406 -0
- package/src/components/agents/agent-tabs.tsx +63 -0
- package/src/components/agents/agent-tools-list.tsx +377 -0
- package/src/components/agents/agents-table.tsx +205 -0
- package/src/components/tools/tool-credentials-form.tsx +572 -0
- package/src/components/tools/tool-form-dialog.tsx +342 -0
- package/src/components/tools/tools-table.tsx +225 -0
- package/src/components/ui/sortable.tsx +577 -0
- package/src/index.ts +16 -0
- package/src/lib/compose-refs.ts +44 -0
- package/src/pages/agent-detail-page.tsx +112 -0
- package/src/pages/agents-page.tsx +45 -0
- package/src/pages/credentials-page.tsx +50 -0
- package/src/pages/index.ts +4 -0
- package/src/pages/tools-page.tsx +52 -0
|
@@ -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
|
+
}
|