@greatapps/greatagents-ui 0.3.17 → 0.3.19
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 +247 -200
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/capabilities/integrations-tab.tsx +211 -160
package/package.json
CHANGED
|
@@ -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";
|
|
@@ -66,7 +67,7 @@ function buildCustomInstructions(
|
|
|
66
67
|
return `- ${f.slug}: ${instruction}`;
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
return `
|
|
70
|
+
return `Funções disponíveis (${integrationSlug}):\n${lines.join("\n")}`;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
-
//
|
|
101
|
-
const [
|
|
102
|
-
|
|
103
|
-
const [
|
|
104
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
[
|
|
200
|
+
[],
|
|
214
201
|
);
|
|
215
202
|
|
|
216
|
-
// Toggle
|
|
203
|
+
// Toggle function checkbox (local only)
|
|
217
204
|
const handleToggleFunction = useCallback(
|
|
218
|
-
(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
if (checked)
|
|
224
|
-
|
|
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
|
-
[
|
|
214
|
+
[],
|
|
239
215
|
);
|
|
240
216
|
|
|
241
|
-
// Update function
|
|
242
|
-
const
|
|
243
|
-
(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
[
|
|
232
|
+
[],
|
|
259
233
|
);
|
|
260
234
|
|
|
261
|
-
//
|
|
262
|
-
const
|
|
263
|
-
(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
316
|
-
const instructions = functionInstructions
|
|
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={
|
|
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(
|
|
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
|
-
|
|
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
|
}
|