Reubencf commited on
Commit
8c43388
·
1 Parent(s): 4bcdacd

major fixes

Browse files
.claude/settings.local.json CHANGED
@@ -14,7 +14,8 @@
14
  "WebSearch",
15
  "Read(//Users/reubenfernandes/Desktop/**)",
16
  "mcp__puppeteer__puppeteer_click",
17
- "mcp__browser-tools__getConsoleErrors"
 
18
  ],
19
  "deny": [],
20
  "ask": []
 
14
  "WebSearch",
15
  "Read(//Users/reubenfernandes/Desktop/**)",
16
  "mcp__puppeteer__puppeteer_click",
17
+ "mcp__browser-tools__getConsoleErrors",
18
+ "mcp__sequential-thinking__sequentialthinking"
19
  ],
20
  "deny": [],
21
  "ask": []
app/api/improve-prompt/route.ts CHANGED
@@ -63,20 +63,10 @@ Keep the character image and background realistic. Make the description rich and
63
 
64
  Original prompt: "${body.prompt}"
65
 
66
- Write an improved background generation prompt:`,
67
 
68
  edit: `You are an expert at writing prompts for AI image editing. Take the following simple editing request and transform it into a clear, detailed prompt that will produce precise, high-quality image modifications.
69
-
70
- Focus on:
71
- - Specific visual changes needed
72
- - Maintaining image quality and realism
73
- - Clear instructions for what to change and what to preserve
74
- - Professional photography/editing terminology
75
- - Realistic and natural-looking results
76
-
77
- Original prompt: "${body.prompt}"
78
-
79
- Write an improved editing prompt:`,
80
 
81
  default: `You are an expert at writing prompts for AI image generation and editing. Take the following simple prompt and transform it into a detailed, effective prompt that will produce better results.
82
 
@@ -98,7 +88,7 @@ Write an improved prompt:`
98
  contents: [{ role: "user", parts: [{ text: improvementPrompt }] }],
99
  });
100
 
101
- const improvedPrompt = response.response.text()?.trim();
102
 
103
  if (!improvedPrompt) {
104
  return NextResponse.json(
 
63
 
64
  Original prompt: "${body.prompt}"
65
 
66
+ Write a short and concise improved background generation prompt and do not include anything unnecessary:`,
67
 
68
  edit: `You are an expert at writing prompts for AI image editing. Take the following simple editing request and transform it into a clear, detailed prompt that will produce precise, high-quality image modifications.
69
+ Original prompt: "${body.prompt}" Return a short and concise improved editing prompt and do not include anything unnecessary:`,
 
 
 
 
 
 
 
 
 
 
70
 
71
  default: `You are an expert at writing prompts for AI image generation and editing. Take the following simple prompt and transform it into a detailed, effective prompt that will produce better results.
72
 
 
88
  contents: [{ role: "user", parts: [{ text: improvementPrompt }] }],
89
  });
90
 
91
+ const improvedPrompt = response?.text?.trim();
92
 
93
  if (!improvedPrompt) {
94
  return NextResponse.json(
app/nodes.tsx CHANGED
@@ -18,17 +18,19 @@
18
  * - AgeNodeView: Transform subject age
19
  * - FaceNodeView: Modify facial features and accessories
20
  */
 
21
  "use client";
22
 
23
- // React imports for component functionality
24
  import React, { useState, useRef, useEffect } from "react";
25
- // UI component imports from shadcn/ui library
26
- import { Button } from "../components/ui/button";
27
- import { Select } from "../components/ui/select";
28
- import { Textarea } from "../components/ui/textarea";
29
- import { Slider } from "../components/ui/slider";
30
- import { ColorPicker } from "../components/ui/color-picker";
31
- import { Checkbox } from "../components/ui/checkbox";
 
32
 
33
  /**
34
  * Helper function to download processed images
@@ -38,12 +40,12 @@ import { Checkbox } from "../components/ui/checkbox";
38
  * @param filename Desired filename for the downloaded image
39
  */
40
  function downloadImage(dataUrl: string, filename: string) {
41
- const link = document.createElement('a'); // Create temporary download link
42
- link.href = dataUrl; // Set the image data as href
43
- link.download = filename; // Set the download filename
44
- document.body.appendChild(link); // Add link to DOM (required for Firefox)
45
- link.click(); // Trigger download
46
- document.body.removeChild(link); // Clean up temporary link
47
  }
48
 
49
  /**
@@ -54,38 +56,46 @@ function downloadImage(dataUrl: string, filename: string) {
54
  */
55
  async function copyImageToClipboard(dataUrl: string) {
56
  try {
57
- const response = await fetch(dataUrl);
58
- const blob = await response.blob();
 
59
 
60
- // Convert to PNG if not already PNG (clipboard API only supports PNG for images)
 
61
  if (blob.type !== 'image/png') {
62
- const canvas = document.createElement('canvas');
63
- const ctx = canvas.getContext('2d');
64
- const img = new Image();
 
65
 
 
66
  await new Promise((resolve) => {
67
- img.onload = () => {
68
- canvas.width = img.width;
69
- canvas.height = img.height;
70
- ctx?.drawImage(img, 0, 0);
71
- resolve(void 0);
72
  };
73
- img.src = dataUrl;
74
  });
75
 
 
76
  const pngBlob = await new Promise<Blob>((resolve) => {
77
- canvas.toBlob((blob) => resolve(blob!), 'image/png');
78
  });
79
 
 
80
  await navigator.clipboard.write([
81
- new ClipboardItem({ 'image/png': pngBlob })
82
  ]);
83
  } else {
 
84
  await navigator.clipboard.write([
85
- new ClipboardItem({ 'image/png': blob })
86
  ]);
87
  }
88
  } catch (error) {
 
89
  console.error('Failed to copy image to clipboard:', error);
90
  }
91
  }
@@ -94,88 +104,63 @@ async function copyImageToClipboard(dataUrl: string) {
94
  * Reusable output section with history navigation for node components
95
  */
96
  function NodeOutputSection({
97
- nodeId,
98
- output,
99
- downloadFileName,
100
- getNodeHistoryInfo,
101
- navigateNodeHistory,
102
- getCurrentNodeImage,
103
  }: {
104
- nodeId: string;
105
- output?: string;
106
- downloadFileName: string;
107
- getNodeHistoryInfo?: (id: string) => any;
108
- navigateNodeHistory?: (id: string, direction: 'prev' | 'next') => void;
109
- getCurrentNodeImage?: (id: string, fallback?: string) => string;
110
  }) {
111
- const currentImage = getCurrentNodeImage ? getCurrentNodeImage(nodeId, output) : output;
112
-
113
- if (!currentImage) return null;
114
-
115
- const historyInfo = getNodeHistoryInfo ? getNodeHistoryInfo(nodeId) : { hasHistory: false, currentDescription: '' };
116
 
117
  return (
 
118
  <div className="space-y-2">
 
119
  <div className="space-y-1">
 
120
  <div className="flex items-center justify-between">
 
121
  <div className="text-xs text-white/70">Output</div>
122
- {historyInfo.hasHistory ? (
123
- <div className="flex items-center gap-1">
124
- <button
125
- className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
126
- onClick={() => navigateNodeHistory && navigateNodeHistory(nodeId, 'prev')}
127
- disabled={!historyInfo.canGoBack}
128
- >
129
-
130
- </button>
131
- <span className="text-xs text-white/60 px-1">
132
- {Math.floor(historyInfo.current || 1)}/{Math.floor(historyInfo.total || 1)}
133
- </span>
134
- <button
135
- className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
136
- onClick={() => navigateNodeHistory && navigateNodeHistory(nodeId, 'next')}
137
- disabled={!historyInfo.canGoForward}
138
- >
139
-
140
- </button>
141
- </div>
142
- ) : null}
143
  </div>
 
144
  <img
145
- src={currentImage}
146
- className="w-full rounded cursor-pointer hover:opacity-80 transition-opacity"
147
- alt="Output"
148
- onClick={() => copyImageToClipboard(currentImage)}
149
- onContextMenu={(e) => {
150
- e.preventDefault();
151
- copyImageToClipboard(currentImage);
152
 
153
- // Show a brief visual feedback
154
- const img = e.currentTarget;
155
- const originalTitle = img.title;
156
- img.title = "Copied to clipboard!";
157
- img.style.filter = "brightness(1.2)";
 
158
 
 
159
  setTimeout(() => {
160
- img.title = originalTitle;
161
- img.style.filter = "";
162
- }, 500);
 
163
  }}
164
- title="Click or right-click to copy image to clipboard"
165
  />
166
- {historyInfo.currentDescription ? (
167
- <div className="text-xs text-white/60 bg-black/20 rounded px-2 py-1">
168
- {historyInfo.currentDescription}
169
- </div>
170
- ) : null}
171
  </div>
 
172
  <Button
173
- className="w-full"
174
- variant="secondary"
175
- onClick={() => downloadImage(currentImage, downloadFileName)}
176
  >
177
  📥 Download Output
178
  </Button>
 
179
  </div>
180
  );
181
  }
@@ -184,20 +169,25 @@ function NodeOutputSection({
184
  TYPE DEFINITIONS (TEMPORARY)
185
  ======================================== */
186
  // Temporary type definitions - these should be imported from page.tsx in production
187
- type BackgroundNode = any;
188
- type ClothesNode = any;
189
- type BlendNode = any;
190
- type EditNode = any;
191
- type CameraNode = any;
192
- type AgeNode = any;
193
- type FaceNode = any;
 
194
 
195
  /**
196
  * Utility function to combine CSS class names conditionally
197
- * Same implementation as in page.tsx for consistent styling
 
 
 
 
198
  */
199
  function cx(...args: Array<string | false | null | undefined>) {
200
- return args.filter(Boolean).join(" ");
201
  }
202
 
203
  /* ========================================
@@ -424,6 +414,18 @@ export function BackgroundNodeView({
424
  </div>
425
  </div>
426
  <div className="p-3 space-y-3">
 
 
 
 
 
 
 
 
 
 
 
 
427
  <Select
428
  className="w-full"
429
  value={node.backgroundType || "color"}
@@ -565,8 +567,10 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
565
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
566
 
567
  const presetClothes = [
568
- { name: "Sukajan", path: "/sukajan.png" },
569
- { name: "Blazer", path: "/blazzer.png" },
 
 
570
  ];
571
 
572
  const onDrop = async (e: React.DragEvent) => {
@@ -666,7 +670,7 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
666
  }`}
667
  onClick={() => selectPreset(preset.path, preset.name)}
668
  >
669
- <img src={preset.path} alt={preset.name} className="w-full h-16 object-cover rounded mb-1" />
670
  <div className="text-xs">{preset.name}</div>
671
  </button>
672
  ))}
@@ -701,8 +705,10 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
701
  }
702
  }}
703
  />
704
- <div className="border-2 border-dashed border-white/20 rounded-lg p-4 text-center cursor-pointer hover:border-white/40">
705
- <p className="text-xs text-white/60">Drop, upload, or paste clothes image</p>
 
 
706
  </div>
707
  </label>
708
  ) : null}
@@ -1057,7 +1063,7 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
1057
  <Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
1058
  </div>
1059
  </div>
1060
- <div className="p-3 space-y-2">
1061
  {node.input && (
1062
  <div className="flex justify-end mb-2">
1063
  <Button
@@ -1141,39 +1147,40 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
1141
 
1142
  <div>
1143
  <label className="text-xs text-white/70">Makeup</label>
1144
- <div className="grid grid-cols-3 gap-2 mt-2">
1145
- {[
1146
- { name: "Natural", path: "/makeup/natural.jpg" },
1147
- { name: "Glam", path: "/makeup/glam.jpg" },
1148
- { name: "Bold", path: "/makeup/bold.jpg" },
1149
- { name: "Smoky", path: "/makeup/smoky.jpg" },
1150
- { name: "Vintage", path: "/makeup/vintage.jpg" },
1151
- { name: "No Makeup", path: "/makeup/none.jpg" }
1152
- ].map((makeup) => (
1153
- <button
1154
- key={makeup.name}
1155
- className={`p-1 rounded border ${
1156
- node.faceOptions?.selectedMakeup === makeup.name
1157
- ? "border-indigo-400 bg-indigo-500/20"
1158
- : "border-white/20 hover:border-white/40"
1159
- }`}
1160
- onClick={() => onUpdate(node.id, {
1161
- faceOptions: { ...node.faceOptions, selectedMakeup: makeup.name, makeupImage: makeup.path }
1162
- })}
1163
- >
1164
- <img
1165
- src={makeup.path}
1166
- alt={makeup.name}
1167
- className="w-full h-8 object-cover rounded mb-1 cursor-pointer hover:opacity-80"
1168
- onClick={(e) => {
1169
- e.stopPropagation();
1170
- copyImageToClipboard(makeup.path);
1171
- }}
1172
- title="Click to copy makeup reference"
1173
- />
1174
- <div className="text-xs">{makeup.name}</div>
1175
- </button>
1176
- ))}
 
1177
  </div>
1178
  </div>
1179
 
@@ -1322,9 +1329,9 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
1322
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1323
 
1324
  const presetLightings = [
1325
- { name: "Studio Light", path: "/lighting/light1.jpg" },
1326
- { name: "Natural Light", path: "/lighting/light2.jpg" },
1327
- { name: "Dramatic Light", path: "/lighting/light3.jpg" },
1328
  ];
1329
 
1330
  const selectLighting = (lightingPath: string, lightingName: string) => {
@@ -1391,29 +1398,14 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
1391
  <img
1392
  src={preset.path}
1393
  alt={preset.name}
1394
- className="w-full h-12 object-cover rounded mb-1 cursor-pointer hover:opacity-80"
1395
- onClick={(e) => {
1396
- e.stopPropagation();
1397
- copyImageToClipboard(preset.path);
1398
- }}
1399
- title="Click to copy lighting reference"
1400
  />
1401
  <div className="text-xs">{preset.name}</div>
1402
  </button>
1403
  ))}
1404
  </div>
1405
 
1406
- <div>
1407
- <Slider
1408
- label="Lighting Strength"
1409
- valueLabel={`${node.lightingStrength || 75}%`}
1410
- min={0}
1411
- max={100}
1412
- value={node.lightingStrength || 75}
1413
- onChange={(e) => onUpdate(node.id, { lightingStrength: parseInt((e.target as HTMLInputElement).value) })}
1414
- />
1415
- </div>
1416
-
1417
  <Button
1418
  className="w-full"
1419
  onClick={() => onProcess(node.id)}
@@ -1444,10 +1436,10 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1444
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1445
 
1446
  const presetPoses = [
1447
- { name: "Standing Pose 1", path: "/poses/stand1.jpg" },
1448
- { name: "Standing Pose 2", path: "/poses/stand2.jpg" },
1449
- { name: "Sitting Pose 1", path: "/poses/sit1.jpg" },
1450
- { name: "Sitting Pose 2", path: "/poses/sit2.jpg" },
1451
  ];
1452
 
1453
  const selectPose = (posePath: string, poseName: string) => {
@@ -1514,29 +1506,14 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1514
  <img
1515
  src={preset.path}
1516
  alt={preset.name}
1517
- className="w-full h-12 object-cover rounded mb-1 cursor-pointer hover:opacity-80"
1518
- onClick={(e) => {
1519
- e.stopPropagation();
1520
- copyImageToClipboard(preset.path);
1521
- }}
1522
- title="Click to copy pose reference"
1523
  />
1524
  <div className="text-xs">{preset.name}</div>
1525
  </button>
1526
  ))}
1527
  </div>
1528
 
1529
- <div>
1530
- <Slider
1531
- label="Pose Strength"
1532
- valueLabel={`${node.poseStrength || 60}%`}
1533
- min={0}
1534
- max={100}
1535
- value={node.poseStrength || 60}
1536
- onChange={(e) => onUpdate(node.id, { poseStrength: parseInt((e.target as HTMLInputElement).value) })}
1537
- />
1538
- </div>
1539
-
1540
  <Button
1541
  className="w-full"
1542
  onClick={() => onProcess(node.id)}
 
18
  * - AgeNodeView: Transform subject age
19
  * - FaceNodeView: Modify facial features and accessories
20
  */
21
+ // Enable React Server Components client-side rendering for this file
22
  "use client";
23
 
24
+ // Import React core functionality for state management and lifecycle hooks
25
  import React, { useState, useRef, useEffect } from "react";
26
+
27
+ // Import reusable UI components from the shadcn/ui component library
28
+ import { Button } from "../components/ui/button"; // Standard button component
29
+ import { Select } from "../components/ui/select"; // Dropdown selection component
30
+ import { Textarea } from "../components/ui/textarea"; // Multi-line text input component
31
+ import { Slider } from "../components/ui/slider"; // Range slider input component
32
+ import { ColorPicker } from "../components/ui/color-picker"; // Color selection component
33
+ import { Checkbox } from "../components/ui/checkbox"; // Checkbox input component
34
 
35
  /**
36
  * Helper function to download processed images
 
40
  * @param filename Desired filename for the downloaded image
41
  */
42
  function downloadImage(dataUrl: string, filename: string) {
43
+ const link = document.createElement('a'); // Create an invisible anchor element for download
44
+ link.href = dataUrl; // Set the base64 image data as the link target
45
+ link.download = filename; // Specify the filename for the downloaded file
46
+ document.body.appendChild(link); // Temporarily add link to DOM (Firefox requirement)
47
+ link.click(); // Programmatically trigger the download
48
+ document.body.removeChild(link); // Remove the temporary link element from DOM
49
  }
50
 
51
  /**
 
56
  */
57
  async function copyImageToClipboard(dataUrl: string) {
58
  try {
59
+ // Fetch the data URL and convert it to a Blob object
60
+ const response = await fetch(dataUrl); // Fetch the base64 data URL
61
+ const blob = await response.blob(); // Convert response to Blob format
62
 
63
+ // The browser clipboard API only supports PNG format for images
64
+ // If the image is not PNG, we need to convert it first
65
  if (blob.type !== 'image/png') {
66
+ // Create a canvas element to handle image format conversion
67
+ const canvas = document.createElement('canvas'); // Create invisible canvas
68
+ const ctx = canvas.getContext('2d'); // Get 2D drawing context
69
+ const img = new Image(); // Create image element
70
 
71
+ // Wait for the image to load before processing
72
  await new Promise((resolve) => {
73
+ img.onload = () => { // When image loads
74
+ canvas.width = img.width; // Set canvas width to match image
75
+ canvas.height = img.height; // Set canvas height to match image
76
+ ctx?.drawImage(img, 0, 0); // Draw image onto canvas
77
+ resolve(void 0); // Resolve the promise
78
  };
79
+ img.src = dataUrl; // Start loading the image
80
  });
81
 
82
+ // Convert the canvas content to PNG blob
83
  const pngBlob = await new Promise<Blob>((resolve) => {
84
+ canvas.toBlob((blob) => resolve(blob!), 'image/png'); // Convert canvas to PNG blob
85
  });
86
 
87
+ // Write the converted PNG blob to clipboard
88
  await navigator.clipboard.write([
89
+ new ClipboardItem({ 'image/png': pngBlob }) // Create clipboard item with PNG data
90
  ]);
91
  } else {
92
+ // Image is already PNG, copy directly to clipboard
93
  await navigator.clipboard.write([
94
+ new ClipboardItem({ 'image/png': blob }) // Copy original blob to clipboard
95
  ]);
96
  }
97
  } catch (error) {
98
+ // Handle any errors that occur during the copy process
99
  console.error('Failed to copy image to clipboard:', error);
100
  }
101
  }
 
104
  * Reusable output section with history navigation for node components
105
  */
106
  function NodeOutputSection({
107
+ nodeId, // Unique identifier for the node
108
+ output, // Optional current output image (base64 data URL)
109
+ downloadFileName, // Filename to use when downloading the image
 
 
 
110
  }: {
111
+ nodeId: string; // Node ID type definition
112
+ output?: string; // Optional output image string
113
+ downloadFileName: string; // Required download filename
 
 
 
114
  }) {
115
+ // If no image is available, don't render anything
116
+ if (!output) return null;
 
 
 
117
 
118
  return (
119
+ // Main container for output section with vertical spacing
120
  <div className="space-y-2">
121
+ {/* Output header container */}
122
  <div className="space-y-1">
123
+ {/* Header row with title */}
124
  <div className="flex items-center justify-between">
125
+ {/* Output section label */}
126
  <div className="text-xs text-white/70">Output</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </div>
128
+ {/* Output image with click-to-copy functionality */}
129
  <img
130
+ src={output} // Display the output image
131
+ className="w-full rounded cursor-pointer hover:opacity-80 transition-all duration-200 hover:ring-2 hover:ring-white/30" // Styling with hover effects
132
+ alt="Output" // Accessibility description
133
+ onClick={() => copyImageToClipboard(output)} // Left-click copies to clipboard
134
+ onContextMenu={(e) => { // Right-click context menu handler
135
+ e.preventDefault(); // Prevent browser context menu from appearing
136
+ copyImageToClipboard(output); // Copy image to clipboard
137
 
138
+ // Show brief visual feedback when image is copied
139
+ const img = e.currentTarget; // Get the image element
140
+ const originalTitle = img.title; // Store original tooltip text
141
+ img.title = "Copied to clipboard!"; // Update tooltip to show success
142
+ img.style.filter = "brightness(1.2)"; // Brighten the image briefly
143
+ img.style.transform = "scale(0.98)"; // Slightly scale down the image
144
 
145
+ // Reset visual feedback after 300ms
146
  setTimeout(() => {
147
+ img.title = originalTitle; // Restore original tooltip
148
+ img.style.filter = ""; // Remove brightness filter
149
+ img.style.transform = ""; // Reset scale transform
150
+ }, 300);
151
  }}
152
+ title="💾 Click or right-click to copy image to clipboard" // Tooltip instruction
153
  />
 
 
 
 
 
154
  </div>
155
+ {/* Download button for saving the current image */}
156
  <Button
157
+ className="w-full" // Full width button
158
+ variant="secondary" // Secondary button styling
159
+ onClick={() => downloadImage(output, downloadFileName)} // Trigger download when clicked
160
  >
161
  📥 Download Output
162
  </Button>
163
+ {/* End of main output section container */}
164
  </div>
165
  );
166
  }
 
169
  TYPE DEFINITIONS (TEMPORARY)
170
  ======================================== */
171
  // Temporary type definitions - these should be imported from page.tsx in production
172
+ // These are placeholder types that allow TypeScript to compile without errors
173
+ type BackgroundNode = any; // Node for background modification operations
174
+ type ClothesNode = any; // Node for clothing modification operations
175
+ type BlendNode = any; // Node for image blending operations
176
+ type EditNode = any; // Node for general image editing operations
177
+ type CameraNode = any; // Node for camera effect operations
178
+ type AgeNode = any; // Node for age transformation operations
179
+ type FaceNode = any; // Node for facial feature modification operations
180
 
181
  /**
182
  * Utility function to combine CSS class names conditionally
183
+ * Filters out falsy values and joins remaining strings with spaces
184
+ * Same implementation as in page.tsx for consistent styling across components
185
+ *
186
+ * @param args Array of class name strings or falsy values
187
+ * @returns Combined class name string with falsy values filtered out
188
  */
189
  function cx(...args: Array<string | false | null | undefined>) {
190
+ return args.filter(Boolean).join(" "); // Remove falsy values and join with spaces
191
  }
192
 
193
  /* ========================================
 
414
  </div>
415
  </div>
416
  <div className="p-3 space-y-3">
417
+ {node.input && (
418
+ <div className="flex justify-end mb-2">
419
+ <Button
420
+ variant="ghost"
421
+ size="sm"
422
+ onClick={() => onUpdate(node.id, { input: undefined })}
423
+ className="text-xs"
424
+ >
425
+ Clear Connection
426
+ </Button>
427
+ </div>
428
+ )}
429
  <Select
430
  className="w-full"
431
  value={node.backgroundType || "color"}
 
567
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
568
 
569
  const presetClothes = [
570
+ { name: "Sukajan", path: "/clothes/sukajan.png" },
571
+ { name: "Blazer", path: "/clothes/blazzer.png" },
572
+ { name: "Suit", path: "/clothes/suit.png" },
573
+ { name: "Women's Outfit", path: "/clothes/womenoutfit.png" },
574
  ];
575
 
576
  const onDrop = async (e: React.DragEvent) => {
 
670
  }`}
671
  onClick={() => selectPreset(preset.path, preset.name)}
672
  >
673
+ <img src={preset.path} alt={preset.name} className="w-full h-28 object-contain rounded mb-1" />
674
  <div className="text-xs">{preset.name}</div>
675
  </button>
676
  ))}
 
705
  }
706
  }}
707
  />
708
+ <div className="border-2 border-dashed border-white/20 rounded-lg p-6 text-center cursor-pointer hover:border-white/40 transition-colors">
709
+ <div className="text-white/40 text-lg mb-2">📁</div>
710
+ <p className="text-sm text-white/70 font-medium">Drop, upload, or paste clothes image</p>
711
+ <p className="text-xs text-white/50 mt-1">JPG, PNG, WebP supported</p>
712
  </div>
713
  </label>
714
  ) : null}
 
1063
  <Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
1064
  </div>
1065
  </div>
1066
+ <div className="p-3 space-y-2 max-h-[500px] overflow-y-auto scrollbar-thin">
1067
  {node.input && (
1068
  <div className="flex justify-end mb-2">
1069
  <Button
 
1147
 
1148
  <div>
1149
  <label className="text-xs text-white/70">Makeup</label>
1150
+ <div className="grid grid-cols-2 gap-2 mt-2">
1151
+ <button
1152
+ className={`p-1 rounded border ${
1153
+ !node.faceOptions?.selectedMakeup || node.faceOptions?.selectedMakeup === "None"
1154
+ ? "border-indigo-400 bg-indigo-500/20"
1155
+ : "border-white/20 hover:border-white/40"
1156
+ }`}
1157
+ onClick={() => onUpdate(node.id, {
1158
+ faceOptions: { ...node.faceOptions, selectedMakeup: "None", makeupImage: null }
1159
+ })}
1160
+ >
1161
+ <div className="w-full h-24 flex items-center justify-center text-xs text-white/60 border border-dashed border-white/20 rounded mb-1">
1162
+ No Makeup
1163
+ </div>
1164
+ <div className="text-xs">None</div>
1165
+ </button>
1166
+ <button
1167
+ className={`p-1 rounded border ${
1168
+ node.faceOptions?.selectedMakeup === "Makeup"
1169
+ ? "border-indigo-400 bg-indigo-500/20"
1170
+ : "border-white/20 hover:border-white/40"
1171
+ }`}
1172
+ onClick={() => onUpdate(node.id, {
1173
+ faceOptions: { ...node.faceOptions, selectedMakeup: "Makeup", makeupImage: "/makeup/makeup1.png" }
1174
+ })}
1175
+ >
1176
+ <img
1177
+ src="/makeup/makeup1.png"
1178
+ alt="Makeup"
1179
+ className="w-full h-24 object-contain rounded mb-1"
1180
+ title="Click to select makeup"
1181
+ />
1182
+ <div className="text-xs">Makeup</div>
1183
+ </button>
1184
  </div>
1185
  </div>
1186
 
 
1329
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1330
 
1331
  const presetLightings = [
1332
+ { name: "Studio Light", path: "/lighting/light1.png" },
1333
+ { name: "Natural Light", path: "/lighting/light2.png" },
1334
+ { name: "Dramatic Light", path: "/lighting/light3.png" },
1335
  ];
1336
 
1337
  const selectLighting = (lightingPath: string, lightingName: string) => {
 
1398
  <img
1399
  src={preset.path}
1400
  alt={preset.name}
1401
+ className="w-full h-24 object-contain rounded mb-1"
1402
+ title="Click to select lighting"
 
 
 
 
1403
  />
1404
  <div className="text-xs">{preset.name}</div>
1405
  </button>
1406
  ))}
1407
  </div>
1408
 
 
 
 
 
 
 
 
 
 
 
 
1409
  <Button
1410
  className="w-full"
1411
  onClick={() => onProcess(node.id)}
 
1436
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1437
 
1438
  const presetPoses = [
1439
+ { name: "Standing Pose 1", path: "/poses/stand1.png" },
1440
+ { name: "Standing Pose 2", path: "/poses/stand2.png" },
1441
+ { name: "Sitting Pose 1", path: "/poses/sit1.png" },
1442
+ { name: "Sitting Pose 2", path: "/poses/sit2.png" },
1443
  ];
1444
 
1445
  const selectPose = (posePath: string, poseName: string) => {
 
1506
  <img
1507
  src={preset.path}
1508
  alt={preset.name}
1509
+ className="w-full h-24 object-contain rounded mb-1"
1510
+ title="Click to select pose"
 
 
 
 
1511
  />
1512
  <div className="text-xs">{preset.name}</div>
1513
  </button>
1514
  ))}
1515
  </div>
1516
 
 
 
 
 
 
 
 
 
 
 
 
1517
  <Button
1518
  className="w-full"
1519
  onClick={() => onProcess(node.id)}
app/page.tsx CHANGED
@@ -363,8 +363,7 @@ type AnyNode = CharacterNode | MergeNode | BackgroundNode | ClothesNode | StyleN
363
  * Default placeholder image for new CHARACTER nodes
364
  * Uses Unsplash image as a starting point before users upload their own images
365
  */
366
- const DEFAULT_PERSON =
367
- "https://images.unsplash.com/photo-1527980965255-d3b416303d12?q=80&w=640&auto=format&fit=crop";
368
 
369
  /**
370
  * Convert File objects to data URLs for image processing
@@ -854,42 +853,17 @@ function MergeNodeView({
854
  <div className="mt-2">
855
  <div className="flex items-center justify-between mb-1">
856
  <div className="text-xs text-white/70">Output</div>
857
- {(() => {
858
- const historyInfo = getNodeHistoryInfo(node.id);
859
- return historyInfo.hasHistory ? (
860
- <div className="flex items-center gap-1">
861
- <button
862
- className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
863
- onClick={() => navigateNodeHistory(node.id, 'prev')}
864
- disabled={!historyInfo.canGoBack}
865
- >
866
-
867
- </button>
868
- <span className="text-xs text-white/60 px-1">
869
- {historyInfo.current}/{historyInfo.total}
870
- </span>
871
- <button
872
- className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
873
- onClick={() => navigateNodeHistory(node.id, 'next')}
874
- disabled={!historyInfo.canGoForward}
875
- >
876
-
877
- </button>
878
- </div>
879
- ) : null;
880
- })()}
881
  </div>
882
  <div className="w-full min-h-[200px] max-h-[400px] rounded-xl bg-black/40 grid place-items-center">
883
- {getCurrentNodeImage(node.id, node.output) ? (
884
  <img
885
- src={getCurrentNodeImage(node.id, node.output)}
886
  className="w-full h-auto max-h-[400px] object-contain rounded-xl cursor-pointer hover:opacity-80 transition-opacity"
887
  alt="output"
888
  onClick={async () => {
889
- const currentImage = getCurrentNodeImage(node.id, node.output);
890
- if (currentImage) {
891
  try {
892
- const response = await fetch(currentImage);
893
  const blob = await response.blob();
894
  await navigator.clipboard.write([
895
  new ClipboardItem({ [blob.type]: blob })
@@ -901,10 +875,9 @@ function MergeNodeView({
901
  }}
902
  onContextMenu={async (e) => {
903
  e.preventDefault();
904
- const currentImage = getCurrentNodeImage(node.id, node.output);
905
- if (currentImage) {
906
  try {
907
- const response = await fetch(currentImage);
908
  const blob = await response.blob();
909
  await navigator.clipboard.write([
910
  new ClipboardItem({ [blob.type]: blob })
@@ -929,23 +902,14 @@ function MergeNodeView({
929
  <span className="text-white/40 text-xs py-16">Run merge to see result</span>
930
  )}
931
  </div>
932
- {getCurrentNodeImage(node.id, node.output) && (
933
- <div className="mt-2 space-y-2">
934
- {(() => {
935
- const historyInfo = getNodeHistoryInfo(node.id);
936
- return historyInfo.currentDescription ? (
937
- <div className="text-xs text-white/60 bg-black/20 rounded px-2 py-1">
938
- {historyInfo.currentDescription}
939
- </div>
940
- ) : null;
941
- })()}
942
  <Button
943
  className="w-full"
944
  variant="secondary"
945
  onClick={() => {
946
  const link = document.createElement('a');
947
- const currentImage = getCurrentNodeImage(node.id, node.output);
948
- link.href = currentImage as string;
949
  link.download = `merge-${Date.now()}.png`;
950
  document.body.appendChild(link);
951
  link.click();
@@ -1070,48 +1034,6 @@ export default function EditorPage() {
1070
  const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
1071
  const [isCheckingAuth, setIsCheckingAuth] = useState(true);
1072
 
1073
- // NODE HISTORY (per-node image history)
1074
- const [nodeHistories, setNodeHistories] = useState<Record<string, Array<{
1075
- id: string;
1076
- image: string;
1077
- timestamp: number;
1078
- description: string;
1079
- }>>>({});
1080
-
1081
- const [nodeHistoryIndex, setNodeHistoryIndex] = useState<Record<string, number>>({});
1082
-
1083
- // Load node histories from localStorage on startup
1084
- useEffect(() => {
1085
- try {
1086
- const savedHistories = localStorage.getItem('nano-banana-node-histories');
1087
- const savedIndices = localStorage.getItem('nano-banana-node-history-indices');
1088
- if (savedHistories) {
1089
- setNodeHistories(JSON.parse(savedHistories));
1090
- }
1091
- if (savedIndices) {
1092
- setNodeHistoryIndex(JSON.parse(savedIndices));
1093
- }
1094
- } catch (error) {
1095
- console.error('Failed to load node histories from localStorage:', error);
1096
- }
1097
- }, []);
1098
-
1099
- // Save node histories to localStorage whenever they change
1100
- useEffect(() => {
1101
- try {
1102
- localStorage.setItem('nano-banana-node-histories', JSON.stringify(nodeHistories));
1103
- } catch (error) {
1104
- console.error('Failed to save node histories to localStorage:', error);
1105
- }
1106
- }, [nodeHistories]);
1107
-
1108
- useEffect(() => {
1109
- try {
1110
- localStorage.setItem('nano-banana-node-history-indices', JSON.stringify(nodeHistoryIndex));
1111
- } catch (error) {
1112
- console.error('Failed to save node history indices to localStorage:', error);
1113
- }
1114
- }, [nodeHistoryIndex]);
1115
 
1116
  const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
1117
  const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
@@ -1190,83 +1112,6 @@ export default function EditorPage() {
1190
  setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, ...updates } : n)));
1191
  };
1192
 
1193
- // Add image to node's history
1194
- const addToNodeHistory = (nodeId: string, image: string, description: string) => {
1195
- const historyEntry = {
1196
- id: uid(),
1197
- image,
1198
- timestamp: Date.now(),
1199
- description
1200
- };
1201
-
1202
- setNodeHistories(prev => {
1203
- const nodeHistory = prev[nodeId] || [];
1204
- const newHistory = [historyEntry, ...nodeHistory].slice(0, 10); // Keep last 10 per node
1205
- return {
1206
- ...prev,
1207
- [nodeId]: newHistory
1208
- };
1209
- });
1210
-
1211
- // Set this as the current (latest) image for the node
1212
- setNodeHistoryIndex(prev => ({
1213
- ...prev,
1214
- [nodeId]: 0
1215
- }));
1216
- };
1217
-
1218
- // Navigate node history
1219
- const navigateNodeHistory = (nodeId: string, direction: 'prev' | 'next') => {
1220
- const history = nodeHistories[nodeId];
1221
- if (!history || history.length <= 1) return;
1222
-
1223
- const currentIndex = nodeHistoryIndex[nodeId] || 0;
1224
- let newIndex = currentIndex;
1225
-
1226
- if (direction === 'prev' && currentIndex < history.length - 1) {
1227
- newIndex = currentIndex + 1;
1228
- } else if (direction === 'next' && currentIndex > 0) {
1229
- newIndex = currentIndex - 1;
1230
- }
1231
-
1232
- if (newIndex !== currentIndex) {
1233
- setNodeHistoryIndex(prev => ({
1234
- ...prev,
1235
- [nodeId]: newIndex
1236
- }));
1237
-
1238
- // Update the node's output to show the historical image
1239
- const historicalImage = history[newIndex].image;
1240
- updateNode(nodeId, { output: historicalImage });
1241
- }
1242
- };
1243
-
1244
- // Get current image for a node (either latest or from history navigation)
1245
- const getCurrentNodeImage = (nodeId: string, defaultOutput?: string) => {
1246
- const history = nodeHistories[nodeId];
1247
- const index = nodeHistoryIndex[nodeId] || 0;
1248
-
1249
- if (history && history[index]) {
1250
- return history[index].image;
1251
- }
1252
-
1253
- return defaultOutput;
1254
- };
1255
-
1256
- // Get history info for a node
1257
- const getNodeHistoryInfo = (nodeId: string) => {
1258
- const history = nodeHistories[nodeId] || [];
1259
- const index = Math.max(0, Math.min(nodeHistoryIndex[nodeId] || 0, history.length - 1)); // Clamp index within bounds
1260
-
1261
- return {
1262
- hasHistory: history.length > 1,
1263
- current: Math.max(1, index + 1), // Ensure current is at least 1
1264
- total: Math.max(0, history.length), // Ensure total is at least 0
1265
- canGoBack: index < history.length - 1,
1266
- canGoForward: index > 0,
1267
- currentDescription: history[index]?.description || ''
1268
- };
1269
- };
1270
 
1271
  // Handle single input connections for new nodes
1272
  const handleEndSingleConnection = (nodeId: string) => {
@@ -1685,7 +1530,6 @@ export default function EditorPage() {
1685
  ? `Combined ${unprocessedNodeCount} transformations`
1686
  : `${node.type} transformation`;
1687
 
1688
- addToNodeHistory(nodeId, data.image, description);
1689
 
1690
  if (unprocessedNodeCount > 1) {
1691
  console.log(`✅ Successfully applied ${unprocessedNodeCount} transformations in ONE API call!`);
@@ -1963,7 +1807,6 @@ export default function EditorPage() {
1963
  return `${inputNode?.type || 'Node'} ${index + 1}`;
1964
  });
1965
 
1966
- addToNodeHistory(mergeId, out, `Merged: ${inputLabels.join(" + ")}`);
1967
  }
1968
  } catch (e: any) {
1969
  console.error("Merge error:", e);
@@ -2029,7 +1872,7 @@ export default function EditorPage() {
2029
  if (inputNode) {
2030
  const start = getNodeOutputPort(inputNode);
2031
  const end = getNodeInputPort(node);
2032
- const isProcessing = merge.isRunning || (inputNode as any).isRunning;
2033
  paths.push({
2034
  path: createPath(start.x, start.y, end.x, end.y),
2035
  processing: isProcessing
@@ -2043,7 +1886,7 @@ export default function EditorPage() {
2043
  if (inputNode) {
2044
  const start = getNodeOutputPort(inputNode);
2045
  const end = getNodeInputPort(node);
2046
- const isProcessing = (node as any).isRunning || (inputNode as any).isRunning;
2047
  paths.push({
2048
  path: createPath(start.x, start.y, end.x, end.y),
2049
  processing: isProcessing
@@ -2116,7 +1959,30 @@ export default function EditorPage() {
2116
  const rect = containerRef.current!.getBoundingClientRect();
2117
  const world = screenToWorld(e.clientX, e.clientY, rect, tx, ty, scale);
2118
  setMenuWorld(world);
2119
- setMenuPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2120
  setMenuOpen(true);
2121
  };
2122
 
@@ -2408,9 +2274,6 @@ export default function EditorPage() {
2408
  onEndConnection={handleEndSingleConnection}
2409
  onProcess={processNode}
2410
  onUpdatePosition={updateNodePosition}
2411
- getNodeHistoryInfo={getNodeHistoryInfo}
2412
- navigateNodeHistory={navigateNodeHistory}
2413
- getCurrentNodeImage={getCurrentNodeImage}
2414
  />
2415
  );
2416
  case "CLOTHES":
@@ -2424,9 +2287,6 @@ export default function EditorPage() {
2424
  onEndConnection={handleEndSingleConnection}
2425
  onProcess={processNode}
2426
  onUpdatePosition={updateNodePosition}
2427
- getNodeHistoryInfo={getNodeHistoryInfo}
2428
- navigateNodeHistory={navigateNodeHistory}
2429
- getCurrentNodeImage={getCurrentNodeImage}
2430
  />
2431
  );
2432
  case "STYLE":
@@ -2440,9 +2300,6 @@ export default function EditorPage() {
2440
  onEndConnection={handleEndSingleConnection}
2441
  onProcess={processNode}
2442
  onUpdatePosition={updateNodePosition}
2443
- getNodeHistoryInfo={getNodeHistoryInfo}
2444
- navigateNodeHistory={navigateNodeHistory}
2445
- getCurrentNodeImage={getCurrentNodeImage}
2446
  />
2447
  );
2448
  case "EDIT":
@@ -2456,9 +2313,6 @@ export default function EditorPage() {
2456
  onEndConnection={handleEndSingleConnection}
2457
  onProcess={processNode}
2458
  onUpdatePosition={updateNodePosition}
2459
- getNodeHistoryInfo={getNodeHistoryInfo}
2460
- navigateNodeHistory={navigateNodeHistory}
2461
- getCurrentNodeImage={getCurrentNodeImage}
2462
  />
2463
  );
2464
  case "CAMERA":
@@ -2472,9 +2326,6 @@ export default function EditorPage() {
2472
  onEndConnection={handleEndSingleConnection}
2473
  onProcess={processNode}
2474
  onUpdatePosition={updateNodePosition}
2475
- getNodeHistoryInfo={getNodeHistoryInfo}
2476
- navigateNodeHistory={navigateNodeHistory}
2477
- getCurrentNodeImage={getCurrentNodeImage}
2478
  />
2479
  );
2480
  case "AGE":
@@ -2488,9 +2339,6 @@ export default function EditorPage() {
2488
  onEndConnection={handleEndSingleConnection}
2489
  onProcess={processNode}
2490
  onUpdatePosition={updateNodePosition}
2491
- getNodeHistoryInfo={getNodeHistoryInfo}
2492
- navigateNodeHistory={navigateNodeHistory}
2493
- getCurrentNodeImage={getCurrentNodeImage}
2494
  />
2495
  );
2496
  case "FACE":
@@ -2504,9 +2352,6 @@ export default function EditorPage() {
2504
  onEndConnection={handleEndSingleConnection}
2505
  onProcess={processNode}
2506
  onUpdatePosition={updateNodePosition}
2507
- getNodeHistoryInfo={getNodeHistoryInfo}
2508
- navigateNodeHistory={navigateNodeHistory}
2509
- getCurrentNodeImage={getCurrentNodeImage}
2510
  />
2511
  );
2512
  case "LIGHTNING":
@@ -2520,9 +2365,6 @@ export default function EditorPage() {
2520
  onEndConnection={handleEndSingleConnection}
2521
  onProcess={processNode}
2522
  onUpdatePosition={updateNodePosition}
2523
- getNodeHistoryInfo={getNodeHistoryInfo}
2524
- navigateNodeHistory={navigateNodeHistory}
2525
- getCurrentNodeImage={getCurrentNodeImage}
2526
  />
2527
  );
2528
  case "POSES":
@@ -2536,9 +2378,6 @@ export default function EditorPage() {
2536
  onEndConnection={handleEndSingleConnection}
2537
  onProcess={processNode}
2538
  onUpdatePosition={updateNodePosition}
2539
- getNodeHistoryInfo={getNodeHistoryInfo}
2540
- navigateNodeHistory={navigateNodeHistory}
2541
- getCurrentNodeImage={getCurrentNodeImage}
2542
  />
2543
  );
2544
  default:
@@ -2555,7 +2394,10 @@ export default function EditorPage() {
2555
  onMouseLeave={() => setMenuOpen(false)}
2556
  >
2557
  <div className="px-3 py-2 text-xs text-white/60">Add node</div>
2558
- <div className="max-h-[400px] overflow-y-auto">
 
 
 
2559
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("CHARACTER")}>CHARACTER</button>
2560
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("MERGE")}>MERGE</button>
2561
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("BACKGROUND")}>BACKGROUND</button>
 
363
  * Default placeholder image for new CHARACTER nodes
364
  * Uses Unsplash image as a starting point before users upload their own images
365
  */
366
+ const DEFAULT_PERSON = "/reo.png";
 
367
 
368
  /**
369
  * Convert File objects to data URLs for image processing
 
853
  <div className="mt-2">
854
  <div className="flex items-center justify-between mb-1">
855
  <div className="text-xs text-white/70">Output</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
  </div>
857
  <div className="w-full min-h-[200px] max-h-[400px] rounded-xl bg-black/40 grid place-items-center">
858
+ {node.output ? (
859
  <img
860
+ src={node.output}
861
  className="w-full h-auto max-h-[400px] object-contain rounded-xl cursor-pointer hover:opacity-80 transition-opacity"
862
  alt="output"
863
  onClick={async () => {
864
+ if (node.output) {
 
865
  try {
866
+ const response = await fetch(node.output);
867
  const blob = await response.blob();
868
  await navigator.clipboard.write([
869
  new ClipboardItem({ [blob.type]: blob })
 
875
  }}
876
  onContextMenu={async (e) => {
877
  e.preventDefault();
878
+ if (node.output) {
 
879
  try {
880
+ const response = await fetch(node.output);
881
  const blob = await response.blob();
882
  await navigator.clipboard.write([
883
  new ClipboardItem({ [blob.type]: blob })
 
902
  <span className="text-white/40 text-xs py-16">Run merge to see result</span>
903
  )}
904
  </div>
905
+ {node.output && (
906
+ <div className="mt-2">
 
 
 
 
 
 
 
 
907
  <Button
908
  className="w-full"
909
  variant="secondary"
910
  onClick={() => {
911
  const link = document.createElement('a');
912
+ link.href = node.output as string;
 
913
  link.download = `merge-${Date.now()}.png`;
914
  document.body.appendChild(link);
915
  link.click();
 
1034
  const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
1035
  const [isCheckingAuth, setIsCheckingAuth] = useState(true);
1036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1037
 
1038
  const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
1039
  const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
 
1112
  setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, ...updates } : n)));
1113
  };
1114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1115
 
1116
  // Handle single input connections for new nodes
1117
  const handleEndSingleConnection = (nodeId: string) => {
 
1530
  ? `Combined ${unprocessedNodeCount} transformations`
1531
  : `${node.type} transformation`;
1532
 
 
1533
 
1534
  if (unprocessedNodeCount > 1) {
1535
  console.log(`✅ Successfully applied ${unprocessedNodeCount} transformations in ONE API call!`);
 
1807
  return `${inputNode?.type || 'Node'} ${index + 1}`;
1808
  });
1809
 
 
1810
  }
1811
  } catch (e: any) {
1812
  console.error("Merge error:", e);
 
1872
  if (inputNode) {
1873
  const start = getNodeOutputPort(inputNode);
1874
  const end = getNodeInputPort(node);
1875
+ const isProcessing = merge.isRunning; // Only animate to the currently processing merge node
1876
  paths.push({
1877
  path: createPath(start.x, start.y, end.x, end.y),
1878
  processing: isProcessing
 
1886
  if (inputNode) {
1887
  const start = getNodeOutputPort(inputNode);
1888
  const end = getNodeInputPort(node);
1889
+ const isProcessing = (node as any).isRunning; // Only animate to the currently processing node
1890
  paths.push({
1891
  path: createPath(start.x, start.y, end.x, end.y),
1892
  processing: isProcessing
 
1959
  const rect = containerRef.current!.getBoundingClientRect();
1960
  const world = screenToWorld(e.clientX, e.clientY, rect, tx, ty, scale);
1961
  setMenuWorld(world);
1962
+
1963
+ // Menu dimensions
1964
+ const menuWidth = 224; // w-56 = 224px
1965
+ const menuHeight = 320; // Approximate height with max-h-[300px] + padding
1966
+
1967
+ // Calculate position relative to container
1968
+ let x = e.clientX - rect.left;
1969
+ let y = e.clientY - rect.top;
1970
+
1971
+ // Adjust if menu would go off right edge
1972
+ if (x + menuWidth > rect.width) {
1973
+ x = rect.width - menuWidth - 10;
1974
+ }
1975
+
1976
+ // Adjust if menu would go off bottom edge
1977
+ if (y + menuHeight > rect.height) {
1978
+ y = rect.height - menuHeight - 10;
1979
+ }
1980
+
1981
+ // Ensure minimum margins from edges
1982
+ x = Math.max(10, x);
1983
+ y = Math.max(10, y);
1984
+
1985
+ setMenuPos({ x, y });
1986
  setMenuOpen(true);
1987
  };
1988
 
 
2274
  onEndConnection={handleEndSingleConnection}
2275
  onProcess={processNode}
2276
  onUpdatePosition={updateNodePosition}
 
 
 
2277
  />
2278
  );
2279
  case "CLOTHES":
 
2287
  onEndConnection={handleEndSingleConnection}
2288
  onProcess={processNode}
2289
  onUpdatePosition={updateNodePosition}
 
 
 
2290
  />
2291
  );
2292
  case "STYLE":
 
2300
  onEndConnection={handleEndSingleConnection}
2301
  onProcess={processNode}
2302
  onUpdatePosition={updateNodePosition}
 
 
 
2303
  />
2304
  );
2305
  case "EDIT":
 
2313
  onEndConnection={handleEndSingleConnection}
2314
  onProcess={processNode}
2315
  onUpdatePosition={updateNodePosition}
 
 
 
2316
  />
2317
  );
2318
  case "CAMERA":
 
2326
  onEndConnection={handleEndSingleConnection}
2327
  onProcess={processNode}
2328
  onUpdatePosition={updateNodePosition}
 
 
 
2329
  />
2330
  );
2331
  case "AGE":
 
2339
  onEndConnection={handleEndSingleConnection}
2340
  onProcess={processNode}
2341
  onUpdatePosition={updateNodePosition}
 
 
 
2342
  />
2343
  );
2344
  case "FACE":
 
2352
  onEndConnection={handleEndSingleConnection}
2353
  onProcess={processNode}
2354
  onUpdatePosition={updateNodePosition}
 
 
 
2355
  />
2356
  );
2357
  case "LIGHTNING":
 
2365
  onEndConnection={handleEndSingleConnection}
2366
  onProcess={processNode}
2367
  onUpdatePosition={updateNodePosition}
 
 
 
2368
  />
2369
  );
2370
  case "POSES":
 
2378
  onEndConnection={handleEndSingleConnection}
2379
  onProcess={processNode}
2380
  onUpdatePosition={updateNodePosition}
 
 
 
2381
  />
2382
  );
2383
  default:
 
2394
  onMouseLeave={() => setMenuOpen(false)}
2395
  >
2396
  <div className="px-3 py-2 text-xs text-white/60">Add node</div>
2397
+ <div
2398
+ className="max-h-[300px] overflow-y-auto scrollbar-thin pr-1"
2399
+ onWheel={(e) => e.stopPropagation()}
2400
+ >
2401
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("CHARACTER")}>CHARACTER</button>
2402
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("MERGE")}>MERGE</button>
2403
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("BACKGROUND")}>BACKGROUND</button>
public/reo.png ADDED

Git LFS Details

  • SHA256: e0e9f5a3dfb575a6353b500ee519581cff46fc86005b4a497320fb8a3175134e
  • Pointer size: 132 Bytes
  • Size of remote file: 2.61 MB