From 2faba0388f93d8e4fb86219eba7883b331d501ff Mon Sep 17 00:00:00 2001 From: soryu Date: Wed, 24 Dec 2025 05:45:22 +0000 Subject: Add versioning to files --- .../components/files/VersionHistoryDropdown.tsx | 263 +++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 makima/frontend/src/components/files/VersionHistoryDropdown.tsx (limited to 'makima/frontend/src/components/files/VersionHistoryDropdown.tsx') diff --git a/makima/frontend/src/components/files/VersionHistoryDropdown.tsx b/makima/frontend/src/components/files/VersionHistoryDropdown.tsx new file mode 100644 index 0000000..50e6f0a --- /dev/null +++ b/makima/frontend/src/components/files/VersionHistoryDropdown.tsx @@ -0,0 +1,263 @@ +import { useState, useEffect, useRef } from "react"; +import type { FileVersionSummary, FileVersion, VersionSource } from "../../lib/api"; + +interface VersionHistoryDropdownProps { + currentVersion: number; + versions: FileVersionSummary[]; + loading: boolean; + selectedVersion: FileVersion | null; + loadingVersion: boolean; + onSelectVersion: (version: number) => void; + onRestoreVersion: (version: number) => void; + onClearSelection: () => void; + restoring: boolean; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function getSourceLabel(source: VersionSource): string { + switch (source) { + case "user": + return "User"; + case "llm": + return "AI"; + case "system": + return "System"; + } +} + +function getSourceColor(source: VersionSource): string { + switch (source) { + case "user": + return "text-blue-400"; + case "llm": + return "text-purple-400"; + case "system": + return "text-gray-400"; + } +} + +export function VersionHistoryDropdown({ + currentVersion, + versions, + loading, + selectedVersion, + loadingVersion, + onSelectVersion, + onRestoreVersion, + onClearSelection, + restoring, +}: VersionHistoryDropdownProps) { + const [isOpen, setIsOpen] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [versionToRestore, setVersionToRestore] = useState(null); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleVersionClick = (version: number) => { + if (version === currentVersion) return; + onSelectVersion(version); + setIsOpen(false); + }; + + const handleRestoreClick = (version: number) => { + setVersionToRestore(version); + setShowConfirm(true); + }; + + const handleConfirmRestore = () => { + if (versionToRestore !== null) { + onRestoreVersion(versionToRestore); + } + setShowConfirm(false); + setVersionToRestore(null); + }; + + const handleCancelRestore = () => { + setShowConfirm(false); + setVersionToRestore(null); + onClearSelection(); + }; + + // If showing the selected version preview + if (selectedVersion) { + return ( +
+
+
+ Viewing Version + + v{selectedVersion.version} + + + ({getSourceLabel(selectedVersion.source)}) + +
+ +
+ +
+ {formatDate(selectedVersion.createdAt)} + {selectedVersion.changeDescription && ( + - {selectedVersion.changeDescription} + )} +
+ + {/* Preview of version content */} +
+ {selectedVersion.body.length === 0 ? ( +
No content
+ ) : ( +
+ {selectedVersion.body.map((element, index) => ( +
+ {element.type === "heading" && ( +
+ {"#".repeat(element.level)} {element.text} +
+ )} + {element.type === "paragraph" && ( +
{element.text}
+ )} + {element.type === "chart" && ( +
[Chart: {element.title || element.chartType}]
+ )} + {element.type === "image" && ( +
[Image: {element.alt || element.src}]
+ )} +
+ ))} +
+ )} +
+ + {/* Restore button */} + {!showConfirm ? ( + + ) : ( +
+
+ Are you sure you want to restore to version {versionToRestore}? +
+
+ This will create a new version with the content from v{versionToRestore}. + Your current changes will be preserved as a separate version. +
+
+ + +
+
+ )} +
+ ); + } + + return ( +
+ + + {isOpen && ( +
+
+ Version History +
+ + {loading ? ( +
Loading...
+ ) : versions.length === 0 ? ( +
No versions found
+ ) : ( +
+ {versions.map((version) => ( + + ))} +
+ )} +
+ )} +
+ ); +} -- cgit v1.2.3