Reubencf commited on
Commit
5dc1071
·
1 Parent(s): 276f2ce

Refactor ClothesNode to use text prompts and add NodeTimer

Browse files
Files changed (3) hide show
  1. app/api/hf-process/route.ts +5 -0
  2. app/nodes.tsx +52 -103
  3. app/page.tsx +56 -19
app/api/hf-process/route.ts CHANGED
@@ -264,6 +264,11 @@ export async function POST(req: NextRequest) {
264
  prompts.push(params.editPrompt);
265
  }
266
 
 
 
 
 
 
267
  // Age transformation
268
  if (params.targetAge) {
269
  prompts.push(`Transform the person to look ${params.targetAge} years old.`);
 
264
  prompts.push(params.editPrompt);
265
  }
266
 
267
+ // Clothing modifications
268
+ if (params.clothesPrompt) {
269
+ prompts.push(`Change clothing to: ${params.clothesPrompt}`);
270
+ }
271
+
272
  // Age transformation
273
  if (params.targetAge) {
274
  prompts.push(`Transform the person to look ${params.targetAge} years old.`);
app/nodes.tsx CHANGED
@@ -61,6 +61,39 @@ import { Textarea } from "../components/ui/textarea"; // Multi-line text input
61
  import { Slider } from "../components/ui/slider"; // Range slider input component
62
  import { ColorPicker } from "../components/ui/color-picker"; // Color selection component
63
  import { Checkbox } from "../components/ui/checkbox"; // Checkbox input component
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  /**
66
  * Helper function to download processed images
@@ -496,6 +529,7 @@ export function BackgroundNodeView({
496
  onDragOver={(e) => e.preventDefault()}
497
  onPaste={handleImagePaste}
498
  >
 
499
  <div
500
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
501
  onPointerDown={onPointerDown}
@@ -835,58 +869,12 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
835
  // Handle node dragging functionality
836
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
837
 
838
- /**
839
- * Preset clothing options available for quick selection
840
- * Each preset includes a display name and path to the reference image
841
- */
842
- const presetClothes = [
843
- { name: "Sukajan", path: "/clothes/sukajan.png" }, // Japanese-style embroidered jacket
844
- { name: "Blazer", path: "/clothes/blazzer.png" }, // Business blazer/jacket
845
- { name: "Suit", path: "/clothes/suit.png" }, // Formal business suit
846
- { name: "Women's Outfit", path: "/clothes/womenoutfit.png" }, // Women's clothing ensemble
847
- ];
848
-
849
- const onDrop = async (e: React.DragEvent) => {
850
- e.preventDefault();
851
- const files = e.dataTransfer.files;
852
- if (files && files.length) {
853
- const reader = new FileReader();
854
- reader.onload = () => onUpdate(node.id, { clothesImage: reader.result, selectedPreset: null });
855
- reader.readAsDataURL(files[0]);
856
- }
857
- };
858
-
859
- const onPaste = async (e: React.ClipboardEvent) => {
860
- const items = e.clipboardData.items;
861
- for (let i = 0; i < items.length; i++) {
862
- if (items[i].type.startsWith("image/")) {
863
- const file = items[i].getAsFile();
864
- if (file) {
865
- const reader = new FileReader();
866
- reader.onload = () => onUpdate(node.id, { clothesImage: reader.result, selectedPreset: null });
867
- reader.readAsDataURL(file);
868
- return;
869
- }
870
- }
871
- }
872
- const text = e.clipboardData.getData("text");
873
- if (text && (text.startsWith("http") || text.startsWith("data:image"))) {
874
- onUpdate(node.id, { clothesImage: text, selectedPreset: null });
875
- }
876
- };
877
-
878
- const selectPreset = (presetPath: string, presetName: string) => {
879
- onUpdate(node.id, { clothesImage: presetPath, selectedPreset: presetName });
880
- };
881
-
882
  return (
883
  <div
884
  className="nb-node absolute w-[320px]"
885
  style={{ left: localPos.x, top: localPos.y }}
886
- onDrop={onDrop}
887
- onDragOver={(e) => e.preventDefault()}
888
- onPaste={onPaste}
889
  >
 
890
  <div
891
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
892
  onPointerDown={onPointerDown}
@@ -930,67 +918,21 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
930
  </Button>
931
  </div>
932
  )}
933
- <div className="text-xs text-muted-foreground">Clothes Reference</div>
934
-
935
- {/* Preset clothes options */}
936
- <div className="flex gap-2">
937
- {presetClothes.map((preset) => (
938
- <button
939
- key={preset.name}
940
- className={`flex-1 p-2 rounded border ${node.selectedPreset === preset.name
941
- ? "border-primary bg-primary/20"
942
- : "border-border hover:border-primary/50"
943
- }`}
944
- onClick={() => selectPreset(preset.path, preset.name)}
945
- >
946
- <img src={preset.path} alt={preset.name} className="w-full h-28 object-contain rounded mb-1" />
947
- <div className="text-xs">{preset.name}</div>
948
- </button>
949
- ))}
950
- </div>
951
-
952
- <div className="text-xs text-muted-foreground/50 text-center">— or —</div>
953
 
954
- {/* Custom image upload */}
955
- {node.clothesImage && !node.selectedPreset ? (
956
- <div className="relative">
957
- <img src={node.clothesImage} className="w-full rounded" alt="Clothes" />
958
- <Button
959
- variant="destructive"
960
- size="sm"
961
- className="absolute top-2 right-2"
962
- onClick={() => onUpdate(node.id, { clothesImage: null, selectedPreset: null })}
963
- >
964
- Remove
965
- </Button>
966
- </div>
967
- ) : !node.selectedPreset ? (
968
- <label className="block">
969
- <input
970
- type="file"
971
- accept="image/*"
972
- className="hidden"
973
- onChange={(e) => {
974
- if (e.target.files?.length) {
975
- const reader = new FileReader();
976
- reader.onload = () => onUpdate(node.id, { clothesImage: reader.result, selectedPreset: null });
977
- reader.readAsDataURL(e.target.files[0]);
978
- }
979
- }}
980
- />
981
- <div className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary/50 transition-colors">
982
- <div className="text-muted-foreground/40 text-lg mb-2">📁</div>
983
- <p className="text-sm text-muted-foreground font-medium">Drop, upload, or paste clothes image</p>
984
- <p className="text-xs text-muted-foreground/50 mt-1">JPG, PNG, WebP supported</p>
985
- </div>
986
- </label>
987
- ) : null}
988
 
989
  <Button
990
  className="w-full"
991
  onClick={() => onProcess(node.id)}
992
- disabled={node.isRunning || !node.clothesImage}
993
- title={!node.input ? "Connect an input first" : "Process all unprocessed nodes in chain"}
994
  >
995
  {node.isRunning ? "Processing..." : "Apply Clothes"}
996
  </Button>
@@ -1043,6 +985,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
1043
 
1044
  return (
1045
  <div className="nb-node absolute w-[280px]" style={{ left: localPos.x, top: localPos.y }}>
 
1046
  <div
1047
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1048
  onPointerDown={onPointerDown}
@@ -1188,6 +1131,7 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
1188
 
1189
  return (
1190
  <div className="nb-node absolute w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
 
1191
  <div
1192
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1193
  onPointerDown={onPointerDown}
@@ -1428,6 +1372,7 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
1428
 
1429
  return (
1430
  <div className="nb-node absolute w-[340px]" style={{ left: localPos.x, top: localPos.y }}>
 
1431
  <div
1432
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1433
  onPointerDown={onPointerDown}
@@ -1694,6 +1639,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1694
  className="nb-node absolute w-[320px]"
1695
  style={{ left: localPos.x, top: localPos.y }}
1696
  >
 
1697
  <div
1698
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1699
  onPointerDown={onPointerDown}
@@ -1866,6 +1812,7 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
1866
 
1867
  return (
1868
  <div className="nb-node absolute text-white w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
 
1869
  <div
1870
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1871
  onPointerDown={onPointerDown}
@@ -2034,6 +1981,7 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
2034
 
2035
  return (
2036
  <div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
 
2037
  <div
2038
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
2039
  onPointerDown={onPointerDown}
@@ -2224,6 +2172,7 @@ export function EditNodeView({
2224
 
2225
  return (
2226
  <div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
 
2227
  {/* Node Header - Contains title, delete button, and connection ports */}
2228
  <div
2229
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
 
61
  import { Slider } from "../components/ui/slider"; // Range slider input component
62
  import { ColorPicker } from "../components/ui/color-picker"; // Color selection component
63
  import { Checkbox } from "../components/ui/checkbox"; // Checkbox input component
64
+ import { Loader2 } from "lucide-react"; // Loading spinner icon
65
+
66
+ /**
67
+ * Timer component that shows execution time
68
+ * Uses a green checkmark when finished or a spinner when running
69
+ */
70
+ function NodeTimer({ startTime, executionTime, isRunning }: { startTime?: number, executionTime?: number, isRunning?: boolean }) {
71
+ const [elapsed, setElapsed] = useState(0);
72
+
73
+ useEffect(() => {
74
+ if (!isRunning || !startTime) return;
75
+ const interval = setInterval(() => {
76
+ setElapsed(Date.now() - startTime);
77
+ }, 100);
78
+ return () => clearInterval(interval);
79
+ }, [isRunning, startTime]);
80
+
81
+ if (!startTime && !executionTime) return null;
82
+
83
+ const timeToShow = isRunning ? elapsed : (executionTime || 0);
84
+ const seconds = (timeToShow / 1000).toFixed(1);
85
+
86
+ return (
87
+ <div className="absolute top-2 right-2 flex items-center gap-1.5 bg-background/80 text-foreground text-[10px] px-2 py-1 rounded-md border shadow-sm z-50 backdrop-blur-sm">
88
+ {isRunning ? (
89
+ <Loader2 className="w-3 h-3 animate-spin text-banana-500" />
90
+ ) : (
91
+ <span className="text-green-500 font-bold">✓</span>
92
+ )}
93
+ <span className="font-mono">{seconds}s</span>
94
+ </div>
95
+ );
96
+ }
97
 
98
  /**
99
  * Helper function to download processed images
 
529
  onDragOver={(e) => e.preventDefault()}
530
  onPaste={handleImagePaste}
531
  >
532
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
533
  <div
534
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
535
  onPointerDown={onPointerDown}
 
869
  // Handle node dragging functionality
870
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  return (
873
  <div
874
  className="nb-node absolute w-[320px]"
875
  style={{ left: localPos.x, top: localPos.y }}
 
 
 
876
  >
877
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
878
  <div
879
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
880
  onPointerDown={onPointerDown}
 
918
  </Button>
919
  </div>
920
  )}
921
+ <div className="text-xs text-muted-foreground">Clothing Description</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
922
 
923
+ <Textarea
924
+ className="w-full"
925
+ placeholder="Describe the clothing (e.g. 'black leather jacket', 'floral summer dress', 'navy blue business suit')"
926
+ value={node.clothesPrompt || ""}
927
+ onChange={(e) => onUpdate(node.id, { clothesPrompt: e.target.value })}
928
+ rows={3}
929
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
 
931
  <Button
932
  className="w-full"
933
  onClick={() => onProcess(node.id)}
934
+ disabled={node.isRunning || !node.clothesPrompt}
935
+ title={!node.input ? "Connect an input first" : !node.clothesPrompt ? "Enter a clothing description" : "Apply Clothing"}
936
  >
937
  {node.isRunning ? "Processing..." : "Apply Clothes"}
938
  </Button>
 
985
 
986
  return (
987
  <div className="nb-node absolute w-[280px]" style={{ left: localPos.x, top: localPos.y }}>
988
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
989
  <div
990
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
991
  onPointerDown={onPointerDown}
 
1131
 
1132
  return (
1133
  <div className="nb-node absolute w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
1134
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
1135
  <div
1136
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1137
  onPointerDown={onPointerDown}
 
1372
 
1373
  return (
1374
  <div className="nb-node absolute w-[340px]" style={{ left: localPos.x, top: localPos.y }}>
1375
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
1376
  <div
1377
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1378
  onPointerDown={onPointerDown}
 
1639
  className="nb-node absolute w-[320px]"
1640
  style={{ left: localPos.x, top: localPos.y }}
1641
  >
1642
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
1643
  <div
1644
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1645
  onPointerDown={onPointerDown}
 
1812
 
1813
  return (
1814
  <div className="nb-node absolute text-white w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
1815
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
1816
  <div
1817
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1818
  onPointerDown={onPointerDown}
 
1981
 
1982
  return (
1983
  <div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
1984
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
1985
  <div
1986
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
1987
  onPointerDown={onPointerDown}
 
2172
 
2173
  return (
2174
  <div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
2175
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
2176
  {/* Node Header - Contains title, delete button, and connection ports */}
2177
  <div
2178
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
app/page.tsx CHANGED
@@ -153,6 +153,14 @@ async function copyImageToClipboard(dataUrl: string) {
153
  */
154
  type NodeType = "CHARACTER" | "MERGE" | "BACKGROUND" | "CLOTHES" | "STYLE" | "EDIT" | "CAMERA" | "AGE" | "FACE" | "BLEND" | "LIGHTNING" | "POSES";
155
 
 
 
 
 
 
 
 
 
156
  /**
157
  * Base properties that all nodes share
158
  * Every node has an ID, type, and position in the editor world space
@@ -162,6 +170,8 @@ type NodeBase = {
162
  type: NodeType; // What kind of operation this node performs
163
  x: number; // X position in world coordinates (not screen pixels)
164
  y: number; // Y position in world coordinates (not screen pixels)
 
 
165
  };
166
 
167
  /**
@@ -514,6 +524,38 @@ function Port({
514
  );
515
  }
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  function CharacterNodeView({
518
  node,
519
  scaleRef,
@@ -731,6 +773,7 @@ function MergeNodeView({
731
  onPointerMove={onPointerMove}
732
  onPointerUp={onPointerUp}
733
  >
 
734
  <Port
735
  className="in"
736
  nodeId={node.id}
@@ -1070,15 +1113,9 @@ export default function EditorPage() {
1070
  type: "image-to-image",
1071
  description: "Powerful image editing and manipulation",
1072
  },
1073
- "FLUX.1-dev": {
1074
- id: "black-forest-labs/FLUX.1-dev",
1075
- name: "FLUX.1 Dev",
1076
- type: "text-to-image",
1077
- description: "High-quality text-to-image generation",
1078
- },
1079
  };
1080
 
1081
- const [selectedHfModel, setSelectedHfModel] = useState<keyof typeof HF_MODELS>("FLUX.1-Kontext-dev");
1082
 
1083
 
1084
  // HF PRO AUTHENTICATION
@@ -1259,9 +1296,8 @@ export default function EditorPage() {
1259
  }
1260
  break;
1261
  case "CLOTHES":
1262
- if ((node as ClothesNode).clothesImage) {
1263
- config.clothesImage = (node as ClothesNode).clothesImage;
1264
- config.selectedPreset = (node as ClothesNode).selectedPreset;
1265
  }
1266
  break;
1267
  case "STYLE":
@@ -1511,9 +1547,10 @@ export default function EditorPage() {
1511
  }
1512
 
1513
  // Set loading state for all nodes being processed
 
1514
  setNodes(prev => prev.map(n => {
1515
  if (n.id === nodeId || processedNodes.includes(n.id)) {
1516
- return { ...n, isRunning: true, error: null };
1517
  }
1518
  return n;
1519
  }));
@@ -1525,12 +1562,9 @@ export default function EditorPage() {
1525
  }
1526
 
1527
  // Check if params contains custom images and validate them
1528
- if (params.clothesImage) {
1529
- // Validate it's a proper data URL
1530
- if (!params.clothesImage.startsWith('data:') && !params.clothesImage.startsWith('http') && !params.clothesImage.startsWith('/')) {
1531
- throw new Error("Invalid clothes image format. Please upload a valid image.");
1532
- }
1533
- }
1534
 
1535
  if (params.customBackgroundImage) {
1536
  // Validate it's a proper data URL
@@ -1617,14 +1651,17 @@ export default function EditorPage() {
1617
 
1618
  // Only update the current node with the output
1619
  // Don't show output in intermediate nodes - they were just used for configuration
 
 
 
1620
  setNodes(prev => prev.map(n => {
1621
  if (n.id === nodeId) {
1622
  // Only the current node gets the final output displayed
1623
- return { ...n, output: data.image, isRunning: false, error: null };
1624
  } else if (processedNodes.includes(n.id)) {
1625
  // Mark intermediate nodes as no longer running but don't give them output
1626
  // This way they remain unprocessed visually but their configs were used
1627
- return { ...n, isRunning: false, error: null };
1628
  }
1629
  return n;
1630
  }));
 
153
  */
154
  type NodeType = "CHARACTER" | "MERGE" | "BACKGROUND" | "CLOTHES" | "STYLE" | "EDIT" | "CAMERA" | "AGE" | "FACE" | "BLEND" | "LIGHTNING" | "POSES";
155
 
156
+ /**
157
+ * Base properties that all nodes share
158
+ * Every node has an ID, type, and position in the editor world space
159
+ */
160
+ import { Loader2, Clock } from "lucide-react";
161
+
162
+ // ... existing imports ...
163
+
164
  /**
165
  * Base properties that all nodes share
166
  * Every node has an ID, type, and position in the editor world space
 
170
  type: NodeType; // What kind of operation this node performs
171
  x: number; // X position in world coordinates (not screen pixels)
172
  y: number; // Y position in world coordinates (not screen pixels)
173
+ startTime?: number; // Timestamp when processing started
174
+ executionTime?: number; // Total processing time in milliseconds
175
  };
176
 
177
  /**
 
524
  );
525
  }
526
 
527
+ /**
528
+ * Timer component that shows execution time
529
+ * Uses a green checkmark when finished or a spinner when running
530
+ */
531
+ export function NodeTimer({ startTime, executionTime, isRunning }: { startTime?: number, executionTime?: number, isRunning?: boolean }) {
532
+ const [elapsed, setElapsed] = React.useState(0);
533
+
534
+ React.useEffect(() => {
535
+ if (!isRunning || !startTime) return;
536
+ const interval = setInterval(() => {
537
+ setElapsed(Date.now() - startTime);
538
+ }, 100);
539
+ return () => clearInterval(interval);
540
+ }, [isRunning, startTime]);
541
+
542
+ if (!startTime && !executionTime) return null;
543
+
544
+ const timeToShow = isRunning ? elapsed : (executionTime || 0);
545
+ const seconds = (timeToShow / 1000).toFixed(1);
546
+
547
+ return (
548
+ <div className="absolute top-2 right-2 flex items-center gap-1.5 bg-background/80 text-foreground text-[10px] px-2 py-1 rounded-md border shadow-sm z-50 backdrop-blur-sm">
549
+ {isRunning ? (
550
+ <Loader2 className="w-3 h-3 animate-spin text-banana-500" />
551
+ ) : (
552
+ <span className="text-green-500 font-bold">✓</span>
553
+ )}
554
+ <span className="font-mono">{seconds}s</span>
555
+ </div>
556
+ );
557
+ }
558
+
559
  function CharacterNodeView({
560
  node,
561
  scaleRef,
 
773
  onPointerMove={onPointerMove}
774
  onPointerUp={onPointerUp}
775
  >
776
+ <NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
777
  <Port
778
  className="in"
779
  nodeId={node.id}
 
1113
  type: "image-to-image",
1114
  description: "Powerful image editing and manipulation",
1115
  },
 
 
 
 
 
 
1116
  };
1117
 
1118
+ const [selectedHfModel, setSelectedHfModel] = useState<keyof typeof HF_MODELS>("Qwen-Image-Edit");
1119
 
1120
 
1121
  // HF PRO AUTHENTICATION
 
1296
  }
1297
  break;
1298
  case "CLOTHES":
1299
+ if ((node as ClothesNode).clothesPrompt) {
1300
+ config.clothesPrompt = (node as ClothesNode).clothesPrompt;
 
1301
  }
1302
  break;
1303
  case "STYLE":
 
1547
  }
1548
 
1549
  // Set loading state for all nodes being processed
1550
+ const startTime = Date.now();
1551
  setNodes(prev => prev.map(n => {
1552
  if (n.id === nodeId || processedNodes.includes(n.id)) {
1553
+ return { ...n, isRunning: true, error: null, startTime, executionTime: undefined };
1554
  }
1555
  return n;
1556
  }));
 
1562
  }
1563
 
1564
  // Check if params contains custom images and validate them
1565
+
1566
+ // Removed clothesImage validation as we now use text prompts
1567
+
 
 
 
1568
 
1569
  if (params.customBackgroundImage) {
1570
  // Validate it's a proper data URL
 
1651
 
1652
  // Only update the current node with the output
1653
  // Don't show output in intermediate nodes - they were just used for configuration
1654
+ const endTime = Date.now();
1655
+ const executionTime = endTime - startTime;
1656
+
1657
  setNodes(prev => prev.map(n => {
1658
  if (n.id === nodeId) {
1659
  // Only the current node gets the final output displayed
1660
+ return { ...n, output: data.image, isRunning: false, error: null, executionTime };
1661
  } else if (processedNodes.includes(n.id)) {
1662
  // Mark intermediate nodes as no longer running but don't give them output
1663
  // This way they remain unprocessed visually but their configs were used
1664
+ return { ...n, isRunning: false, error: null, executionTime };
1665
  }
1666
  return n;
1667
  }));