Browse Source

Merge pull request #26 from shrimbly/develop

Bugfixes: duplicate images, connection handles, Gemini parameters
handoff-20260429-1057
Willie 6 months ago
committed by GitHub
parent
commit
6aa8a984b3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      src/components/WorkflowCanvas.tsx
  2. 2
      src/components/__tests__/QuickstartInitialView.test.tsx
  3. 8
      src/components/nodes/GenerateImageNode.tsx
  4. 51
      src/utils/imageStorage.ts

22
src/components/WorkflowCanvas.tsx

@ -333,12 +333,22 @@ export function WorkflowCanvas() {
const nodeData = node.data as { inputSchema?: Array<{ name: string; type: string }> }; const nodeData = node.data as { inputSchema?: Array<{ name: string; type: string }> };
if (nodeData.inputSchema && nodeData.inputSchema.length > 0) { if (nodeData.inputSchema && nodeData.inputSchema.length > 0) {
if (needInput) { if (needInput) {
// Find first input handle matching the type // Find input handles matching the type
const matchingInputs = nodeData.inputSchema.filter(i => i.type === handleType); const matchingInputs = nodeData.inputSchema.filter(i => i.type === handleType);
if (matchingInputs.length > 0) { const numHandles = matchingInputs.length;
// Return normalized handle ID, not schema name if (numHandles > 0) {
// Always use indexed IDs (image-0, text-0) for schema inputs for consistency // Find the first unoccupied indexed handle by checking existing edges
return `${handleType}-0`; for (let i = 0; i < numHandles; i++) {
const candidateHandle = `${handleType}-${i}`;
const isOccupied = edges.some(
(edge) => edge.target === node.id && edge.targetHandle === candidateHandle
);
if (!isOccupied) {
return candidateHandle;
}
}
// All handles are occupied
return null;
} }
} }
// Output handle - check for video or image type // Output handle - check for video or image type
@ -425,7 +435,7 @@ export function WorkflowCanvas() {
sourceHandleId: fromHandleId, sourceHandleId: fromHandleId,
}); });
}, },
[screenToFlowPosition, nodes, handleConnect] [screenToFlowPosition, nodes, edges, handleConnect]
); );
// Handle the splitGrid action - uses automated grid detection // Handle the splitGrid action - uses automated grid detection

2
src/components/__tests__/QuickstartInitialView.test.tsx

@ -75,7 +75,7 @@ describe("QuickstartInitialView", () => {
expect(screen.getByText("Start from scratch")).toBeInTheDocument(); expect(screen.getByText("Start from scratch")).toBeInTheDocument();
expect(screen.getByText("Open existing file")).toBeInTheDocument(); expect(screen.getByText("Open existing file")).toBeInTheDocument();
expect(screen.getByText("Pre-built workflows")).toBeInTheDocument(); expect(screen.getByText("Pre-built workflows")).toBeInTheDocument();
expect(screen.getByText("Describe what you want")).toBeInTheDocument(); expect(screen.getByText("Prompt a workflow")).toBeInTheDocument();
}); });
}); });

8
src/components/nodes/GenerateImageNode.tsx

@ -831,8 +831,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
/> />
)} )}
{/* Aspect ratio and resolution row - only for Gemini-only mode */} {/* Aspect ratio and resolution row - only for Gemini models */}
{isGeminiOnly && ( {currentProvider === "gemini" && (
<div className="flex gap-1.5 shrink-0"> <div className="flex gap-1.5 shrink-0">
<select <select
value={nodeData.aspectRatio} value={nodeData.aspectRatio}
@ -861,8 +861,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</div> </div>
)} )}
{/* Google Search toggle - only for Nano Banana Pro in Gemini-only mode */} {/* Google Search toggle - only for Nano Banana Pro */}
{isGeminiOnly && isNanoBananaPro && ( {currentProvider === "gemini" && isNanoBananaPro && (
<label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer"> <label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer">
<input <input
type="checkbox" type="checkbox"

51
src/utils/imageStorage.ts

@ -177,6 +177,36 @@ async function externalizeNodeImages(
break; break;
} }
case "generateVideo": {
const d = data as import("@/types").GenerateVideoNodeData;
let inputImageRefs = d.inputImageRefs ? [...d.inputImageRefs] : [];
const inputImages: string[] = [];
// Handle input images array (save to inputs)
// Skip if corresponding inputImageRef already exists
for (let i = 0; i < d.inputImages.length; i++) {
const img = d.inputImages[i];
const existingRef = d.inputImageRefs?.[i];
if (existingRef && isBase64DataUrl(img)) {
inputImages.push(""); // Already has ref, just clear the base64
} else if (isBase64DataUrl(img)) {
const ref = await saveImageAndGetId(img, workflowPath, savedImageIds, "inputs");
inputImageRefs[i] = ref;
inputImages.push(""); // Empty placeholder
} else {
inputImages.push(img);
}
}
// Note: outputVideo is a video URL, not saved as an image
newData = {
...d,
inputImages: inputImages.length > 0 && inputImages.every(i => i === "") ? [] : inputImages,
inputImageRefs: inputImageRefs.length > 0 ? inputImageRefs : undefined,
};
break;
}
case "output": { case "output": {
const d = data as import("@/types").OutputNodeData; const d = data as import("@/types").OutputNodeData;
// Output displays generated content, save to generations // Output displays generated content, save to generations
@ -374,6 +404,27 @@ async function hydrateNodeImages(
break; break;
} }
case "generateVideo": {
const d = data as import("@/types").GenerateVideoNodeData;
const inputImages = [...d.inputImages];
// Hydrate input images from refs
if (d.inputImageRefs && d.inputImageRefs.length > 0) {
for (let i = 0; i < d.inputImageRefs.length; i++) {
const ref = d.inputImageRefs[i];
if (ref) {
inputImages[i] = await loadImageById(ref, workflowPath, loadedImages, "inputs");
}
}
}
newData = {
...d,
inputImages,
};
break;
}
case "output": { case "output": {
const d = data as import("@/types").OutputNodeData; const d = data as import("@/types").OutputNodeData;
if (d.imageRef && !d.image) { if (d.imageRef && !d.image) {

Loading…
Cancel
Save