@greatapps/greatagents-ui 0.3.20 → 0.3.22
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 +1449 -1109
- 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 +305 -0
- package/src/components/agents/agent-objectives-list.tsx +76 -62
- 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/components/capabilities/integrations-tab.tsx +11 -6
- 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
|
@@ -26,7 +26,10 @@ export function useCreateObjective(config: GagentsHookConfig) {
|
|
|
26
26
|
body: {
|
|
27
27
|
title: string;
|
|
28
28
|
slug?: string;
|
|
29
|
-
|
|
29
|
+
instruction?: string | null;
|
|
30
|
+
description?: string | null;
|
|
31
|
+
conversation_flow?: string | null;
|
|
32
|
+
rules?: string | null;
|
|
30
33
|
order?: number;
|
|
31
34
|
active?: boolean;
|
|
32
35
|
};
|
|
@@ -52,7 +55,10 @@ export function useUpdateObjective(config: GagentsHookConfig) {
|
|
|
52
55
|
body: Partial<{
|
|
53
56
|
title: string;
|
|
54
57
|
slug: string;
|
|
55
|
-
|
|
58
|
+
instruction: string | null;
|
|
59
|
+
description: string | null;
|
|
60
|
+
conversation_flow: string | null;
|
|
61
|
+
rules: string | null;
|
|
56
62
|
order: number;
|
|
57
63
|
active: boolean;
|
|
58
64
|
}>;
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type {
|
|
|
19
19
|
CapabilitiesResponse,
|
|
20
20
|
AgentCapability,
|
|
21
21
|
AgentCapabilitiesPayload,
|
|
22
|
+
ConversationFlowStep,
|
|
22
23
|
} from "./types";
|
|
23
24
|
|
|
24
25
|
// Client
|
|
@@ -36,7 +37,9 @@ export { AgentsTable } from "./components/agents/agents-table";
|
|
|
36
37
|
export { AgentFormDialog } from "./components/agents/agent-form-dialog";
|
|
37
38
|
export { AgentEditForm } from "./components/agents/agent-edit-form";
|
|
38
39
|
export { AgentTabs } from "./components/agents/agent-tabs";
|
|
39
|
-
export {
|
|
40
|
+
export { AgentDefinitionEditor } from "./components/agents/agent-definition-editor";
|
|
41
|
+
export { AgentRevisionTab } from "./components/agents/agent-revision-tab";
|
|
42
|
+
export { ConversationFlowEditor } from "./components/agents/conversation-flow-editor";
|
|
40
43
|
export { AgentObjectivesList } from "./components/agents/agent-objectives-list";
|
|
41
44
|
export { AgentToolsList } from "./components/agents/agent-tools-list";
|
|
42
45
|
export { ToolsTable } from "./components/tools/tools-table";
|
package/src/types/index.ts
CHANGED
|
@@ -14,19 +14,34 @@ export interface Agent {
|
|
|
14
14
|
openai_assistant_id: string | null;
|
|
15
15
|
delay_typing: number | null;
|
|
16
16
|
waiting_time: number | null;
|
|
17
|
+
identity: string | null;
|
|
18
|
+
mission: string | null;
|
|
19
|
+
tone_style_format: string | null;
|
|
20
|
+
rules: string | null;
|
|
21
|
+
conversation_flow: string | null;
|
|
22
|
+
context: string | null;
|
|
17
23
|
active: boolean;
|
|
18
24
|
deleted: number;
|
|
19
25
|
datetime_add: string;
|
|
20
26
|
datetime_alt: string | null;
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
export interface ConversationFlowStep {
|
|
30
|
+
order: number;
|
|
31
|
+
instruction: string;
|
|
32
|
+
example: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
export interface Objective {
|
|
24
36
|
id: number;
|
|
25
37
|
id_account: number;
|
|
26
38
|
id_agent: number;
|
|
27
39
|
title: string;
|
|
28
40
|
slug: string | null;
|
|
29
|
-
|
|
41
|
+
instruction: string | null;
|
|
42
|
+
description: string | null;
|
|
43
|
+
conversation_flow: string | null;
|
|
44
|
+
rules: string | null;
|
|
30
45
|
order: number;
|
|
31
46
|
active: boolean;
|
|
32
47
|
deleted: number;
|
|
@@ -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
|
-
}
|