@greatapps/greatagents-ui 0.3.21 → 0.3.22

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.21",
3
+ "version": "0.3.22",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -89,8 +89,8 @@ export function createGagentsClient(config: GagentsClientConfig) {
89
89
  idAccount: number,
90
90
  id: number,
91
91
  body: Partial<
92
- Pick<Agent, "title" | "photo" | "delay_typing" | "waiting_time" | "active">
93
- > & { prompt?: string; change_notes?: string },
92
+ Pick<Agent, "title" | "photo" | "delay_typing" | "waiting_time" | "active" | "identity" | "mission" | "tone_style_format" | "rules" | "conversation_flow" | "context">
93
+ > & { change_notes?: string },
94
94
  ) => request<Agent>("PUT", idAccount, `agents/${id}`, body),
95
95
 
96
96
  deleteAgent: (idAccount: number, id: number) =>
@@ -154,14 +154,14 @@ export function createGagentsClient(config: GagentsClientConfig) {
154
154
  idAccount: number,
155
155
  idAgent: number,
156
156
  body: Pick<Objective, "title"> &
157
- Partial<Pick<Objective, "slug" | "prompt" | "order" | "active">>,
157
+ Partial<Pick<Objective, "slug" | "instruction" | "description" | "conversation_flow" | "rules" | "order" | "active">>,
158
158
  ) => request<Objective>("POST", idAccount, `agents/${idAgent}/objectives`, body),
159
159
 
160
160
  updateObjective: (
161
161
  idAccount: number,
162
162
  idAgent: number,
163
163
  id: number,
164
- body: Partial<Pick<Objective, "title" | "slug" | "prompt" | "order" | "active">>,
164
+ body: Partial<Pick<Objective, "title" | "slug" | "instruction" | "description" | "conversation_flow" | "rules" | "order" | "active">>,
165
165
  ) => request<Objective>("PUT", idAccount, `agents/${idAgent}/objectives/${id}`, body),
166
166
 
167
167
  deleteObjective: (idAccount: number, idAgent: number, id: number) =>
@@ -0,0 +1,305 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import type { Agent, ConversationFlowStep } from "../../types";
3
+ import type { GagentsHookConfig } from "../../hooks/types";
4
+ import { useUpdateAgent } from "../../hooks";
5
+ import { Button, Input, Label } from "@greatapps/greatauth-ui/ui";
6
+ import { Loader2 } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { ConversationFlowEditor } from "./conversation-flow-editor";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Section config
12
+ // ---------------------------------------------------------------------------
13
+
14
+ interface SectionDef {
15
+ key: "identity" | "mission" | "tone_style_format" | "rules" | "conversation_flow" | "context";
16
+ label: string;
17
+ helper: string;
18
+ required?: boolean;
19
+ field: keyof Agent;
20
+ type: "textarea" | "conversation_flow";
21
+ }
22
+
23
+ const SECTIONS: SectionDef[] = [
24
+ {
25
+ key: "identity",
26
+ label: "Identidade",
27
+ helper: "Descreva quem é o agente, o seu nome e personalidade",
28
+ required: true,
29
+ field: "identity",
30
+ type: "textarea",
31
+ },
32
+ {
33
+ key: "mission",
34
+ label: "Missão",
35
+ helper: "Qual é a missão principal deste agente",
36
+ required: true,
37
+ field: "mission",
38
+ type: "textarea",
39
+ },
40
+ {
41
+ key: "tone_style_format",
42
+ label: "Tom, Estilo & Formato",
43
+ helper: "Defina como o agente comunica e formata as respostas",
44
+ required: false,
45
+ field: "tone_style_format",
46
+ type: "textarea",
47
+ },
48
+ {
49
+ key: "rules",
50
+ label: "Regras",
51
+ helper: "Limites, restrições e comportamentos obrigatórios",
52
+ required: false,
53
+ field: "rules",
54
+ type: "textarea",
55
+ },
56
+ {
57
+ key: "conversation_flow",
58
+ label: "Fluxo de Conversa",
59
+ helper: "Etapas que o agente segue no início de cada conversa",
60
+ required: false,
61
+ field: "conversation_flow",
62
+ type: "conversation_flow",
63
+ },
64
+ {
65
+ key: "context",
66
+ label: "Contexto",
67
+ helper: "Informações adicionais que o agente deve saber",
68
+ required: false,
69
+ field: "context",
70
+ type: "textarea",
71
+ },
72
+ ];
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function parseConversationFlow(raw: string | null | undefined): ConversationFlowStep[] {
79
+ if (!raw) return [];
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ if (Array.isArray(parsed)) return parsed;
83
+ return [];
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+
89
+ function computeTotals(fields: Record<string, string>, steps: ConversationFlowStep[]) {
90
+ const textChars = Object.values(fields).reduce((sum, v) => sum + v.length, 0);
91
+ const flowChars = JSON.stringify(steps).length;
92
+ const total = textChars + flowChars;
93
+ return { chars: total, tokens: Math.ceil(total / 4) };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Auto-resize textarea component
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function AutoTextarea({
101
+ value,
102
+ onChange,
103
+ disabled,
104
+ placeholder,
105
+ ariaLabel,
106
+ }: {
107
+ value: string;
108
+ onChange: (v: string) => void;
109
+ disabled?: boolean;
110
+ placeholder?: string;
111
+ ariaLabel?: string;
112
+ }) {
113
+ const ref = useRef<HTMLTextAreaElement>(null);
114
+
115
+ const autoResize = useCallback(() => {
116
+ const el = ref.current;
117
+ if (!el) return;
118
+ el.style.height = "auto";
119
+ el.style.height = `${Math.max(120, el.scrollHeight)}px`;
120
+ }, []);
121
+
122
+ useEffect(() => {
123
+ autoResize();
124
+ }, [value, autoResize]);
125
+
126
+ function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
127
+ if (e.key === "Tab") {
128
+ e.preventDefault();
129
+ const el = e.currentTarget;
130
+ const start = el.selectionStart;
131
+ const end = el.selectionEnd;
132
+ const newValue = el.value.substring(0, start) + " " + el.value.substring(end);
133
+ onChange(newValue);
134
+ requestAnimationFrame(() => {
135
+ el.selectionStart = el.selectionEnd = start + 2;
136
+ });
137
+ }
138
+ }
139
+
140
+ return (
141
+ <textarea
142
+ ref={ref}
143
+ aria-label={ariaLabel}
144
+ value={value}
145
+ onChange={(e) => onChange(e.target.value)}
146
+ onKeyDown={handleKeyDown}
147
+ placeholder={placeholder}
148
+ disabled={disabled}
149
+ className="w-full resize-none rounded-lg border bg-background p-3 text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
150
+ style={{ minHeight: "120px" }}
151
+ />
152
+ );
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // AgentDefinitionEditor
157
+ // ---------------------------------------------------------------------------
158
+
159
+ interface AgentDefinitionEditorProps {
160
+ agent: Agent;
161
+ config: GagentsHookConfig;
162
+ }
163
+
164
+ export function AgentDefinitionEditor({ agent, config }: AgentDefinitionEditorProps) {
165
+ const updateAgent = useUpdateAgent(config);
166
+
167
+ // Track agent ID to reset state on agent switch
168
+ const [trackedAgentId, setTrackedAgentId] = useState(agent.id);
169
+
170
+ // Text field state
171
+ const [fields, setFields] = useState<Record<string, string>>(() => ({
172
+ identity: agent.identity ?? "",
173
+ mission: agent.mission ?? "",
174
+ tone_style_format: agent.tone_style_format ?? "",
175
+ rules: agent.rules ?? "",
176
+ context: agent.context ?? "",
177
+ }));
178
+
179
+ // Conversation flow state
180
+ const [conversationFlowSteps, setConversationFlowSteps] = useState<ConversationFlowStep[]>(
181
+ () => parseConversationFlow(agent.conversation_flow),
182
+ );
183
+
184
+ const [changeNotes, setChangeNotes] = useState("");
185
+
186
+ // Reset state when agent changes
187
+ if (trackedAgentId !== agent.id) {
188
+ setTrackedAgentId(agent.id);
189
+ setFields({
190
+ identity: agent.identity ?? "",
191
+ mission: agent.mission ?? "",
192
+ tone_style_format: agent.tone_style_format ?? "",
193
+ rules: agent.rules ?? "",
194
+ context: agent.context ?? "",
195
+ });
196
+ setConversationFlowSteps(parseConversationFlow(agent.conversation_flow));
197
+ setChangeNotes("");
198
+ }
199
+
200
+ function updateField(key: string, value: string) {
201
+ setFields((prev) => ({ ...prev, [key]: value }));
202
+ }
203
+
204
+ const { chars, tokens } = computeTotals(fields, conversationFlowSteps);
205
+
206
+ async function handleSave() {
207
+ if (!fields.identity.trim() || !fields.mission.trim()) {
208
+ toast.error("Identidade e Missão são campos obrigatórios");
209
+ return;
210
+ }
211
+
212
+ const body: Record<string, unknown> = {
213
+ identity: fields.identity.trim(),
214
+ mission: fields.mission.trim(),
215
+ tone_style_format: fields.tone_style_format.trim() || null,
216
+ rules: fields.rules.trim() || null,
217
+ conversation_flow: conversationFlowSteps.length > 0
218
+ ? JSON.stringify(conversationFlowSteps)
219
+ : null,
220
+ context: fields.context.trim() || null,
221
+ };
222
+
223
+ if (changeNotes.trim()) {
224
+ body.change_notes = changeNotes.trim();
225
+ }
226
+
227
+ try {
228
+ await updateAgent.mutateAsync({ id: agent.id, body });
229
+ setChangeNotes("");
230
+ toast.success("Definição do agente salva com sucesso");
231
+ } catch {
232
+ toast.error("Erro ao salvar definição do agente");
233
+ }
234
+ }
235
+
236
+ return (
237
+ <div className="space-y-6 p-4">
238
+ {/* Sections */}
239
+ {SECTIONS.map((section) => (
240
+ <div key={section.key} className="space-y-2">
241
+ <Label className="text-sm font-medium">
242
+ {section.label}
243
+ {section.required && <span className="ml-0.5 text-destructive">*</span>}
244
+ </Label>
245
+ <p className="text-xs text-muted-foreground">{section.helper}</p>
246
+
247
+ {section.type === "textarea" ? (
248
+ <AutoTextarea
249
+ value={fields[section.key] ?? ""}
250
+ onChange={(v) => updateField(section.key, v)}
251
+ disabled={updateAgent.isPending}
252
+ placeholder={`${section.label}...`}
253
+ ariaLabel={section.label}
254
+ />
255
+ ) : (
256
+ <ConversationFlowEditor
257
+ steps={conversationFlowSteps}
258
+ onChange={setConversationFlowSteps}
259
+ disabled={updateAgent.isPending}
260
+ />
261
+ )}
262
+ </div>
263
+ ))}
264
+
265
+ {/* Character count + token estimate */}
266
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
267
+ <span className="tabular-nums">{chars.toLocaleString("pt-BR")} caracteres</span>
268
+ <span>·</span>
269
+ <span className="tabular-nums">~{tokens.toLocaleString("pt-BR")} tokens</span>
270
+ </div>
271
+
272
+ {/* Footer: change notes + save */}
273
+ <div className="flex items-center gap-3">
274
+ <Input
275
+ aria-label="Notas da alteração"
276
+ name="changeNotes"
277
+ value={changeNotes}
278
+ onChange={(e) => setChangeNotes(e.target.value)}
279
+ placeholder="O que mudou? (opcional)"
280
+ disabled={updateAgent.isPending}
281
+ className="flex-1"
282
+ onKeyDown={(e) => {
283
+ if (e.key === "Enter") {
284
+ e.preventDefault();
285
+ handleSave();
286
+ }
287
+ }}
288
+ />
289
+ <Button
290
+ onClick={handleSave}
291
+ disabled={
292
+ updateAgent.isPending ||
293
+ !fields.identity.trim() ||
294
+ !fields.mission.trim()
295
+ }
296
+ >
297
+ {updateAgent.isPending && (
298
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
299
+ )}
300
+ Salvar
301
+ </Button>
302
+ </div>
303
+ </div>
304
+ );
305
+ }
@@ -1,5 +1,5 @@
1
1
  import { useState } from "react";
2
- import type { Agent, Objective } from "../../types";
2
+ import type { Agent, Objective, ConversationFlowStep } from "../../types";
3
3
  import type { GagentsHookConfig } from "../../hooks/types";
4
4
  import {
5
5
  useObjectives,
@@ -36,6 +36,7 @@ import {
36
36
  SortableItemHandle,
37
37
  SortableOverlay,
38
38
  } from "../ui/sortable";
39
+ import { ConversationFlowEditor } from "./conversation-flow-editor";
39
40
  import { Trash2, Target, Pencil, Plus, GripVertical } from "lucide-react";
40
41
  import { toast } from "sonner";
41
42
 
@@ -57,28 +58,12 @@ interface ObjectiveFormState {
57
58
  title: string;
58
59
  slug: string;
59
60
  instruction: string;
60
- prompt: string;
61
+ description: string;
62
+ conversation_flow: ConversationFlowStep[];
63
+ rules: string;
61
64
  }
62
65
 
63
- const EMPTY_FORM: ObjectiveFormState = { title: "", slug: "", instruction: "", prompt: "" };
64
-
65
- /** Split a stored prompt into instruction (first line) and body (rest). */
66
- function splitPrompt(prompt: string | null | undefined): { instruction: string; body: string } {
67
- if (!prompt) return { instruction: "", body: "" };
68
- const idx = prompt.indexOf("\n");
69
- if (idx === -1) return { instruction: prompt.trim(), body: "" };
70
- return { instruction: prompt.slice(0, idx).trim(), body: prompt.slice(idx + 1).trim() };
71
- }
72
-
73
- /** Merge instruction + body into a single prompt string. */
74
- function mergePrompt(instruction: string, body: string): string {
75
- const i = instruction.trim();
76
- const b = body.trim();
77
- if (!i && !b) return "";
78
- if (!b) return i;
79
- if (!i) return b;
80
- return `${i}\n${b}`;
81
- }
66
+ const EMPTY_FORM: ObjectiveFormState = { title: "", slug: "", instruction: "", description: "", conversation_flow: [], rules: "" };
82
67
 
83
68
  export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps) {
84
69
  const { data: objectivesData, isLoading } = useObjectives(config, agent.id);
@@ -123,12 +108,17 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
123
108
 
124
109
  function openEdit(objective: Objective) {
125
110
  setEditTarget(objective);
126
- const { instruction, body } = splitPrompt(objective.prompt);
111
+ let parsedFlow: ConversationFlowStep[] = [];
112
+ if (objective.conversation_flow) {
113
+ try { parsedFlow = JSON.parse(objective.conversation_flow); } catch { /* invalid JSON */ }
114
+ }
127
115
  setForm({
128
116
  title: objective.title,
129
117
  slug: objective.slug || "",
130
- instruction,
131
- prompt: body,
118
+ instruction: objective.instruction || "",
119
+ description: objective.description || "",
120
+ conversation_flow: parsedFlow,
121
+ rules: objective.rules || "",
132
122
  });
133
123
  setSlugManual(true);
134
124
  setFormOpen(true);
@@ -138,33 +128,32 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
138
128
  if (!form.title.trim()) return;
139
129
 
140
130
  const effectiveSlug = form.slug.trim() || slugify(form.title);
141
- const mergedPrompt = mergePrompt(form.instruction, form.prompt) || null;
142
131
  const nextOrder =
143
132
  sortedObjectives.length > 0
144
133
  ? Math.max(...sortedObjectives.map((o) => o.order)) + 1
145
134
  : 1;
146
135
 
147
136
  try {
137
+ const bodyFields = {
138
+ title: form.title.trim(),
139
+ slug: effectiveSlug,
140
+ instruction: form.instruction.trim() || null,
141
+ description: form.description.trim() || null,
142
+ conversation_flow: form.conversation_flow.length > 0 ? JSON.stringify(form.conversation_flow) : null,
143
+ rules: form.rules.trim() || null,
144
+ };
145
+
148
146
  if (editTarget) {
149
147
  await updateMutation.mutateAsync({
150
148
  idAgent: agent.id,
151
149
  id: editTarget.id,
152
- body: {
153
- title: form.title.trim(),
154
- slug: effectiveSlug,
155
- prompt: mergedPrompt,
156
- },
150
+ body: bodyFields,
157
151
  });
158
152
  toast.success("Objetivo atualizado");
159
153
  } else {
160
154
  await createMutation.mutateAsync({
161
155
  idAgent: agent.id,
162
- body: {
163
- title: form.title.trim(),
164
- slug: effectiveSlug,
165
- prompt: mergedPrompt,
166
- order: nextOrder,
167
- },
156
+ body: { ...bodyFields, order: nextOrder },
168
157
  });
169
158
  toast.success("Objetivo criado");
170
159
  }
@@ -274,23 +263,20 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
274
263
  </Badge>
275
264
  )}
276
265
  </div>
277
- {objective.prompt && (() => {
278
- const { instruction, body } = splitPrompt(objective.prompt);
279
- return (
280
- <div className="space-y-0.5">
281
- {instruction && (
282
- <p className="text-xs font-medium text-muted-foreground">
283
- Quando: {instruction}
284
- </p>
285
- )}
286
- {body && (
287
- <p className="line-clamp-1 text-xs text-muted-foreground">
288
- {body}
289
- </p>
290
- )}
291
- </div>
292
- );
293
- })()}
266
+ {(objective.instruction || objective.description) && (
267
+ <div className="space-y-0.5">
268
+ {objective.instruction && (
269
+ <p className="text-xs font-medium text-muted-foreground">
270
+ Quando: {objective.instruction}
271
+ </p>
272
+ )}
273
+ {objective.description && (
274
+ <p className="line-clamp-1 text-xs text-muted-foreground">
275
+ {objective.description}
276
+ </p>
277
+ )}
278
+ </div>
279
+ )}
294
280
  </div>
295
281
 
296
282
  <Switch
@@ -395,24 +381,52 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
395
381
  placeholder="Ex: Quando o utilizador quer agendar uma consulta"
396
382
  />
397
383
  <p className="text-xs text-muted-foreground">
398
- Instrução curta que diz ao agente QUANDO ativar este objetivo. Aparece na secção [SKILLS] do prompt.
384
+ Instrução curta que diz ao agente QUANDO ativar este objetivo. Aparece na secção [OBJETIVOS] do prompt.
385
+ </p>
386
+ </div>
387
+
388
+ <div className="space-y-2">
389
+ <Label htmlFor="objective-description">Descrição</Label>
390
+ <Textarea
391
+ id="objective-description"
392
+ name="description"
393
+ value={form.description}
394
+ onChange={(e) =>
395
+ setForm((f) => ({ ...f, description: e.target.value }))
396
+ }
397
+ placeholder="Descreva o propósito deste objectivo..."
398
+ rows={3}
399
+ />
400
+ <p className="text-xs text-muted-foreground">
401
+ Missão e propósito deste objectivo — o que o agente deve alcançar.
402
+ </p>
403
+ </div>
404
+
405
+ <div className="space-y-2">
406
+ <Label>Fluxo de Conversa</Label>
407
+ <ConversationFlowEditor
408
+ steps={form.conversation_flow}
409
+ onChange={(steps) => setForm((f) => ({ ...f, conversation_flow: steps }))}
410
+ />
411
+ <p className="text-xs text-muted-foreground">
412
+ Etapas que o agente segue quando este objectivo é activado.
399
413
  </p>
400
414
  </div>
401
415
 
402
416
  <div className="space-y-2">
403
- <Label htmlFor="objective-prompt">Instruções detalhadas</Label>
417
+ <Label htmlFor="objective-rules">Regras</Label>
404
418
  <Textarea
405
- id="objective-prompt"
406
- name="prompt"
407
- value={form.prompt}
419
+ id="objective-rules"
420
+ name="rules"
421
+ value={form.rules}
408
422
  onChange={(e) =>
409
- setForm((f) => ({ ...f, prompt: e.target.value }))
423
+ setForm((f) => ({ ...f, rules: e.target.value }))
410
424
  }
411
- placeholder="Instruções detalhadas que o agente seguirá quando este objetivo for ativado..."
412
- rows={6}
425
+ placeholder="Regras específicas deste objectivo..."
426
+ rows={3}
413
427
  />
414
428
  <p className="text-xs text-muted-foreground">
415
- Passos, regras e contexto detalhado para o agente seguir quando este objetivo está ativo.
429
+ Restrições e limites específicos quando este objectivo está activo.
416
430
  </p>
417
431
  </div>
418
432
  </div>