@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.
- package/dist/index.d.ts +52 -16
- package/dist/index.js +1452 -1115
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -4
- package/src/components/agents/agent-definition-editor.tsx +310 -0
- package/src/components/agents/agent-edit-form.tsx +14 -14
- package/src/components/agents/agent-form-dialog.tsx +15 -15
- package/src/components/agents/agent-objectives-list.tsx +92 -78
- package/src/components/agents/agent-revision-tab.tsx +368 -0
- package/src/components/agents/agent-tabs.tsx +17 -8
- package/src/components/agents/conversation-flow-editor.tsx +180 -0
- package/src/hooks/use-agents.ts +7 -2
- package/src/hooks/use-objectives.ts +8 -2
- package/src/index.ts +4 -1
- package/src/types/index.ts +16 -1
- package/src/components/agents/agent-prompt-editor.tsx +0 -442
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
61
|
+
description: string;
|
|
62
|
+
conversation_flow: ConversationFlowStep[];
|
|
63
|
+
rules: string;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
const EMPTY_FORM: ObjectiveFormState = { title: "", slug: "", instruction: "",
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
342
|
-
<
|
|
343
|
-
<
|
|
344
|
-
<
|
|
345
|
-
<
|
|
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
|
-
</
|
|
348
|
-
</
|
|
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 [
|
|
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-
|
|
417
|
+
<Label htmlFor="objective-rules">Regras</Label>
|
|
404
418
|
<Textarea
|
|
405
|
-
id="objective-
|
|
406
|
-
name="
|
|
407
|
-
value={form.
|
|
419
|
+
id="objective-rules"
|
|
420
|
+
name="rules"
|
|
421
|
+
value={form.rules}
|
|
408
422
|
onChange={(e) =>
|
|
409
|
-
setForm((f) => ({ ...f,
|
|
423
|
+
setForm((f) => ({ ...f, rules: e.target.value }))
|
|
410
424
|
}
|
|
411
|
-
placeholder="
|
|
412
|
-
rows={
|
|
425
|
+
placeholder="Regras específicas deste objectivo..."
|
|
426
|
+
rows={3}
|
|
413
427
|
/>
|
|
414
428
|
<p className="text-xs text-muted-foreground">
|
|
415
|
-
|
|
429
|
+
Restrições e limites específicos quando este objectivo está activo.
|
|
416
430
|
</p>
|
|
417
431
|
</div>
|
|
418
432
|
</div>
|
|
419
|
-
<
|
|
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
|
-
</
|
|
437
|
-
</
|
|
438
|
-
</
|
|
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
|
+
}
|