@greatapps/greatagents-ui 0.3.16 → 0.3.17

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.16",
3
+ "version": "0.3.17",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,10 +56,29 @@ function slugify(text: string): string {
56
56
  interface ObjectiveFormState {
57
57
  title: string;
58
58
  slug: string;
59
+ instruction: string;
59
60
  prompt: string;
60
61
  }
61
62
 
62
- const EMPTY_FORM: ObjectiveFormState = { title: "", slug: "", prompt: "" };
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
+ }
63
82
 
64
83
  export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps) {
65
84
  const { data: objectivesData, isLoading } = useObjectives(config, agent.id);
@@ -104,10 +123,12 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
104
123
 
105
124
  function openEdit(objective: Objective) {
106
125
  setEditTarget(objective);
126
+ const { instruction, body } = splitPrompt(objective.prompt);
107
127
  setForm({
108
128
  title: objective.title,
109
129
  slug: objective.slug || "",
110
- prompt: objective.prompt || "",
130
+ instruction,
131
+ prompt: body,
111
132
  });
112
133
  setSlugManual(true);
113
134
  setFormOpen(true);
@@ -117,6 +138,7 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
117
138
  if (!form.title.trim()) return;
118
139
 
119
140
  const effectiveSlug = form.slug.trim() || slugify(form.title);
141
+ const mergedPrompt = mergePrompt(form.instruction, form.prompt) || null;
120
142
  const nextOrder =
121
143
  sortedObjectives.length > 0
122
144
  ? Math.max(...sortedObjectives.map((o) => o.order)) + 1
@@ -130,7 +152,7 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
130
152
  body: {
131
153
  title: form.title.trim(),
132
154
  slug: effectiveSlug,
133
- prompt: form.prompt.trim() || null,
155
+ prompt: mergedPrompt,
134
156
  },
135
157
  });
136
158
  toast.success("Objetivo atualizado");
@@ -140,7 +162,7 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
140
162
  body: {
141
163
  title: form.title.trim(),
142
164
  slug: effectiveSlug,
143
- prompt: form.prompt.trim() || null,
165
+ prompt: mergedPrompt,
144
166
  order: nextOrder,
145
167
  },
146
168
  });
@@ -252,11 +274,23 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
252
274
  </Badge>
253
275
  )}
254
276
  </div>
255
- {objective.prompt && (
256
- <p className="line-clamp-2 text-xs text-muted-foreground">
257
- {objective.prompt}
258
- </p>
259
- )}
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
+ })()}
260
294
  </div>
261
295
 
262
296
  <Switch
@@ -350,7 +384,23 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
350
384
  </div>
351
385
 
352
386
  <div className="space-y-2">
353
- <Label htmlFor="objective-prompt">Instruções do Objetivo</Label>
387
+ <Label htmlFor="objective-instruction">Quando ativar (instrução curta) *</Label>
388
+ <Input
389
+ id="objective-instruction"
390
+ name="instruction"
391
+ value={form.instruction}
392
+ onChange={(e) =>
393
+ setForm((f) => ({ ...f, instruction: e.target.value }))
394
+ }
395
+ placeholder="Ex: Quando o utilizador quer agendar uma consulta"
396
+ />
397
+ <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.
399
+ </p>
400
+ </div>
401
+
402
+ <div className="space-y-2">
403
+ <Label htmlFor="objective-prompt">Instruções detalhadas</Label>
354
404
  <Textarea
355
405
  id="objective-prompt"
356
406
  name="prompt"
@@ -358,11 +408,11 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
358
408
  onChange={(e) =>
359
409
  setForm((f) => ({ ...f, prompt: e.target.value }))
360
410
  }
361
- placeholder="Instru\u00e7\u00f5es detalhadas que o agente seguir\u00e1 quando este objetivo for ativado\u2026"
362
- rows={8}
411
+ placeholder="Instruções detalhadas que o agente seguirá quando este objetivo for ativado..."
412
+ rows={6}
363
413
  />
364
414
  <p className="text-xs text-muted-foreground">
365
- Estas instruções são carregadas automaticamente quando o agente detecta que o utilizador precisa deste objetivo.
415
+ Passos, regras e contexto detalhado para o agente seguir quando este objetivo está ativo.
366
416
  </p>
367
417
  </div>
368
418
  </div>
@@ -62,60 +62,39 @@ function buildPreview(
62
62
  ): string {
63
63
  let preview = promptText;
64
64
 
65
- const activeObjectives = objectives.filter((o) => o.active);
65
+ const activeObjectives = objectives.filter((o) => o.active && o.slug);
66
66
  const enabledAgentTools = agentTools.filter((at) => at.enabled);
67
-
68
67
  const toolMap = new Map(allTools.map((t) => [t.id, t]));
69
68
 
70
- // Separate capabilities vs integrations
71
- const capabilityTools: { at: AgentTool; tool: Tool }[] = [];
72
- const integrationTools: { at: AgentTool; tool: Tool }[] = [];
73
- for (const at of enabledAgentTools) {
74
- const tool = toolMap.get(at.id_tool);
75
- if (!tool) continue;
76
- if (tool.type === "integration") {
77
- integrationTools.push({ at, tool });
78
- } else {
79
- capabilityTools.push({ at, tool });
69
+ // [SKILLS] section objectives
70
+ if (activeObjectives.length > 0) {
71
+ preview += "\n\n[SKILLS]";
72
+ for (const obj of activeObjectives) {
73
+ preview += `\n- ${obj.slug}: ${obj.title}`;
74
+ if (obj.prompt) {
75
+ // Show first line of prompt as summary
76
+ const firstLine = obj.prompt.split("\n")[0].trim();
77
+ if (firstLine) preview += ` — ${firstLine}`;
78
+ }
80
79
  }
81
80
  }
82
81
 
83
- const hasContent = activeObjectives.length > 0 || capabilityTools.length > 0 || integrationTools.length > 0;
84
-
85
- if (hasContent) {
86
- preview += "\n\n[CAPACIDADES E INTEGRAÇÕES]";
87
-
88
- // Internal capabilities
89
- if (activeObjectives.length > 0 || capabilityTools.length > 0) {
90
- preview += "\n\n## Capacidades Internas (GClinic)";
91
-
92
- for (const obj of activeObjectives) {
93
- preview += `\n\n### ${obj.title} (${obj.slug})`;
94
- if (obj.prompt) preview += `\n${obj.prompt}`;
95
- }
96
-
97
- for (const { at, tool } of capabilityTools) {
82
+ // [TOOLS] section capabilities + integrations
83
+ const toolBindings = enabledAgentTools
84
+ .map((at) => ({ at, tool: toolMap.get(at.id_tool) }))
85
+ .filter((x): x is { at: AgentTool; tool: Tool } => !!x.tool);
86
+
87
+ if (toolBindings.length > 0) {
88
+ preview += "\n\n[TOOLS]";
89
+ for (const { at, tool } of toolBindings) {
90
+ if (at.custom_instructions) {
91
+ preview += `\n\n${at.custom_instructions}`;
92
+ } else {
93
+ // Fallback: show tool name and description
98
94
  preview += `\n\n### ${tool.name} (${tool.slug})`;
99
95
  if (tool.description) preview += `\n${tool.description}`;
100
- if (at.custom_instructions) preview += `\n${at.custom_instructions}`;
101
96
  }
102
97
  }
103
-
104
- // External integrations
105
- if (integrationTools.length > 0) {
106
- preview += "\n\n## Integrações Externas";
107
-
108
- for (const { at, tool } of integrationTools) {
109
- preview += `\n\n### ${tool.name} (${tool.slug})`;
110
- if (at.custom_instructions) preview += `\n${at.custom_instructions}`;
111
- }
112
- }
113
-
114
- // Rules
115
- preview += "\n\n## Regras";
116
- preview += "\n- Sempre confirme com o usuário antes de criar ou alterar registros.";
117
- preview += "\n- Nunca invente dados — sempre consulte primeiro.";
118
- preview += "\n- Use EXATAMENTE os nomes de função listados acima.";
119
98
  }
120
99
 
121
100
  return preview;
@@ -307,7 +286,7 @@ export function AgentPromptEditor({ config, agent }: AgentPromptEditorProps) {
307
286
  <div className="border-t px-4 py-3">
308
287
  <pre className="max-h-96 overflow-auto whitespace-pre-wrap font-mono text-sm leading-relaxed">
309
288
  {previewText.split("\n").map((line, i) => {
310
- const isTopSection = line.startsWith("[CAPACIDADES E INTEGRAÇÕES]");
289
+ const isTopSection = line.startsWith("[TOOLS]") || line.startsWith("[SKILLS]");
311
290
  const isH2 = line.startsWith("## ");
312
291
  const isH3 = line.startsWith("### ");
313
292
  const cls = isTopSection
@@ -153,9 +153,13 @@ function cloneInstructions(state: InstructionsState): InstructionsState {
153
153
  }
154
154
 
155
155
  function statesEqual(a: CapabilityState, b: CapabilityState): boolean {
156
- if (a.size !== b.size) return false;
157
- for (const [mod, opsA] of a) {
158
- const opsB = b.get(mod);
156
+ // Compare only modules that have operations (non-empty sets)
157
+ const aEffective = new Map([...a].filter(([, ops]) => ops.size > 0));
158
+ const bEffective = new Map([...b].filter(([, ops]) => ops.size > 0));
159
+
160
+ if (aEffective.size !== bEffective.size) return false;
161
+ for (const [mod, opsA] of aEffective) {
162
+ const opsB = bEffective.get(mod);
159
163
  if (!opsB || opsA.size !== opsB.size) return false;
160
164
  for (const op of opsA) {
161
165
  if (!opsB.has(op)) return false;
@@ -164,11 +168,14 @@ function statesEqual(a: CapabilityState, b: CapabilityState): boolean {
164
168
  return true;
165
169
  }
166
170
 
167
- function instructionsEqual(a: InstructionsState, b: InstructionsState): boolean {
168
- if (a.size !== b.size) return false;
169
- for (const [mod, instrA] of a) {
170
- const instrB = b.get(mod);
171
- if (!instrB) return false;
171
+ function instructionsEqual(a: InstructionsState, b: InstructionsState, activeModules: CapabilityState): boolean {
172
+ // Only compare instructions for modules that are active in the current state.
173
+ // Orphaned instructions (for disabled modules) are irrelevant.
174
+ const relevantModules = new Set([...activeModules.keys()].filter(k => (activeModules.get(k)?.size ?? 0) > 0));
175
+
176
+ for (const mod of relevantModules) {
177
+ const instrA = a.get(mod) ?? {};
178
+ const instrB = b.get(mod) ?? {};
172
179
  const keysA = Object.keys(instrA);
173
180
  const keysB = Object.keys(instrB);
174
181
  if (keysA.length !== keysB.length) return false;
@@ -176,6 +183,16 @@ function instructionsEqual(a: InstructionsState, b: InstructionsState): boolean
176
183
  if (instrA[k] !== instrB[k]) return false;
177
184
  }
178
185
  }
186
+
187
+ // Also check if server had instructions for modules that are now active but local doesn't
188
+ for (const mod of relevantModules) {
189
+ const instrA = a.get(mod);
190
+ const instrB = b.get(mod);
191
+ const hasA = instrA && Object.keys(instrA).length > 0;
192
+ const hasB = instrB && Object.keys(instrB).length > 0;
193
+ if (hasA !== hasB) return false;
194
+ }
195
+
179
196
  return true;
180
197
  }
181
198
 
@@ -225,7 +242,7 @@ export function CapabilitiesTab({ config, agentId }: CapabilitiesTabProps) {
225
242
  const hasChanges = useMemo(
226
243
  () =>
227
244
  initialized &&
228
- (!statesEqual(localState, serverState) || !instructionsEqual(localInstructions, serverInstructions)),
245
+ (!statesEqual(localState, serverState) || !instructionsEqual(localInstructions, serverInstructions, localState)),
229
246
  [localState, serverState, localInstructions, serverInstructions, initialized],
230
247
  );
231
248
 
@@ -99,22 +99,26 @@ export function useIntegrationState(
99
99
  ? credentials.filter((c) => c.id_tool === matchedTool.id)
100
100
  : [];
101
101
 
102
- // Also check credentials linked via platform_integration slug
103
- // (platform_integrations use the same slug convention)
102
+ // Also check credentials linked via platform_integration
103
+ // These credentials have id_platform_integration set but no id_tool,
104
+ // typically created by OAuth callbacks (e.g. Google Calendar)
104
105
  const piCredentials = credentials.filter(
105
106
  (c) =>
106
107
  c.id_platform_integration != null &&
107
- !c.id_tool &&
108
- // We can't directly match slug here since we don't have
109
- // platform_integrations data, but credentials with
110
- // id_platform_integration are for calendar integrations
111
- // which match by the registry slug
112
- matchedTool == null,
108
+ !c.id_tool,
113
109
  );
114
110
 
115
- // Combine prefer tool-based credentials, fallback to PI-based
116
- const allCredentials =
117
- matchedCredentials.length > 0 ? matchedCredentials : piCredentials;
111
+ // Combine: merge tool-based and PI-based credentials, dedup by id.
112
+ // Previously PI credentials were only included when matchedTool was null,
113
+ // which caused connected integrations to not appear after OAuth.
114
+ const seenIds = new Set<number>();
115
+ const allCredentials: ToolCredential[] = [];
116
+ for (const cred of [...matchedCredentials, ...piCredentials]) {
117
+ if (!seenIds.has(cred.id)) {
118
+ seenIds.add(cred.id);
119
+ allCredentials.push(cred);
120
+ }
121
+ }
118
122
 
119
123
  // Check if this agent has a linked agent_tool for this tool
120
124
  const linkedToAgent = matchedTool
@@ -79,10 +79,13 @@ export function IntegrationsManagementPage({
79
79
  }, []);
80
80
 
81
81
  const handleWizardComplete = useCallback(() => {
82
- // Invalidate queries BEFORE closing so data refreshes
82
+ // Invalidate ALL queries that useIntegrationState and related hooks depend on.
83
+ // Use broad prefix-based invalidation to cover every accountId/param variant.
83
84
  queryClient.invalidateQueries({ queryKey: ["greatagents", "tool-credentials"] });
84
85
  queryClient.invalidateQueries({ queryKey: ["greatagents", "tools"] });
85
86
  queryClient.invalidateQueries({ queryKey: ["greatagents", "agent-tools"] });
87
+ queryClient.invalidateQueries({ queryKey: ["greatagents", "agent-capabilities"] });
88
+ queryClient.invalidateQueries({ queryKey: ["greatagents", "capabilities"] });
86
89
 
87
90
  setWizardOpen(false);
88
91
  setActiveCard(null);