@greatapps/greatagents-ui 0.3.21 → 0.3.23

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.
@@ -1,442 +0,0 @@
1
- import { useState, useRef, useCallback, useEffect } from "react";
2
- import type { Agent, PromptVersion, Objective, AgentTool, Tool } from "../../types";
3
- import type { GagentsHookConfig } from "../../hooks/types";
4
- import { usePromptVersions } from "../../hooks";
5
- import { useUpdateAgent } from "../../hooks";
6
- import { useAgentTools } from "../../hooks";
7
- import { useObjectives } from "../../hooks";
8
- import { useTools } from "../../hooks";
9
- import { Button, Input, Skeleton, Badge } from "@greatapps/greatauth-ui/ui";
10
- import { FileText, Loader2, ChevronDown, ChevronUp, RotateCcw } from "lucide-react";
11
- import { toast } from "sonner";
12
-
13
- interface AgentPromptEditorProps {
14
- config: GagentsHookConfig;
15
- agent: Agent;
16
- }
17
-
18
- function formatDate(dateStr: string): string {
19
- const date = new Date(dateStr);
20
- return date.toLocaleDateString("pt-BR", {
21
- day: "2-digit",
22
- month: "2-digit",
23
- year: "numeric",
24
- hour: "2-digit",
25
- minute: "2-digit",
26
- });
27
- }
28
-
29
- function computeDiff(
30
- oldText: string,
31
- newText: string,
32
- ): { type: "added" | "removed" | "equal"; line: string }[] {
33
- const oldLines = oldText.split("\n");
34
- const newLines = newText.split("\n");
35
- const result: { type: "added" | "removed" | "equal"; line: string }[] = [];
36
- const maxLen = Math.max(oldLines.length, newLines.length);
37
-
38
- for (let i = 0; i < maxLen; i++) {
39
- const oldLine = i < oldLines.length ? oldLines[i] : undefined;
40
- const newLine = i < newLines.length ? newLines[i] : undefined;
41
-
42
- if (oldLine === newLine) {
43
- result.push({ type: "equal", line: newLine! });
44
- } else {
45
- if (oldLine !== undefined) {
46
- result.push({ type: "removed", line: oldLine });
47
- }
48
- if (newLine !== undefined) {
49
- result.push({ type: "added", line: newLine });
50
- }
51
- }
52
- }
53
-
54
- return result;
55
- }
56
-
57
- function buildPreview(
58
- promptText: string,
59
- objectives: Objective[],
60
- agentTools: AgentTool[],
61
- allTools: Tool[],
62
- ): string {
63
- let preview = promptText;
64
-
65
- const activeObjectives = objectives.filter((o) => o.active && o.slug);
66
- const enabledAgentTools = agentTools.filter((at) => at.enabled);
67
- const toolMap = new Map(allTools.map((t) => [t.id, t]));
68
-
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
- }
79
- }
80
- }
81
-
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
- // 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) {
96
- preview += "\n\n[TOOLS]";
97
- for (const { at, tool } of dedupedBindings) {
98
- if (at.custom_instructions) {
99
- preview += `\n\n${at.custom_instructions}`;
100
- } else {
101
- // Fallback: show tool name and description
102
- preview += `\n\n### ${tool.name} (${tool.slug})`;
103
- if (tool.description) preview += `\n${tool.description}`;
104
- }
105
- }
106
- }
107
-
108
- return preview;
109
- }
110
-
111
- export function AgentPromptEditor({ config, agent }: AgentPromptEditorProps) {
112
- const { data: versionsData, isLoading } = usePromptVersions(config, agent.id);
113
- const updateAgent = useUpdateAgent(config);
114
- const { data: objectivesData } = useObjectives(config, agent.id);
115
- const { data: agentToolsData } = useAgentTools(config, agent.id);
116
- const { data: toolsData } = useTools(config);
117
-
118
- const versions = (versionsData?.data || []) as PromptVersion[];
119
- const sortedVersions = [...versions].sort(
120
- (a, b) => new Date(b.datetime_add).getTime() - new Date(a.datetime_add).getTime(),
121
- );
122
- const currentVersion = sortedVersions.find((v) => v.is_current) || sortedVersions[0] || null;
123
- const currentPromptContent = currentVersion?.prompt_content ?? "";
124
-
125
- const [trackedAgentId, setTrackedAgentId] = useState(agent.id);
126
- const [promptText, setPromptText] = useState(currentPromptContent);
127
- const [promptInitialized, setPromptInitialized] = useState(false);
128
- const [changeNotes, setChangeNotes] = useState("");
129
- const [showPreview, setShowPreview] = useState(false);
130
- const [compareVersionId, setCompareVersionId] = useState<number | null>(null);
131
-
132
- const textareaRef = useRef<HTMLTextAreaElement>(null);
133
-
134
- // Initialize prompt text from current version when data loads
135
- if (!promptInitialized && currentPromptContent && !isLoading) {
136
- setPromptText(currentPromptContent);
137
- setPromptInitialized(true);
138
- }
139
-
140
- // Reset prompt text when agent changes
141
- if (trackedAgentId !== agent.id) {
142
- setTrackedAgentId(agent.id);
143
- setPromptText(currentPromptContent);
144
- setPromptInitialized(!!currentPromptContent);
145
- setCompareVersionId(null);
146
- }
147
-
148
- // Auto-resize textarea
149
- const autoResize = useCallback(() => {
150
- const el = textareaRef.current;
151
- if (!el) return;
152
- el.style.height = "auto";
153
- el.style.height = `${Math.max(300, el.scrollHeight)}px`;
154
- }, []);
155
-
156
- useEffect(() => {
157
- autoResize();
158
- }, [promptText, autoResize]);
159
-
160
- // Tab key support
161
- function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
162
- if (e.key === "Tab") {
163
- e.preventDefault();
164
- const el = e.currentTarget;
165
- const start = el.selectionStart;
166
- const end = el.selectionEnd;
167
- const value = el.value;
168
- const newValue = value.substring(0, start) + " " + value.substring(end);
169
- setPromptText(newValue);
170
- // Restore cursor position after React re-render
171
- requestAnimationFrame(() => {
172
- el.selectionStart = el.selectionEnd = start + 2;
173
- });
174
- }
175
- }
176
-
177
- const compareVersion = sortedVersions.find((v) => v.id === compareVersionId);
178
-
179
- // Diff: always compare selected older version against current
180
- const diffLines =
181
- currentVersion && compareVersion && compareVersion.id !== currentVersion.id
182
- ? computeDiff(compareVersion.prompt_content ?? "", currentVersion.prompt_content ?? "")
183
- : null;
184
-
185
- async function handleSave() {
186
- const body: Record<string, unknown> = {
187
- prompt: promptText.trim(),
188
- };
189
- if (changeNotes.trim()) {
190
- body.change_notes = changeNotes.trim();
191
- }
192
-
193
- try {
194
- await updateAgent.mutateAsync({ id: agent.id, body });
195
- setChangeNotes("");
196
- toast.success("Prompt salvo com sucesso");
197
- } catch {
198
- toast.error("Erro ao salvar prompt");
199
- }
200
- }
201
-
202
- function handleRestore(version: PromptVersion) {
203
- setPromptText(version.prompt_content ?? "");
204
- setChangeNotes(`Restaurado da v${version.version_number}`);
205
- toast.info("Prompt restaurado no editor. Clique em Salvar para confirmar.");
206
- }
207
-
208
- const charCount = promptText.length;
209
- const tokenEstimate = Math.ceil(charCount / 4);
210
-
211
- const objectives = objectivesData?.data || [];
212
- const agentTools = agentToolsData?.data || [];
213
- const allTools = toolsData?.data || [];
214
- const previewText = buildPreview(promptText, objectives, agentTools, allTools);
215
-
216
- if (isLoading) {
217
- return (
218
- <div className="space-y-3 p-4">
219
- {Array.from({ length: 3 }).map((_, i) => (
220
- <Skeleton key={i} className="h-14 w-full" />
221
- ))}
222
- </div>
223
- );
224
- }
225
-
226
- return (
227
- <div className="flex flex-col gap-4 p-4 lg:flex-row">
228
- {/* Editor section */}
229
- <div className="min-w-0 flex-1 space-y-4">
230
- {/* Textarea */}
231
- <div className="space-y-2">
232
- <textarea
233
- ref={textareaRef}
234
- aria-label="Prompt do sistema"
235
- name="prompt"
236
- value={promptText}
237
- onChange={(e) => setPromptText(e.target.value)}
238
- onKeyDown={handleKeyDown}
239
- placeholder="Escreva o prompt do sistema aqui\u2026"
240
- disabled={updateAgent.isPending}
241
- className="w-full resize-none rounded-lg border bg-background p-3 font-mono text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
242
- style={{ minHeight: "300px" }}
243
- />
244
- <div className="flex items-center gap-3 text-xs text-muted-foreground">
245
- <span className="tabular-nums">{charCount.toLocaleString("pt-BR")} caracteres</span>
246
- <span>·</span>
247
- <span className="tabular-nums">~{tokenEstimate.toLocaleString("pt-BR")} tokens</span>
248
- </div>
249
- </div>
250
-
251
- {/* Save row */}
252
- <div className="flex items-center gap-3">
253
- <Input
254
- aria-label="Notas da alteração"
255
- name="changeNotes"
256
- value={changeNotes}
257
- onChange={(e) => setChangeNotes(e.target.value)}
258
- placeholder="O que mudou? (opcional)"
259
- disabled={updateAgent.isPending}
260
- className="flex-1"
261
- onKeyDown={(e) => {
262
- if (e.key === "Enter") {
263
- e.preventDefault();
264
- handleSave();
265
- }
266
- }}
267
- />
268
- <Button
269
- onClick={handleSave}
270
- disabled={updateAgent.isPending || !promptText.trim()}
271
- >
272
- {updateAgent.isPending && (
273
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
274
- )}
275
- Salvar
276
- </Button>
277
- </div>
278
-
279
- {/* Preview section */}
280
- <div className="rounded-lg border">
281
- <button
282
- type="button"
283
- onClick={() => setShowPreview((prev) => !prev)}
284
- className="flex w-full items-center justify-between px-4 py-3 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
285
- >
286
- <span>Preview do prompt final</span>
287
- {showPreview ? (
288
- <ChevronUp className="h-4 w-4" />
289
- ) : (
290
- <ChevronDown className="h-4 w-4" />
291
- )}
292
- </button>
293
- {showPreview && (
294
- <div className="border-t px-4 py-3">
295
- <pre className="max-h-96 overflow-auto whitespace-pre-wrap font-mono text-sm leading-relaxed">
296
- {previewText.split("\n").map((line, i) => {
297
- const isTopSection = line.startsWith("[TOOLS]") || line.startsWith("[SKILLS]");
298
- const isH2 = line.startsWith("## ");
299
- const isH3 = line.startsWith("### ");
300
- const cls = isTopSection
301
- ? "font-bold text-foreground"
302
- : isH2
303
- ? "font-semibold text-muted-foreground"
304
- : isH3
305
- ? "font-medium text-muted-foreground"
306
- : "";
307
- return (
308
- <span
309
- key={i}
310
- className={cls}
311
- >
312
- {line}
313
- {"\n"}
314
- </span>
315
- );
316
- })}
317
- </pre>
318
- </div>
319
- )}
320
- </div>
321
-
322
- {/* Diff panel (when comparing) */}
323
- {diffLines && compareVersion && currentVersion && (
324
- <div>
325
- <div className="mb-2 flex items-center justify-between">
326
- <h3 className="text-sm font-medium text-muted-foreground">
327
- Diferenças: v{compareVersion.version_number} → v{currentVersion.version_number} (actual)
328
- </h3>
329
- <Button
330
- variant="ghost"
331
- size="sm"
332
- onClick={() => setCompareVersionId(null)}
333
- className="text-xs"
334
- >
335
- Fechar
336
- </Button>
337
- </div>
338
- <div className="max-h-64 overflow-auto rounded-lg border font-mono text-sm">
339
- {diffLines.map((line, i) => (
340
- <div
341
- key={i}
342
- className={`whitespace-pre-wrap px-3 py-0.5 ${
343
- line.type === "added"
344
- ? "bg-green-500/10 text-green-700 dark:text-green-400"
345
- : line.type === "removed"
346
- ? "bg-red-500/10 text-red-700 dark:text-red-400"
347
- : ""
348
- }`}
349
- >
350
- <span className="mr-2 inline-block w-4 select-none text-muted-foreground">
351
- {line.type === "added" ? "+" : line.type === "removed" ? "-" : " "}
352
- </span>
353
- {line.line || " "}
354
- </div>
355
- ))}
356
- </div>
357
- </div>
358
- )}
359
- </div>
360
-
361
- {/* Timeline section (right) */}
362
- <div className="w-full space-y-2 lg:w-80 lg:shrink-0">
363
- <h3 className="text-sm font-medium text-muted-foreground">
364
- Histórico de Versões
365
- </h3>
366
- {sortedVersions.length === 0 ? (
367
- <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-8 text-center">
368
- <FileText className="mb-2 h-8 w-8 text-muted-foreground" />
369
- <p className="text-sm text-muted-foreground">
370
- Nenhuma versão encontrada. Salve o prompt para criar a primeira versão.
371
- </p>
372
- </div>
373
- ) : (
374
- <div className="space-y-1">
375
- {sortedVersions.map((version, idx) => {
376
- const isCurrent = idx === 0;
377
- const isComparing = version.id === compareVersionId;
378
- return (
379
- <div
380
- key={version.id}
381
- className={`rounded-lg border p-3 transition-colors ${
382
- isCurrent
383
- ? "border-primary bg-primary/5"
384
- : isComparing
385
- ? "border-muted-foreground/30 bg-muted/50"
386
- : ""
387
- }`}
388
- >
389
- <div className="flex items-center justify-between gap-2">
390
- <span className="text-sm font-medium">
391
- v{version.version_number}
392
- </span>
393
- {isCurrent && (
394
- <Badge variant="default" className="text-[10px] px-1.5 py-0">
395
- Actual
396
- </Badge>
397
- )}
398
- </div>
399
- <div className="mt-1 text-xs text-muted-foreground">
400
- {formatDate(version.datetime_add)}
401
- </div>
402
- <div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
403
- <span>{(version.prompt_content ?? "").length} chars</span>
404
- <span>·</span>
405
- <span className="truncate font-mono">
406
- {(version.prompt_hash ?? "").slice(0, 8)}
407
- </span>
408
- </div>
409
- {version.change_notes && (
410
- <div className="mt-1.5 text-xs italic text-muted-foreground">
411
- {version.change_notes}
412
- </div>
413
- )}
414
- {!isCurrent && (
415
- <div className="mt-2 flex items-center gap-3">
416
- <button
417
- type="button"
418
- onClick={() => setCompareVersionId(isComparing ? null : version.id)}
419
- className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
420
- >
421
- <FileText aria-hidden="true" className="h-3 w-3" />
422
- {isComparing ? "Ocultar diff" : "Comparar"}
423
- </button>
424
- <button
425
- type="button"
426
- onClick={() => handleRestore(version)}
427
- className="flex items-center gap-1 text-xs text-primary hover:underline"
428
- >
429
- <RotateCcw aria-hidden="true" className="h-3 w-3" />
430
- Restaurar
431
- </button>
432
- </div>
433
- )}
434
- </div>
435
- );
436
- })}
437
- </div>
438
- )}
439
- </div>
440
- </div>
441
- );
442
- }