Browse Source

Subtle connectors, zip download to context menu

handoff-20260429-1057
shrimbly 6 months ago
parent
commit
926590adeb
  1. 123
      package-lock.json
  2. 2
      package.json
  3. 75
      src/components/MultiSelectToolbar.tsx
  4. 43
      src/components/edges/EditableEdge.tsx
  5. 33
      src/components/edges/ReferenceEdge.tsx
  6. 42
      src/components/modals/PromptEditorModal.tsx

123
package-lock.json

@ -12,6 +12,7 @@
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@xyflow/react": "^12.9.3", "@xyflow/react": "^12.9.3",
"autoprefixer": "^10.4.22", "autoprefixer": "^10.4.22",
"jszip": "^3.10.1",
"konva": "^10.0.12", "konva": "^10.0.12",
"next": "^16.0.6", "next": "^16.0.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@ -22,6 +23,7 @@
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@types/jszip": "^3.4.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@ -1057,6 +1059,16 @@
"@types/d3-selection": "*" "@types/d3-selection": "*"
} }
}, },
"node_modules/@types/jszip": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"jszip": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.1", "version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
@ -1072,7 +1084,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -1298,7 +1309,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@ -1369,6 +1379,12 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -1446,7 +1462,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@ -1757,6 +1772,18 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-fullwidth-code-point": { "node_modules/is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -1766,6 +1793,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -1826,6 +1859,18 @@
"bignumber.js": "^9.0.0" "bignumber.js": "^9.0.0"
} }
}, },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jwa": { "node_modules/jwa": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@ -1865,8 +1910,16 @@
"url": "https://github.com/sponsors/lavrton" "url": "https://github.com/sponsors/lavrton"
} }
], ],
"license": "MIT"
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT", "license": "MIT",
"peer": true "dependencies": {
"immediate": "~3.0.5"
}
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.2", "version": "1.30.2",
@ -2319,6 +2372,12 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -2369,7 +2428,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -2385,6 +2443,12 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/react": { "node_modules/react": {
"version": "19.2.0", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
@ -2399,7 +2463,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -2453,6 +2516,27 @@
"react": "^19.2.0" "react": "^19.2.0"
} }
}, },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "5.0.10", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@ -2507,6 +2591,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sharp": { "node_modules/sharp": {
"version": "0.34.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@ -2594,6 +2684,21 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -2798,6 +2903,12 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/web-streams-polyfill": { "node_modules/web-streams-polyfill": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

2
package.json

@ -13,6 +13,7 @@
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@xyflow/react": "^12.9.3", "@xyflow/react": "^12.9.3",
"autoprefixer": "^10.4.22", "autoprefixer": "^10.4.22",
"jszip": "^3.10.1",
"konva": "^10.0.12", "konva": "^10.0.12",
"next": "^16.0.6", "next": "^16.0.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@ -23,6 +24,7 @@
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@types/jszip": "^3.4.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",

75
src/components/MultiSelectToolbar.tsx

@ -2,7 +2,14 @@
import { useReactFlow } from "@xyflow/react"; import { useReactFlow } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { useMemo } from "react"; import { useMemo, useCallback } from "react";
import JSZip from "jszip";
import type {
ImageInputNodeData,
AnnotationNodeData,
NanoBananaNodeData,
OutputNodeData,
} from "@/types";
const STACK_GAP = 20; const STACK_GAP = 20;
@ -156,6 +163,58 @@ export function MultiSelectToolbar() {
removeNodesFromGroup(nodeIds); removeNodesFromGroup(nodeIds);
}; };
const handleDownloadImages = useCallback(async () => {
// Extract images from selected nodes based on node type
const images: { data: string; name: string }[] = [];
selectedNodes.forEach((node, index) => {
let imageData: string | null = null;
switch (node.type) {
case "imageInput":
imageData = (node.data as ImageInputNodeData).image;
break;
case "annotation":
imageData = (node.data as AnnotationNodeData).outputImage;
break;
case "nanoBanana":
imageData = (node.data as NanoBananaNodeData).outputImage;
break;
case "output":
imageData = (node.data as OutputNodeData).image;
break;
}
if (imageData) {
images.push({
data: imageData,
name: `image-${index + 1}.png`,
});
}
});
if (images.length === 0) return;
// Create ZIP file
const zip = new JSZip();
images.forEach(({ data, name }) => {
// Remove data URL prefix to get raw base64
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
zip.file(name, base64Data, { base64: true });
});
// Generate and download
const blob = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `images-${Date.now()}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, [selectedNodes]);
if (!toolbarPosition || selectedNodes.length < 2) return null; if (!toolbarPosition || selectedNodes.length < 2) return null;
return ( return (
@ -220,6 +279,20 @@ export function MultiSelectToolbar() {
</svg> </svg>
</button> </button>
)} )}
{/* Separator */}
<div className="w-px h-4 bg-neutral-600 mx-0.5" />
{/* Download images button */}
<button
onClick={handleDownloadImages}
className="p-1.5 rounded hover:bg-neutral-700 text-neutral-400 hover:text-neutral-100 transition-colors"
title="Download images as ZIP"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</button>
</div> </div>
); );
} }

43
src/components/edges/EditableEdge.tsx

@ -16,12 +16,12 @@ interface EdgeData extends WorkflowEdgeData {
offsetY?: number; offsetY?: number;
} }
// Colors for different connection types // Colors for different connection types (dimmed for softer appearance)
const EDGE_COLORS = { const EDGE_COLORS = {
image: "#10b981", // Green for image connections image: "#0d9668", // Dimmed green for image connections
prompt: "#3b82f6", // Blue for prompt connections prompt: "#2563eb", // Dimmed blue for prompt connections
default: "#94a3b8", // Gray for unknown default: "#64748b", // Dimmed gray for unknown
pause: "#f97316", // Orange for paused edges pause: "#ea580c", // Dimmed orange for paused edges
}; };
export function EditableEdge({ export function EditableEdge({
@ -38,6 +38,7 @@ export function EditableEdge({
data, data,
sourceHandleId, sourceHandleId,
targetHandleId, targetHandleId,
source,
target, target,
}: EdgeProps) { }: EdgeProps) {
const { setEdges } = useReactFlow(); const { setEdges } = useReactFlow();
@ -46,6 +47,15 @@ export function EditableEdge({
const nodes = useWorkflowStore((state) => state.nodes); const nodes = useWorkflowStore((state) => state.nodes);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
// Check if any node is selected and if this edge is connected to it
const isConnectedToSelection = useMemo(() => {
const selectedNodes = nodes.filter((n) => n.selected);
if (selectedNodes.length === 0) return false; // No selection, show all dimmed
// Check if this edge connects to any selected node
return selectedNodes.some((n) => n.id === source || n.id === target);
}, [nodes, source, target]);
const edgeData = data as EdgeData | undefined; const edgeData = data as EdgeData | undefined;
const offsetX = edgeData?.offsetX ?? 0; const offsetX = edgeData?.offsetX ?? 0;
const offsetY = edgeData?.offsetY ?? 0; const offsetY = edgeData?.offsetY ?? 0;
@ -71,8 +81,12 @@ export function EditableEdge({
return EDGE_COLORS.default; return EDGE_COLORS.default;
}, [hasPause, sourceHandleId, targetHandleId]); }, [hasPause, sourceHandleId, targetHandleId]);
// Generate a unique gradient ID for this edge // Generate a unique gradient ID based on edge color and selection state
const gradientId = `pulse-gradient-${id}`; const gradientId = useMemo(() => {
const colorKey = hasPause ? "pause" : (sourceHandleId || targetHandleId || "default");
const selectionKey = isConnectedToSelection ? "active" : "dimmed";
return `edge-gradient-${colorKey}-${selectionKey}-${id}`;
}, [hasPause, sourceHandleId, targetHandleId, isConnectedToSelection, id]);
// Calculate the path based on edge style // Calculate the path based on edge style
const [edgePath, labelX, labelY] = useMemo(() => { const [edgePath, labelX, labelY] = useMemo(() => {
@ -167,13 +181,22 @@ export function EditableEdge({
return ( return (
<> <>
{/* SVG gradient definition for bright-dim-bright effect */}
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={edgeColor} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
<stop offset="50%" stopColor={edgeColor} stopOpacity={isConnectedToSelection ? 0.55 : 0.1} />
<stop offset="100%" stopColor={edgeColor} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
</linearGradient>
</defs>
<BaseEdge <BaseEdge
id={id} id={id}
path={edgePath} path={edgePath}
markerEnd={markerEnd} markerEnd={markerEnd}
style={{ style={{
...style, ...style,
stroke: edgeColor, stroke: `url(#${gradientId})`,
strokeWidth: 3, strokeWidth: 3,
strokeLinecap: "round", strokeLinecap: "round",
strokeLinejoin: "round", strokeLinejoin: "round",
@ -187,7 +210,7 @@ export function EditableEdge({
<path <path
d={edgePath} d={edgePath}
fill="none" fill="none"
stroke={edgeColor} stroke={`url(#${gradientId})`}
strokeWidth={10} strokeWidth={10}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@ -200,7 +223,7 @@ export function EditableEdge({
<path <path
d={edgePath} d={edgePath}
fill="none" fill="none"
stroke={edgeColor} stroke={`url(#${gradientId})`}
strokeWidth={5} strokeWidth={5}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"

33
src/components/edges/ReferenceEdge.tsx

@ -8,8 +8,8 @@ import {
} from "@xyflow/react"; } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
// Grey color for reference connections // Grey color for reference connections (dimmed for softer appearance)
const REFERENCE_COLOR = "#6b7280"; const REFERENCE_COLOR = "#52525b";
export function ReferenceEdge({ export function ReferenceEdge({
id, id,
@ -21,8 +21,20 @@ export function ReferenceEdge({
targetPosition, targetPosition,
style, style,
markerEnd, markerEnd,
source,
target,
}: EdgeProps) { }: EdgeProps) {
const edgeStyle = useWorkflowStore((state) => state.edgeStyle); const edgeStyle = useWorkflowStore((state) => state.edgeStyle);
const nodes = useWorkflowStore((state) => state.nodes);
// Check if any node is selected and if this edge is connected to it
const isConnectedToSelection = useMemo(() => {
const selectedNodes = nodes.filter((n) => n.selected);
if (selectedNodes.length === 0) return false; // No selection, show all dimmed
// Check if this edge connects to any selected node
return selectedNodes.some((n) => n.id === source || n.id === target);
}, [nodes, source, target]);
// Calculate the path - always use curved for reference edges for softer look // Calculate the path - always use curved for reference edges for softer look
const [edgePath] = useMemo(() => { const [edgePath] = useMemo(() => {
@ -37,15 +49,30 @@ export function ReferenceEdge({
}); });
}, [sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]); }, [sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]);
// Generate a unique gradient ID based on selection state
const gradientId = useMemo(() => {
const selectionKey = isConnectedToSelection ? "active" : "dimmed";
return `reference-gradient-${selectionKey}-${id}`;
}, [isConnectedToSelection, id]);
return ( return (
<> <>
{/* SVG gradient definition for bright-dim-bright effect */}
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={REFERENCE_COLOR} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
<stop offset="50%" stopColor={REFERENCE_COLOR} stopOpacity={isConnectedToSelection ? 0.55 : 0.1} />
<stop offset="100%" stopColor={REFERENCE_COLOR} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
</linearGradient>
</defs>
<BaseEdge <BaseEdge
id={id} id={id}
path={edgePath} path={edgePath}
markerEnd={markerEnd} markerEnd={markerEnd}
style={{ style={{
...style, ...style,
stroke: REFERENCE_COLOR, stroke: `url(#${gradientId})`,
strokeWidth: 2, strokeWidth: 2,
strokeDasharray: "6 4", strokeDasharray: "6 4",
strokeLinecap: "round", strokeLinecap: "round",

42
src/components/modals/PromptEditorModal.tsx

@ -95,6 +95,20 @@ export const PromptEditorModal: React.FC<PromptEditorModalProps> = ({
[handleAttemptClose] [handleAttemptClose]
); );
const handleDismissConfirmation = useCallback(() => {
setShowConfirmation(false);
}, []);
const handleConfirmationBackdropClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// Only dismiss if clicking the backdrop itself, not the confirmation dialog
if (e.target === e.currentTarget) {
handleDismissConfirmation();
}
},
[handleDismissConfirmation]
);
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@ -157,8 +171,32 @@ export const PromptEditorModal: React.FC<PromptEditorModalProps> = ({
{/* Confirmation overlay */} {/* Confirmation overlay */}
{showConfirmation && ( {showConfirmation && (
<div className="absolute inset-0 flex items-center justify-center bg-black/60 rounded-lg"> <div
<div className="bg-neutral-800 border border-neutral-600 rounded-lg p-6 mx-4 max-w-sm shadow-xl"> className="absolute inset-0 flex items-center justify-center bg-black/60 rounded-lg"
onClick={handleConfirmationBackdropClick}
>
<div className="relative bg-neutral-800 border border-neutral-600 rounded-lg p-6 mx-4 max-w-sm shadow-xl">
{/* Close button */}
<button
onClick={handleDismissConfirmation}
className="absolute top-3 right-3 text-neutral-400 hover:text-neutral-200 transition-colors focus:outline-none"
aria-label="Close"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<p className="text-neutral-100 text-center mb-6"> <p className="text-neutral-100 text-center mb-6">
You have unsaved changes You have unsaved changes
</p> </p>

Loading…
Cancel
Save