Reubencf commited on
Commit
f6d2014
·
1 Parent(s): 58d2bbb

adding light mode

Browse files
app/api/merge/route.ts CHANGED
@@ -60,25 +60,34 @@ export async function POST(req: NextRequest) {
60
  let prompt = body.prompt;
61
 
62
  if (!prompt) {
63
- prompt = `MERGE TASK: You are provided with exactly ${imgs.length} source images.
64
-
65
- INSTRUCTIONS:
66
- 1. EXTRACT the exact people/subjects from EACH provided image
67
- 2. DO NOT generate new people - use ONLY the people visible in the provided images
68
- 3. COMBINE all extracted people into ONE single group photo
69
- 4. The output must contain ALL people from ALL ${imgs.length} input images together
70
-
71
- Requirements:
72
- - Use the ACTUAL people from the provided images (do not create new ones)
73
- - If an image has multiple people, include ALL of them
74
- - Arrange everyone naturally in the same scene
75
- - Match lighting and proportions realistically
76
- - Output exactly ONE image with everyone combined
77
-
78
- DO NOT create artistic interpretations or new people. EXTRACT and COMBINE the actual subjects from the provided photographs.`;
 
 
 
 
 
 
 
 
 
79
  } else {
80
- // Even with custom prompt, append extraction requirements
81
- const enforcement = `\n\nIMPORTANT: Extract and use the EXACT people from the provided images. Do not generate new people or artistic interpretations. Combine the actual subjects from all ${imgs.length} images into one output.`;
82
  prompt = `${prompt}${enforcement}`;
83
  }
84
 
 
60
  let prompt = body.prompt;
61
 
62
  if (!prompt) {
63
+ prompt = `MERGE TASK: Create a natural, cohesive group photo combining ALL subjects from ${imgs.length} provided images.
64
+
65
+ CRITICAL REQUIREMENTS:
66
+ 1. Extract ALL people/subjects from EACH image exactly as they appear
67
+ 2. Place them together in a SINGLE UNIFIED SCENE with:
68
+ - Consistent lighting direction and color temperature
69
+ - Matching shadows and ambient lighting
70
+ - Proper scale relationships (realistic relative sizes)
71
+ - Natural spacing as if they were photographed together
72
+ - Shared environment/background that looks cohesive
73
+
74
+ 3. Composition guidelines:
75
+ - Arrange subjects at similar depth (not one far behind another)
76
+ - Use natural group photo positioning (slight overlap is ok)
77
+ - Ensure all faces are clearly visible
78
+ - Create visual balance in the composition
79
+ - Apply consistent color grading across all subjects
80
+
81
+ 4. Environmental unity:
82
+ - Use a single, coherent background for all subjects
83
+ - Match the perspective as if taken with one camera
84
+ - Ensure ground plane continuity (all standing on same level)
85
+ - Apply consistent atmospheric effects (if any)
86
+
87
+ The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`;
88
  } else {
89
+ // Even with custom prompt, append cohesion requirements
90
+ const enforcement = `\n\nIMPORTANT: Create a COHESIVE group photo where all subjects appear to be in the same scene with consistent lighting, scale, and environment. The result should look naturally photographed together, not composited.`;
91
  prompt = `${prompt}${enforcement}`;
92
  }
93
 
app/api/process/route.ts CHANGED
@@ -107,7 +107,7 @@ export async function POST(req: NextRequest) {
107
  // Style blending
108
  if (params.styleImage) {
109
  const strength = params.blendStrength || 50;
110
- prompts.push(`Apply artistic style transfer: Take the visual style, colors, textures, and artistic techniques from the provided style reference image (attached below) and apply them to the main subject image. Blend at ${strength}% strength - at 100% the output should look like it was painted/created in the exact style of the reference, at 0% it should look unchanged. Preserve the content and structure of the original image while adopting the artistic style.`);
111
  const styleRef = await toInlineDataFromAny(params.styleImage);
112
  if (styleRef) referenceParts.push({ inlineData: styleRef });
113
  }
@@ -119,7 +119,7 @@ export async function POST(req: NextRequest) {
119
 
120
  // Camera settings
121
  if (params.focalLength || params.aperture || params.shutterSpeed || params.whiteBalance || params.angle ||
122
- params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition || params.aspectRatio) {
123
  const cameraSettings: string[] = [];
124
  if (params.focalLength) {
125
  if (params.focalLength === "8mm fisheye") {
@@ -137,7 +137,6 @@ export async function POST(req: NextRequest) {
137
  if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
138
  if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
139
  if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
140
- if (params.aspectRatio) cameraSettings.push(`Aspect Ratio: ${params.aspectRatio}`);
141
 
142
  if (cameraSettings.length > 0) {
143
  prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
 
107
  // Style blending
108
  if (params.styleImage) {
109
  const strength = params.blendStrength || 50;
110
+ prompts.push(`Apply artistic style blending using the provided style reference image (attached below) at ${strength}% strength.`);
111
  const styleRef = await toInlineDataFromAny(params.styleImage);
112
  if (styleRef) referenceParts.push({ inlineData: styleRef });
113
  }
 
119
 
120
  // Camera settings
121
  if (params.focalLength || params.aperture || params.shutterSpeed || params.whiteBalance || params.angle ||
122
+ params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition) {
123
  const cameraSettings: string[] = [];
124
  if (params.focalLength) {
125
  if (params.focalLength === "8mm fisheye") {
 
137
  if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
138
  if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
139
  if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
 
140
 
141
  if (cameraSettings.length > 0) {
142
  prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
app/editor/nodes.tsx CHANGED
@@ -180,8 +180,15 @@ export function BackgroundNodeView({
180
  <Button
181
  variant="ghost"
182
  size="icon"
183
- className="text-destructive"
184
- onClick={() => onDelete(node.id)}
 
 
 
 
 
 
 
185
  title="Delete node"
186
  aria-label="Delete node"
187
  >
@@ -358,8 +365,15 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
358
  <Button
359
  variant="ghost"
360
  size="icon"
361
- className="text-destructive"
362
- onClick={() => onDelete(node.id)}
 
 
 
 
 
 
 
363
  title="Delete node"
364
  aria-label="Delete node"
365
  >
@@ -487,8 +501,15 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
487
  <Button
488
  variant="ghost"
489
  size="icon"
490
- className="text-destructive"
491
- onClick={() => onDelete(node.id)}
 
 
 
 
 
 
 
492
  title="Delete node"
493
  aria-label="Delete node"
494
  >
@@ -576,8 +597,15 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
576
  <Button
577
  variant="ghost"
578
  size="icon"
579
- className="text-destructive"
580
- onClick={() => onDelete(node.id)}
 
 
 
 
 
 
 
581
  title="Delete node"
582
  aria-label="Delete node"
583
  >
@@ -771,8 +799,15 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
771
  <Button
772
  variant="ghost"
773
  size="icon"
774
- className="text-destructive"
775
- onClick={() => onDelete(node.id)}
 
 
 
 
 
 
 
776
  title="Delete node"
777
  aria-label="Delete node"
778
  >
@@ -943,8 +978,15 @@ export function BlendNodeView({ node, onDelete, onUpdate, onStartConnection, onE
943
  <Button
944
  variant="ghost"
945
  size="icon"
946
- className="text-destructive"
947
- onClick={() => onDelete(node.id)}
 
 
 
 
 
 
 
948
  title="Delete node"
949
  aria-label="Delete node"
950
  >
 
180
  <Button
181
  variant="ghost"
182
  size="icon"
183
+ className="text-destructive hover:bg-destructive/20"
184
+ onClick={(e) => {
185
+ e.stopPropagation();
186
+ e.preventDefault();
187
+ if (confirm('Delete this node?')) {
188
+ onDelete(node.id);
189
+ }
190
+ }}
191
+ onPointerDown={(e) => e.stopPropagation()}
192
  title="Delete node"
193
  aria-label="Delete node"
194
  >
 
365
  <Button
366
  variant="ghost"
367
  size="icon"
368
+ className="text-destructive hover:bg-destructive/20"
369
+ onClick={(e) => {
370
+ e.stopPropagation();
371
+ e.preventDefault();
372
+ if (confirm('Delete this node?')) {
373
+ onDelete(node.id);
374
+ }
375
+ }}
376
+ onPointerDown={(e) => e.stopPropagation()}
377
  title="Delete node"
378
  aria-label="Delete node"
379
  >
 
501
  <Button
502
  variant="ghost"
503
  size="icon"
504
+ className="text-destructive hover:bg-destructive/20"
505
+ onClick={(e) => {
506
+ e.stopPropagation();
507
+ e.preventDefault();
508
+ if (confirm('Delete this node?')) {
509
+ onDelete(node.id);
510
+ }
511
+ }}
512
+ onPointerDown={(e) => e.stopPropagation()}
513
  title="Delete node"
514
  aria-label="Delete node"
515
  >
 
597
  <Button
598
  variant="ghost"
599
  size="icon"
600
+ className="text-destructive hover:bg-destructive/20"
601
+ onClick={(e) => {
602
+ e.stopPropagation();
603
+ e.preventDefault();
604
+ if (confirm('Delete this node?')) {
605
+ onDelete(node.id);
606
+ }
607
+ }}
608
+ onPointerDown={(e) => e.stopPropagation()}
609
  title="Delete node"
610
  aria-label="Delete node"
611
  >
 
799
  <Button
800
  variant="ghost"
801
  size="icon"
802
+ className="text-destructive hover:bg-destructive/20"
803
+ onClick={(e) => {
804
+ e.stopPropagation();
805
+ e.preventDefault();
806
+ if (confirm('Delete this node?')) {
807
+ onDelete(node.id);
808
+ }
809
+ }}
810
+ onPointerDown={(e) => e.stopPropagation()}
811
  title="Delete node"
812
  aria-label="Delete node"
813
  >
 
978
  <Button
979
  variant="ghost"
980
  size="icon"
981
+ className="text-destructive hover:bg-destructive/20"
982
+ onClick={(e) => {
983
+ e.stopPropagation();
984
+ e.preventDefault();
985
+ if (confirm('Delete this node?')) {
986
+ onDelete(node.id);
987
+ }
988
+ }}
989
+ onPointerDown={(e) => e.stopPropagation()}
990
  title="Delete node"
991
  aria-label="Delete node"
992
  >
app/editor/page.tsx CHANGED
@@ -25,25 +25,34 @@ function generateMergePrompt(characterData: { image: string; label: string }[]):
25
 
26
  const labels = characterData.map((d, i) => `Image ${i + 1} (${d.label})`).join(", ");
27
 
28
- return `MERGE TASK: You are provided with exactly ${count} source images.
29
 
30
  Images provided:
31
  ${characterData.map((d, i) => `- Image ${i + 1}: ${d.label}`).join("\n")}
32
 
33
- INSTRUCTIONS:
34
- 1. EXTRACT the exact people/subjects from EACH provided image
35
- 2. DO NOT generate new people - use ONLY the people visible in the provided images
36
- 3. COMBINE all extracted people into ONE single group photo
37
- 4. The output must contain ALL people from ALL ${count} input images together
38
-
39
- Requirements:
40
- - Use the ACTUAL people from the provided images (do not create new ones)
41
- - If an image has multiple people, include ALL of them
42
- - Arrange everyone naturally in the same scene
43
- - Match lighting and proportions realistically
44
- - Output exactly ONE image with everyone combined
45
-
46
- DO NOT create artistic interpretations or new people. EXTRACT and COMBINE the actual subjects from the provided photographs.`;
 
 
 
 
 
 
 
 
 
47
  }
48
 
49
  // Types
@@ -583,6 +592,18 @@ export default function EditorPage() {
583
  } as CharacterNode,
584
  ]);
585
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  // Viewport state
587
  const [scale, setScale] = useState(1);
588
  const [tx, setTx] = useState(0);
@@ -694,6 +715,9 @@ export default function EditorPage() {
694
  }
695
  setDraggingFrom(null);
696
  setDragPos(null);
 
 
 
697
  }
698
  };
699
 
@@ -1051,6 +1075,9 @@ export default function EditorPage() {
1051
  // Connection drag handlers
1052
  const handleStartConnection = (characterId: string) => {
1053
  setDraggingFrom(characterId);
 
 
 
1054
  };
1055
 
1056
  const handleEndConnection = (mergeId: string) => {
@@ -1058,6 +1085,9 @@ export default function EditorPage() {
1058
  connectToMerge(mergeId, draggingFrom);
1059
  setDraggingFrom(null);
1060
  setDragPos(null);
 
 
 
1061
  }
1062
  };
1063
 
@@ -1073,6 +1103,9 @@ export default function EditorPage() {
1073
  if (draggingFrom) {
1074
  setDraggingFrom(null);
1075
  setDragPos(null);
 
 
 
1076
  }
1077
  };
1078
  const disconnectFromMerge = (mergeId: string, characterId: string) => {
@@ -1376,7 +1409,34 @@ export default function EditorPage() {
1376
  return (
1377
  <div className="min-h-[100svh] bg-background text-foreground">
1378
  <header className="flex items-center justify-between px-6 py-4 border-b border-border/60 bg-card/70 backdrop-blur">
1379
- <h1 className="text-lg font-semibold tracking-wide"><span className="mr-2" aria-hidden>🍌</span>Nano Banana Editor</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1380
  </header>
1381
 
1382
  <div
 
25
 
26
  const labels = characterData.map((d, i) => `Image ${i + 1} (${d.label})`).join(", ");
27
 
28
+ return `MERGE TASK: Create a natural, cohesive group photo combining ALL subjects from ${count} provided images.
29
 
30
  Images provided:
31
  ${characterData.map((d, i) => `- Image ${i + 1}: ${d.label}`).join("\n")}
32
 
33
+ CRITICAL REQUIREMENTS:
34
+ 1. Extract ALL people/subjects from EACH image exactly as they appear
35
+ 2. Place them together in a SINGLE UNIFIED SCENE with:
36
+ - Consistent lighting direction and color temperature
37
+ - Matching shadows and ambient lighting
38
+ - Proper scale relationships (realistic relative sizes)
39
+ - Natural spacing as if they were photographed together
40
+ - Shared environment/background that looks cohesive
41
+
42
+ 3. Composition guidelines:
43
+ - Arrange subjects at similar depth (not one far behind another)
44
+ - Use natural group photo positioning (slight overlap is ok)
45
+ - Ensure all faces are clearly visible
46
+ - Create visual balance in the composition
47
+ - Apply consistent color grading across all subjects
48
+
49
+ 4. Environmental unity:
50
+ - Use a single, coherent background for all subjects
51
+ - Match the perspective as if taken with one camera
52
+ - Ensure ground plane continuity (all standing on same level)
53
+ - Apply consistent atmospheric effects (if any)
54
+
55
+ The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`;
56
  }
57
 
58
  // Types
 
592
  } as CharacterNode,
593
  ]);
594
 
595
+ // Theme state
596
+ const [theme, setTheme] = useState<'dark' | 'light'>('dark');
597
+
598
+ // Apply theme to document
599
+ useEffect(() => {
600
+ if (theme === 'light') {
601
+ document.documentElement.classList.remove('dark');
602
+ } else {
603
+ document.documentElement.classList.add('dark');
604
+ }
605
+ }, [theme]);
606
+
607
  // Viewport state
608
  const [scale, setScale] = useState(1);
609
  const [tx, setTx] = useState(0);
 
715
  }
716
  setDraggingFrom(null);
717
  setDragPos(null);
718
+ // Re-enable text selection
719
+ document.body.style.userSelect = '';
720
+ document.body.style.webkitUserSelect = '';
721
  }
722
  };
723
 
 
1075
  // Connection drag handlers
1076
  const handleStartConnection = (characterId: string) => {
1077
  setDraggingFrom(characterId);
1078
+ // Prevent text selection during dragging
1079
+ document.body.style.userSelect = 'none';
1080
+ document.body.style.webkitUserSelect = 'none';
1081
  };
1082
 
1083
  const handleEndConnection = (mergeId: string) => {
 
1085
  connectToMerge(mergeId, draggingFrom);
1086
  setDraggingFrom(null);
1087
  setDragPos(null);
1088
+ // Re-enable text selection
1089
+ document.body.style.userSelect = '';
1090
+ document.body.style.webkitUserSelect = '';
1091
  }
1092
  };
1093
 
 
1103
  if (draggingFrom) {
1104
  setDraggingFrom(null);
1105
  setDragPos(null);
1106
+ // Re-enable text selection
1107
+ document.body.style.userSelect = '';
1108
+ document.body.style.webkitUserSelect = '';
1109
  }
1110
  };
1111
  const disconnectFromMerge = (mergeId: string, characterId: string) => {
 
1409
  return (
1410
  <div className="min-h-[100svh] bg-background text-foreground">
1411
  <header className="flex items-center justify-between px-6 py-4 border-b border-border/60 bg-card/70 backdrop-blur">
1412
+ <h1 className="text-lg font-semibold tracking-wide">
1413
+ <span className="mr-2" aria-hidden>🍌</span>Nano Banana Editor
1414
+ </h1>
1415
+ <Button
1416
+ variant="ghost"
1417
+ size="icon"
1418
+ onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
1419
+ title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
1420
+ className="rounded-lg"
1421
+ >
1422
+ {theme === 'dark' ? (
1423
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1424
+ <circle cx="12" cy="12" r="5"/>
1425
+ <line x1="12" y1="1" x2="12" y2="3"/>
1426
+ <line x1="12" y1="21" x2="12" y2="23"/>
1427
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
1428
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
1429
+ <line x1="1" y1="12" x2="3" y2="12"/>
1430
+ <line x1="21" y1="12" x2="23" y2="12"/>
1431
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
1432
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
1433
+ </svg>
1434
+ ) : (
1435
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1436
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
1437
+ </svg>
1438
+ )}
1439
+ </Button>
1440
  </header>
1441
 
1442
  <div
app/globals.css CHANGED
@@ -40,8 +40,39 @@
40
  --chart-5: 27 87% 67%;
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  @media (prefers-color-scheme: dark) {
44
- :root {
45
  --background: 240 10% 3.9%;
46
  --foreground: 0 0% 98%;
47
 
@@ -146,6 +177,19 @@ body {
146
  backface-visibility: hidden;
147
  perspective: 1000px;
148
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  .nb-node .nb-header {
150
  background: linear-gradient(to bottom, hsl(var(--muted) / 0.35), hsl(var(--muted) / 0.08));
151
  }
@@ -158,6 +202,8 @@ body {
158
  cursor: crosshair;
159
  transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
160
  position: relative;
 
 
161
  }
162
  .nb-port:hover {
163
  transform: scale(1.25);
 
40
  --chart-5: 27 87% 67%;
41
  }
42
 
43
+ .dark {
44
+ --background: 240 10% 3.9%;
45
+ --foreground: 0 0% 98%;
46
+
47
+ --card: 240 10% 3.9%;
48
+ --card-foreground: 0 0% 98%;
49
+
50
+ --popover: 240 10% 3.9%;
51
+ --popover-foreground: 0 0% 98%;
52
+
53
+ /* Brand in dark mode */
54
+ --primary: 14 87% 60%;
55
+ --primary-foreground: 240 10% 3.9%;
56
+
57
+ --secondary: 240 3.7% 15.9%;
58
+ --secondary-foreground: 0 0% 98%;
59
+
60
+ --muted: 240 3.7% 15.9%;
61
+ --muted-foreground: 240 5% 64.9%;
62
+
63
+ --accent: 14 40% 20%;
64
+ --accent-foreground: 0 0% 98%;
65
+
66
+ --destructive: 0 62.8% 30.6%;
67
+ --destructive-foreground: 0 85.7% 97.3%;
68
+
69
+ --border: 240 3.7% 15.9%;
70
+ --input: 240 3.7% 15.9%;
71
+ --ring: 14 87% 55%;
72
+ }
73
+
74
  @media (prefers-color-scheme: dark) {
75
+ :root:not(.light) {
76
  --background: 240 10% 3.9%;
77
  --foreground: 0 0% 98%;
78
 
 
177
  backface-visibility: hidden;
178
  perspective: 1000px;
179
  }
180
+
181
+ /* Prevent text selection on node elements except inputs */
182
+ .nb-node * {
183
+ user-select: none;
184
+ -webkit-user-select: none;
185
+ }
186
+
187
+ .nb-node input,
188
+ .nb-node textarea,
189
+ .nb-node select {
190
+ user-select: text;
191
+ -webkit-user-select: text;
192
+ }
193
  .nb-node .nb-header {
194
  background: linear-gradient(to bottom, hsl(var(--muted) / 0.35), hsl(var(--muted) / 0.08));
195
  }
 
202
  cursor: crosshair;
203
  transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
204
  position: relative;
205
+ user-select: none;
206
+ -webkit-user-select: none;
207
  }
208
  .nb-port:hover {
209
  transform: scale(1.25);