diff --git "a/app/nodes.tsx" "b/app/nodes.tsx" --- "a/app/nodes.tsx" +++ "b/app/nodes.tsx" @@ -1,110 +1,426 @@ +/** + * 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 { Button } from "../components/ui/button"; -import { Select } from "../components/ui/select"; -import { Textarea } from "../components/ui/textarea"; -import { Label } from "../components/ui/label"; -import { Slider } from "../components/ui/slider"; -import { ColorPicker } from "../components/ui/color-picker"; -import { Checkbox } from "../components/ui/checkbox"; -// Helper function to download image +// 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'); - link.href = dataUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + 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 */} +
+ ); } -// Import types (we'll need to export these from page.tsx) -type BackgroundNode = any; -type ClothesNode = any; -type BlendNode = any; -type EditNode = any; -type CameraNode = any; -type AgeNode = any; -type FaceNode = any; +/* ======================================== + 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(" "); + return args.filter(Boolean).join(" "); // Remove falsy values and join with spaces } -// Reusable drag hook for all nodes +/* ======================================== + 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 }); - const dragging = useRef(false); - const start = useRef<{ sx: number; sy: number; ox: number; oy: number } | null>(null); + 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(); - dragging.current = true; - start.current = { sx: e.clientX, sy: e.clientY, ox: localPos.x, oy: localPos.y }; - (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + 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; - const dx = e.clientX - start.current.sx; - const dy = e.clientY - start.current.sy; - const newX = start.current.ox + dx; - const newY = start.current.oy + dy; - setLocalPos({ x: newX, y: newY }); - if (onUpdatePosition) onUpdatePosition(node.id, newX, newY); + 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; - start.current = null; - (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); + 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 + 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(); + e.stopPropagation(); // Prevent triggering node drag if (isOutput && nodeId && onStartConnection) { - onStartConnection(nodeId); + onStartConnection(nodeId); // Start connection from this output port } }; + /** + * Handle ending a connection (pointer up on input port) + */ const handlePointerUp = (e: React.PointerEvent) => { - e.stopPropagation(); + e.stopPropagation(); // Prevent bubbling if (!isOutput && nodeId && onEndConnection) { - onEndConnection(nodeId); + 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, @@ -114,36 +430,49 @@ export function BackgroundNodeView({ onProcess, onUpdatePosition, }: 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(); + const reader = new FileReader(); // Create file reader reader.onload = () => { - onUpdate(node.id, { customBackgroundImage: reader.result }); + onUpdate(node.id, { customBackgroundImage: reader.result }); // Store base64 data URL }; - reader.readAsDataURL(e.target.files[0]); + 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; + 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/")) { - const file = items[i].getAsFile(); + 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(); + const reader = new FileReader(); // Create file reader reader.onload = () => { - onUpdate(node.id, { customBackgroundImage: reader.result }); + onUpdate(node.id, { customBackgroundImage: reader.result }); // Store base64 data }; - reader.readAsDataURL(file); - return; + reader.readAsDataURL(file); // Convert to base64 + return; // Exit early if image found } } } - const text = e.clipboardData.getData("text"); + + // 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 }); + onUpdate(node.id, { customBackgroundImage: text }); // Use URL directly } }; @@ -173,7 +502,7 @@ export function BackgroundNodeView({ onPointerMove={onPointerMove} onPointerUp={onPointerUp} > - + onUpdate(nodeId, { input: undefined })} />
BACKGROUND
+ {/* Node Content Area - Contains all controls, inputs, and outputs */}
+ {node.input && ( +
+ +
+ )} @@ -216,6 +561,48 @@ export function BackgroundNodeView({ /> )} + {node.backgroundType === "gradient" && ( +
+ + + + onUpdate(node.id, { gradientStartColor: (e.target as HTMLInputElement).value })} + /> + + onUpdate(node.id, { gradientEndColor: (e.target as HTMLInputElement).value })} + /> +
+
+ )} + {node.backgroundType === "image" && ( onUpdate(node.id, { citySceneType: (e.target as HTMLSelectElement).value })} + > + + + + + + + + + + + + + + +
+ )} + + {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 ? ( @@ -263,13 +736,50 @@ export function BackgroundNodeView({ )} {node.backgroundType === "custom" && ( -