diff --git a/src/components/__tests__/QuickstartInitialView.test.tsx b/src/components/__tests__/QuickstartInitialView.test.tsx index 98e13193..31a89128 100644 --- a/src/components/__tests__/QuickstartInitialView.test.tsx +++ b/src/components/__tests__/QuickstartInitialView.test.tsx @@ -209,7 +209,7 @@ describe("QuickstartInitialView", () => { expect(twitterLink).toHaveAttribute("rel", "noopener noreferrer"); }); - it("should render docs coming soon text (not as a link)", () => { + it("should render docs link", () => { render( { /> ); - expect(screen.getByText("Docs (coming soon)")).toBeInTheDocument(); - expect(screen.getByText("Docs (coming soon)").closest("a")).toBeNull(); + const docsLink = screen.getByText("Docs").closest("a"); + expect(docsLink).toHaveAttribute("href", "https://node-banana-docs.vercel.app/"); + expect(docsLink).toHaveAttribute("target", "_blank"); + expect(docsLink).toHaveAttribute("rel", "noopener noreferrer"); }); }); diff --git a/src/utils/imageStorage.ts b/src/utils/imageStorage.ts index 79875395..bc9eab44 100644 --- a/src/utils/imageStorage.ts +++ b/src/utils/imageStorage.ts @@ -62,13 +62,12 @@ async function externalizeNodeImages( switch (node.type) { case "imageInput": { const d = data as import("@/types").ImageInputNodeData; - if (isBase64DataUrl(d.image)) { + // Skip if already has a valid imageRef (prevents duplicates on re-save after hydration) + if (d.imageRef && isBase64DataUrl(d.image)) { + newData = { ...d, image: null }; + } else if (isBase64DataUrl(d.image)) { const imageId = await saveImageAndGetId(d.image, workflowPath, savedImageIds, "inputs"); - newData = { - ...d, - image: null, - imageRef: imageId, - }; + newData = { ...d, image: null, imageRef: imageId }; } else { newData = d; } @@ -83,11 +82,16 @@ async function externalizeNodeImages( let outputImage = d.outputImage; // Annotation images are user-created, save to inputs - if (isBase64DataUrl(d.sourceImage)) { + // Skip if already has ref (prevents duplicates on re-save after hydration) + if (d.sourceImageRef && isBase64DataUrl(d.sourceImage)) { + sourceImage = null; + } else if (isBase64DataUrl(d.sourceImage)) { sourceImageRef = await saveImageAndGetId(d.sourceImage, workflowPath, savedImageIds, "inputs"); sourceImage = null; } - if (isBase64DataUrl(d.outputImage)) { + if (d.outputImageRef && isBase64DataUrl(d.outputImage)) { + outputImage = null; + } else if (isBase64DataUrl(d.outputImage)) { outputImageRef = await saveImageAndGetId(d.outputImage, workflowPath, savedImageIds, "inputs"); outputImage = null; } @@ -106,20 +110,28 @@ async function externalizeNodeImages( const d = data as import("@/types").NanoBananaNodeData; let outputImageRef = d.outputImageRef; let outputImage = d.outputImage; - const inputImageRefs: string[] = []; + let inputImageRefs = d.inputImageRefs ? [...d.inputImageRefs] : []; const inputImages: string[] = []; // Handle output image - AI generated, save to generations - if (isBase64DataUrl(d.outputImage)) { + // Skip if already has ref (prevents duplicates on re-save after hydration) + if (d.outputImageRef && isBase64DataUrl(d.outputImage)) { + outputImage = null; + } else if (isBase64DataUrl(d.outputImage)) { outputImageRef = await saveImageAndGetId(d.outputImage, workflowPath, savedImageIds, "generations"); outputImage = null; } // Handle input images array (these come from connected nodes, save to inputs if present) - for (const img of d.inputImages) { - if (isBase64DataUrl(img)) { + // 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.push(ref); + inputImageRefs[i] = ref; inputImages.push(""); // Empty placeholder } else { inputImages.push(img); @@ -138,14 +150,19 @@ async function externalizeNodeImages( case "llmGenerate": { const d = data as import("@/types").LLMGenerateNodeData; - const inputImageRefs: string[] = []; + let inputImageRefs = d.inputImageRefs ? [...d.inputImageRefs] : []; const inputImages: string[] = []; // Handle input images array (save to inputs) - for (const img of d.inputImages) { - if (isBase64DataUrl(img)) { + // 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.push(ref); + inputImageRefs[i] = ref; inputImages.push(""); // Empty placeholder } else { inputImages.push(img); @@ -163,13 +180,12 @@ async function externalizeNodeImages( case "output": { const d = data as import("@/types").OutputNodeData; // Output displays generated content, save to generations - if (isBase64DataUrl(d.image)) { + // Skip if already has ref (prevents duplicates on re-save after hydration) + if (d.imageRef && isBase64DataUrl(d.image)) { + newData = { ...d, image: null }; + } else if (isBase64DataUrl(d.image)) { const imageId = await saveImageAndGetId(d.image, workflowPath, savedImageIds, "generations"); - newData = { - ...d, - image: null, - imageRef: imageId, - }; + newData = { ...d, image: null, imageRef: imageId }; } else { newData = d; } @@ -179,13 +195,12 @@ async function externalizeNodeImages( case "splitGrid": { const d = data as import("@/types").SplitGridNodeData; // SplitGrid source is input content, save to inputs - if (isBase64DataUrl(d.sourceImage)) { + // Skip if already has ref (prevents duplicates on re-save after hydration) + if (d.sourceImageRef && isBase64DataUrl(d.sourceImage)) { + newData = { ...d, sourceImage: null }; + } else if (isBase64DataUrl(d.sourceImage)) { const imageId = await saveImageAndGetId(d.sourceImage, workflowPath, savedImageIds, "inputs"); - newData = { - ...d, - sourceImage: null, - sourceImageRef: imageId, - }; + newData = { ...d, sourceImage: null, sourceImageRef: imageId }; } else { newData = d; }