Reubencf commited on
Commit
ecb19b7
·
1 Parent(s): 7988550

Adding More features

Browse files
.claude/settings.local.json CHANGED
@@ -10,7 +10,8 @@
10
  "Bash(npm run build:*)",
11
  "Bash(git add:*)",
12
  "Bash(git commit:*)",
13
- "Bash(git push:*)"
 
14
  ],
15
  "deny": [],
16
  "ask": []
 
10
  "Bash(npm run build:*)",
11
  "Bash(git add:*)",
12
  "Bash(git commit:*)",
13
+ "Bash(git push:*)",
14
+ "WebSearch"
15
  ],
16
  "deny": [],
17
  "ask": []
app/api/hf-process/route.ts DELETED
@@ -1,178 +0,0 @@
1
- /**
2
- * API ROUTE: /api/hf-process
3
- *
4
- * Hugging Face Inference API integration with fal.ai Gemini 2.5 Flash Image.
5
- * Uses HF Inference API to access fal.ai's Gemini 2.5 Flash Image models.
6
- */
7
-
8
- import { NextRequest, NextResponse } from "next/server";
9
- import { HfInference } from "@huggingface/inference";
10
- import { cookies } from "next/headers";
11
-
12
- export const runtime = "nodejs";
13
- export const maxDuration = 60;
14
-
15
- export async function POST(req: NextRequest) {
16
- try {
17
- // Check if user is authenticated with HF Pro
18
- const cookieStore = await cookies();
19
- const hfToken = cookieStore.get('hf_token');
20
-
21
- if (!hfToken?.value) {
22
- return NextResponse.json(
23
- { error: "Please login with HF Pro to use fal.ai Gemini 2.5 Flash Image." },
24
- { status: 401 }
25
- );
26
- }
27
-
28
- // Initialize HF Inference client
29
- const hf = new HfInference(hfToken.value);
30
-
31
- const body = await req.json() as {
32
- type: string;
33
- image?: string;
34
- images?: string[];
35
- prompt?: string;
36
- params?: any;
37
- };
38
-
39
- // Convert data URL to blob for HF API
40
- const dataUrlToBlob = (dataUrl: string): Blob => {
41
- const arr = dataUrl.split(',');
42
- const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png';
43
- const bstr = atob(arr[1]);
44
- let n = bstr.length;
45
- const u8arr = new Uint8Array(n);
46
- while (n--) {
47
- u8arr[n] = bstr.charCodeAt(n);
48
- }
49
- return new Blob([u8arr], { type: mime });
50
- };
51
-
52
- // Handle MERGE operation using Stable Diffusion
53
- if (body.type === "MERGE") {
54
- if (!body.images || body.images.length < 2) {
55
- return NextResponse.json(
56
- { error: "MERGE requires at least two images" },
57
- { status: 400 }
58
- );
59
- }
60
-
61
- const prompt = body.prompt || `Create a cohesive group photo combining all subjects from the provided images. Ensure consistent lighting, natural positioning, and unified background.`;
62
-
63
- try {
64
- // Use fal.ai's Gemini 2.5 Flash Image through HF
65
- const result = await hf.textToImage({
66
- model: "fal-ai/gemini-25-flash-image/edit",
67
- inputs: prompt,
68
- parameters: {
69
- width: 1024,
70
- height: 1024,
71
- num_inference_steps: 20,
72
- }
73
- });
74
-
75
- // HF returns a Blob, convert to base64
76
- const arrayBuffer = await (result as unknown as Blob).arrayBuffer();
77
- const base64 = Buffer.from(arrayBuffer).toString('base64');
78
-
79
- return NextResponse.json({
80
- image: `data:image/png;base64,${base64}`,
81
- model: "fal-ai/gemini-25-flash-image/edit"
82
- });
83
- } catch (error: unknown) {
84
- console.error('HF Merge error:', error);
85
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
86
- return NextResponse.json(
87
- { error: `HF processing failed: ${errorMessage}` },
88
- { status: 500 }
89
- );
90
- }
91
- }
92
-
93
- // Handle COMBINED and single image processing
94
- if (body.type === "COMBINED" || !body.image) {
95
- if (!body.image) {
96
- return NextResponse.json(
97
- { error: "No image provided" },
98
- { status: 400 }
99
- );
100
- }
101
- }
102
-
103
- const inputBlob = dataUrlToBlob(body.image);
104
-
105
- // Build prompt from parameters
106
- const prompts: string[] = [];
107
- const params = body.params || {};
108
-
109
- // Background changes
110
- if (params.backgroundType) {
111
- if (params.backgroundType === "color") {
112
- prompts.push(`Change background to ${params.backgroundColor || "white"}`);
113
- } else if (params.backgroundType === "image") {
114
- prompts.push(`Change background to ${params.backgroundImage || "beautiful landscape"}`);
115
- } else if (params.customPrompt) {
116
- prompts.push(params.customPrompt);
117
- }
118
- }
119
-
120
- // Style applications
121
- if (params.stylePreset) {
122
- const styleMap: { [key: string]: string } = {
123
- "90s-anime": "90s anime style, classic animation",
124
- "cyberpunk": "cyberpunk aesthetic, neon lights, futuristic",
125
- "van-gogh": "Van Gogh painting style, impressionist",
126
- "simpsons": "The Simpsons cartoon style",
127
- "arcane": "Arcane League of Legends art style"
128
- };
129
- const styleDesc = styleMap[params.stylePreset] || params.stylePreset;
130
- prompts.push(`Apply ${styleDesc} art style`);
131
- }
132
-
133
- // Other modifications
134
- if (params.editPrompt) {
135
- prompts.push(params.editPrompt);
136
- }
137
-
138
- const prompt = prompts.length > 0
139
- ? prompts.join(", ")
140
- : "High quality image processing";
141
-
142
- try {
143
- // Use fal.ai's Gemini 2.5 Flash Image for image editing
144
- const result = await hf.imageToImage({
145
- model: "fal-ai/gemini-25-flash-image/edit",
146
- inputs: inputBlob,
147
- parameters: {
148
- prompt: prompt,
149
- strength: 0.8,
150
- num_inference_steps: 25,
151
- }
152
- });
153
-
154
- const arrayBuffer = await (result as unknown as Blob).arrayBuffer();
155
- const base64 = Buffer.from(arrayBuffer).toString('base64');
156
-
157
- return NextResponse.json({
158
- image: `data:image/png;base64,${base64}`,
159
- model: "fal-ai/gemini-25-flash-image/edit"
160
- });
161
- } catch (error: unknown) {
162
- console.error('HF processing error:', error);
163
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
164
- return NextResponse.json(
165
- { error: `HF processing failed: ${errorMessage}` },
166
- { status: 500 }
167
- );
168
- }
169
-
170
- } catch (error: unknown) {
171
- console.error('HF API error:', error);
172
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
173
- return NextResponse.json(
174
- { error: `API error: ${errorMessage}` },
175
- { status: 500 }
176
- );
177
- }
178
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/process/route.ts CHANGED
@@ -358,7 +358,7 @@ The result should look like all subjects were photographed together in the same
358
  if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
359
  if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
360
  if (params.iso) cameraSettings.push(`${params.iso}`);
361
- if (params.filmStyle) cameraSettings.push(`Film style: ${params.filmStyle}`);
362
  if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
363
  if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
364
  if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
 
358
  if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
359
  if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
360
  if (params.iso) cameraSettings.push(`${params.iso}`);
361
+ if (params.filmStyle) cameraSettings.push(`${params.filmStyle}`);
362
  if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
363
  if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
364
  if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
app/nodes.tsx CHANGED
@@ -47,6 +47,75 @@ function downloadImage(dataUrl: string, filename: string) {
47
  document.body.removeChild(link); // Clean up temporary link
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  /* ========================================
51
  TYPE DEFINITIONS (TEMPORARY)
52
  ======================================== */
@@ -204,6 +273,9 @@ export function BackgroundNodeView({
204
  onEndConnection,
205
  onProcess,
206
  onUpdatePosition,
 
 
 
207
  }: any) {
208
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
209
 
@@ -372,18 +444,14 @@ export function BackgroundNodeView({
372
  {node.isRunning ? "Processing..." : "Apply Background"}
373
  </Button>
374
 
375
- {node.output && (
376
- <div className="space-y-2">
377
- <img src={node.output} className="w-full rounded" alt="Output" />
378
- <Button
379
- className="w-full"
380
- variant="secondary"
381
- onClick={() => downloadImage(node.output, `background-${Date.now()}.png`)}
382
- >
383
- 📥 Download Output
384
- </Button>
385
- </div>
386
- )}
387
  {node.error && (
388
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
389
  )}
@@ -392,7 +460,7 @@ export function BackgroundNodeView({
392
  );
393
  }
394
 
395
- export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
396
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
397
 
398
  const presetClothes = [
@@ -546,18 +614,14 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
546
  >
547
  {node.isRunning ? "Processing..." : "Apply Clothes"}
548
  </Button>
549
- {node.output && (
550
- <div className="space-y-2">
551
- <img src={node.output} className="w-full rounded" alt="Output" />
552
- <Button
553
- className="w-full"
554
- variant="secondary"
555
- onClick={() => downloadImage(node.output, `clothes-${Date.now()}.png`)}
556
- >
557
- 📥 Download Output
558
- </Button>
559
- </div>
560
- )}
561
  {node.error && (
562
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
563
  )}
@@ -566,7 +630,7 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
566
  );
567
  }
568
 
569
- export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
570
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
571
 
572
  return (
@@ -631,18 +695,14 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
631
  >
632
  {node.isRunning ? "Processing..." : "Apply Age"}
633
  </Button>
634
- {node.output && (
635
- <div className="space-y-2">
636
- <img src={node.output} className="w-full rounded" alt="Output" />
637
- <Button
638
- className="w-full"
639
- variant="secondary"
640
- onClick={() => downloadImage(node.output, `age-${Date.now()}.png`)}
641
- >
642
- 📥 Download Output
643
- </Button>
644
- </div>
645
- )}
646
  {node.error && (
647
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
648
  )}
@@ -651,7 +711,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
651
  );
652
  }
653
 
654
- export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
655
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
656
  const focalLengths = ["None", "8mm fisheye", "12mm", "24mm", "35mm", "50mm", "85mm", "135mm", "200mm", "300mm", "400mm"];
657
  const apertures = ["None", "f/0.95", "f/1.2", "f/1.4", "f/1.8", "f/2", "f/2.8", "f/4", "f/5.6", "f/8", "f/11", "f/16", "f/22"];
@@ -841,18 +901,16 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
841
  >
842
  {node.isRunning ? "Processing..." : "Apply Camera Settings"}
843
  </Button>
844
- {node.output && (
845
- <div className="space-y-2 mt-2">
846
- <img src={node.output} className="w-full rounded" alt="Output" />
847
- <Button
848
- className="w-full"
849
- variant="secondary"
850
- onClick={() => downloadImage(node.output, `camera-${Date.now()}.png`)}
851
- >
852
- 📥 Download Output
853
- </Button>
854
- </div>
855
- )}
856
  {node.error && (
857
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
858
  )}
@@ -861,7 +919,7 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
861
  );
862
  }
863
 
864
- export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
865
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
866
  const hairstyles = ["None", "short", "long", "curly", "straight", "bald", "mohawk", "ponytail"];
867
  const expressions = ["None", "happy", "serious", "smiling", "laughing", "sad", "surprised", "angry"];
@@ -988,18 +1046,16 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
988
  >
989
  {node.isRunning ? "Processing..." : "Apply Face Changes"}
990
  </Button>
991
- {node.output && (
992
- <div className="space-y-2 mt-2">
993
- <img src={node.output} className="w-full rounded" alt="Output" />
994
- <Button
995
- className="w-full"
996
- variant="secondary"
997
- onClick={() => downloadImage(node.output, `face-${Date.now()}.png`)}
998
- >
999
- 📥 Download Output
1000
- </Button>
1001
- </div>
1002
- )}
1003
  {node.error && (
1004
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
1005
  )}
@@ -1008,7 +1064,7 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
1008
  );
1009
  }
1010
 
1011
- export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
1012
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1013
 
1014
  const styleOptions = [
@@ -1107,18 +1163,14 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1107
  >
1108
  {node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
1109
  </Button>
1110
- {node.output && (
1111
- <div className="space-y-2">
1112
- <img src={node.output} className="w-full rounded" alt="Output" />
1113
- <Button
1114
- className="w-full"
1115
- variant="secondary"
1116
- onClick={() => downloadImage(node.output, `style-${Date.now()}.png`)}
1117
- >
1118
- 📥 Download Output
1119
- </Button>
1120
- </div>
1121
- )}
1122
  {node.error && (
1123
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
1124
  )}
@@ -1127,7 +1179,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1127
  );
1128
  }
1129
 
1130
- export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
1131
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1132
 
1133
  return (
@@ -1182,18 +1234,14 @@ export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
1182
  >
1183
  {node.isRunning ? "Processing..." : "Apply Edit"}
1184
  </Button>
1185
- {node.output && (
1186
- <div className="space-y-2">
1187
- <img src={node.output} className="w-full rounded" alt="Output" />
1188
- <Button
1189
- className="w-full"
1190
- variant="secondary"
1191
- onClick={() => downloadImage(node.output, `edit-${Date.now()}.png`)}
1192
- >
1193
- 📥 Download Output
1194
- </Button>
1195
- </div>
1196
- )}
1197
  </div>
1198
  </div>
1199
  );
 
47
  document.body.removeChild(link); // Clean up temporary link
48
  }
49
 
50
+ /**
51
+ * Reusable output section with history navigation for node components
52
+ */
53
+ function NodeOutputSection({
54
+ nodeId,
55
+ output,
56
+ downloadFileName,
57
+ getNodeHistoryInfo,
58
+ navigateNodeHistory,
59
+ getCurrentNodeImage,
60
+ }: {
61
+ nodeId: string;
62
+ output?: string;
63
+ downloadFileName: string;
64
+ getNodeHistoryInfo?: (id: string) => any;
65
+ navigateNodeHistory?: (id: string, direction: 'prev' | 'next') => void;
66
+ getCurrentNodeImage?: (id: string, fallback?: string) => string;
67
+ }) {
68
+ const currentImage = getCurrentNodeImage ? getCurrentNodeImage(nodeId, output) : output;
69
+
70
+ if (!currentImage) return null;
71
+
72
+ const historyInfo = getNodeHistoryInfo ? getNodeHistoryInfo(nodeId) : { hasHistory: false, currentDescription: '' };
73
+
74
+ return (
75
+ <div className="space-y-2">
76
+ <div className="space-y-1">
77
+ <div className="flex items-center justify-between">
78
+ <div className="text-xs text-white/70">Output</div>
79
+ {historyInfo.hasHistory ? (
80
+ <div className="flex items-center gap-1">
81
+ <button
82
+ className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
83
+ onClick={() => navigateNodeHistory && navigateNodeHistory(nodeId, 'prev')}
84
+ disabled={!historyInfo.canGoBack}
85
+ >
86
+
87
+ </button>
88
+ <span className="text-xs text-white/60 px-1">
89
+ {historyInfo.current}/{historyInfo.total}
90
+ </span>
91
+ <button
92
+ className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
93
+ onClick={() => navigateNodeHistory && navigateNodeHistory(nodeId, 'next')}
94
+ disabled={!historyInfo.canGoForward}
95
+ >
96
+
97
+ </button>
98
+ </div>
99
+ ) : null}
100
+ </div>
101
+ <img src={currentImage} className="w-full rounded" alt="Output" />
102
+ {historyInfo.currentDescription ? (
103
+ <div className="text-xs text-white/60 bg-black/20 rounded px-2 py-1">
104
+ {historyInfo.currentDescription}
105
+ </div>
106
+ ) : null}
107
+ </div>
108
+ <Button
109
+ className="w-full"
110
+ variant="secondary"
111
+ onClick={() => downloadImage(currentImage, downloadFileName)}
112
+ >
113
+ 📥 Download Output
114
+ </Button>
115
+ </div>
116
+ );
117
+ }
118
+
119
  /* ========================================
120
  TYPE DEFINITIONS (TEMPORARY)
121
  ======================================== */
 
273
  onEndConnection,
274
  onProcess,
275
  onUpdatePosition,
276
+ getNodeHistoryInfo,
277
+ navigateNodeHistory,
278
+ getCurrentNodeImage,
279
  }: any) {
280
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
281
 
 
444
  {node.isRunning ? "Processing..." : "Apply Background"}
445
  </Button>
446
 
447
+ <NodeOutputSection
448
+ nodeId={node.id}
449
+ output={node.output}
450
+ downloadFileName={`background-${Date.now()}.png`}
451
+ getNodeHistoryInfo={getNodeHistoryInfo}
452
+ navigateNodeHistory={navigateNodeHistory}
453
+ getCurrentNodeImage={getCurrentNodeImage}
454
+ />
 
 
 
 
455
  {node.error && (
456
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
457
  )}
 
460
  );
461
  }
462
 
463
+ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
464
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
465
 
466
  const presetClothes = [
 
614
  >
615
  {node.isRunning ? "Processing..." : "Apply Clothes"}
616
  </Button>
617
+ <NodeOutputSection
618
+ nodeId={node.id}
619
+ output={node.output}
620
+ downloadFileName={`clothes-${Date.now()}.png`}
621
+ getNodeHistoryInfo={getNodeHistoryInfo}
622
+ navigateNodeHistory={navigateNodeHistory}
623
+ getCurrentNodeImage={getCurrentNodeImage}
624
+ />
 
 
 
 
625
  {node.error && (
626
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
627
  )}
 
630
  );
631
  }
632
 
633
+ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
634
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
635
 
636
  return (
 
695
  >
696
  {node.isRunning ? "Processing..." : "Apply Age"}
697
  </Button>
698
+ <NodeOutputSection
699
+ nodeId={node.id}
700
+ output={node.output}
701
+ downloadFileName={`age-${Date.now()}.png`}
702
+ getNodeHistoryInfo={getNodeHistoryInfo}
703
+ navigateNodeHistory={navigateNodeHistory}
704
+ getCurrentNodeImage={getCurrentNodeImage}
705
+ />
 
 
 
 
706
  {node.error && (
707
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
708
  )}
 
711
  );
712
  }
713
 
714
+ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
715
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
716
  const focalLengths = ["None", "8mm fisheye", "12mm", "24mm", "35mm", "50mm", "85mm", "135mm", "200mm", "300mm", "400mm"];
717
  const apertures = ["None", "f/0.95", "f/1.2", "f/1.4", "f/1.8", "f/2", "f/2.8", "f/4", "f/5.6", "f/8", "f/11", "f/16", "f/22"];
 
901
  >
902
  {node.isRunning ? "Processing..." : "Apply Camera Settings"}
903
  </Button>
904
+ <div className="mt-2">
905
+ <NodeOutputSection
906
+ nodeId={node.id}
907
+ output={node.output}
908
+ downloadFileName={`camera-${Date.now()}.png`}
909
+ getNodeHistoryInfo={getNodeHistoryInfo}
910
+ navigateNodeHistory={navigateNodeHistory}
911
+ getCurrentNodeImage={getCurrentNodeImage}
912
+ />
913
+ </div>
 
 
914
  {node.error && (
915
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
916
  )}
 
919
  );
920
  }
921
 
922
+ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
923
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
924
  const hairstyles = ["None", "short", "long", "curly", "straight", "bald", "mohawk", "ponytail"];
925
  const expressions = ["None", "happy", "serious", "smiling", "laughing", "sad", "surprised", "angry"];
 
1046
  >
1047
  {node.isRunning ? "Processing..." : "Apply Face Changes"}
1048
  </Button>
1049
+ <div className="mt-2">
1050
+ <NodeOutputSection
1051
+ nodeId={node.id}
1052
+ output={node.output}
1053
+ downloadFileName={`face-${Date.now()}.png`}
1054
+ getNodeHistoryInfo={getNodeHistoryInfo}
1055
+ navigateNodeHistory={navigateNodeHistory}
1056
+ getCurrentNodeImage={getCurrentNodeImage}
1057
+ />
1058
+ </div>
 
 
1059
  {node.error && (
1060
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
1061
  )}
 
1064
  );
1065
  }
1066
 
1067
+ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
1068
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1069
 
1070
  const styleOptions = [
 
1163
  >
1164
  {node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
1165
  </Button>
1166
+ <NodeOutputSection
1167
+ nodeId={node.id}
1168
+ output={node.output}
1169
+ downloadFileName={`style-${Date.now()}.png`}
1170
+ getNodeHistoryInfo={getNodeHistoryInfo}
1171
+ navigateNodeHistory={navigateNodeHistory}
1172
+ getCurrentNodeImage={getCurrentNodeImage}
1173
+ />
 
 
 
 
1174
  {node.error && (
1175
  <div className="text-xs text-red-400 mt-2">{node.error}</div>
1176
  )}
 
1179
  );
1180
  }
1181
 
1182
+ export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
1183
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
1184
 
1185
  return (
 
1234
  >
1235
  {node.isRunning ? "Processing..." : "Apply Edit"}
1236
  </Button>
1237
+ <NodeOutputSection
1238
+ nodeId={node.id}
1239
+ output={node.output}
1240
+ downloadFileName={`edit-${Date.now()}.png`}
1241
+ getNodeHistoryInfo={getNodeHistoryInfo}
1242
+ navigateNodeHistory={navigateNodeHistory}
1243
+ getCurrentNodeImage={getCurrentNodeImage}
1244
+ />
 
 
 
 
1245
  </div>
1246
  </div>
1247
  );
app/page.tsx CHANGED
@@ -31,8 +31,8 @@ import {
31
  // UI components from shadcn/ui library
32
  import { Button } from "../components/ui/button";
33
  import { Input } from "../components/ui/input";
34
- // Hugging Face OAuth functionality - COMMENTED OUT FOR MANUAL REVIEW
35
- // import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
36
 
37
  /**
38
  * Utility function to combine CSS class names conditionally
@@ -698,29 +698,66 @@ function MergeNodeView({
698
  </div>
699
 
700
  <div className="mt-2">
701
- <div className="text-xs text-white/70 mb-1">Output</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  <div className="w-full min-h-[200px] max-h-[400px] rounded-xl bg-black/40 grid place-items-center">
703
- {node.output ? (
704
- <img src={node.output} className="w-full h-auto max-h-[400px] object-contain rounded-xl" alt="output" />
705
  ) : (
706
  <span className="text-white/40 text-xs py-16">Run merge to see result</span>
707
  )}
708
  </div>
709
- {node.output && (
710
- <Button
711
- className="w-full mt-2"
712
- variant="secondary"
713
- onClick={() => {
714
- const link = document.createElement('a');
715
- link.href = node.output as string;
716
- link.download = `merge-${Date.now()}.png`;
717
- document.body.appendChild(link);
718
- link.click();
719
- document.body.removeChild(link);
720
- }}
721
- >
722
- 📥 Download Merged Image
723
- </Button>
 
 
 
 
 
 
 
 
 
 
 
724
  )}
725
  {node.error && (
726
  <div className="mt-2">
@@ -767,10 +804,10 @@ export default function EditorPage() {
767
  scaleRef.current = scale;
768
  }, [scale]);
769
 
770
- // HF OAUTH CHECK - COMMENTED OUT FOR MANUAL REVIEW
771
- /*
772
  useEffect(() => {
773
  (async () => {
 
774
  try {
775
  // Handle OAuth redirect if present
776
  const oauth = await oauthHandleRedirectIfPresent();
@@ -797,10 +834,8 @@ export default function EditorPage() {
797
  }
798
  })();
799
  }, []);
800
- */
801
 
802
- // HF PRO LOGIN HANDLER - COMMENTED OUT FOR MANUAL REVIEW
803
- /*
804
  const handleHfProLogin = async () => {
805
  if (isHfProLoggedIn) {
806
  // Logout: clear the token
@@ -825,12 +860,6 @@ export default function EditorPage() {
825
  });
826
  }
827
  };
828
- */
829
-
830
- // Placeholder function for manual review
831
- const handleHfProLogin = () => {
832
- console.log('HF Pro login disabled - see HF_INTEGRATION_CHANGES.md for details');
833
- };
834
 
835
  // Connection dragging state
836
  const [draggingFrom, setDraggingFrom] = useState<string | null>(null);
@@ -840,11 +869,52 @@ export default function EditorPage() {
840
  const [apiToken, setApiToken] = useState("");
841
  const [showHelpSidebar, setShowHelpSidebar] = useState(false);
842
 
843
- // HF PRO AUTHENTICATION - COMMENTED OUT FOR MANUAL REVIEW
844
- // const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
845
- // const [isCheckingAuth, setIsCheckingAuth] = useState(true);
846
- const [isHfProLoggedIn] = useState(false); // Disabled for manual review
847
- const [isCheckingAuth] = useState(false); // Disabled for manual review
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
 
849
  const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
850
  const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
@@ -923,6 +993,84 @@ export default function EditorPage() {
923
  setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, ...updates } : n)));
924
  };
925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  // Handle single input connections for new nodes
927
  const handleEndSingleConnection = (nodeId: string) => {
928
  if (draggingFrom) {
@@ -1334,6 +1482,13 @@ export default function EditorPage() {
1334
  }
1335
  return n;
1336
  }));
 
 
 
 
 
 
 
1337
 
1338
  if (unprocessedNodeCount > 1) {
1339
  console.log(`✅ Successfully applied ${unprocessedNodeCount} transformations in ONE API call!`);
@@ -1600,6 +1755,19 @@ export default function EditorPage() {
1600
  }
1601
  const out = js.image || (js.images?.[0] as string) || null;
1602
  setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, output: out, isRunning: false } : n)));
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  } catch (e: any) {
1604
  console.error("Merge error:", e);
1605
  setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, isRunning: false, error: e?.message || "Error" } : n)));
@@ -1824,25 +1992,7 @@ export default function EditorPage() {
1824
  Help
1825
  </Button>
1826
 
1827
- {/* HF PRO BUTTON - COMMENTED OUT FOR MANUAL REVIEW */}
1828
- {/*
1829
- <Button
1830
- variant={isHfProLoggedIn ? "default" : "secondary"}
1831
- size="sm"
1832
- className="h-8 px-3"
1833
- type="button"
1834
- onClick={handleHfProLogin}
1835
- disabled={isCheckingAuth}
1836
- title={isHfProLoggedIn ? "Using fal.ai Gemini 2.5 Flash Image via HF" : "Click to login and use fal.ai Gemini 2.5 Flash"}
1837
- >
1838
- {isCheckingAuth ? "Checking..." : (isHfProLoggedIn ? "🤗 HF PRO ✓" : "Login HF PRO")}
1839
- </Button>
1840
- {isHfProLoggedIn && (
1841
- <div className="text-xs text-muted-foreground">
1842
- Using fal.ai Gemini 2.5 Flash
1843
- </div>
1844
- )}
1845
- */}
1846
  </div>
1847
  </header>
1848
 
@@ -2052,6 +2202,9 @@ export default function EditorPage() {
2052
  onEndConnection={handleEndSingleConnection}
2053
  onProcess={processNode}
2054
  onUpdatePosition={updateNodePosition}
 
 
 
2055
  />
2056
  );
2057
  case "CLOTHES":
@@ -2065,6 +2218,9 @@ export default function EditorPage() {
2065
  onEndConnection={handleEndSingleConnection}
2066
  onProcess={processNode}
2067
  onUpdatePosition={updateNodePosition}
 
 
 
2068
  />
2069
  );
2070
  case "STYLE":
@@ -2078,6 +2234,9 @@ export default function EditorPage() {
2078
  onEndConnection={handleEndSingleConnection}
2079
  onProcess={processNode}
2080
  onUpdatePosition={updateNodePosition}
 
 
 
2081
  />
2082
  );
2083
  case "EDIT":
@@ -2091,6 +2250,9 @@ export default function EditorPage() {
2091
  onEndConnection={handleEndSingleConnection}
2092
  onProcess={processNode}
2093
  onUpdatePosition={updateNodePosition}
 
 
 
2094
  />
2095
  );
2096
  case "CAMERA":
@@ -2104,6 +2266,9 @@ export default function EditorPage() {
2104
  onEndConnection={handleEndSingleConnection}
2105
  onProcess={processNode}
2106
  onUpdatePosition={updateNodePosition}
 
 
 
2107
  />
2108
  );
2109
  case "AGE":
@@ -2117,6 +2282,9 @@ export default function EditorPage() {
2117
  onEndConnection={handleEndSingleConnection}
2118
  onProcess={processNode}
2119
  onUpdatePosition={updateNodePosition}
 
 
 
2120
  />
2121
  );
2122
  case "FACE":
@@ -2130,6 +2298,9 @@ export default function EditorPage() {
2130
  onEndConnection={handleEndSingleConnection}
2131
  onProcess={processNode}
2132
  onUpdatePosition={updateNodePosition}
 
 
 
2133
  />
2134
  );
2135
  default:
 
31
  // UI components from shadcn/ui library
32
  import { Button } from "../components/ui/button";
33
  import { Input } from "../components/ui/input";
34
+ // Hugging Face OAuth functionality
35
+ import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
36
 
37
  /**
38
  * Utility function to combine CSS class names conditionally
 
698
  </div>
699
 
700
  <div className="mt-2">
701
+ <div className="flex items-center justify-between mb-1">
702
+ <div className="text-xs text-white/70">Output</div>
703
+ {(() => {
704
+ const historyInfo = getNodeHistoryInfo(node.id);
705
+ return historyInfo.hasHistory ? (
706
+ <div className="flex items-center gap-1">
707
+ <button
708
+ className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
709
+ onClick={() => navigateNodeHistory(node.id, 'prev')}
710
+ disabled={!historyInfo.canGoBack}
711
+ >
712
+
713
+ </button>
714
+ <span className="text-xs text-white/60 px-1">
715
+ {historyInfo.current}/{historyInfo.total}
716
+ </span>
717
+ <button
718
+ className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
719
+ onClick={() => navigateNodeHistory(node.id, 'next')}
720
+ disabled={!historyInfo.canGoForward}
721
+ >
722
+
723
+ </button>
724
+ </div>
725
+ ) : null;
726
+ })()}
727
+ </div>
728
  <div className="w-full min-h-[200px] max-h-[400px] rounded-xl bg-black/40 grid place-items-center">
729
+ {getCurrentNodeImage(node.id, node.output) ? (
730
+ <img src={getCurrentNodeImage(node.id, node.output)} className="w-full h-auto max-h-[400px] object-contain rounded-xl" alt="output" />
731
  ) : (
732
  <span className="text-white/40 text-xs py-16">Run merge to see result</span>
733
  )}
734
  </div>
735
+ {getCurrentNodeImage(node.id, node.output) && (
736
+ <div className="mt-2 space-y-2">
737
+ {(() => {
738
+ const historyInfo = getNodeHistoryInfo(node.id);
739
+ return historyInfo.currentDescription ? (
740
+ <div className="text-xs text-white/60 bg-black/20 rounded px-2 py-1">
741
+ {historyInfo.currentDescription}
742
+ </div>
743
+ ) : null;
744
+ })()}
745
+ <Button
746
+ className="w-full"
747
+ variant="secondary"
748
+ onClick={() => {
749
+ const link = document.createElement('a');
750
+ const currentImage = getCurrentNodeImage(node.id, node.output);
751
+ link.href = currentImage as string;
752
+ link.download = `merge-${Date.now()}.png`;
753
+ document.body.appendChild(link);
754
+ link.click();
755
+ document.body.removeChild(link);
756
+ }}
757
+ >
758
+ 📥 Download Merged Image
759
+ </Button>
760
+ </div>
761
  )}
762
  {node.error && (
763
  <div className="mt-2">
 
804
  scaleRef.current = scale;
805
  }, [scale]);
806
 
807
+ // HF OAUTH CHECK
 
808
  useEffect(() => {
809
  (async () => {
810
+ setIsCheckingAuth(true);
811
  try {
812
  // Handle OAuth redirect if present
813
  const oauth = await oauthHandleRedirectIfPresent();
 
834
  }
835
  })();
836
  }, []);
 
837
 
838
+ // HF PRO LOGIN HANDLER
 
839
  const handleHfProLogin = async () => {
840
  if (isHfProLoggedIn) {
841
  // Logout: clear the token
 
860
  });
861
  }
862
  };
 
 
 
 
 
 
863
 
864
  // Connection dragging state
865
  const [draggingFrom, setDraggingFrom] = useState<string | null>(null);
 
869
  const [apiToken, setApiToken] = useState("");
870
  const [showHelpSidebar, setShowHelpSidebar] = useState(false);
871
 
872
+ // HF PRO AUTHENTICATION
873
+ const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
874
+ const [isCheckingAuth, setIsCheckingAuth] = useState(true);
875
+
876
+ // NODE HISTORY (per-node image history)
877
+ const [nodeHistories, setNodeHistories] = useState<Record<string, Array<{
878
+ id: string;
879
+ image: string;
880
+ timestamp: number;
881
+ description: string;
882
+ }>>>({});
883
+
884
+ const [nodeHistoryIndex, setNodeHistoryIndex] = useState<Record<string, number>>({});
885
+
886
+ // Load node histories from localStorage on startup
887
+ useEffect(() => {
888
+ try {
889
+ const savedHistories = localStorage.getItem('nano-banana-node-histories');
890
+ const savedIndices = localStorage.getItem('nano-banana-node-history-indices');
891
+ if (savedHistories) {
892
+ setNodeHistories(JSON.parse(savedHistories));
893
+ }
894
+ if (savedIndices) {
895
+ setNodeHistoryIndex(JSON.parse(savedIndices));
896
+ }
897
+ } catch (error) {
898
+ console.error('Failed to load node histories from localStorage:', error);
899
+ }
900
+ }, []);
901
+
902
+ // Save node histories to localStorage whenever they change
903
+ useEffect(() => {
904
+ try {
905
+ localStorage.setItem('nano-banana-node-histories', JSON.stringify(nodeHistories));
906
+ } catch (error) {
907
+ console.error('Failed to save node histories to localStorage:', error);
908
+ }
909
+ }, [nodeHistories]);
910
+
911
+ useEffect(() => {
912
+ try {
913
+ localStorage.setItem('nano-banana-node-history-indices', JSON.stringify(nodeHistoryIndex));
914
+ } catch (error) {
915
+ console.error('Failed to save node history indices to localStorage:', error);
916
+ }
917
+ }, [nodeHistoryIndex]);
918
 
919
  const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
920
  const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
 
993
  setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, ...updates } : n)));
994
  };
995
 
996
+ // Add image to node's history
997
+ const addToNodeHistory = (nodeId: string, image: string, description: string) => {
998
+ const historyEntry = {
999
+ id: uid(),
1000
+ image,
1001
+ timestamp: Date.now(),
1002
+ description
1003
+ };
1004
+
1005
+ setNodeHistories(prev => {
1006
+ const nodeHistory = prev[nodeId] || [];
1007
+ const newHistory = [historyEntry, ...nodeHistory].slice(0, 10); // Keep last 10 per node
1008
+ return {
1009
+ ...prev,
1010
+ [nodeId]: newHistory
1011
+ };
1012
+ });
1013
+
1014
+ // Set this as the current (latest) image for the node
1015
+ setNodeHistoryIndex(prev => ({
1016
+ ...prev,
1017
+ [nodeId]: 0
1018
+ }));
1019
+ };
1020
+
1021
+ // Navigate node history
1022
+ const navigateNodeHistory = (nodeId: string, direction: 'prev' | 'next') => {
1023
+ const history = nodeHistories[nodeId];
1024
+ if (!history || history.length <= 1) return;
1025
+
1026
+ const currentIndex = nodeHistoryIndex[nodeId] || 0;
1027
+ let newIndex = currentIndex;
1028
+
1029
+ if (direction === 'prev' && currentIndex < history.length - 1) {
1030
+ newIndex = currentIndex + 1;
1031
+ } else if (direction === 'next' && currentIndex > 0) {
1032
+ newIndex = currentIndex - 1;
1033
+ }
1034
+
1035
+ if (newIndex !== currentIndex) {
1036
+ setNodeHistoryIndex(prev => ({
1037
+ ...prev,
1038
+ [nodeId]: newIndex
1039
+ }));
1040
+
1041
+ // Update the node's output to show the historical image
1042
+ const historicalImage = history[newIndex].image;
1043
+ updateNode(nodeId, { output: historicalImage });
1044
+ }
1045
+ };
1046
+
1047
+ // Get current image for a node (either latest or from history navigation)
1048
+ const getCurrentNodeImage = (nodeId: string, defaultOutput?: string) => {
1049
+ const history = nodeHistories[nodeId];
1050
+ const index = nodeHistoryIndex[nodeId] || 0;
1051
+
1052
+ if (history && history[index]) {
1053
+ return history[index].image;
1054
+ }
1055
+
1056
+ return defaultOutput;
1057
+ };
1058
+
1059
+ // Get history info for a node
1060
+ const getNodeHistoryInfo = (nodeId: string) => {
1061
+ const history = nodeHistories[nodeId] || [];
1062
+ const index = nodeHistoryIndex[nodeId] || 0;
1063
+
1064
+ return {
1065
+ hasHistory: history.length > 1,
1066
+ current: index + 1,
1067
+ total: history.length,
1068
+ canGoBack: index < history.length - 1,
1069
+ canGoForward: index > 0,
1070
+ currentDescription: history[index]?.description || ''
1071
+ };
1072
+ };
1073
+
1074
  // Handle single input connections for new nodes
1075
  const handleEndSingleConnection = (nodeId: string) => {
1076
  if (draggingFrom) {
 
1482
  }
1483
  return n;
1484
  }));
1485
+
1486
+ // Add to node's history
1487
+ const description = unprocessedNodeCount > 1
1488
+ ? `Combined ${unprocessedNodeCount} transformations`
1489
+ : `${node.type} transformation`;
1490
+
1491
+ addToNodeHistory(nodeId, data.image, description);
1492
 
1493
  if (unprocessedNodeCount > 1) {
1494
  console.log(`✅ Successfully applied ${unprocessedNodeCount} transformations in ONE API call!`);
 
1755
  }
1756
  const out = js.image || (js.images?.[0] as string) || null;
1757
  setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, output: out, isRunning: false } : n)));
1758
+
1759
+ // Add merge result to node's history
1760
+ if (out) {
1761
+ const inputLabels = merge.inputs.map((id, index) => {
1762
+ const inputNode = nodes.find(n => n.id === id);
1763
+ if (inputNode?.type === "CHARACTER") {
1764
+ return (inputNode as CharacterNode).label || `Character ${index + 1}`;
1765
+ }
1766
+ return `${inputNode?.type || 'Node'} ${index + 1}`;
1767
+ });
1768
+
1769
+ addToNodeHistory(mergeId, out, `Merged: ${inputLabels.join(" + ")}`);
1770
+ }
1771
  } catch (e: any) {
1772
  console.error("Merge error:", e);
1773
  setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, isRunning: false, error: e?.message || "Error" } : n)));
 
1992
  Help
1993
  </Button>
1994
 
1995
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1996
  </div>
1997
  </header>
1998
 
 
2202
  onEndConnection={handleEndSingleConnection}
2203
  onProcess={processNode}
2204
  onUpdatePosition={updateNodePosition}
2205
+ getNodeHistoryInfo={getNodeHistoryInfo}
2206
+ navigateNodeHistory={navigateNodeHistory}
2207
+ getCurrentNodeImage={getCurrentNodeImage}
2208
  />
2209
  );
2210
  case "CLOTHES":
 
2218
  onEndConnection={handleEndSingleConnection}
2219
  onProcess={processNode}
2220
  onUpdatePosition={updateNodePosition}
2221
+ getNodeHistoryInfo={getNodeHistoryInfo}
2222
+ navigateNodeHistory={navigateNodeHistory}
2223
+ getCurrentNodeImage={getCurrentNodeImage}
2224
  />
2225
  );
2226
  case "STYLE":
 
2234
  onEndConnection={handleEndSingleConnection}
2235
  onProcess={processNode}
2236
  onUpdatePosition={updateNodePosition}
2237
+ getNodeHistoryInfo={getNodeHistoryInfo}
2238
+ navigateNodeHistory={navigateNodeHistory}
2239
+ getCurrentNodeImage={getCurrentNodeImage}
2240
  />
2241
  );
2242
  case "EDIT":
 
2250
  onEndConnection={handleEndSingleConnection}
2251
  onProcess={processNode}
2252
  onUpdatePosition={updateNodePosition}
2253
+ getNodeHistoryInfo={getNodeHistoryInfo}
2254
+ navigateNodeHistory={navigateNodeHistory}
2255
+ getCurrentNodeImage={getCurrentNodeImage}
2256
  />
2257
  );
2258
  case "CAMERA":
 
2266
  onEndConnection={handleEndSingleConnection}
2267
  onProcess={processNode}
2268
  onUpdatePosition={updateNodePosition}
2269
+ getNodeHistoryInfo={getNodeHistoryInfo}
2270
+ navigateNodeHistory={navigateNodeHistory}
2271
+ getCurrentNodeImage={getCurrentNodeImage}
2272
  />
2273
  );
2274
  case "AGE":
 
2282
  onEndConnection={handleEndSingleConnection}
2283
  onProcess={processNode}
2284
  onUpdatePosition={updateNodePosition}
2285
+ getNodeHistoryInfo={getNodeHistoryInfo}
2286
+ navigateNodeHistory={navigateNodeHistory}
2287
+ getCurrentNodeImage={getCurrentNodeImage}
2288
  />
2289
  );
2290
  case "FACE":
 
2298
  onEndConnection={handleEndSingleConnection}
2299
  onProcess={processNode}
2300
  onUpdatePosition={updateNodePosition}
2301
+ getNodeHistoryInfo={getNodeHistoryInfo}
2302
+ navigateNodeHistory={navigateNodeHistory}
2303
+ getCurrentNodeImage={getCurrentNodeImage}
2304
  />
2305
  );
2306
  default:
next.config.ts CHANGED
@@ -8,11 +8,6 @@ const nextConfig: NextConfig = {
8
  serverRuntimeConfig: {
9
  bodySizeLimit: '50mb',
10
  },
11
- api: {
12
- bodyParser: {
13
- sizeLimit: '50mb',
14
- },
15
- },
16
  };
17
 
18
  export default nextConfig;
 
8
  serverRuntimeConfig: {
9
  bodySizeLimit: '50mb',
10
  },
 
 
 
 
 
11
  };
12
 
13
  export default nextConfig;
package-lock.json CHANGED
@@ -11,7 +11,7 @@
11
  "@fal-ai/serverless-client": "^0.15.0",
12
  "@google/genai": "^1.17.0",
13
  "@huggingface/hub": "^2.6.3",
14
- "@huggingface/inference": "^4.7.1",
15
  "class-variance-authority": "^0.7.0",
16
  "clsx": "^2.1.1",
17
  "lucide-react": "^0.542.0",
@@ -273,13 +273,13 @@
273
  }
274
  },
275
  "node_modules/@huggingface/inference": {
276
- "version": "4.7.1",
277
- "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.7.1.tgz",
278
- "integrity": "sha512-gXrMocGDsE6kUZPEj82c3O+/OKnIfbHvg9rYjGA6svbWrYVmHCIAdCrrgCwNl2v5GELfPJrrfIv0bvzCTfa64A==",
279
  "license": "MIT",
280
  "dependencies": {
281
  "@huggingface/jinja": "^0.5.1",
282
- "@huggingface/tasks": "^0.19.35"
283
  },
284
  "engines": {
285
  "node": ">=18"
 
11
  "@fal-ai/serverless-client": "^0.15.0",
12
  "@google/genai": "^1.17.0",
13
  "@huggingface/hub": "^2.6.3",
14
+ "@huggingface/inference": "^4.8.0",
15
  "class-variance-authority": "^0.7.0",
16
  "clsx": "^2.1.1",
17
  "lucide-react": "^0.542.0",
 
273
  }
274
  },
275
  "node_modules/@huggingface/inference": {
276
+ "version": "4.8.0",
277
+ "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.8.0.tgz",
278
+ "integrity": "sha512-Eq98EAXqYn4rKMfrbEXuhc3IjKfaeIO6eXNOZk9xk6v5akrIWRtd6d1h0fjAWyX4zRbdUpXRh6MvsqXnzGvXCA==",
279
  "license": "MIT",
280
  "dependencies": {
281
  "@huggingface/jinja": "^0.5.1",
282
+ "@huggingface/tasks": "^0.19.45"
283
  },
284
  "engines": {
285
  "node": ">=18"
package.json CHANGED
@@ -12,7 +12,7 @@
12
  "@fal-ai/serverless-client": "^0.15.0",
13
  "@google/genai": "^1.17.0",
14
  "@huggingface/hub": "^2.6.3",
15
- "@huggingface/inference": "^4.7.1",
16
  "class-variance-authority": "^0.7.0",
17
  "clsx": "^2.1.1",
18
  "lucide-react": "^0.542.0",
 
12
  "@fal-ai/serverless-client": "^0.15.0",
13
  "@google/genai": "^1.17.0",
14
  "@huggingface/hub": "^2.6.3",
15
+ "@huggingface/inference": "^4.8.0",
16
  "class-variance-authority": "^0.7.0",
17
  "clsx": "^2.1.1",
18
  "lucide-react": "^0.542.0",