diff --git a/.github/workflows/block-master-prs.yml b/.github/workflows/block-master-prs.yml new file mode 100644 index 00000000..32d3f1a8 --- /dev/null +++ b/.github/workflows/block-master-prs.yml @@ -0,0 +1,16 @@ +name: Enforce Develop Branch +on: + pull_request: + branches: + - master # Triggers only on PRs targeting master + +jobs: + guard-master: + runs-on: ubuntu-latest + steps: + - name: Check base branch + # logic: If the PR is NOT coming from a release or hotfix branch, block it. + if: "!startsWith(github.head_ref, 'release/') && !startsWith(github.head_ref, 'hotfix/')" + run: | + echo "::error::⛔ WRONG BRANCH: You are trying to merge into 'master'. Please change the base branch of this PR to 'develop'." + exit 1 diff --git a/package-lock.json b/package-lock.json index e8b59649..b3441a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/postcss": "^4.1.17", "@xyflow/react": "^12.9.3", "autoprefixer": "^10.4.22", + "jszip": "^3.10.1", "konva": "^10.0.12", "next": "^16.0.6", "postcss": "^8.5.6", @@ -24,6 +25,7 @@ "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", + "@types/jszip": "^3.4.0", "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -2496,6 +2498,7 @@ "@types/d3-selection": "*" } }, +<<<<<<< HEAD "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2509,6 +2512,17 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" +======= + "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": "*" + } +>>>>>>> develop }, "node_modules/@types/node": { "version": "24.10.1", @@ -3043,11 +3057,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, +<<<<<<< HEAD "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, +======= + "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==", +>>>>>>> develop "license": "MIT" }, "node_modules/cross-spawn": { @@ -3692,6 +3713,7 @@ "node": ">= 14" } }, +<<<<<<< HEAD "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3714,6 +3736,19 @@ "engines": { "node": ">=8" } +======= + "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" +>>>>>>> develop }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -3724,11 +3759,18 @@ "node": ">=8" } }, +<<<<<<< HEAD "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, +======= + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", +>>>>>>> develop "license": "MIT" }, "node_modules/isexe": { @@ -3905,6 +3947,7 @@ "bignumber.js": "^9.0.0" } }, +<<<<<<< HEAD "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3916,6 +3959,18 @@ }, "engines": { "node": ">=6" +======= + "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" +>>>>>>> develop } }, "node_modules/jwa": { @@ -3958,6 +4013,18 @@ } ], "license": "MIT" +<<<<<<< HEAD +======= + }, + "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", + "dependencies": { + "immediate": "~3.0.5" + } +>>>>>>> develop }, "node_modules/lightningcss": { "version": "1.30.2", @@ -4477,6 +4544,7 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, +<<<<<<< HEAD "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -4489,6 +4557,13 @@ "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } +======= + "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)" +>>>>>>> develop }, "node_modules/path-key": { "version": "3.1.1", @@ -4575,6 +4650,7 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, +<<<<<<< HEAD "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -4625,6 +4701,13 @@ "engines": { "node": ">=6" } +======= + "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" +>>>>>>> develop }, "node_modules/react": { "version": "19.2.0", @@ -4701,6 +4784,7 @@ "react": "^19.2.0" } }, +<<<<<<< HEAD "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4734,6 +4818,28 @@ "engines": { "node": ">=0.10.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" +>>>>>>> develop }, "node_modules/rimraf": { "version": "5.0.10", @@ -4858,6 +4964,12 @@ "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": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -4952,6 +5064,7 @@ "node": ">=0.10.0" } }, +<<<<<<< HEAD "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4964,6 +5077,21 @@ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, +======= + "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==", +>>>>>>> develop "license": "MIT" }, "node_modules/string-width": { @@ -5293,6 +5421,7 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, +<<<<<<< HEAD "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -5458,6 +5587,13 @@ "engines": { "node": ">=18" } +======= + "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" +>>>>>>> develop }, "node_modules/web-streams-polyfill": { "version": "3.3.3", diff --git a/package.json b/package.json index e181a67d..2adf53f6 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tailwindcss/postcss": "^4.1.17", "@xyflow/react": "^12.9.3", "autoprefixer": "^10.4.22", + "jszip": "^3.10.1", "konva": "^10.0.12", "next": "^16.0.6", "postcss": "^8.5.6", @@ -28,6 +29,7 @@ "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", + "@types/jszip": "^3.4.0", "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", diff --git a/src/app/api/open-directory/route.ts b/src/app/api/open-directory/route.ts new file mode 100644 index 00000000..1b6ad731 --- /dev/null +++ b/src/app/api/open-directory/route.ts @@ -0,0 +1,54 @@ + +import { NextRequest, NextResponse } from "next/server"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import os from "os"; + +const execFileAsync = promisify(execFile); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { path } = body; + + if (!path) { + return NextResponse.json( + { success: false, error: "Path is required" }, + { status: 400 } + ); + } + + let command = ""; + let args: string[] = []; + const platform = os.platform(); + + switch (platform) { + case "darwin": + command = "open"; + args = [path]; + break; + case "win32": + command = "explorer"; + args = [path]; + break; + case "linux": + command = "xdg-open"; + args = [path]; + break; + default: + // Fallback for other Unix-like systems + command = "xdg-open"; + args = [path]; + } + + await execFileAsync(command, args); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Failed to open directory:", error); + return NextResponse.json( + { success: false, error: "Failed to open directory" }, + { status: 500 } + ); + } +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d200c5dd..8a66c44a 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -82,6 +82,31 @@ export function Header() { }, 50); }; + const handleOpenDirectory = async () => { + if (!saveDirectoryPath) return; + + try { + const response = await fetch("/api/open-directory", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ path: saveDirectoryPath }), + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + console.error("Failed to open directory:", result.error); + alert(`Failed to open project folder: ${result.error || "Unknown error"}`); + return; + } + } catch (error) { + console.error("Failed to open directory:", error); + alert("Failed to open project folder. Please try again."); + } + }; + return ( <> )} + {saveDirectoryPath && ( + + )} )} + + {/* Separator */} +
+ + {/* Download images button */} +
); } diff --git a/src/components/edges/EditableEdge.tsx b/src/components/edges/EditableEdge.tsx index c1b0644d..8e7d0477 100644 --- a/src/components/edges/EditableEdge.tsx +++ b/src/components/edges/EditableEdge.tsx @@ -16,12 +16,12 @@ interface EdgeData extends WorkflowEdgeData { offsetY?: number; } -// Colors for different connection types +// Colors for different connection types (dimmed for softer appearance) const EDGE_COLORS = { - image: "#10b981", // Green for image connections - prompt: "#3b82f6", // Blue for prompt connections - default: "#94a3b8", // Gray for unknown - pause: "#f97316", // Orange for paused edges + image: "#0d9668", // Dimmed green for image connections + prompt: "#2563eb", // Dimmed blue for prompt connections + default: "#64748b", // Dimmed gray for unknown + pause: "#ea580c", // Dimmed orange for paused edges }; export function EditableEdge({ @@ -38,6 +38,7 @@ export function EditableEdge({ data, sourceHandleId, targetHandleId, + source, target, }: EdgeProps) { const { setEdges } = useReactFlow(); @@ -46,6 +47,15 @@ export function EditableEdge({ const nodes = useWorkflowStore((state) => state.nodes); 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 offsetX = edgeData?.offsetX ?? 0; const offsetY = edgeData?.offsetY ?? 0; @@ -71,8 +81,12 @@ export function EditableEdge({ return EDGE_COLORS.default; }, [hasPause, sourceHandleId, targetHandleId]); - // Generate a unique gradient ID for this edge - const gradientId = `pulse-gradient-${id}`; + // Generate a unique gradient ID based on edge color and selection state + 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 const [edgePath, labelX, labelY] = useMemo(() => { @@ -167,13 +181,22 @@ export function EditableEdge({ return ( <> + {/* SVG gradient definition for bright-dim-bright effect */} + + + + + + + + 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 const [edgePath] = useMemo(() => { @@ -37,15 +49,30 @@ export function ReferenceEdge({ }); }, [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 ( <> + {/* SVG gradient definition for bright-dim-bright effect */} + + + + + + + + = ({ onClose, }) => { const [prompt, setPrompt] = useState(initialPrompt); + const [showConfirmation, setShowConfirmation] = useState(false); const [fontSize, setFontSize] = useState(() => { // Load font size from localStorage on mount if (typeof window !== 'undefined') { @@ -46,11 +47,23 @@ export const PromptEditorModal: React.FC = ({ } }, [fontSize]); + // Track unsaved changes + const hasUnsavedChanges = prompt !== initialPrompt; + + // Handle close attempt - show confirmation if there are unsaved changes + const handleAttemptClose = useCallback(() => { + if (hasUnsavedChanges) { + setShowConfirmation(true); + } else { + onClose(); + } + }, [hasUnsavedChanges, onClose]); + // Handle Escape key to close useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { - onClose(); + handleAttemptClose(); } }; @@ -61,7 +74,7 @@ export const PromptEditorModal: React.FC = ({ return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, [isOpen, onClose]); + }, [isOpen, handleAttemptClose]); const handleSubmit = useCallback(() => { onSubmit(prompt); @@ -76,10 +89,24 @@ export const PromptEditorModal: React.FC = ({ (e: React.MouseEvent) => { // Only close if clicking the backdrop itself, not the dialog content if (e.target === e.currentTarget) { - onClose(); + handleAttemptClose(); + } + }, + [handleAttemptClose] + ); + + const handleDismissConfirmation = useCallback(() => { + setShowConfirmation(false); + }, []); + + const handleConfirmationBackdropClick = useCallback( + (e: React.MouseEvent) => { + // Only dismiss if clicking the backdrop itself, not the confirmation dialog + if (e.target === e.currentTarget) { + handleDismissConfirmation(); } }, - [onClose] + [handleDismissConfirmation] ); if (!isOpen) return null; @@ -89,7 +116,7 @@ export const PromptEditorModal: React.FC = ({ className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50" onClick={handleBackdropClick} > -
+
{/* Header */}

@@ -129,7 +156,7 @@ export const PromptEditorModal: React.FC = ({ {/* Footer with buttons */}
+ + {/* Confirmation overlay */} + {showConfirmation && ( +
+
+ {/* Close button */} + + +

+ You have unsaved changes +

+
+ + +
+
+
+ )}

);