@greatapps/greatagents-ui 0.3.18 → 0.3.20

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.18",
3
+ "version": "0.3.20",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -84,9 +84,17 @@ function buildPreview(
84
84
  .map((at) => ({ at, tool: toolMap.get(at.id_tool) }))
85
85
  .filter((x): x is { at: AgentTool; tool: Tool } => !!x.tool);
86
86
 
87
- if (toolBindings.length > 0) {
87
+ // Deduplicate by id_tool — keep only the first (most recent) binding per tool
88
+ const seenToolIds = new Set<number>();
89
+ const dedupedBindings = toolBindings.filter(({ at }) => {
90
+ if (seenToolIds.has(at.id_tool)) return false;
91
+ seenToolIds.add(at.id_tool);
92
+ return true;
93
+ });
94
+
95
+ if (dedupedBindings.length > 0) {
88
96
  preview += "\n\n[TOOLS]";
89
- for (const { at, tool } of toolBindings) {
97
+ for (const { at, tool } of dedupedBindings) {
90
98
  if (at.custom_instructions) {
91
99
  preview += `\n\n${at.custom_instructions}`;
92
100
  } else {
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useState } from "react";
3
+ import { useCallback, useState, useEffect, useMemo } from "react";
4
4
  import type { GagentsHookConfig } from "../../hooks/types";
5
5
  import { useIntegrationState } from "../../hooks/use-integrations";
6
6
  import { useAgentTools, useAddAgentTool, useRemoveAgentTool, useUpdateAgentTool } from "../../hooks/use-agent-tools";
7
7
  import { useCreateTool, useTools } from "../../hooks/use-tools";
8
- import { Switch, Tooltip, TooltipContent, TooltipTrigger, Checkbox } from "@greatapps/greatauth-ui/ui";
8
+ import { Switch, Tooltip, TooltipContent, TooltipTrigger, Checkbox, Button } from "@greatapps/greatauth-ui/ui";
9
9
  import { Plug, Loader2, ChevronDown, Pencil } from "lucide-react";
10
+ import { toast } from "sonner";
10
11
  import type { LucideIcon } from "lucide-react";
11
12
  import { CalendarSync } from "lucide-react";
12
13
  import { cn } from "../../lib";
@@ -82,6 +83,13 @@ export interface IntegrationsTabProps {
82
83
  // Component
83
84
  // ---------------------------------------------------------------------------
84
85
 
86
+ // Local state types
87
+ interface IntegrationLocalState {
88
+ enabled: boolean;
89
+ selectedFunctions: Set<string>;
90
+ functionInstructions: Record<string, string>;
91
+ }
92
+
85
93
  export function IntegrationsTab({
86
94
  config,
87
95
  agentId,
@@ -97,13 +105,12 @@ export function IntegrationsTab({
97
105
  const agentTools = agentToolsData?.data ?? [];
98
106
  const allTools = toolsData?.data ?? [];
99
107
 
100
- // Per-integration function selection state: integrationSlug -> Set of selected function slugs
101
- const [selectedFunctions, setSelectedFunctions] = useState<Record<string, Set<string>>>({});
102
- // Per-integration per-function custom instructions: integrationSlug -> { fnSlug -> instruction }
103
- const [functionInstructions, setFunctionInstructions] = useState<Record<string, Record<string, string>>>({});
104
- // Which function is being edited (integrationSlug:fnSlug)
108
+ // Local state: integrationSlug -> state
109
+ const [localState, setLocalState] = useState<Record<string, IntegrationLocalState>>({});
110
+ const [serverState, setServerState] = useState<Record<string, IntegrationLocalState>>({});
111
+ const [initialized, setInitialized] = useState(false);
112
+ const [isSaving, setIsSaving] = useState(false);
105
113
  const [editingFunction, setEditingFunction] = useState<string | null>(null);
106
- // Which integration cards are expanded
107
114
  const [expandedCards, setExpandedCards] = useState<Set<string>>(new Set());
108
115
 
109
116
  // Only show connected credentials (account-level)
@@ -111,169 +118,197 @@ export function IntegrationsTab({
111
118
  (c) => !c.isAddNew && (c.state === "connected" || c.state === "expired"),
112
119
  );
113
120
 
114
- // Initialize function selections for a card when toggled on
115
- const initFunctionsForCard = useCallback((slug: string) => {
116
- const fns = INTEGRATION_FUNCTIONS[slug];
117
- if (!fns) return;
118
-
119
- setSelectedFunctions((prev) => ({
120
- ...prev,
121
- [slug]: new Set(fns.map((f) => f.slug)),
122
- }));
123
- setFunctionInstructions((prev) => ({
124
- ...prev,
125
- [slug]: Object.fromEntries(fns.map((f) => [f.slug, f.defaultInstructions])),
126
- }));
127
- }, []);
128
-
129
- const handleToggle = useCallback(
130
- async (card: IntegrationCardData, checked: boolean) => {
131
- if (checked) {
132
- let toolId = card.tool?.id;
133
-
134
- // If no tool record exists for this integration, create one on-the-fly
135
- if (!toolId) {
136
- const existingTool = allTools.find((t) => t.slug === card.definition.slug);
137
- if (existingTool) {
138
- toolId = existingTool.id;
139
- } else {
140
- try {
141
- const result = await createTool.mutateAsync({
142
- name: card.definition.name,
143
- slug: card.definition.slug,
144
- type: "integration",
145
- description: card.definition.description,
146
- });
147
- const d = result?.data;
148
- toolId = (Array.isArray(d) ? d[0]?.id : (d as Tool | undefined)?.id) ?? undefined;
149
- if (!toolId) {
150
- console.error("[IntegrationsTab] Failed to create tool — no ID returned");
151
- return;
152
- }
153
- } catch (err) {
154
- console.error("[IntegrationsTab] Error creating tool:", err);
155
- return;
156
- }
157
- }
158
- }
159
-
160
- // Initialize function selections
161
- initFunctionsForCard(card.definition.slug);
162
-
163
- // Build custom_instructions from all functions (all selected by default)
164
- const fns = INTEGRATION_FUNCTIONS[card.definition.slug];
165
- let customInstructions: string;
166
- if (fns) {
167
- const allFnSlugs = new Set(fns.map((f) => f.slug));
168
- const defaultInstr = Object.fromEntries(fns.map((f) => [f.slug, f.defaultInstructions]));
169
- customInstructions = buildCustomInstructions(card.definition.slug, allFnSlugs, defaultInstr);
170
- } else {
171
- customInstructions = "";
172
- }
121
+ // Initialize state from server data
122
+ useEffect(() => {
123
+ if (!connectedCards.length || initialized) return;
173
124
 
174
- // Expand the card to show functions
175
- setExpandedCards((prev) => new Set([...prev, card.definition.slug]));
125
+ const state: Record<string, IntegrationLocalState> = {};
126
+ for (const card of connectedCards) {
127
+ const slug = card.definition.slug;
128
+ const isLinked = card.linkedToAgent;
129
+ const fns = INTEGRATION_FUNCTIONS[slug] ?? [];
176
130
 
177
- addAgentTool.mutate({
178
- idAgent: agentId,
179
- body: {
180
- id_tool: toolId,
181
- enabled: true,
182
- ...(customInstructions ? { custom_instructions: customInstructions } : {}),
183
- },
184
- });
185
- } else {
186
- // Find the agent_tool to remove
131
+ if (isLinked) {
132
+ // Parse existing custom_instructions to restore function selections
187
133
  const toolId = card.tool?.id;
188
- if (toolId) {
189
- const agentTool = agentTools.find((at) => at.id_tool === toolId);
190
- if (agentTool) {
191
- removeAgentTool.mutate({ idAgent: agentId, id: agentTool.id });
134
+ const agentTool = toolId ? agentTools.find((at) => at.id_tool === toolId) : null;
135
+ const existingInstructions = agentTool?.custom_instructions || "";
136
+
137
+ const selectedFns = new Set<string>();
138
+ const fnInstr: Record<string, string> = {};
139
+ for (const fn of fns) {
140
+ if (!existingInstructions || existingInstructions.includes(fn.slug)) {
141
+ selectedFns.add(fn.slug);
192
142
  }
143
+ // Extract instruction from custom_instructions if present
144
+ const match = existingInstructions.match(new RegExp(`- ${fn.slug}: (.+)`));
145
+ fnInstr[fn.slug] = match?.[1] ?? fn.defaultInstructions;
193
146
  }
194
147
 
195
- // Clean up local state
196
- setExpandedCards((prev) => {
197
- const next = new Set(prev);
198
- next.delete(card.definition.slug);
199
- return next;
200
- });
201
- setSelectedFunctions((prev) => {
202
- const next = { ...prev };
203
- delete next[card.definition.slug];
204
- return next;
205
- });
206
- setFunctionInstructions((prev) => {
207
- const next = { ...prev };
208
- delete next[card.definition.slug];
209
- return next;
210
- });
148
+ state[slug] = { enabled: true, selectedFunctions: selectedFns, functionInstructions: fnInstr };
149
+ } else {
150
+ state[slug] = {
151
+ enabled: false,
152
+ selectedFunctions: new Set(fns.map((f) => f.slug)),
153
+ functionInstructions: Object.fromEntries(fns.map((f) => [f.slug, f.defaultInstructions])),
154
+ };
155
+ }
156
+ }
157
+
158
+ setLocalState(state);
159
+ setServerState(JSON.parse(JSON.stringify(state, (_k, v) => v instanceof Set ? [...v] : v)));
160
+ setInitialized(true);
161
+ }, [connectedCards, agentTools, initialized]);
162
+
163
+ // Detect changes
164
+ const hasChanges = useMemo(() => {
165
+ if (!initialized) return false;
166
+ const localKeys = Object.keys(localState);
167
+ for (const slug of localKeys) {
168
+ const local = localState[slug];
169
+ const server = serverState[slug];
170
+ if (!server) return true;
171
+ if (local.enabled !== server.enabled) return true;
172
+ const serverFns = server.selectedFunctions instanceof Set ? server.selectedFunctions : new Set(server.selectedFunctions as unknown as string[]);
173
+ if (local.selectedFunctions.size !== serverFns.size) return true;
174
+ for (const fn of local.selectedFunctions) {
175
+ if (!serverFns.has(fn)) return true;
176
+ }
177
+ // Check instructions for selected functions
178
+ for (const fn of local.selectedFunctions) {
179
+ if ((local.functionInstructions[fn] ?? '') !== (server.functionInstructions[fn] ?? '')) return true;
180
+ }
181
+ }
182
+ return false;
183
+ }, [localState, serverState, initialized]);
184
+
185
+ // Toggle integration on/off (local only)
186
+ const handleToggle = useCallback(
187
+ (card: IntegrationCardData, checked: boolean) => {
188
+ const slug = card.definition.slug;
189
+ setLocalState((prev) => ({
190
+ ...prev,
191
+ [slug]: {
192
+ ...prev[slug],
193
+ enabled: checked,
194
+ },
195
+ }));
196
+ if (checked) {
197
+ setExpandedCards((prev) => new Set([...prev, slug]));
211
198
  }
212
199
  },
213
- [agentTools, allTools, agentId, addAgentTool, removeAgentTool, createTool, initFunctionsForCard],
200
+ [],
214
201
  );
215
202
 
216
- // Toggle a single function on/off and update agent_tool
203
+ // Toggle function checkbox (local only)
217
204
  const handleToggleFunction = useCallback(
218
- (card: IntegrationCardData, fnSlug: string, checked: boolean) => {
219
- const integrationSlug = card.definition.slug;
220
-
221
- setSelectedFunctions((prev) => {
222
- const current = new Set(prev[integrationSlug] ?? []);
223
- if (checked) {
224
- current.add(fnSlug);
225
- } else {
226
- current.delete(fnSlug);
227
- }
228
- const next = { ...prev, [integrationSlug]: current };
229
-
230
- // Update agent_tool with new custom_instructions
231
- const instructions = functionInstructions[integrationSlug] ?? {};
232
- const customInstructions = buildCustomInstructions(integrationSlug, current, instructions);
233
- updateAgentToolInstructions(card, customInstructions);
234
-
235
- return next;
205
+ (slug: string, fnSlug: string, checked: boolean) => {
206
+ setLocalState((prev) => {
207
+ const current = prev[slug];
208
+ if (!current) return prev;
209
+ const fns = new Set(current.selectedFunctions);
210
+ if (checked) fns.add(fnSlug); else fns.delete(fnSlug);
211
+ return { ...prev, [slug]: { ...current, selectedFunctions: fns } };
236
212
  });
237
213
  },
238
- [functionInstructions],
214
+ [],
239
215
  );
240
216
 
241
- // Update function instructions text
242
- const handleUpdateFunctionInstruction = useCallback(
243
- (card: IntegrationCardData, fnSlug: string, instruction: string) => {
244
- const integrationSlug = card.definition.slug;
245
-
246
- setFunctionInstructions((prev) => {
247
- const current = { ...(prev[integrationSlug] ?? {}), [fnSlug]: instruction };
248
- const next = { ...prev, [integrationSlug]: current };
249
-
250
- // Update agent_tool with new custom_instructions
251
- const selected = selectedFunctions[integrationSlug] ?? new Set();
252
- const customInstructions = buildCustomInstructions(integrationSlug, selected, current);
253
- updateAgentToolInstructions(card, customInstructions);
254
-
255
- return next;
217
+ // Update function instruction (local only)
218
+ const handleUpdateInstruction = useCallback(
219
+ (slug: string, fnSlug: string, instruction: string) => {
220
+ setLocalState((prev) => {
221
+ const current = prev[slug];
222
+ if (!current) return prev;
223
+ return {
224
+ ...prev,
225
+ [slug]: {
226
+ ...current,
227
+ functionInstructions: { ...current.functionInstructions, [fnSlug]: instruction },
228
+ },
229
+ };
256
230
  });
257
231
  },
258
- [selectedFunctions],
232
+ [],
259
233
  );
260
234
 
261
- // Helper: update agent_tool custom_instructions
262
- const updateAgentToolInstructions = useCallback(
263
- (card: IntegrationCardData, customInstructions: string) => {
264
- const toolId = card.tool?.id;
265
- if (!toolId) return;
266
- const agentTool = agentTools.find((at) => at.id_tool === toolId);
267
- if (!agentTool) return;
268
-
269
- updateAgentTool.mutate({
270
- idAgent: agentId,
271
- id: agentTool.id,
272
- body: { custom_instructions: customInstructions },
273
- });
274
- },
275
- [agentTools, agentId, updateAgentTool],
276
- );
235
+ // Save all changes
236
+ const saveNow = useCallback(async () => {
237
+ setIsSaving(true);
238
+ try {
239
+ for (const slug of Object.keys(localState)) {
240
+ const local = localState[slug];
241
+ const card = connectedCards.find((c) => c.definition.slug === slug);
242
+ if (!card) continue;
243
+
244
+ const serverEntry = serverState[slug];
245
+ const wasEnabled = serverEntry?.enabled ?? false;
246
+
247
+ if (local.enabled && !wasEnabled) {
248
+ // Activate: create tool if needed, then add agent_tool
249
+ let toolId = card.tool?.id;
250
+ if (!toolId) {
251
+ const existingTool = allTools.find((t) => t.slug === slug);
252
+ if (existingTool) {
253
+ toolId = existingTool.id;
254
+ } else {
255
+ const result = await createTool.mutateAsync({
256
+ name: card.definition.name, slug, type: "integration", description: card.definition.description,
257
+ });
258
+ const d = result?.data;
259
+ toolId = (Array.isArray(d) ? d[0]?.id : (d as Tool | undefined)?.id) ?? undefined;
260
+ if (!toolId) continue;
261
+ }
262
+ }
263
+ const customInstructions = buildCustomInstructions(slug, local.selectedFunctions, local.functionInstructions);
264
+ await addAgentTool.mutateAsync({
265
+ idAgent: agentId,
266
+ body: { id_tool: toolId, enabled: true, ...(customInstructions ? { custom_instructions: customInstructions } : {}) },
267
+ });
268
+ } else if (!local.enabled && wasEnabled) {
269
+ // Deactivate: remove agent_tool
270
+ const toolId = card.tool?.id;
271
+ if (toolId) {
272
+ const agentTool = agentTools.find((at) => at.id_tool === toolId);
273
+ if (agentTool) {
274
+ await removeAgentTool.mutateAsync({ idAgent: agentId, id: agentTool.id });
275
+ }
276
+ }
277
+ } else if (local.enabled && wasEnabled) {
278
+ // Update: functions or instructions changed
279
+ const toolId = card.tool?.id;
280
+ if (toolId) {
281
+ const agentTool = agentTools.find((at) => at.id_tool === toolId);
282
+ if (agentTool) {
283
+ const customInstructions = buildCustomInstructions(slug, local.selectedFunctions, local.functionInstructions);
284
+ await updateAgentTool.mutateAsync({
285
+ idAgent: agentId, id: agentTool.id,
286
+ body: { custom_instructions: customInstructions },
287
+ });
288
+ }
289
+ }
290
+ }
291
+ }
292
+ // Sync server state
293
+ setServerState(JSON.parse(JSON.stringify(localState, (_k, v) => v instanceof Set ? [...v] : v)));
294
+ toast.success("Integrações salvas");
295
+ } catch {
296
+ toast.error("Erro ao salvar integrações");
297
+ } finally {
298
+ setIsSaving(false);
299
+ }
300
+ }, [localState, serverState, connectedCards, allTools, agentTools, agentId, addAgentTool, removeAgentTool, updateAgentTool, createTool]);
301
+
302
+ const discard = useCallback(() => {
303
+ const restored: Record<string, IntegrationLocalState> = {};
304
+ for (const [slug, entry] of Object.entries(serverState)) {
305
+ restored[slug] = {
306
+ ...entry,
307
+ selectedFunctions: entry.selectedFunctions instanceof Set ? entry.selectedFunctions : new Set(entry.selectedFunctions as unknown as string[]),
308
+ };
309
+ }
310
+ setLocalState(restored);
311
+ }, [serverState]);
277
312
 
278
313
  // Loading state
279
314
  if (isLoading || agentToolsLoading) {
@@ -307,13 +342,13 @@ export function IntegrationsTab({
307
342
  <div className="grid grid-cols-1 gap-3">
308
343
  {connectedCards.map((card) => {
309
344
  const Icon = resolveIcon(card.definition.icon);
310
- const isLinked = card.linkedToAgent;
311
- const isMutating = addAgentTool.isPending || removeAgentTool.isPending || createTool.isPending;
312
345
  const integrationSlug = card.definition.slug;
346
+ const local = localState[integrationSlug];
347
+ const isLinked = local?.enabled ?? false;
313
348
  const fns = INTEGRATION_FUNCTIONS[integrationSlug];
314
349
  const isExpanded = expandedCards.has(integrationSlug);
315
- const selected = selectedFunctions[integrationSlug] ?? new Set();
316
- const instructions = functionInstructions[integrationSlug] ?? {};
350
+ const selected = local?.selectedFunctions ?? new Set();
351
+ const instructions = local?.functionInstructions ?? {};
317
352
 
318
353
  return (
319
354
  <div
@@ -380,7 +415,7 @@ export function IntegrationsTab({
380
415
  {/* Toggle */}
381
416
  <Switch
382
417
  checked={isLinked}
383
- disabled={isMutating}
418
+ disabled={isSaving}
384
419
  onCheckedChange={(checked) => handleToggle(card, checked)}
385
420
  aria-label={`${isLinked ? "Desativar" : "Ativar"} ${card.definition.name} para este agente`}
386
421
  />
@@ -403,7 +438,7 @@ export function IntegrationsTab({
403
438
  <Checkbox
404
439
  checked={isSelected}
405
440
  onCheckedChange={(val) =>
406
- handleToggleFunction(card, fn.slug, val === true)
441
+ handleToggleFunction(integrationSlug, fn.slug, val === true)
407
442
  }
408
443
  aria-label={fn.label}
409
444
  />
@@ -436,7 +471,7 @@ export function IntegrationsTab({
436
471
  className="w-full rounded-md border bg-background px-3 py-2 text-sm resize-y min-h-[60px] focus:outline-none focus:ring-1 focus:ring-ring"
437
472
  value={currentInstruction}
438
473
  onChange={(e) =>
439
- handleUpdateFunctionInstruction(card, fn.slug, e.target.value)
474
+ handleUpdateInstruction(integrationSlug, fn.slug, e.target.value)
440
475
  }
441
476
  placeholder="Instruções personalizadas para esta função..."
442
477
  rows={2}
@@ -452,6 +487,22 @@ export function IntegrationsTab({
452
487
  );
453
488
  })}
454
489
  </div>
490
+
491
+ {/* Save bar */}
492
+ {hasChanges && (
493
+ <div className="sticky bottom-0 z-10 flex items-center justify-between gap-2 rounded-lg border bg-background p-3 shadow-sm">
494
+ <p className="text-sm text-muted-foreground">Você tem alterações não salvas.</p>
495
+ <div className="flex gap-2">
496
+ <Button variant="ghost" size="sm" onClick={discard} disabled={isSaving}>
497
+ Descartar
498
+ </Button>
499
+ <Button size="sm" onClick={saveNow} disabled={isSaving}>
500
+ {isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
501
+ Salvar
502
+ </Button>
503
+ </div>
504
+ </div>
505
+ )}
455
506
  </div>
456
507
  );
457
508
  }