Spaces:
Running
Running
lets see whats fixed
Browse files- .claude/settings.local.json +4 -1
- app/nodes.tsx +168 -48
- app/page.tsx +45 -0
- public/clothes/{suit.jpg β suit.png} +2 -2
- public/lighting/light1.png +3 -0
- public/lighting/{light1.jpg β light2.png} +2 -2
- public/lighting/{light2.jpg β light3.png} +2 -2
- public/makeup/makeup1.jpg +0 -3
- public/{lighting/light3.jpg β makeup/makeup1.png} +2 -2
- public/poses/sit1.jpg +0 -3
- public/poses/sit1.png +3 -0
- public/poses/sit2.jpg +0 -3
- public/poses/sit2.png +3 -0
- public/poses/stand1.jpg +0 -3
- public/poses/stand1.png +3 -0
- public/poses/stand2.jpg +0 -3
- public/poses/stand2.png +3 -0
.claude/settings.local.json
CHANGED
|
@@ -11,7 +11,10 @@
|
|
| 11 |
"Bash(git add:*)",
|
| 12 |
"Bash(git commit:*)",
|
| 13 |
"Bash(git push:*)",
|
| 14 |
-
"WebSearch"
|
|
|
|
|
|
|
|
|
|
| 15 |
],
|
| 16 |
"deny": [],
|
| 17 |
"ask": []
|
|
|
|
| 11 |
"Bash(git add:*)",
|
| 12 |
"Bash(git commit:*)",
|
| 13 |
"Bash(git push:*)",
|
| 14 |
+
"WebSearch",
|
| 15 |
+
"Read(//Users/reubenfernandes/Desktop/**)",
|
| 16 |
+
"mcp__puppeteer__puppeteer_click",
|
| 17 |
+
"mcp__browser-tools__getConsoleErrors"
|
| 18 |
],
|
| 19 |
"deny": [],
|
| 20 |
"ask": []
|
app/nodes.tsx
CHANGED
|
@@ -26,7 +26,6 @@ import React, { useState, useRef, useEffect } from "react";
|
|
| 26 |
import { Button } from "../components/ui/button";
|
| 27 |
import { Select } from "../components/ui/select";
|
| 28 |
import { Textarea } from "../components/ui/textarea";
|
| 29 |
-
import { Label } from "../components/ui/label";
|
| 30 |
import { Slider } from "../components/ui/slider";
|
| 31 |
import { ColorPicker } from "../components/ui/color-picker";
|
| 32 |
import { Checkbox } from "../components/ui/checkbox";
|
|
@@ -57,9 +56,35 @@ async function copyImageToClipboard(dataUrl: string) {
|
|
| 57 |
try {
|
| 58 |
const response = await fetch(dataUrl);
|
| 59 |
const blob = await response.blob();
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
} catch (error) {
|
| 64 |
console.error('Failed to copy image to clipboard:', error);
|
| 65 |
}
|
|
@@ -1537,105 +1562,196 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1537 |
);
|
| 1538 |
}
|
| 1539 |
|
| 1540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1541 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1543 |
return (
|
| 1544 |
<div className="nb-node absolute text-white w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1545 |
<div
|
| 1546 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1547 |
-
onPointerDown={onPointerDown}
|
| 1548 |
-
onPointerMove={onPointerMove}
|
| 1549 |
-
onPointerUp={onPointerUp}
|
| 1550 |
>
|
|
|
|
| 1551 |
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
|
|
|
|
|
|
| 1552 |
<div className="font-semibold text-sm flex-1 text-center">EDIT</div>
|
|
|
|
| 1553 |
<div className="flex items-center gap-1">
|
|
|
|
| 1554 |
<Button
|
| 1555 |
variant="ghost"
|
| 1556 |
size="icon"
|
| 1557 |
className="text-destructive hover:bg-destructive/20 h-6 w-6"
|
| 1558 |
-
onClick={
|
| 1559 |
-
|
| 1560 |
-
e.preventDefault();
|
| 1561 |
-
if (confirm('Delete this node?')) {
|
| 1562 |
-
onDelete(node.id);
|
| 1563 |
-
}
|
| 1564 |
-
}}
|
| 1565 |
-
onPointerDown={(e) => e.stopPropagation()}
|
| 1566 |
title="Delete node"
|
| 1567 |
aria-label="Delete node"
|
| 1568 |
>
|
| 1569 |
Γ
|
| 1570 |
</Button>
|
|
|
|
|
|
|
| 1571 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1572 |
</div>
|
| 1573 |
</div>
|
|
|
|
|
|
|
| 1574 |
<div className="p-3 space-y-3">
|
|
|
|
| 1575 |
{node.input && (
|
| 1576 |
<div className="flex justify-end mb-2">
|
| 1577 |
<Button
|
| 1578 |
variant="ghost"
|
| 1579 |
size="sm"
|
| 1580 |
-
onClick={
|
| 1581 |
className="text-xs"
|
|
|
|
| 1582 |
>
|
| 1583 |
Clear Connection
|
| 1584 |
</Button>
|
| 1585 |
</div>
|
| 1586 |
)}
|
|
|
|
|
|
|
| 1587 |
<div className="space-y-2">
|
|
|
|
| 1588 |
<Textarea
|
| 1589 |
className="w-full"
|
| 1590 |
placeholder="Describe what to edit (e.g., 'make it brighter', 'add more contrast', 'make it look vintage')"
|
| 1591 |
value={node.editPrompt || ""}
|
| 1592 |
-
onChange={
|
| 1593 |
rows={3}
|
| 1594 |
/>
|
|
|
|
|
|
|
| 1595 |
<Button
|
| 1596 |
variant="outline"
|
| 1597 |
size="sm"
|
| 1598 |
className="w-full text-xs"
|
| 1599 |
-
onClick={
|
| 1600 |
-
if (!node.editPrompt) {
|
| 1601 |
-
alert('Please enter an edit description first');
|
| 1602 |
-
return;
|
| 1603 |
-
}
|
| 1604 |
-
|
| 1605 |
-
try {
|
| 1606 |
-
const response = await fetch('/api/improve-prompt', {
|
| 1607 |
-
method: 'POST',
|
| 1608 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1609 |
-
body: JSON.stringify({
|
| 1610 |
-
prompt: node.editPrompt,
|
| 1611 |
-
type: 'edit'
|
| 1612 |
-
})
|
| 1613 |
-
});
|
| 1614 |
-
|
| 1615 |
-
if (response.ok) {
|
| 1616 |
-
const { improvedPrompt } = await response.json();
|
| 1617 |
-
onUpdate(node.id, { editPrompt: improvedPrompt });
|
| 1618 |
-
} else {
|
| 1619 |
-
alert('Failed to improve prompt. Please try again.');
|
| 1620 |
-
}
|
| 1621 |
-
} catch (error) {
|
| 1622 |
-
console.error('Error improving prompt:', error);
|
| 1623 |
-
alert('Failed to improve prompt. Please try again.');
|
| 1624 |
-
}
|
| 1625 |
-
}}
|
| 1626 |
title="Use Gemini 2.5 Flash to improve your edit prompt"
|
|
|
|
| 1627 |
>
|
| 1628 |
β¨ Improve with Gemini
|
| 1629 |
</Button>
|
| 1630 |
</div>
|
|
|
|
|
|
|
| 1631 |
<Button
|
| 1632 |
className="w-full"
|
| 1633 |
onClick={() => onProcess(node.id)}
|
| 1634 |
-
disabled={node.isRunning}
|
| 1635 |
-
title={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1636 |
>
|
| 1637 |
{node.isRunning ? "Processing..." : "Apply Edit"}
|
| 1638 |
</Button>
|
|
|
|
|
|
|
| 1639 |
<NodeOutputSection
|
| 1640 |
nodeId={node.id}
|
| 1641 |
output={node.output}
|
|
@@ -1644,8 +1760,12 @@ export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1644 |
navigateNodeHistory={navigateNodeHistory}
|
| 1645 |
getCurrentNodeImage={getCurrentNodeImage}
|
| 1646 |
/>
|
|
|
|
|
|
|
| 1647 |
{node.error && (
|
| 1648 |
-
<div className="text-xs text-red-400 mt-2">
|
|
|
|
|
|
|
| 1649 |
)}
|
| 1650 |
</div>
|
| 1651 |
</div>
|
|
|
|
| 26 |
import { Button } from "../components/ui/button";
|
| 27 |
import { Select } from "../components/ui/select";
|
| 28 |
import { Textarea } from "../components/ui/textarea";
|
|
|
|
| 29 |
import { Slider } from "../components/ui/slider";
|
| 30 |
import { ColorPicker } from "../components/ui/color-picker";
|
| 31 |
import { Checkbox } from "../components/ui/checkbox";
|
|
|
|
| 56 |
try {
|
| 57 |
const response = await fetch(dataUrl);
|
| 58 |
const blob = await response.blob();
|
| 59 |
+
|
| 60 |
+
// Convert to PNG if not already PNG (clipboard API only supports PNG for images)
|
| 61 |
+
if (blob.type !== 'image/png') {
|
| 62 |
+
const canvas = document.createElement('canvas');
|
| 63 |
+
const ctx = canvas.getContext('2d');
|
| 64 |
+
const img = new Image();
|
| 65 |
+
|
| 66 |
+
await new Promise((resolve) => {
|
| 67 |
+
img.onload = () => {
|
| 68 |
+
canvas.width = img.width;
|
| 69 |
+
canvas.height = img.height;
|
| 70 |
+
ctx?.drawImage(img, 0, 0);
|
| 71 |
+
resolve(void 0);
|
| 72 |
+
};
|
| 73 |
+
img.src = dataUrl;
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const pngBlob = await new Promise<Blob>((resolve) => {
|
| 77 |
+
canvas.toBlob((blob) => resolve(blob!), 'image/png');
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
await navigator.clipboard.write([
|
| 81 |
+
new ClipboardItem({ 'image/png': pngBlob })
|
| 82 |
+
]);
|
| 83 |
+
} else {
|
| 84 |
+
await navigator.clipboard.write([
|
| 85 |
+
new ClipboardItem({ 'image/png': blob })
|
| 86 |
+
]);
|
| 87 |
+
}
|
| 88 |
} catch (error) {
|
| 89 |
console.error('Failed to copy image to clipboard:', error);
|
| 90 |
}
|
|
|
|
| 1562 |
);
|
| 1563 |
}
|
| 1564 |
|
| 1565 |
+
/**
|
| 1566 |
+
* EDIT NODE VIEW COMPONENT
|
| 1567 |
+
*
|
| 1568 |
+
* This node allows users to perform general text-based image editing operations.
|
| 1569 |
+
* Users can describe what they want to change about an image using natural language,
|
| 1570 |
+
* and the AI will attempt to apply those changes.
|
| 1571 |
+
*
|
| 1572 |
+
* Features:
|
| 1573 |
+
* - Natural language editing prompts (e.g., "make it brighter", "add vintage effect")
|
| 1574 |
+
* - AI-powered prompt improvement using Gemini
|
| 1575 |
+
* - Real-time editing processing
|
| 1576 |
+
* - Output history with navigation
|
| 1577 |
+
* - Connection management for input/output workflow
|
| 1578 |
+
*
|
| 1579 |
+
* @param node - The edit node data containing editPrompt, input, output, etc.
|
| 1580 |
+
* @param onDelete - Callback to delete this node
|
| 1581 |
+
* @param onUpdate - Callback to update node properties
|
| 1582 |
+
* @param onStartConnection - Callback when starting a connection from output port
|
| 1583 |
+
* @param onEndConnection - Callback when ending a connection at input port
|
| 1584 |
+
* @param onProcess - Callback to process this node
|
| 1585 |
+
* @param onUpdatePosition - Callback to update node position when dragged
|
| 1586 |
+
* @param getNodeHistoryInfo - Function to get history information for this node
|
| 1587 |
+
* @param navigateNodeHistory - Function to navigate through node history
|
| 1588 |
+
* @param getCurrentNodeImage - Function to get the current image for this node
|
| 1589 |
+
*/
|
| 1590 |
+
export function EditNodeView({
|
| 1591 |
+
node,
|
| 1592 |
+
onDelete,
|
| 1593 |
+
onUpdate,
|
| 1594 |
+
onStartConnection,
|
| 1595 |
+
onEndConnection,
|
| 1596 |
+
onProcess,
|
| 1597 |
+
onUpdatePosition,
|
| 1598 |
+
getNodeHistoryInfo,
|
| 1599 |
+
navigateNodeHistory,
|
| 1600 |
+
getCurrentNodeImage
|
| 1601 |
+
}: any) {
|
| 1602 |
+
// Use custom hook for drag functionality - handles position updates during dragging
|
| 1603 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1604 |
|
| 1605 |
+
/**
|
| 1606 |
+
* Handle prompt improvement using Gemini API
|
| 1607 |
+
* Takes the user's basic edit description and enhances it for better AI processing
|
| 1608 |
+
*/
|
| 1609 |
+
const handlePromptImprovement = async () => {
|
| 1610 |
+
// Validate that user has entered a prompt
|
| 1611 |
+
if (!node.editPrompt?.trim()) {
|
| 1612 |
+
alert('Please enter an edit description first');
|
| 1613 |
+
return;
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
+
try {
|
| 1617 |
+
// Call the API to improve the prompt
|
| 1618 |
+
const response = await fetch('/api/improve-prompt', {
|
| 1619 |
+
method: 'POST',
|
| 1620 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1621 |
+
body: JSON.stringify({
|
| 1622 |
+
prompt: node.editPrompt.trim(),
|
| 1623 |
+
type: 'edit'
|
| 1624 |
+
})
|
| 1625 |
+
});
|
| 1626 |
+
|
| 1627 |
+
if (response.ok) {
|
| 1628 |
+
const { improvedPrompt } = await response.json();
|
| 1629 |
+
onUpdate(node.id, { editPrompt: improvedPrompt });
|
| 1630 |
+
} else {
|
| 1631 |
+
alert('Failed to improve prompt. Please try again.');
|
| 1632 |
+
}
|
| 1633 |
+
} catch (error) {
|
| 1634 |
+
console.error('Error improving prompt:', error);
|
| 1635 |
+
alert('Failed to improve prompt. Please try again.');
|
| 1636 |
+
}
|
| 1637 |
+
};
|
| 1638 |
+
|
| 1639 |
+
/**
|
| 1640 |
+
* Handle delete node action with confirmation
|
| 1641 |
+
*/
|
| 1642 |
+
const handleDeleteNode = (e: React.MouseEvent) => {
|
| 1643 |
+
e.stopPropagation(); // Prevent triggering drag
|
| 1644 |
+
e.preventDefault();
|
| 1645 |
+
|
| 1646 |
+
if (confirm('Delete this node?')) {
|
| 1647 |
+
onDelete(node.id);
|
| 1648 |
+
}
|
| 1649 |
+
};
|
| 1650 |
+
|
| 1651 |
+
/**
|
| 1652 |
+
* Handle clearing the input connection
|
| 1653 |
+
*/
|
| 1654 |
+
const handleClearConnection = () => {
|
| 1655 |
+
onUpdate(node.id, { input: undefined });
|
| 1656 |
+
};
|
| 1657 |
+
|
| 1658 |
+
/**
|
| 1659 |
+
* Handle edit prompt changes
|
| 1660 |
+
*/
|
| 1661 |
+
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
| 1662 |
+
onUpdate(node.id, { editPrompt: e.target.value });
|
| 1663 |
+
};
|
| 1664 |
+
|
| 1665 |
return (
|
| 1666 |
<div className="nb-node absolute text-white w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 1667 |
+
{/* Node Header - Contains title, delete button, and connection ports */}
|
| 1668 |
<div
|
| 1669 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1670 |
+
onPointerDown={onPointerDown} // Start dragging
|
| 1671 |
+
onPointerMove={onPointerMove} // Handle drag movement
|
| 1672 |
+
onPointerUp={onPointerUp} // End dragging
|
| 1673 |
>
|
| 1674 |
+
{/* Input port (left side) - where connections come in */}
|
| 1675 |
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 1676 |
+
|
| 1677 |
+
{/* Node title */}
|
| 1678 |
<div className="font-semibold text-sm flex-1 text-center">EDIT</div>
|
| 1679 |
+
|
| 1680 |
<div className="flex items-center gap-1">
|
| 1681 |
+
{/* Delete button */}
|
| 1682 |
<Button
|
| 1683 |
variant="ghost"
|
| 1684 |
size="icon"
|
| 1685 |
className="text-destructive hover:bg-destructive/20 h-6 w-6"
|
| 1686 |
+
onClick={handleDeleteNode}
|
| 1687 |
+
onPointerDown={(e) => e.stopPropagation()} // Prevent drag when clicking delete
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1688 |
title="Delete node"
|
| 1689 |
aria-label="Delete node"
|
| 1690 |
>
|
| 1691 |
Γ
|
| 1692 |
</Button>
|
| 1693 |
+
|
| 1694 |
+
{/* Output port (right side) - where connections go out */}
|
| 1695 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1696 |
</div>
|
| 1697 |
</div>
|
| 1698 |
+
|
| 1699 |
+
{/* Node Content - Contains all the controls and outputs */}
|
| 1700 |
<div className="p-3 space-y-3">
|
| 1701 |
+
{/* Show clear connection button if node has input */}
|
| 1702 |
{node.input && (
|
| 1703 |
<div className="flex justify-end mb-2">
|
| 1704 |
<Button
|
| 1705 |
variant="ghost"
|
| 1706 |
size="sm"
|
| 1707 |
+
onClick={handleClearConnection}
|
| 1708 |
className="text-xs"
|
| 1709 |
+
title="Remove input connection"
|
| 1710 |
>
|
| 1711 |
Clear Connection
|
| 1712 |
</Button>
|
| 1713 |
</div>
|
| 1714 |
)}
|
| 1715 |
+
|
| 1716 |
+
{/* Edit prompt input and improvement section */}
|
| 1717 |
<div className="space-y-2">
|
| 1718 |
+
<div className="text-xs text-white/70 mb-1">Edit Instructions</div>
|
| 1719 |
<Textarea
|
| 1720 |
className="w-full"
|
| 1721 |
placeholder="Describe what to edit (e.g., 'make it brighter', 'add more contrast', 'make it look vintage')"
|
| 1722 |
value={node.editPrompt || ""}
|
| 1723 |
+
onChange={handlePromptChange}
|
| 1724 |
rows={3}
|
| 1725 |
/>
|
| 1726 |
+
|
| 1727 |
+
{/* AI-powered prompt improvement button */}
|
| 1728 |
<Button
|
| 1729 |
variant="outline"
|
| 1730 |
size="sm"
|
| 1731 |
className="w-full text-xs"
|
| 1732 |
+
onClick={handlePromptImprovement}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1733 |
title="Use Gemini 2.5 Flash to improve your edit prompt"
|
| 1734 |
+
disabled={!node.editPrompt?.trim()}
|
| 1735 |
>
|
| 1736 |
β¨ Improve with Gemini
|
| 1737 |
</Button>
|
| 1738 |
</div>
|
| 1739 |
+
|
| 1740 |
+
{/* Process button - starts the editing operation */}
|
| 1741 |
<Button
|
| 1742 |
className="w-full"
|
| 1743 |
onClick={() => onProcess(node.id)}
|
| 1744 |
+
disabled={node.isRunning || !node.editPrompt?.trim()}
|
| 1745 |
+
title={
|
| 1746 |
+
!node.input ? "Connect an input first" :
|
| 1747 |
+
!node.editPrompt?.trim() ? "Enter edit instructions first" :
|
| 1748 |
+
"Apply the edit to the input image"
|
| 1749 |
+
}
|
| 1750 |
>
|
| 1751 |
{node.isRunning ? "Processing..." : "Apply Edit"}
|
| 1752 |
</Button>
|
| 1753 |
+
|
| 1754 |
+
{/* Output section with history navigation and download */}
|
| 1755 |
<NodeOutputSection
|
| 1756 |
nodeId={node.id}
|
| 1757 |
output={node.output}
|
|
|
|
| 1760 |
navigateNodeHistory={navigateNodeHistory}
|
| 1761 |
getCurrentNodeImage={getCurrentNodeImage}
|
| 1762 |
/>
|
| 1763 |
+
|
| 1764 |
+
{/* Error display */}
|
| 1765 |
{node.error && (
|
| 1766 |
+
<div className="text-xs text-red-400 mt-2 p-2 bg-red-900/20 rounded">
|
| 1767 |
+
{node.error}
|
| 1768 |
+
</div>
|
| 1769 |
)}
|
| 1770 |
</div>
|
| 1771 |
</div>
|
app/page.tsx
CHANGED
|
@@ -101,6 +101,48 @@ CRITICAL REQUIREMENTS:
|
|
| 101 |
The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`;
|
| 102 |
}
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
/* ========================================
|
| 105 |
TYPE DEFINITIONS
|
| 106 |
======================================== */
|
|
@@ -2113,6 +2155,9 @@ export default function EditorPage() {
|
|
| 2113 |
case "FACE":
|
| 2114 |
setNodes(prev => [...prev, { ...commonProps, type: "FACE", faceOptions: {} } as FaceNode]);
|
| 2115 |
break;
|
|
|
|
|
|
|
|
|
|
| 2116 |
case "LIGHTNING":
|
| 2117 |
setNodes(prev => [...prev, { ...commonProps, type: "LIGHTNING", lightingStrength: 75 } as LightningNode]);
|
| 2118 |
break;
|
|
|
|
| 101 |
The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`;
|
| 102 |
}
|
| 103 |
|
| 104 |
+
/**
|
| 105 |
+
* Copy image to clipboard with PNG conversion
|
| 106 |
+
* The clipboard API only supports PNG format for images, so we convert other formats
|
| 107 |
+
*/
|
| 108 |
+
async function copyImageToClipboard(dataUrl: string) {
|
| 109 |
+
try {
|
| 110 |
+
const response = await fetch(dataUrl);
|
| 111 |
+
const blob = await response.blob();
|
| 112 |
+
|
| 113 |
+
// Convert to PNG if not already PNG
|
| 114 |
+
if (blob.type !== 'image/png') {
|
| 115 |
+
const canvas = document.createElement('canvas');
|
| 116 |
+
const ctx = canvas.getContext('2d');
|
| 117 |
+
const img = new Image();
|
| 118 |
+
|
| 119 |
+
await new Promise((resolve) => {
|
| 120 |
+
img.onload = () => {
|
| 121 |
+
canvas.width = img.width;
|
| 122 |
+
canvas.height = img.height;
|
| 123 |
+
ctx?.drawImage(img, 0, 0);
|
| 124 |
+
resolve(void 0);
|
| 125 |
+
};
|
| 126 |
+
img.src = dataUrl;
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
const pngBlob = await new Promise<Blob>((resolve) => {
|
| 130 |
+
canvas.toBlob((blob) => resolve(blob!), 'image/png');
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
await navigator.clipboard.write([
|
| 134 |
+
new ClipboardItem({ 'image/png': pngBlob })
|
| 135 |
+
]);
|
| 136 |
+
} else {
|
| 137 |
+
await navigator.clipboard.write([
|
| 138 |
+
new ClipboardItem({ 'image/png': blob })
|
| 139 |
+
]);
|
| 140 |
+
}
|
| 141 |
+
} catch (error) {
|
| 142 |
+
console.error('Failed to copy image to clipboard:', error);
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
/* ========================================
|
| 147 |
TYPE DEFINITIONS
|
| 148 |
======================================== */
|
|
|
|
| 2155 |
case "FACE":
|
| 2156 |
setNodes(prev => [...prev, { ...commonProps, type: "FACE", faceOptions: {} } as FaceNode]);
|
| 2157 |
break;
|
| 2158 |
+
case "EDIT":
|
| 2159 |
+
setNodes(prev => [...prev, { ...commonProps, type: "EDIT" } as EditNode]);
|
| 2160 |
+
break;
|
| 2161 |
case "LIGHTNING":
|
| 2162 |
setNodes(prev => [...prev, { ...commonProps, type: "LIGHTNING", lightingStrength: 75 } as LightningNode]);
|
| 2163 |
break;
|
public/clothes/{suit.jpg β suit.png}
RENAMED
|
File without changes
|
public/lighting/light1.png
ADDED
|
Git LFS Details
|
public/lighting/{light1.jpg β light2.png}
RENAMED
|
File without changes
|
public/lighting/{light2.jpg β light3.png}
RENAMED
|
File without changes
|
public/makeup/makeup1.jpg
DELETED
Git LFS Details
|
public/{lighting/light3.jpg β makeup/makeup1.png}
RENAMED
|
File without changes
|
public/poses/sit1.jpg
DELETED
Git LFS Details
|
public/poses/sit1.png
ADDED
|
Git LFS Details
|
public/poses/sit2.jpg
DELETED
Git LFS Details
|
public/poses/sit2.png
ADDED
|
Git LFS Details
|
public/poses/stand1.jpg
DELETED
Git LFS Details
|
public/poses/stand1.png
ADDED
|
Git LFS Details
|
public/poses/stand2.jpg
DELETED
Git LFS Details
|
public/poses/stand2.png
ADDED
|
Git LFS Details
|