@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,5 +1,5 @@
1
1
  import { useState } from "react";
2
- import type { Agent, Objective } from "../../types";
2
+ import type { Agent, Objective, ConversationFlowStep } from "../../types";
3
3
  import type { GagentsHookConfig } from "../../hooks/types";
4
4
  import {
5
5
  useObjectives,
@@ -15,11 +15,11 @@ import {
15
15
  Textarea,
16
16
  Label,
17
17
  Badge,
18
- Dialog,
19
- DialogContent,
20
- DialogHeader,
21
- DialogTitle,
22
- DialogFooter,
18
+ Sheet,
19
+ SheetContent,
20
+ SheetHeader,
21
+ SheetTitle,
22
+ SheetFooter,
23
23
  AlertDialog,
24
24
  AlertDialogAction,
25
25
  AlertDialogCancel,
@@ -36,6 +36,7 @@ import {
36
36
  SortableItemHandle,
37
37
  SortableOverlay,
38
38
  } from "../ui/sortable";
39
+ import { ConversationFlowEditor } from "./conversation-flow-editor";
39
40
  import { Trash2, Target, Pencil, Plus, GripVertical } from "lucide-react";
40
41
  import { toast } from "sonner";
41
42
 
@@ -57,28 +58,12 @@ interface ObjectiveFormState {
57
58
  title: string;
58
59
  slug: string;
59
60
  instruction: string;
60
- prompt: string;
61
+ description: string;
62
+ conversation_flow: ConversationFlowStep[];
63
+ rules: string;
61
64
  }
62
65
 
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
- }
66
+ const EMPTY_FORM: ObjectiveFormState = { title: "", slug: "", instruction: "", description: "", conversation_flow: [], rules: "" };
82
67
 
83
68
  export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps) {
84
69
  const { data: objectivesData, isLoading } = useObjectives(config, agent.id);
@@ -123,12 +108,17 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
123
108
 
124
109
  function openEdit(objective: Objective) {
125
110
  setEditTarget(objective);
126
- const { instruction, body } = splitPrompt(objective.prompt);
111
+ let parsedFlow: ConversationFlowStep[] = [];
112
+ if (objective.conversation_flow) {
113
+ try { parsedFlow = JSON.parse(objective.conversation_flow); } catch { /* invalid JSON */ }
114
+ }
127
115
  setForm({
128
116
  title: objective.title,
129
117
  slug: objective.slug || "",
130
- instruction,
131
- prompt: body,
118
+ instruction: objective.instruction || "",
119
+ description: objective.description || "",
120
+ conversation_flow: parsedFlow,
121
+ rules: objective.rules || "",
132
122
  });
133
123
  setSlugManual(true);
134
124
  setFormOpen(true);
@@ -138,33 +128,32 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
138
128
  if (!form.title.trim()) return;
139
129
 
140
130
  const effectiveSlug = form.slug.trim() || slugify(form.title);
141
- const mergedPrompt = mergePrompt(form.instruction, form.prompt) || null;
142
131
  const nextOrder =
143
132
  sortedObjectives.length > 0
144
133
  ? Math.max(...sortedObjectives.map((o) => o.order)) + 1
145
134
  : 1;
146
135
 
147
136
  try {
137
+ const bodyFields = {
138
+ title: form.title.trim(),
139
+ slug: effectiveSlug,
140
+ instruction: form.instruction.trim() || null,
141
+ description: form.description.trim() || null,
142
+ conversation_flow: form.conversation_flow.length > 0 ? JSON.stringify(form.conversation_flow) : null,
143
+ rules: form.rules.trim() || null,
144
+ };
145
+
148
146
  if (editTarget) {
149
147
  await updateMutation.mutateAsync({
150
148
  idAgent: agent.id,
151
149
  id: editTarget.id,
152
- body: {
153
- title: form.title.trim(),
154
- slug: effectiveSlug,
155
- prompt: mergedPrompt,
156
- },
150
+ body: bodyFields,
157
151
  });
158
152
  toast.success("Objetivo atualizado");
159
153
  } else {
160
154
  await createMutation.mutateAsync({
161
155
  idAgent: agent.id,
162
- body: {
163
- title: form.title.trim(),
164
- slug: effectiveSlug,
165
- prompt: mergedPrompt,
166
- order: nextOrder,
167
- },
156
+ body: { ...bodyFields, order: nextOrder },
168
157
  });
169
158
  toast.success("Objetivo criado");
170
159
  }
@@ -274,23 +263,20 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
274
263
  </Badge>
275
264
  )}
276
265
  </div>
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
- })()}
266
+ {(objective.instruction || objective.description) && (
267
+ <div className="space-y-0.5">
268
+ {objective.instruction && (
269
+ <p className="text-xs font-medium text-muted-foreground">
270
+ Quando: {objective.instruction}
271
+ </p>
272
+ )}
273
+ {objective.description && (
274
+ <p className="line-clamp-1 text-xs text-muted-foreground">
275
+ {objective.description}
276
+ </p>
277
+ )}
278
+ </div>
279
+ )}
294
280
  </div>
295
281
 
296
282
  <Switch
@@ -338,14 +324,14 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
338
324
  </Sortable>
339
325
  )}
340
326
 
341
- {/* Create/Edit Dialog */}
342
- <Dialog open={formOpen} onOpenChange={setFormOpen}>
343
- <DialogContent className="sm:max-w-lg">
344
- <DialogHeader>
345
- <DialogTitle>
327
+ {/* Create/Edit Sheet */}
328
+ <Sheet open={formOpen} onOpenChange={setFormOpen}>
329
+ <SheetContent className="sm:max-w-lg overflow-y-auto">
330
+ <SheetHeader>
331
+ <SheetTitle>
346
332
  {editTarget ? "Editar Objetivo" : "Novo Objetivo"}
347
- </DialogTitle>
348
- </DialogHeader>
333
+ </SheetTitle>
334
+ </SheetHeader>
349
335
  <div className="space-y-4">
350
336
  <div className="space-y-2">
351
337
  <Label htmlFor="objective-title">Título *</Label>
@@ -395,28 +381,56 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
395
381
  placeholder="Ex: Quando o utilizador quer agendar uma consulta"
396
382
  />
397
383
  <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.
384
+ Instrução curta que diz ao agente QUANDO ativar este objetivo. Aparece na secção [OBJETIVOS] do prompt.
385
+ </p>
386
+ </div>
387
+
388
+ <div className="space-y-2">
389
+ <Label htmlFor="objective-description">Descrição</Label>
390
+ <Textarea
391
+ id="objective-description"
392
+ name="description"
393
+ value={form.description}
394
+ onChange={(e) =>
395
+ setForm((f) => ({ ...f, description: e.target.value }))
396
+ }
397
+ placeholder="Descreva o propósito deste objectivo..."
398
+ rows={3}
399
+ />
400
+ <p className="text-xs text-muted-foreground">
401
+ Missão e propósito deste objectivo — o que o agente deve alcançar.
402
+ </p>
403
+ </div>
404
+
405
+ <div className="space-y-2">
406
+ <Label>Fluxo de Conversa</Label>
407
+ <ConversationFlowEditor
408
+ steps={form.conversation_flow}
409
+ onChange={(steps) => setForm((f) => ({ ...f, conversation_flow: steps }))}
410
+ />
411
+ <p className="text-xs text-muted-foreground">
412
+ Etapas que o agente segue quando este objectivo é activado.
399
413
  </p>
400
414
  </div>
401
415
 
402
416
  <div className="space-y-2">
403
- <Label htmlFor="objective-prompt">Instruções detalhadas</Label>
417
+ <Label htmlFor="objective-rules">Regras</Label>
404
418
  <Textarea
405
- id="objective-prompt"
406
- name="prompt"
407
- value={form.prompt}
419
+ id="objective-rules"
420
+ name="rules"
421
+ value={form.rules}
408
422
  onChange={(e) =>
409
- setForm((f) => ({ ...f, prompt: e.target.value }))
423
+ setForm((f) => ({ ...f, rules: e.target.value }))
410
424
  }
411
- placeholder="Instruções detalhadas que o agente seguirá quando este objetivo for ativado..."
412
- rows={6}
425
+ placeholder="Regras específicas deste objectivo..."
426
+ rows={3}
413
427
  />
414
428
  <p className="text-xs text-muted-foreground">
415
- Passos, regras e contexto detalhado para o agente seguir quando este objetivo está ativo.
429
+ Restrições e limites específicos quando este objectivo está activo.
416
430
  </p>
417
431
  </div>
418
432
  </div>
419
- <DialogFooter>
433
+ <SheetFooter>
420
434
  <Button
421
435
  variant="outline"
422
436
  onClick={() => setFormOpen(false)}
@@ -433,9 +447,9 @@ export function AgentObjectivesList({ agent, config }: AgentObjectivesListProps)
433
447
  >
434
448
  {editTarget ? "Salvar" : "Criar"}
435
449
  </Button>
436
- </DialogFooter>
437
- </DialogContent>
438
- </Dialog>
450
+ </SheetFooter>
451
+ </SheetContent>
452
+ </Sheet>
439
453
 
440
454
  {/* Delete confirmation */}
441
455
  <AlertDialog
@@ -0,0 +1,368 @@
1
+ import { useState, useMemo } from "react";
2
+ import type { Agent, PromptVersion, ConversationFlowStep } from "../../types";
3
+ import type { GagentsHookConfig } from "../../hooks/types";
4
+ import { usePromptVersions } from "../../hooks";
5
+ import {
6
+ Button,
7
+ Badge,
8
+ Skeleton,
9
+ Dialog,
10
+ DialogContent,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@greatapps/greatauth-ui/ui";
14
+ import { FileText, RotateCcw, X, AlertTriangle } from "lucide-react";
15
+ import { toast } from "sonner";
16
+
17
+ interface AgentRevisionTabProps {
18
+ agent: Agent;
19
+ config: GagentsHookConfig;
20
+ }
21
+
22
+ const STRUCTURED_MARKERS = ["[IDENTIDADE]", "[MISSÃO]", "[TOM, ESTILO & FORMATO]"];
23
+
24
+ function formatDate(dateStr: string): string {
25
+ const date = new Date(dateStr);
26
+ return date.toLocaleDateString("pt-BR", {
27
+ day: "2-digit",
28
+ month: "2-digit",
29
+ year: "numeric",
30
+ hour: "2-digit",
31
+ minute: "2-digit",
32
+ });
33
+ }
34
+
35
+ function computeDiff(
36
+ oldText: string,
37
+ newText: string,
38
+ ): { type: "added" | "removed" | "equal"; line: string }[] {
39
+ const oldLines = oldText.split("\n");
40
+ const newLines = newText.split("\n");
41
+ const result: { type: "added" | "removed" | "equal"; line: string }[] = [];
42
+ const maxLen = Math.max(oldLines.length, newLines.length);
43
+
44
+ for (let i = 0; i < maxLen; i++) {
45
+ const oldLine = i < oldLines.length ? oldLines[i] : undefined;
46
+ const newLine = i < newLines.length ? newLines[i] : undefined;
47
+
48
+ if (oldLine === newLine) {
49
+ result.push({ type: "equal", line: newLine! });
50
+ } else {
51
+ if (oldLine !== undefined) {
52
+ result.push({ type: "removed", line: oldLine });
53
+ }
54
+ if (newLine !== undefined) {
55
+ result.push({ type: "added", line: newLine });
56
+ }
57
+ }
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ function formatConversationFlow(raw: string | null): string {
64
+ if (!raw) return "";
65
+ try {
66
+ const steps: ConversationFlowStep[] = JSON.parse(raw);
67
+ if (!Array.isArray(steps) || steps.length === 0) return "";
68
+ return steps
69
+ .sort((a, b) => a.order - b.order)
70
+ .map((s) => {
71
+ let line = `${s.order}. ${s.instruction}`;
72
+ if (s.example) line += `\n Exemplo: ${s.example}`;
73
+ return line;
74
+ })
75
+ .join("\n");
76
+ } catch {
77
+ // If it's not valid JSON, return raw text
78
+ return raw;
79
+ }
80
+ }
81
+
82
+ function buildAssembledPrompt(agent: Agent): string {
83
+ const sections: string[] = [];
84
+
85
+ if (agent.identity?.trim()) {
86
+ sections.push(`[IDENTIDADE]\n${agent.identity.trim()}`);
87
+ }
88
+ if (agent.mission?.trim()) {
89
+ sections.push(`[MISSÃO]\n${agent.mission.trim()}`);
90
+ }
91
+ if (agent.tone_style_format?.trim()) {
92
+ sections.push(`[TOM, ESTILO & FORMATO]\n${agent.tone_style_format.trim()}`);
93
+ }
94
+ if (agent.rules?.trim()) {
95
+ sections.push(`[REGRAS]\n${agent.rules.trim()}`);
96
+ }
97
+
98
+ const flowFormatted = formatConversationFlow(agent.conversation_flow);
99
+ if (flowFormatted) {
100
+ sections.push(`[FLUXO DE CONVERSA]\n${flowFormatted}`);
101
+ }
102
+
103
+ if (agent.context?.trim()) {
104
+ sections.push(`[CONTEXTO]\n${agent.context.trim()}`);
105
+ }
106
+
107
+ return sections.join("\n\n");
108
+ }
109
+
110
+ function isLegacyVersion(version: PromptVersion): boolean {
111
+ const content = version.prompt_content ?? "";
112
+ return !STRUCTURED_MARKERS.some((marker) => content.includes(marker));
113
+ }
114
+
115
+ export function AgentRevisionTab({ agent, config }: AgentRevisionTabProps) {
116
+ const { data: versionsData, isLoading } = usePromptVersions(config, agent.id);
117
+
118
+ const [compareVersionId, setCompareVersionId] = useState<number | null>(null);
119
+ const [legacyModalVersion, setLegacyModalVersion] = useState<PromptVersion | null>(null);
120
+
121
+ const versions = (versionsData?.data || []) as PromptVersion[];
122
+ const sortedVersions = [...versions].sort(
123
+ (a, b) => new Date(b.datetime_add).getTime() - new Date(a.datetime_add).getTime(),
124
+ );
125
+ const currentVersion = sortedVersions.find((v) => v.is_current) || sortedVersions[0] || null;
126
+
127
+ const assembledPrompt = useMemo(() => buildAssembledPrompt(agent), [
128
+ agent.identity,
129
+ agent.mission,
130
+ agent.tone_style_format,
131
+ agent.rules,
132
+ agent.conversation_flow,
133
+ agent.context,
134
+ ]);
135
+
136
+ const charCount = assembledPrompt.length;
137
+ const tokenEstimate = Math.ceil(charCount / 4);
138
+
139
+ const compareVersion = sortedVersions.find((v) => v.id === compareVersionId);
140
+
141
+ const diffLines =
142
+ currentVersion && compareVersion && compareVersion.id !== currentVersion.id
143
+ ? computeDiff(compareVersion.prompt_content ?? "", currentVersion.prompt_content ?? "")
144
+ : null;
145
+
146
+ function handleRestore(version: PromptVersion) {
147
+ if (isLegacyVersion(version)) {
148
+ setLegacyModalVersion(version);
149
+ } else {
150
+ toast.info("Restaurar versão estruturada — funcionalidade em desenvolvimento");
151
+ }
152
+ }
153
+
154
+ if (isLoading) {
155
+ return (
156
+ <div className="space-y-3 p-4">
157
+ {Array.from({ length: 3 }).map((_, i) => (
158
+ <Skeleton key={i} className="h-14 w-full" />
159
+ ))}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ return (
165
+ <div className="flex flex-col gap-4 p-4 lg:flex-row">
166
+ {/* Left: Preview + Diff */}
167
+ <div className="min-w-0 flex-1 space-y-4">
168
+ {/* Assembled prompt preview */}
169
+ <div className="space-y-2">
170
+ <h3 className="text-sm font-medium text-muted-foreground">
171
+ Preview do Prompt Montado
172
+ </h3>
173
+ <div className="rounded-lg border">
174
+ <pre className="max-h-[32rem] overflow-auto whitespace-pre-wrap p-4 font-mono text-sm leading-relaxed">
175
+ {assembledPrompt ? (
176
+ assembledPrompt.split("\n").map((line, i) => {
177
+ const isSectionHeader = /^\[.+\]$/.test(line.trim());
178
+ return (
179
+ <span
180
+ key={i}
181
+ className={isSectionHeader ? "font-bold text-foreground" : ""}
182
+ >
183
+ {line}
184
+ {"\n"}
185
+ </span>
186
+ );
187
+ })
188
+ ) : (
189
+ <span className="italic text-muted-foreground">
190
+ Nenhum campo estruturado preenchido.
191
+ </span>
192
+ )}
193
+ </pre>
194
+ </div>
195
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
196
+ <span className="tabular-nums">{charCount.toLocaleString("pt-BR")} caracteres</span>
197
+ <span>·</span>
198
+ <span className="tabular-nums">~{tokenEstimate.toLocaleString("pt-BR")} tokens</span>
199
+ </div>
200
+ </div>
201
+
202
+ {/* Diff panel (conditional) */}
203
+ {diffLines && compareVersion && currentVersion && (
204
+ <div>
205
+ <div className="mb-2 flex items-center justify-between">
206
+ <h3 className="text-sm font-medium text-muted-foreground">
207
+ Diferenças: v{compareVersion.version_number} → v{currentVersion.version_number} (actual)
208
+ </h3>
209
+ <Button
210
+ variant="ghost"
211
+ size="sm"
212
+ onClick={() => setCompareVersionId(null)}
213
+ className="text-xs"
214
+ >
215
+ <X className="mr-1 h-3 w-3" />
216
+ Fechar
217
+ </Button>
218
+ </div>
219
+ <div className="max-h-64 overflow-auto rounded-lg border font-mono text-sm">
220
+ {diffLines.map((line, i) => (
221
+ <div
222
+ key={i}
223
+ className={`whitespace-pre-wrap px-3 py-0.5 ${
224
+ line.type === "added"
225
+ ? "bg-green-500/10 text-green-700 dark:text-green-400"
226
+ : line.type === "removed"
227
+ ? "bg-red-500/10 text-red-700 dark:text-red-400"
228
+ : ""
229
+ }`}
230
+ >
231
+ <span className="mr-2 inline-block w-4 select-none text-muted-foreground">
232
+ {line.type === "added" ? "+" : line.type === "removed" ? "-" : " "}
233
+ </span>
234
+ {line.line || " "}
235
+ </div>
236
+ ))}
237
+ </div>
238
+ </div>
239
+ )}
240
+ </div>
241
+
242
+ {/* Right: Version history timeline */}
243
+ <div className="w-full space-y-2 lg:w-80 lg:shrink-0">
244
+ <h3 className="text-sm font-medium text-muted-foreground">
245
+ Histórico de Versões
246
+ </h3>
247
+ {sortedVersions.length === 0 ? (
248
+ <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-8 text-center">
249
+ <FileText className="mb-2 h-8 w-8 text-muted-foreground" />
250
+ <p className="text-sm text-muted-foreground">
251
+ Nenhuma versão encontrada.
252
+ </p>
253
+ </div>
254
+ ) : (
255
+ <div className="space-y-1">
256
+ {sortedVersions.map((version) => {
257
+ const isCurrent = currentVersion?.id === version.id;
258
+ const isComparing = version.id === compareVersionId;
259
+ return (
260
+ <div
261
+ key={version.id}
262
+ className={`rounded-lg border p-3 transition-colors ${
263
+ isCurrent
264
+ ? "border-primary bg-primary/5"
265
+ : isComparing
266
+ ? "border-muted-foreground/30 bg-muted/50"
267
+ : ""
268
+ }`}
269
+ >
270
+ <div className="flex items-center justify-between gap-2">
271
+ <span className="text-sm font-medium">
272
+ v{version.version_number}
273
+ </span>
274
+ {isCurrent && (
275
+ <Badge variant="default" className="text-[10px] px-1.5 py-0">
276
+ Actual
277
+ </Badge>
278
+ )}
279
+ </div>
280
+ <div className="mt-1 text-xs text-muted-foreground">
281
+ {formatDate(version.datetime_add)}
282
+ </div>
283
+ <div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
284
+ <span>{(version.prompt_content ?? "").length} chars</span>
285
+ <span>·</span>
286
+ <span className="truncate font-mono">
287
+ {(version.prompt_hash ?? "").slice(0, 8)}
288
+ </span>
289
+ </div>
290
+ {version.change_notes && (
291
+ <div className="mt-1.5 text-xs italic text-muted-foreground">
292
+ {version.change_notes}
293
+ </div>
294
+ )}
295
+ {!isCurrent && (
296
+ <div className="mt-2 flex items-center gap-3">
297
+ <button
298
+ type="button"
299
+ onClick={() => setCompareVersionId(isComparing ? null : version.id)}
300
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
301
+ >
302
+ <FileText aria-hidden="true" className="h-3 w-3" />
303
+ {isComparing ? "Ocultar diff" : "Comparar"}
304
+ </button>
305
+ <button
306
+ type="button"
307
+ onClick={() => handleRestore(version)}
308
+ className="flex items-center gap-1 text-xs text-primary hover:underline"
309
+ >
310
+ <RotateCcw aria-hidden="true" className="h-3 w-3" />
311
+ Restaurar
312
+ </button>
313
+ </div>
314
+ )}
315
+ </div>
316
+ );
317
+ })}
318
+ </div>
319
+ )}
320
+ </div>
321
+
322
+ {/* Legacy version modal */}
323
+ <Dialog
324
+ open={!!legacyModalVersion}
325
+ onOpenChange={(open) => {
326
+ if (!open) setLegacyModalVersion(null);
327
+ }}
328
+ >
329
+ <DialogContent className="max-w-2xl">
330
+ <DialogHeader>
331
+ <DialogTitle className="flex items-center gap-2">
332
+ <AlertTriangle className="h-5 w-5 text-amber-500" />
333
+ Versão Legada — v{legacyModalVersion?.version_number}
334
+ </DialogTitle>
335
+ </DialogHeader>
336
+ <div className="space-y-3">
337
+ <div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3">
338
+ <p className="text-sm text-amber-700 dark:text-amber-400">
339
+ Esta versão foi criada antes da reestruturação e não pode ser restaurada nos campos estruturados.
340
+ </p>
341
+ </div>
342
+ <div className="max-h-96 overflow-auto rounded-lg border p-4">
343
+ <pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
344
+ {legacyModalVersion?.prompt_content ?? ""}
345
+ </pre>
346
+ </div>
347
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
348
+ <span>
349
+ {(legacyModalVersion?.prompt_content ?? "").length.toLocaleString("pt-BR")} caracteres
350
+ </span>
351
+ {legacyModalVersion?.change_notes && (
352
+ <>
353
+ <span>·</span>
354
+ <span className="italic">{legacyModalVersion.change_notes}</span>
355
+ </>
356
+ )}
357
+ </div>
358
+ </div>
359
+ <div className="flex justify-end pt-2">
360
+ <Button variant="outline" onClick={() => setLegacyModalVersion(null)}>
361
+ Fechar
362
+ </Button>
363
+ </div>
364
+ </DialogContent>
365
+ </Dialog>
366
+ </div>
367
+ );
368
+ }