/** * NODE COMPONENT VIEWS FOR NANO BANANA EDITOR * * This file contains all the visual node components for the Nano Banana Editor, * a visual node-based AI image processing application. Each node represents a * specific image transformation or effect that can be chained together to create * complex image processing workflows. * * ARCHITECTURE OVERVIEW: * - Each node is a self-contained React component with its own state and UI * - Nodes use a common dragging system (useNodeDrag hook) for positioning * - All nodes follow a consistent structure: Header + Content + Output * - Nodes communicate through a connection system using input/output ports * - Processing is handled asynchronously with loading states and error handling * * NODE TYPES AVAILABLE: * - BackgroundNodeView: Change/generate image backgrounds (color, preset, upload, AI-generated) * - ClothesNodeView: Add/modify clothing on subjects (preset garments or custom uploads) * - StyleNodeView: Apply artistic styles and filters (anime, fine art, cinematic styles) * - EditNodeView: General text-based image editing (natural language instructions) * - CameraNodeView: Apply camera effects and settings (focal length, aperture, film styles) * - AgeNodeView: Transform subject age (AI-powered age progression/regression) * - FaceNodeView: Modify facial features and accessories (hair, makeup, expressions) * - LightningNodeView: Apply professional lighting effects * - PosesNodeView: Modify body poses and positioning * * COMMON PATTERNS: * - All nodes support drag-and-drop for repositioning in the editor * - Input/output ports allow chaining nodes together in processing pipelines * - File upload via drag-drop, file picker, or clipboard paste where applicable * - Real-time preview of settings and processed results * - History navigation for viewing different processing results * - Error handling with user-friendly error messages * - AI-powered prompt improvement using Gemini API where applicable * * USER WORKFLOW: * 1. Add nodes to the editor canvas * 2. Configure each node's settings (colors, styles, uploaded images, etc.) * 3. Connect nodes using input/output ports to create processing chains * 4. Process individual nodes or entire chains * 5. Preview results, navigate history, and download final images * * TECHNICAL DETAILS: * - Uses React hooks for state management (useState, useEffect, useRef) * - Custom useNodeDrag hook handles node positioning and drag interactions * - Port component manages connection logic between nodes * - All image data is handled as base64 data URLs for browser compatibility * - Processing results are cached with history navigation support * - Responsive UI components from shadcn/ui component library */ // Enable React Server Components client-side rendering for this file "use client"; // Import React core functionality for state management and lifecycle hooks import React, { useState, useRef, useEffect } from "react"; // Import reusable UI components from the shadcn/ui component library import { Button } from "../components/ui/button"; // Standard button component import { Select } from "../components/ui/select"; // Dropdown selection component import { Textarea } from "../components/ui/textarea"; // Multi-line text input component import { Slider } from "../components/ui/slider"; // Range slider input component import { ColorPicker } from "../components/ui/color-picker"; // Color selection component import { Checkbox } from "../components/ui/checkbox"; // Checkbox input component /** * Helper function to download processed images * Creates a temporary download link and triggers the browser's download mechanism * * @param dataUrl Base64 data URL of the image to download * @param filename Desired filename for the downloaded image */ function downloadImage(dataUrl: string, filename: string) { const link = document.createElement('a'); // Create an invisible anchor element for download link.href = dataUrl; // Set the base64 image data as the link target link.download = filename; // Specify the filename for the downloaded file document.body.appendChild(link); // Temporarily add link to DOM (Firefox requirement) link.click(); // Programmatically trigger the download document.body.removeChild(link); // Remove the temporary link element from DOM } /** * Helper function to copy image to clipboard * Converts the image data URL to blob and copies it to clipboard * * @param dataUrl Base64 data URL of the image to copy */ async function copyImageToClipboard(dataUrl: string) { try { // Fetch the data URL and convert it to a Blob object const response = await fetch(dataUrl); // Fetch the base64 data URL const blob = await response.blob(); // Convert response to Blob format // The browser clipboard API only supports PNG format for images // If the image is not PNG, we need to convert it first if (blob.type !== 'image/png') { // Create a canvas element to handle image format conversion const canvas = document.createElement('canvas'); // Create invisible canvas const ctx = canvas.getContext('2d'); // Get 2D drawing context const img = new Image(); // Create image element // Wait for the image to load before processing await new Promise((resolve) => { img.onload = () => { // When image loads canvas.width = img.width; // Set canvas width to match image canvas.height = img.height; // Set canvas height to match image ctx?.drawImage(img, 0, 0); // Draw image onto canvas resolve(void 0); // Resolve the promise }; img.src = dataUrl; // Start loading the image }); // Convert the canvas content to PNG blob const pngBlob = await new Promise((resolve) => { canvas.toBlob((blob) => resolve(blob!), 'image/png'); // Convert canvas to PNG blob }); // Write the converted PNG blob to clipboard await navigator.clipboard.write([ new ClipboardItem({ 'image/png': pngBlob }) // Create clipboard item with PNG data ]); } else { // Image is already PNG, copy directly to clipboard await navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) // Copy original blob to clipboard ]); } } catch (error) { // Handle any errors that occur during the copy process console.error('Failed to copy image to clipboard:', error); } } /** * REUSABLE OUTPUT SECTION COMPONENT * * This component provides a standardized output display for all node types. * It handles the common functionality that every node needs for showing results: * * Key Features: * - Displays processed output images with click-to-copy functionality * - Provides download functionality with custom filenames * - Visual feedback when images are copied to clipboard * - Consistent styling across all node types * - Hover effects and tooltips for better UX * * User Interactions: * - Left-click or right-click image to copy to clipboard * - Click download button to save image with timestamp * - Visual feedback shows when image is successfully copied * * Technical Implementation: * - Converts images to clipboard-compatible format (PNG) * - Uses browser's native download API * - Provides visual feedback through temporary styling changes * - Handles both base64 data URLs and regular image URLs * * @param nodeId - Unique identifier for the node (for potential future features) * @param output - Optional current output image (base64 data URL or image URL) * @param downloadFileName - Filename to use when downloading (should include extension) */ function NodeOutputSection({ nodeId, // Unique identifier for the node output, // Optional current output image (base64 data URL) downloadFileName, // Filename to use when downloading the image }: { nodeId: string; // Node ID type definition output?: string; // Optional output image string downloadFileName: string; // Required download filename }) { // If no image is available, don't render anything if (!output) return null; return ( // Main container for output section with vertical spacing
{/* Output header container */}
{/* Header row with title */}
{/* Output section label */}
Output
{/* Output image with click-to-copy functionality */} Output copyImageToClipboard(output)} // Left-click copies to clipboard onContextMenu={(e) => { // Right-click context menu handler e.preventDefault(); // Prevent browser context menu from appearing copyImageToClipboard(output); // Copy image to clipboard // Show brief visual feedback when image is copied const img = e.currentTarget; // Get the image element const originalTitle = img.title; // Store original tooltip text img.title = "Copied to clipboard!"; // Update tooltip to show success img.style.filter = "brightness(1.2)"; // Brighten the image briefly img.style.transform = "scale(0.98)"; // Slightly scale down the image // Reset visual feedback after 300ms setTimeout(() => { img.title = originalTitle; // Restore original tooltip img.style.filter = ""; // Remove brightness filter img.style.transform = ""; // Reset scale transform }, 300); }} title="💾 Click or right-click to copy image to clipboard" // Tooltip instruction />
{/* Download button for saving the current image */} {/* End of main output section container */}
); } /* ======================================== TYPE DEFINITIONS (TEMPORARY) ======================================== */ // Temporary type definitions - these should be imported from page.tsx in production // These are placeholder types that allow TypeScript to compile without errors type BackgroundNode = any; // Node for background modification operations type ClothesNode = any; // Node for clothing modification operations type BlendNode = any; // Node for image blending operations type EditNode = any; // Node for general image editing operations type CameraNode = any; // Node for camera effect operations type AgeNode = any; // Node for age transformation operations type FaceNode = any; // Node for facial feature modification operations /** * Utility function to combine CSS class names conditionally * Filters out falsy values and joins remaining strings with spaces * Same implementation as in page.tsx for consistent styling across components * * @param args Array of class name strings or falsy values * @returns Combined class name string with falsy values filtered out */ function cx(...args: Array) { return args.filter(Boolean).join(" "); // Remove falsy values and join with spaces } /* ======================================== SHARED COMPONENTS AND HOOKS ======================================== */ /** * Custom React hook for node dragging functionality * * Handles the complex pointer event logic for dragging nodes around the editor. * Maintains local position state for smooth dragging while updating the parent * component's position when the drag operation completes. * * Key Features: * - Smooth local position updates during drag * - Pointer capture for reliable drag behavior * - Prevents event bubbling to avoid conflicts * - Syncs with parent position updates * * @param node The node object containing current position * @param onUpdatePosition Callback to update node position in parent state * @returns Object with position and event handlers for dragging */ function useNodeDrag(node: any, onUpdatePosition?: (id: string, x: number, y: number) => void) { const [localPos, setLocalPos] = useState({ x: node.x, y: node.y }); // Local position for smooth dragging const dragging = useRef(false); // Track drag state const start = useRef<{ sx: number; sy: number; ox: number; oy: number } | null>(null); // Drag start coordinates // Sync local position when parent position changes useEffect(() => { setLocalPos({ x: node.x, y: node.y }); }, [node.x, node.y]); /** * Handle pointer down - start dragging * Captures the pointer and records starting positions */ const onPointerDown = (e: React.PointerEvent) => { e.stopPropagation(); // Prevent event bubbling dragging.current = true; // Mark as dragging start.current = { sx: e.clientX, sy: e.clientY, ox: localPos.x, oy: localPos.y }; // Record start positions (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); // Capture pointer for reliable tracking }; /** * Handle pointer move - update position during drag * Calculates new position based on mouse movement delta */ const onPointerMove = (e: React.PointerEvent) => { if (!dragging.current || !start.current) return; // Only process if actively dragging const dx = e.clientX - start.current.sx; // Calculate horizontal movement const dy = e.clientY - start.current.sy; // Calculate vertical movement const newX = start.current.ox + dx; // New X position const newY = start.current.oy + dy; // New Y position setLocalPos({ x: newX, y: newY }); // Update local position for immediate visual feedback if (onUpdatePosition) onUpdatePosition(node.id, newX, newY); // Update parent state }; /** * Handle pointer up - end dragging * Releases pointer capture and resets drag state */ const onPointerUp = (e: React.PointerEvent) => { dragging.current = false; // End dragging start.current = null; // Clear start position (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); // Release pointer }; return { localPos, onPointerDown, onPointerMove, onPointerUp }; } /** * Port component for node connections * * Renders the small circular connection points on nodes that users can * drag between to create connections. Handles the pointer events for * starting and ending connection operations. * * Types of ports: * - Input ports (left side): Receive connections from other nodes * - Output ports (right side): Send connections to other nodes * * @param className Additional CSS classes to apply * @param nodeId The ID of the node this port belongs to * @param isOutput Whether this is an output port (true) or input port (false) * @param onStartConnection Callback when starting a connection from this port * @param onEndConnection Callback when ending a connection at this port */ function Port({ className, nodeId, isOutput, onStartConnection, onEndConnection, onDisconnect }: { className?: string; nodeId?: string; isOutput?: boolean; onStartConnection?: (nodeId: string) => void; onEndConnection?: (nodeId: string) => void; onDisconnect?: (nodeId: string) => void; }) { /** * Handle starting a connection (pointer down on output port) */ const handlePointerDown = (e: React.PointerEvent) => { e.stopPropagation(); // Prevent triggering node drag if (isOutput && nodeId && onStartConnection) { onStartConnection(nodeId); // Start connection from this output port } }; /** * Handle ending a connection (pointer up on input port) */ const handlePointerUp = (e: React.PointerEvent) => { e.stopPropagation(); // Prevent bubbling if (!isOutput && nodeId && onEndConnection) { onEndConnection(nodeId); // End connection at this input port } }; /** * Handle clicking on input port to disconnect * Allows users to remove connections by clicking on input ports */ const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent event from bubbling to parent elements if (!isOutput && nodeId && onDisconnect) { onDisconnect(nodeId); // Disconnect from this input port } }; return (
); } /** * BACKGROUND NODE VIEW COMPONENT * * Allows users to change or generate image backgrounds using various methods: * - Solid colors with color picker * - Preset background images (beach, office, studio, etc.) * - Custom uploaded images via file upload or drag/drop * - AI-generated backgrounds from text descriptions * * Key Features: * - Multiple background source types (color/preset/upload/custom prompt) * - Drag and drop image upload functionality * - Paste image from clipboard support * - AI-powered prompt improvement using Gemini * - Real-time preview of uploaded images * - Connection management for node-based workflow * * @param node - Background node data containing backgroundType, backgroundColor, etc. * @param onDelete - Callback to delete this node from the editor * @param onUpdate - Callback to update node properties (backgroundType, colors, images, etc.) * @param onStartConnection - Callback when user starts dragging from output port * @param onEndConnection - Callback when user drops connection on input port * @param onProcess - Callback to process this node and apply background changes * @param onUpdatePosition - Callback to update node position when dragged * @param getNodeHistoryInfo - Function to get processing history for this node * @param navigateNodeHistory - Function to navigate through different processing results * @param getCurrentNodeImage - Function to get the current processed image */ export function BackgroundNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, apiToken }: any) { // Use custom drag hook to handle node positioning in the editor const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition); /** * Handle image file upload from file input * Converts uploaded file to base64 data URL for storage and preview */ const handleImageUpload = (e: React.ChangeEvent) => { if (e.target.files?.length) { const reader = new FileReader(); // Create file reader reader.onload = () => { onUpdate(node.id, { customBackgroundImage: reader.result }); // Store base64 data URL }; reader.readAsDataURL(e.target.files[0]); // Convert file to base64 } }; /** * Handle image paste from clipboard * Supports both image files and image URLs pasted from clipboard */ const handleImagePaste = (e: React.ClipboardEvent) => { const items = e.clipboardData.items; // Get clipboard items // First, try to find image files in clipboard for (let i = 0; i < items.length; i++) { if (items[i].type.startsWith("image/")) { // Check if item is an image const file = items[i].getAsFile(); // Get image file if (file) { const reader = new FileReader(); // Create file reader reader.onload = () => { onUpdate(node.id, { customBackgroundImage: reader.result }); // Store base64 data }; reader.readAsDataURL(file); // Convert to base64 return; // Exit early if image found } } } // If no image files, check for text that might be image URLs const text = e.clipboardData.getData("text"); // Get text from clipboard if (text && (text.startsWith("http") || text.startsWith("data:image"))) { onUpdate(node.id, { customBackgroundImage: text }); // Use URL directly } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); const files = e.dataTransfer.files; if (files && files.length) { const reader = new FileReader(); reader.onload = () => { onUpdate(node.id, { customBackgroundImage: reader.result }); }; reader.readAsDataURL(files[0]); } }; return (
e.preventDefault()} onPaste={handleImagePaste} >
onUpdate(nodeId, { input: undefined })} />
BACKGROUND
{/* Node Content Area - Contains all controls, inputs, and outputs */}
{node.input && (
)} {node.backgroundType === "color" && ( onUpdate(node.id, { backgroundColor: (e.target as HTMLInputElement).value })} /> )} {node.backgroundType === "gradient" && (
onUpdate(node.id, { gradientStartColor: (e.target as HTMLInputElement).value })} /> onUpdate(node.id, { gradientEndColor: (e.target as HTMLInputElement).value })} />
)} {node.backgroundType === "image" && ( )} {node.backgroundType === "city" && (
)} {node.backgroundType === "photostudio" && (
{node.studioSetup === "colored_seamless" && ( <> onUpdate(node.id, { studioBackgroundColor: (e.target as HTMLInputElement).value })} /> )}
onUpdate(node.id, { faceCamera: (e.target as HTMLInputElement).checked })} className="w-4 h-4" />
)} {node.backgroundType === "upload" && (
{node.customBackgroundImage ? (
Custom Background
) : ( )}
)} {node.backgroundType === "custom" && (