summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/files/VersionHistoryDropdown.tsx
blob: 50e6f0a042709f24317df1b3d76ec7d4745f8368 (plain) (tree)






































































































































































































































































                                                                                                                                                                                         
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<number | null>(null);
  const dropdownRef = useRef<HTMLDivElement>(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 (
      <div className="border border-[#3f6fb3]/50 bg-[#0d1b2d] p-4">
        <div className="flex items-center justify-between mb-3">
          <div className="flex items-center gap-2">
            <span className="font-mono text-xs text-[#75aafc] uppercase">Viewing Version</span>
            <span className="font-mono text-sm text-[#dbe7ff] font-bold">
              v{selectedVersion.version}
            </span>
            <span className={`font-mono text-xs ${getSourceColor(selectedVersion.source)}`}>
              ({getSourceLabel(selectedVersion.source)})
            </span>
          </div>
          <button
            onClick={onClearSelection}
            className="font-mono text-xs text-[#555] hover:text-white/70"
          >
            Close
          </button>
        </div>

        <div className="text-[10px] font-mono text-[#555] mb-3">
          {formatDate(selectedVersion.createdAt)}
          {selectedVersion.changeDescription && (
            <span className="ml-2 text-[#9bc3ff]">- {selectedVersion.changeDescription}</span>
          )}
        </div>

        {/* Preview of version content */}
        <div className="max-h-48 overflow-y-auto mb-4 border border-[#333] bg-black/20 p-3">
          {selectedVersion.body.length === 0 ? (
            <div className="text-[#555] text-xs italic">No content</div>
          ) : (
            <div className="space-y-2">
              {selectedVersion.body.map((element, index) => (
                <div key={index} className="font-mono text-xs text-white/70">
                  {element.type === "heading" && (
                    <div className="text-[#9bc3ff] font-bold">
                      {"#".repeat(element.level)} {element.text}
                    </div>
                  )}
                  {element.type === "paragraph" && (
                    <div className="text-white/60">{element.text}</div>
                  )}
                  {element.type === "chart" && (
                    <div className="text-purple-400">[Chart: {element.title || element.chartType}]</div>
                  )}
                  {element.type === "image" && (
                    <div className="text-green-400">[Image: {element.alt || element.src}]</div>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>

        {/* Restore button */}
        {!showConfirm ? (
          <button
            onClick={() => handleRestoreClick(selectedVersion.version)}
            disabled={restoring}
            className="w-full px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50"
          >
            {restoring ? "Restoring..." : "Restore This Version"}
          </button>
        ) : (
          <div className="border border-yellow-500/30 bg-yellow-500/10 p-3">
            <div className="font-mono text-xs text-yellow-400 mb-2">
              Are you sure you want to restore to version {versionToRestore}?
            </div>
            <div className="font-mono text-[10px] text-white/50 mb-3">
              This will create a new version with the content from v{versionToRestore}.
              Your current changes will be preserved as a separate version.
            </div>
            <div className="flex gap-2">
              <button
                onClick={handleConfirmRestore}
                disabled={restoring}
                className="flex-1 px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-yellow-600/50 border border-yellow-500/50 hover:bg-yellow-600/70 transition-colors disabled:opacity-50"
              >
                {restoring ? "Restoring..." : "Confirm Restore"}
              </button>
              <button
                onClick={handleCancelRestore}
                disabled={restoring}
                className="px-3 py-1.5 font-mono text-xs text-[#555] border border-[#333] hover:text-white/70"
              >
                Cancel
              </button>
            </div>
          </div>
        )}
      </div>
    );
  }

  return (
    <div ref={dropdownRef} className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-2 px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
      >
        <span>v{currentVersion}</span>
        <svg
          width="10"
          height="6"
          viewBox="0 0 10 6"
          fill="none"
          className={`transition-transform ${isOpen ? "rotate-180" : ""}`}
        >
          <path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
        </svg>
      </button>

      {isOpen && (
        <div className="absolute top-full left-0 mt-1 w-64 max-h-72 overflow-y-auto bg-[#0d1b2d] border border-[#3f6fb3]/50 shadow-lg z-50">
          <div className="p-2 border-b border-[#333] font-mono text-[10px] text-[#555] uppercase">
            Version History
          </div>

          {loading ? (
            <div className="p-4 text-center font-mono text-xs text-[#555]">Loading...</div>
          ) : versions.length === 0 ? (
            <div className="p-4 text-center font-mono text-xs text-[#555]">No versions found</div>
          ) : (
            <div>
              {versions.map((version) => (
                <button
                  key={version.version}
                  onClick={() => handleVersionClick(version.version)}
                  disabled={version.version === currentVersion || loadingVersion}
                  className={`w-full text-left px-3 py-2 font-mono text-xs transition-colors ${
                    version.version === currentVersion
                      ? "bg-[#0f3c78]/50 text-[#dbe7ff]"
                      : "hover:bg-[#0f3c78]/30 text-white/70"
                  } ${loadingVersion ? "opacity-50" : ""}`}
                >
                  <div className="flex items-center justify-between">
                    <span className="font-bold">v{version.version}</span>
                    <span className={getSourceColor(version.source)}>
                      {getSourceLabel(version.source)}
                    </span>
                  </div>
                  <div className="text-[10px] text-[#555] mt-0.5">
                    {formatDate(version.createdAt)}
                  </div>
                  {version.changeDescription && (
                    <div className="text-[10px] text-[#75aafc] mt-0.5 truncate">
                      {version.changeDescription}
                    </div>
                  )}
                  {version.version === currentVersion && (
                    <div className="text-[10px] text-green-400 mt-0.5">(current)</div>
                  )}
                </button>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}