Spaces:
Running
Running
major fixes
Browse files- .claude/settings.local.json +2 -1
- app/api/improve-prompt/route.ts +3 -13
- app/nodes.tsx +157 -180
- app/page.tsx +40 -198
- public/reo.png +3 -0
.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
|
| 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
|
| 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
|
| 24 |
import React, { useState, useRef, useEffect } from "react";
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
import {
|
| 28 |
-
import {
|
| 29 |
-
import {
|
| 30 |
-
import {
|
| 31 |
-
import {
|
|
|
|
| 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
|
| 42 |
-
link.href = dataUrl; // Set the image data as
|
| 43 |
-
link.download = filename; //
|
| 44 |
-
document.body.appendChild(link); //
|
| 45 |
-
link.click(); //
|
| 46 |
-
document.body.removeChild(link); //
|
| 47 |
}
|
| 48 |
|
| 49 |
/**
|
|
@@ -54,38 +56,46 @@ function downloadImage(dataUrl: string, filename: string) {
|
|
| 54 |
*/
|
| 55 |
async function copyImageToClipboard(dataUrl: string) {
|
| 56 |
try {
|
| 57 |
-
|
| 58 |
-
const
|
|
|
|
| 59 |
|
| 60 |
-
//
|
|
|
|
| 61 |
if (blob.type !== 'image/png') {
|
| 62 |
-
|
| 63 |
-
const
|
| 64 |
-
const
|
|
|
|
| 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 |
-
|
| 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={
|
| 146 |
-
className="w-full rounded cursor-pointer hover:opacity-80 transition-
|
| 147 |
-
alt="Output"
|
| 148 |
-
onClick={() => copyImageToClipboard(
|
| 149 |
-
onContextMenu={(e) => {
|
| 150 |
-
e.preventDefault();
|
| 151 |
-
copyImageToClipboard(
|
| 152 |
|
| 153 |
-
// Show
|
| 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 |
-
|
|
|
|
| 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(
|
| 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 |
-
|
| 188 |
-
type
|
| 189 |
-
type
|
| 190 |
-
type
|
| 191 |
-
type
|
| 192 |
-
type
|
| 193 |
-
type
|
|
|
|
| 194 |
|
| 195 |
/**
|
| 196 |
* Utility function to combine CSS class names conditionally
|
| 197 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 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-
|
| 705 |
-
<
|
|
|
|
|
|
|
| 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-
|
| 1145 |
-
|
| 1146 |
-
{
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
{
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 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.
|
| 1326 |
-
{ name: "Natural Light", path: "/lighting/light2.
|
| 1327 |
-
{ name: "Dramatic Light", path: "/lighting/light3.
|
| 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-
|
| 1395 |
-
|
| 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.
|
| 1448 |
-
{ name: "Standing Pose 2", path: "/poses/stand2.
|
| 1449 |
-
{ name: "Sitting Pose 1", path: "/poses/sit1.
|
| 1450 |
-
{ name: "Sitting Pose 2", path: "/poses/sit2.
|
| 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-
|
| 1518 |
-
|
| 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 |
-
{
|
| 884 |
<img
|
| 885 |
-
src={
|
| 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 |
-
|
| 890 |
-
if (currentImage) {
|
| 891 |
try {
|
| 892 |
-
const response = await fetch(
|
| 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 |
-
|
| 905 |
-
if (currentImage) {
|
| 906 |
try {
|
| 907 |
-
const response = await fetch(
|
| 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 |
-
{
|
| 933 |
-
<div className="mt-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 |
-
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|