"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; import { useWorkflowStore } from "@/store/workflowStore"; import { ImageHistoryItem } from "@/types"; // Helper function for relative time display function formatRelativeTime(timestamp: number): string { const diff = Date.now() - timestamp; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) return `${hours}h ago`; if (minutes > 0) return `${minutes}m ago`; return "Just now"; } // Calculate fan position for each item (vertical stack with slight curve to the right, like macOS Downloads) function calculateFanPosition(index: number, total: number) { // Vertical spacing between items const verticalSpacing = 60; // Curve to the right as items go up - slight quadratic curve const curveStrength = 0.15; const xOffset = index * index * curveStrength; const x = -28 + xOffset; // start centered above icon, then curve to the right const y = -(index * verticalSpacing + 56); // stack upward, start above icon with gap return { x, y }; } // Fan Item Component function FanItem({ item, index, total, onDragStart, }: { item: ImageHistoryItem; index: number; total: number; onDragStart: (e: React.DragEvent, item: ImageHistoryItem) => void; }) { const { x, y } = calculateFanPosition(index, total); const delay = index * 30; return ( ); } // Floating Sidebar for showing all history items function HistorySidebar({ history, onClear, onClose, onDragStart, triggerRect, }: { history: ImageHistoryItem[]; onClear: () => void; onClose: () => void; onDragStart: (e: React.DragEvent, item: ImageHistoryItem) => void; triggerRect: DOMRect | null; }) { const sidebarRef = useRef(null); // Close on click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( sidebarRef.current && !sidebarRef.current.contains(event.target as Node) ) { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [onClose]); // Close on Escape useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [onClose]); // Position the sidebar near the trigger, but ensure it stays on screen const sidebarStyle: React.CSSProperties = { position: "fixed", zIndex: 200, }; if (triggerRect) { // Position above the trigger const left = Math.max(16, triggerRect.left - 140); const bottom = window.innerHeight - triggerRect.top + 8; sidebarStyle.left = `${left}px`; sidebarStyle.bottom = `${bottom}px`; } else { // Fallback to bottom right sidebarStyle.right = "100px"; sidebarStyle.bottom = "100px"; } return createPortal(
{/* Header */}
All History ({history.length})
{/* Scrollable list */}
{history.map((item, index) => (
onDragStart(e, item)} className="flex gap-3 p-2 rounded-lg hover:bg-neutral-700/50 cursor-grab active:cursor-grabbing group transition-colors" > {/* Thumbnail */}
{`History
{/* Info */}

{item.prompt?.substring(0, 60) || "No prompt"}

{formatRelativeTime(item.timestamp)} ยท {item.model === "nano-banana-pro" ? "Pro" : "Standard"}

))}
{/* Footer */}
Drag images to canvas to create nodes
, document.body ); } export function GlobalImageHistory() { const [isOpen, setIsOpen] = useState(false); const [showSidebar, setShowSidebar] = useState(false); const drawerRef = useRef(null); const triggerRef = useRef(null); const history = useWorkflowStore((state) => state.globalImageHistory); const clearGlobalHistory = useWorkflowStore((state) => state.clearGlobalHistory); // Show max 10 items in fan const fanItems = history.slice(0, 10); const hasOverflow = history.length > 10; // Close fan on click outside (but not sidebar) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( drawerRef.current && !drawerRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen && !showSidebar) { document.addEventListener("mousedown", handleClickOutside); } return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen, showSidebar]); // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { if (showSidebar) { setShowSidebar(false); } else { setIsOpen(false); } } }; if (isOpen || showSidebar) { document.addEventListener("keydown", handleKeyDown); } return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, showSidebar]); const handleDragStart = useCallback( (e: React.DragEvent, item: ImageHistoryItem) => { e.dataTransfer.setData( "application/history-image", JSON.stringify({ image: item.image, prompt: item.prompt, timestamp: item.timestamp, }) ); e.dataTransfer.effectAllowed = "copy"; // Close the fan/sidebar after drag has started // Using setTimeout to defer state change until after the drag is properly initiated // This prevents the draggable element from being unmounted mid-drag setTimeout(() => { setIsOpen(false); setShowSidebar(false); }, 0); }, [] ); const handleShowAll = useCallback(() => { setIsOpen(false); setShowSidebar(true); }, []); const handleCloseSidebar = useCallback(() => { setShowSidebar(false); }, []); const handleClear = useCallback(() => { clearGlobalHistory(); setIsOpen(false); setShowSidebar(false); }, [clearGlobalHistory]); if (history.length === 0) return null; return (
{/* Trigger Button */} {/* Fan Layout */} {isOpen && (
{/* Fan items */}
{fanItems.map((item, index) => ( ))}
{/* Show All button (if more than 10 items) - positioned relative to top fan item */} {hasOverflow && (() => { const topItemPos = calculateFanPosition(fanItems.length - 1, fanItems.length); return ( ); })()}
)} {/* Sidebar for all items */} {showSidebar && ( )}
); }