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 }> };
if (nodeData.inputSchema && nodeData.inputSchema.length > 0) {
if (needInput) {
// Find first input handle matching the type
// Find input handles matching the type
const matchingInputs = nodeData.inputSchema.filter(i => i.type === handleType);
if (matchingInputs.length > 0) {
// Return normalized handle ID, not schema name
// Always use indexed IDs (image-0, text-0) for schema inputs for consistency
return `${handleType}-0`;
const numHandles = matchingInputs.length;
if (numHandles > 0) {
// Find the first unoccupied indexed handle by checking existing edges
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
@ -425,7 +435,7 @@ export function WorkflowCanvas() {
sourceHandleId: fromHandleId,
});
},
[screenToFlowPosition, nodes, handleConnect]
[screenToFlowPosition, nodes, edges, handleConnect]
);
// 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("Open existing file")).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 */}
{isGeminiOnly && (
{/* Aspect ratio and resolution row - only for Gemini models */}
{currentProvider === "gemini" && (
<div className="flex gap-1.5 shrink-0">
<select
value={nodeData.aspectRatio}
@ -861,8 +861,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</div>
)}
{/* Google Search toggle - only for Nano Banana Pro in Gemini-only mode */}
{isGeminiOnly && isNanoBananaPro && (
{/* Google Search toggle - only for Nano Banana Pro */}
{currentProvider === "gemini" && isNanoBananaPro && (
<label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer">
<input
type="checkbox"

51
src/utils/imageStorage.ts

@ -177,6 +177,36 @@ async function externalizeNodeImages(
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": {
const d = data as import("@/types").OutputNodeData;
// Output displays generated content, save to generations
@ -374,6 +404,27 @@ async function hydrateNodeImages(
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": {
const d = data as import("@/types").OutputNodeData;
if (d.imageRef && !d.image) {

Loading…
Cancel
Save