"use client"; /* Reusable version-history drawer: lists recent versions of an entity (workflow, agent, tool, …) with author + timestamp + label and a Restore action. Wired to the /v1/versions/{entity_type}/{entity_id} endpoints. Drop into any editor toolbar. */ import { useCallback, useEffect, useState } from "react"; import { Icon } from "./icons"; import { Drawer } from "./primitives"; import { api, EntityType, EntityVersion } from "@/lib/api"; /** Compact "3m ago" / "2d ago" relative time, falling back to a locale date. */ function relTime(iso?: string | null): string { if (!iso) return ""; const t = new Date(iso).getTime(); if (Number.isNaN(t)) return String(iso); const s = Math.round((Date.now() - t) / 1000); if (s < 60) return "just now"; const m = Math.round(s / 60); if (m < 60) return `${m}m ago`; const h = Math.round(m / 60); if (h < 24) return `${h}h ago`; const d = Math.round(h / 24); if (d < 30) return `${d}d ago`; return new Date(iso).toLocaleDateString(); } export function VersionHistory({ entityType, entityId, entityLabel, onRestored, buttonClassName = "btn btn-secondary btn-sm", buttonLabel = "History", allowRestore = true, }: { entityType: EntityType; entityId?: string | null; entityLabel?: string; onRestored?: () => void; buttonClassName?: string; buttonLabel?: string; // Some entities (e.g. knowledge sources) version only their config metadata, not the // embedded content, so a "restore" would be misleading - show read-only history instead. allowRestore?: boolean; }) { const [open, setOpen] = useState(false); const [rows, setRows] = useState(null); const [err, setErr] = useState(null); const [restoring, setRestoring] = useState(null); const [expanded, setExpanded] = useState(null); const [snapshots, setSnapshots] = useState>({}); const load = useCallback(() => { if (!entityId) return; setRows(null); setErr(null); api .listVersions(entityType, entityId) .then((v) => setRows(v)) .catch((e) => setErr(String(e?.message || e))); }, [entityType, entityId]); useEffect(() => { if (open) load(); }, [open, load]); async function peek(versionNo: number) { if (expanded === versionNo) { setExpanded(null); return; } setExpanded(versionNo); if (snapshots[versionNo] || !entityId) return; try { const v = await api.getVersion(entityType, entityId, versionNo); setSnapshots((s) => ({ ...s, [versionNo]: JSON.stringify(v.snapshot ?? {}, null, 2) })); } catch { setSnapshots((s) => ({ ...s, [versionNo]: "(could not load snapshot)" })); } } async function restore(versionNo: number) { if (!entityId) return; if (!window.confirm(`Restore version ${versionNo}? The current state is saved as a new version first, so this is reversible.`)) return; setRestoring(versionNo); try { await api.restoreVersion(entityType, entityId, versionNo); setOpen(false); onRestored?.(); } catch (e: any) { setErr(String(e?.message || e)); } finally { setRestoring(null); } } const latest = rows && rows.length ? Math.max(...rows.map((r) => r.version_no)) : null; return ( <> setOpen(false)} title="Version history" sub={entityLabel} width={420}>
{err && (
{err}
)} {!err && rows === null && (
Loading versions…
)} {!err && rows !== null && rows.length === 0 && (
No saved versions yet.
Versions are captured each time you save or publish.
)} {rows?.map((v) => { const isLatest = v.version_no === latest; const isOpen = expanded === v.version_no; return (
v{v.version_no} {isLatest && current} {v.label && {v.label}}
{v.author_email || "unknown"} {v.created_at ? ` · ${relTime(v.created_at)}` : ""}
{allowRestore && ( )}
{isOpen && (
                    {snapshots[v.version_no] ?? "Loading…"}
                  
)}
); })}
); }