@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/dist/index.js +97 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/agents/agent-objectives-list.tsx +63 -13
- package/src/components/agents/agent-prompt-editor.tsx +24 -45
- package/src/components/capabilities/capabilities-tab.tsx +26 -9
- package/src/hooks/use-integrations.ts +15 -11
- package/src/pages/integrations-management-page.tsx +4 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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-
|
|
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="
|
|
362
|
-
rows={
|
|
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
|
-
|
|
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
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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("[
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
103
|
-
//
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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);
|