You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

172 lines
7.1 KiB

"use client";
import { useCallback, useState, useEffect } from "react";
import { Handle, Position, NodeProps, Node } from "@xyflow/react";
import { BaseNode } from "./BaseNode";
import { useWorkflowStore } from "@/store/workflowStore";
import { SplitGridNodeData } from "@/types";
import { SplitGridSettingsModal } from "../SplitGridSettingsModal";
type SplitGridNodeType = Node<SplitGridNodeData, "splitGrid">;
export function SplitGridNode({ id, data, selected }: NodeProps<SplitGridNodeType>) {
const nodeData = data;
const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
const isRunning = useWorkflowStore((state) => state.isRunning);
const [showSettings, setShowSettings] = useState(false);
// Show settings modal on first creation (when not configured)
useEffect(() => {
if (!nodeData.isConfigured && (!nodeData.childNodeIds || nodeData.childNodeIds.length === 0)) {
setShowSettings(true);
}
}, [nodeData.isConfigured, nodeData.childNodeIds]);
const handleOpenSettings = useCallback(() => {
setShowSettings(true);
}, []);
const handleCloseSettings = useCallback(() => {
setShowSettings(false);
}, []);
const handleSplit = useCallback(() => {
regenerateNode(id);
}, [id, regenerateNode]);
return (
<>
<BaseNode
id={id}
selected={selected}
hasError={nodeData.status === "error"}
fullBleed
aspectFitMedia={nodeData.sourceImage}
>
{/* Image input handle */}
<Handle
type="target"
position={Position.Left}
id="image"
data-handletype="image"
style={{ zIndex: 10 }}
/>
{/* Reference output handle for visual links to child nodes */}
<Handle
type="source"
position={Position.Right}
id="reference"
data-handletype="reference"
className="!bg-gray-500"
style={{ zIndex: 10 }}
/>
{/* Full-bleed preview area */}
{nodeData.sourceImage ? (
<div className="relative w-full h-full">
<img
src={nodeData.sourceImage}
alt="Source grid"
className="w-full h-full object-contain rounded-lg"
/>
{/* Grid overlay visualization */}
<div
className="absolute inset-0 pointer-events-none"
style={{
display: "grid",
gridTemplateColumns: `repeat(${nodeData.gridCols}, 1fr)`,
gridTemplateRows: `repeat(${nodeData.gridRows}, 1fr)`,
}}
>
{Array.from({ length: nodeData.targetCount }).map((_, i) => (
<div
key={i}
className="border border-blue-400/50"
/>
))}
</div>
{/* Loading overlay */}
{nodeData.status === "loading" && (
<div className="absolute inset-0 bg-neutral-900/70 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 animate-spin text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
)}
</div>
) : (
<div className="w-full h-full min-h-[112px] bg-neutral-900/40 flex flex-col items-center justify-center rounded-lg">
{nodeData.status === "error" ? (
<span className="text-[10px] text-red-400 text-center px-2">
{nodeData.error || "Error"}
</span>
) : nodeData.status === "loading" ? (
<svg className="w-4 h-4 animate-spin text-neutral-400" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
) : (
<>
<svg className="w-5 h-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
<span className="text-neutral-500 text-[10px] mt-1">
Connect image
</span>
</>
)}
</div>
)}
{/* Controls overlay pinned at bottom */}
<div className="absolute bottom-0 left-0 right-0 z-10 px-3 py-2 bg-neutral-900/90 rounded-b-lg space-y-1">
{/* Config summary */}
<div className="flex items-center justify-between text-[10px] text-neutral-400">
<span>{nodeData.gridRows}x{nodeData.gridCols} grid ({nodeData.targetCount} images)</span>
<button
onClick={handleOpenSettings}
className="nodrag nopan text-blue-400 hover:text-blue-300 transition-colors"
>
Settings
</button>
</div>
{/* Child node count / status */}
<div className="flex items-center justify-between">
{nodeData.isConfigured ? (
<div className="text-[10px] text-neutral-500">
{nodeData.childNodeIds?.length ?? 0} generate sets created
</div>
) : (
<div className="text-[10px] text-amber-400">
Not configured - click Settings
</div>
)}
{/* Split button */}
<button
onClick={handleSplit}
disabled={isRunning || !nodeData.isConfigured || !nodeData.sourceImage}
className="nodrag nopan px-2 py-0.5 text-[10px] border border-white hover:bg-white hover:text-neutral-900 disabled:border-neutral-600 disabled:text-neutral-600 disabled:cursor-not-allowed text-white rounded transition-colors"
title={!nodeData.isConfigured ? "Configure node first" : !nodeData.sourceImage ? "Connect an image first" : "Split grid"}
>
Split
</button>
</div>
</div>
</BaseNode>
{/* Settings Modal */}
{showSettings && (
<SplitGridSettingsModal
nodeId={id}
nodeData={nodeData}
onClose={handleCloseSettings}
/>
)}
</>
);
}