"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useShallow } from "zustand/shallow"; import { useWorkflowStore } from "@/store/workflowStore"; import type { GenerateVideoNodeData, NanoBananaNodeData, ProviderType, WorkflowNode, } from "@/types"; import { MediaPreviewModal } from "@/components/MediaPreviewModal"; type WorkflowGenerationHistoryItem = { key: string; id: string; nodeId: string; historyIndex: number; mediaType: "image" | "video"; mediaUrl?: string; coverUrl?: string; timestamp: number; prompt: string; model: string; modelDisplayName?: string; modelProvider?: ProviderType; }; 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"; } function calculateFanPosition(index: number) { const verticalSpacing = 60; const curveStrength = 0.15; return { x: -28 + index * index * curveStrength, y: -(index * verticalSpacing + 56), }; } export function collectWorkflowGenerationHistory(nodes: WorkflowNode[]): WorkflowGenerationHistoryItem[] { const items: WorkflowGenerationHistoryItem[] = []; nodes.forEach((node) => { if (node.type === "nanoBanana" || node.type === "smartImage") { const data = node.data as NanoBananaNodeData; (data.imageHistory || []).forEach((item, historyIndex) => { items.push({ key: `${node.id}:image:${item.id}:${historyIndex}`, id: item.id, nodeId: node.id, historyIndex, mediaType: "image", mediaUrl: item.image ?? item.previewImg, timestamp: item.timestamp, prompt: item.prompt, model: item.model, modelDisplayName: item.modelDisplayName, modelProvider: item.modelProvider, }); }); } if (node.type === "generateVideo" || node.type === "smartVideo") { const data = node.data as GenerateVideoNodeData; (data.videoHistory || []).forEach((item, historyIndex) => { items.push({ key: `${node.id}:video:${item.id}:${historyIndex}`, id: item.id, nodeId: node.id, historyIndex, mediaType: "video", mediaUrl: item.video, coverUrl: item.videoCover, timestamp: item.timestamp, prompt: item.prompt, model: item.model, modelDisplayName: item.modelDisplayName, modelProvider: item.modelProvider, }); }); } }); return items.sort((a, b) => b.timestamp - a.timestamp); } function MediaThumb({ item, src, className, }: { item: WorkflowGenerationHistoryItem; src?: string; className: string; }) { if (!src) { return (
{item.mediaType === "video" ? "Video" : "Image"}
); } if (item.mediaType === "video" && item.coverUrl) { return ( ); } return item.mediaType === "video" ? (