9 changed files with 1128 additions and 439 deletions
File diff suppressed because it is too large
@ -0,0 +1,230 @@ |
|||||
|
import { NextRequest, NextResponse } from "next/server"; |
||||
|
import * as fs from "fs/promises"; |
||||
|
import * as path from "path"; |
||||
|
import { logger } from "@/utils/logger"; |
||||
|
|
||||
|
export const maxDuration = 300; // 5 minute timeout for large image operations
|
||||
|
|
||||
|
const IMAGES_FOLDER = "inputs"; |
||||
|
const LEGACY_IMAGES_FOLDER = ".images"; // For backward compatibility
|
||||
|
|
||||
|
// POST: Save an image to the workflow's inputs or generations folder
|
||||
|
export async function POST(request: NextRequest) { |
||||
|
let workflowPath: string | undefined; |
||||
|
let imageId: string | undefined; |
||||
|
let folder: string | undefined; |
||||
|
try { |
||||
|
const body = await request.json(); |
||||
|
workflowPath = body.workflowPath; |
||||
|
imageId = body.imageId; |
||||
|
folder = body.folder || IMAGES_FOLDER; // Default to "inputs"
|
||||
|
const imageData = body.imageData; // Base64 data URL
|
||||
|
|
||||
|
// Validate folder is one of the allowed values
|
||||
|
if (folder !== IMAGES_FOLDER && folder !== "generations") { |
||||
|
folder = IMAGES_FOLDER; |
||||
|
} |
||||
|
|
||||
|
logger.info('file.save', 'Workflow image save request received', { |
||||
|
workflowPath, |
||||
|
imageId, |
||||
|
folder, |
||||
|
hasImageData: !!imageData, |
||||
|
}); |
||||
|
|
||||
|
if (!workflowPath || !imageId || !imageData) { |
||||
|
logger.warn('file.save', 'Workflow image save validation failed: missing fields', { |
||||
|
hasWorkflowPath: !!workflowPath, |
||||
|
hasImageId: !!imageId, |
||||
|
hasImageData: !!imageData, |
||||
|
}); |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Missing required fields (workflowPath, imageId, imageData)" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Validate workflow directory exists
|
||||
|
try { |
||||
|
const stats = await fs.stat(workflowPath); |
||||
|
if (!stats.isDirectory()) { |
||||
|
logger.warn('file.error', 'Workflow image save failed: path is not a directory', { |
||||
|
workflowPath, |
||||
|
}); |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Workflow path is not a directory" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
} catch (dirError) { |
||||
|
logger.warn('file.error', 'Workflow image save failed: directory does not exist', { |
||||
|
workflowPath, |
||||
|
}); |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Workflow directory does not exist" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Create target folder if it doesn't exist
|
||||
|
const targetFolder = path.join(workflowPath, folder); |
||||
|
try { |
||||
|
await fs.mkdir(targetFolder, { recursive: true }); |
||||
|
} catch (mkdirError) { |
||||
|
logger.error('file.error', 'Failed to create target folder', { |
||||
|
targetFolder, |
||||
|
}, mkdirError instanceof Error ? mkdirError : undefined); |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Failed to create target folder" }, |
||||
|
{ status: 500 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Construct file path
|
||||
|
const filename = `${imageId}.png`; |
||||
|
const filePath = path.join(targetFolder, filename); |
||||
|
|
||||
|
// Extract base64 data and convert to buffer
|
||||
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, ""); |
||||
|
const buffer = Buffer.from(base64Data, "base64"); |
||||
|
|
||||
|
// Write the image file
|
||||
|
await fs.writeFile(filePath, buffer); |
||||
|
|
||||
|
logger.info('file.save', 'Workflow image saved successfully', { |
||||
|
filePath, |
||||
|
imageId, |
||||
|
fileSize: buffer.length, |
||||
|
}); |
||||
|
|
||||
|
return NextResponse.json({ |
||||
|
success: true, |
||||
|
imageId, |
||||
|
filePath, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
logger.error('file.error', 'Failed to save workflow image', { |
||||
|
workflowPath, |
||||
|
imageId, |
||||
|
}, error instanceof Error ? error : undefined); |
||||
|
return NextResponse.json( |
||||
|
{ |
||||
|
success: false, |
||||
|
error: error instanceof Error ? error.message : "Save failed", |
||||
|
}, |
||||
|
{ status: 500 } |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// GET: Load an image from the workflow's folders (inputs, generations, or legacy .images)
|
||||
|
export async function GET(request: NextRequest) { |
||||
|
const workflowPath = request.nextUrl.searchParams.get("workflowPath"); |
||||
|
const imageId = request.nextUrl.searchParams.get("imageId"); |
||||
|
const folder = request.nextUrl.searchParams.get("folder"); // Optional hint for which folder to check first
|
||||
|
|
||||
|
logger.info('file.load', 'Workflow image load request received', { |
||||
|
workflowPath, |
||||
|
imageId, |
||||
|
folder, |
||||
|
}); |
||||
|
|
||||
|
if (!workflowPath || !imageId) { |
||||
|
logger.warn('file.load', 'Workflow image load validation failed: missing parameters', { |
||||
|
hasWorkflowPath: !!workflowPath, |
||||
|
hasImageId: !!imageId, |
||||
|
}); |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Missing required parameters (workflowPath, imageId)" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// Validate workflow directory exists
|
||||
|
try { |
||||
|
const stats = await fs.stat(workflowPath); |
||||
|
if (!stats.isDirectory()) { |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Workflow path is not a directory" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
} catch { |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Workflow directory does not exist" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Construct file path - check folders in order based on hint
|
||||
|
const filename = `${imageId}.png`; |
||||
|
const inputsFolder = path.join(workflowPath, IMAGES_FOLDER); |
||||
|
const generationsFolder = path.join(workflowPath, "generations"); |
||||
|
const legacyFolder = path.join(workflowPath, LEGACY_IMAGES_FOLDER); |
||||
|
|
||||
|
// Build search order based on folder hint
|
||||
|
const searchOrder = folder === "generations" |
||||
|
? [generationsFolder, inputsFolder, legacyFolder] |
||||
|
: [inputsFolder, generationsFolder, legacyFolder]; |
||||
|
|
||||
|
let filePath: string | null = null; |
||||
|
|
||||
|
// Check each folder in order
|
||||
|
for (const searchFolder of searchOrder) { |
||||
|
const candidatePath = path.join(searchFolder, filename); |
||||
|
try { |
||||
|
await fs.access(candidatePath); |
||||
|
filePath = candidatePath; |
||||
|
if (searchFolder === legacyFolder) { |
||||
|
logger.info('file.load', 'Found image in legacy .images folder', { filePath }); |
||||
|
} |
||||
|
break; |
||||
|
} catch { |
||||
|
// File not found in this folder, try next
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!filePath) { |
||||
|
logger.warn('file.error', 'Workflow image load failed: file not found', { |
||||
|
imageId, |
||||
|
searchedFolders: searchOrder, |
||||
|
}); |
||||
|
return NextResponse.json( |
||||
|
{ success: false, error: "Image file not found" }, |
||||
|
{ status: 404 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Read the image file
|
||||
|
const buffer = await fs.readFile(filePath); |
||||
|
|
||||
|
// Convert to base64 data URL
|
||||
|
const base64 = buffer.toString("base64"); |
||||
|
const dataUrl = `data:image/png;base64,${base64}`; |
||||
|
|
||||
|
logger.info('file.load', 'Workflow image loaded successfully', { |
||||
|
filePath, |
||||
|
imageId, |
||||
|
fileSize: buffer.length, |
||||
|
}); |
||||
|
|
||||
|
return NextResponse.json({ |
||||
|
success: true, |
||||
|
imageId, |
||||
|
image: dataUrl, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
logger.error('file.error', 'Failed to load workflow image', { |
||||
|
workflowPath, |
||||
|
imageId, |
||||
|
}, error instanceof Error ? error : undefined); |
||||
|
return NextResponse.json( |
||||
|
{ |
||||
|
success: false, |
||||
|
error: error instanceof Error ? error.message : "Load failed", |
||||
|
}, |
||||
|
{ status: 500 } |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,427 @@ |
|||||
|
import { WorkflowNode, WorkflowNodeData } from "@/types"; |
||||
|
import { WorkflowFile } from "@/store/workflowStore"; |
||||
|
|
||||
|
/** |
||||
|
* Generate a unique image ID for external storage |
||||
|
*/ |
||||
|
export function generateImageId(): string { |
||||
|
const timestamp = Date.now().toString(36); |
||||
|
const random = Math.random().toString(36).substring(2, 8); |
||||
|
return `img-${timestamp}-${random}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a string is a base64 data URL |
||||
|
*/ |
||||
|
function isBase64DataUrl(str: string | null | undefined): str is string { |
||||
|
return typeof str === "string" && str.startsWith("data:"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Extract and save all images from a workflow, replacing base64 data with refs |
||||
|
* Returns a new workflow object with image refs instead of base64 data |
||||
|
*/ |
||||
|
export async function externalizeWorkflowImages( |
||||
|
workflow: WorkflowFile, |
||||
|
workflowPath: string |
||||
|
): Promise<WorkflowFile> { |
||||
|
const externalizedNodes: WorkflowNode[] = []; |
||||
|
const savedImageIds = new Map<string, string>(); // base64 hash -> imageId (for deduplication)
|
||||
|
|
||||
|
for (const node of workflow.nodes) { |
||||
|
const newNode = await externalizeNodeImages(node, workflowPath, savedImageIds); |
||||
|
externalizedNodes.push(newNode); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...workflow, |
||||
|
nodes: externalizedNodes, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Externalize images from a single node |
||||
|
*/ |
||||
|
async function externalizeNodeImages( |
||||
|
node: WorkflowNode, |
||||
|
workflowPath: string, |
||||
|
savedImageIds: Map<string, string> |
||||
|
): Promise<WorkflowNode> { |
||||
|
const data = node.data as WorkflowNodeData; |
||||
|
let newData: WorkflowNodeData; |
||||
|
|
||||
|
switch (node.type) { |
||||
|
case "imageInput": { |
||||
|
const d = data as import("@/types").ImageInputNodeData; |
||||
|
if (isBase64DataUrl(d.image)) { |
||||
|
const imageId = await saveImageAndGetId(d.image, workflowPath, savedImageIds, "inputs"); |
||||
|
newData = { |
||||
|
...d, |
||||
|
image: null, |
||||
|
imageRef: imageId, |
||||
|
}; |
||||
|
} else { |
||||
|
newData = d; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "annotation": { |
||||
|
const d = data as import("@/types").AnnotationNodeData; |
||||
|
let sourceImageRef = d.sourceImageRef; |
||||
|
let outputImageRef = d.outputImageRef; |
||||
|
let sourceImage = d.sourceImage; |
||||
|
let outputImage = d.outputImage; |
||||
|
|
||||
|
// Annotation images are user-created, save to inputs
|
||||
|
if (isBase64DataUrl(d.sourceImage)) { |
||||
|
sourceImageRef = await saveImageAndGetId(d.sourceImage, workflowPath, savedImageIds, "inputs"); |
||||
|
sourceImage = null; |
||||
|
} |
||||
|
if (isBase64DataUrl(d.outputImage)) { |
||||
|
outputImageRef = await saveImageAndGetId(d.outputImage, workflowPath, savedImageIds, "inputs"); |
||||
|
outputImage = null; |
||||
|
} |
||||
|
|
||||
|
newData = { |
||||
|
...d, |
||||
|
sourceImage, |
||||
|
sourceImageRef, |
||||
|
outputImage, |
||||
|
outputImageRef, |
||||
|
}; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "nanoBanana": { |
||||
|
const d = data as import("@/types").NanoBananaNodeData; |
||||
|
let outputImageRef = d.outputImageRef; |
||||
|
let outputImage = d.outputImage; |
||||
|
const inputImageRefs: string[] = []; |
||||
|
const inputImages: string[] = []; |
||||
|
|
||||
|
// Handle output image - AI generated, save to generations
|
||||
|
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)) { |
||||
|
const ref = await saveImageAndGetId(img, workflowPath, savedImageIds, "inputs"); |
||||
|
inputImageRefs.push(ref); |
||||
|
inputImages.push(""); // Empty placeholder
|
||||
|
} else { |
||||
|
inputImages.push(img); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
newData = { |
||||
|
...d, |
||||
|
inputImages: inputImages.length > 0 && inputImages.every(i => i === "") ? [] : inputImages, |
||||
|
inputImageRefs: inputImageRefs.length > 0 ? inputImageRefs : undefined, |
||||
|
outputImage, |
||||
|
outputImageRef, |
||||
|
}; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "llmGenerate": { |
||||
|
const d = data as import("@/types").LLMGenerateNodeData; |
||||
|
const inputImageRefs: string[] = []; |
||||
|
const inputImages: string[] = []; |
||||
|
|
||||
|
// Handle input images array (save to inputs)
|
||||
|
for (const img of d.inputImages) { |
||||
|
if (isBase64DataUrl(img)) { |
||||
|
const ref = await saveImageAndGetId(img, workflowPath, savedImageIds, "inputs"); |
||||
|
inputImageRefs.push(ref); |
||||
|
inputImages.push(""); // Empty placeholder
|
||||
|
} else { |
||||
|
inputImages.push(img); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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
|
||||
|
if (isBase64DataUrl(d.image)) { |
||||
|
const imageId = await saveImageAndGetId(d.image, workflowPath, savedImageIds, "generations"); |
||||
|
newData = { |
||||
|
...d, |
||||
|
image: null, |
||||
|
imageRef: imageId, |
||||
|
}; |
||||
|
} else { |
||||
|
newData = d; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "splitGrid": { |
||||
|
const d = data as import("@/types").SplitGridNodeData; |
||||
|
// SplitGrid source is input content, save to inputs
|
||||
|
if (isBase64DataUrl(d.sourceImage)) { |
||||
|
const imageId = await saveImageAndGetId(d.sourceImage, workflowPath, savedImageIds, "inputs"); |
||||
|
newData = { |
||||
|
...d, |
||||
|
sourceImage: null, |
||||
|
sourceImageRef: imageId, |
||||
|
}; |
||||
|
} else { |
||||
|
newData = d; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
newData = data; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...node, |
||||
|
data: newData, |
||||
|
} as WorkflowNode; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Save an image and return its ID (with deduplication) |
||||
|
* @param folder - "inputs" for user-uploaded images, "generations" for AI-generated images |
||||
|
*/ |
||||
|
async function saveImageAndGetId( |
||||
|
imageData: string, |
||||
|
workflowPath: string, |
||||
|
savedImageIds: Map<string, string>, |
||||
|
folder: "inputs" | "generations" = "inputs" |
||||
|
): Promise<string> { |
||||
|
// Create a hash using length + samples from different parts of the data
|
||||
|
// This avoids issues where all images of the same format have identical headers
|
||||
|
// Include folder in hash so same image in different folders gets different IDs
|
||||
|
const len = imageData.length; |
||||
|
const mid = Math.floor(len / 2); |
||||
|
const hash = `${folder}-${len}-${imageData.substring(50, 100)}-${imageData.substring(mid, mid + 50)}-${imageData.substring(Math.max(0, len - 50))}`; |
||||
|
|
||||
|
if (savedImageIds.has(hash)) { |
||||
|
return savedImageIds.get(hash)!; |
||||
|
} |
||||
|
|
||||
|
const imageId = generateImageId(); |
||||
|
|
||||
|
const response = await fetch("/api/workflow-images", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ |
||||
|
workflowPath, |
||||
|
imageId, |
||||
|
imageData, |
||||
|
folder, |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
const result = await response.json(); |
||||
|
|
||||
|
if (!result.success) { |
||||
|
throw new Error(`Failed to save image: ${result.error}`); |
||||
|
} |
||||
|
|
||||
|
savedImageIds.set(hash, imageId); |
||||
|
return imageId; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Load all external images into a workflow, replacing refs with base64 data |
||||
|
* Returns a new workflow object with base64 data instead of refs |
||||
|
*/ |
||||
|
export async function hydrateWorkflowImages( |
||||
|
workflow: WorkflowFile, |
||||
|
workflowPath: string |
||||
|
): Promise<WorkflowFile> { |
||||
|
const hydratedNodes: WorkflowNode[] = []; |
||||
|
const loadedImages = new Map<string, string>(); // imageId -> base64 (for caching)
|
||||
|
|
||||
|
for (const node of workflow.nodes) { |
||||
|
const newNode = await hydrateNodeImages(node, workflowPath, loadedImages); |
||||
|
hydratedNodes.push(newNode); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...workflow, |
||||
|
nodes: hydratedNodes, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Hydrate images for a single node |
||||
|
*/ |
||||
|
async function hydrateNodeImages( |
||||
|
node: WorkflowNode, |
||||
|
workflowPath: string, |
||||
|
loadedImages: Map<string, string> |
||||
|
): Promise<WorkflowNode> { |
||||
|
const data = node.data as WorkflowNodeData; |
||||
|
let newData: WorkflowNodeData; |
||||
|
|
||||
|
switch (node.type) { |
||||
|
case "imageInput": { |
||||
|
const d = data as import("@/types").ImageInputNodeData; |
||||
|
if (d.imageRef && !d.image) { |
||||
|
const image = await loadImageById(d.imageRef, workflowPath, loadedImages, "inputs"); |
||||
|
newData = { |
||||
|
...d, |
||||
|
image, |
||||
|
}; |
||||
|
} else { |
||||
|
newData = d; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "annotation": { |
||||
|
const d = data as import("@/types").AnnotationNodeData; |
||||
|
let sourceImage = d.sourceImage; |
||||
|
let outputImage = d.outputImage; |
||||
|
|
||||
|
if (d.sourceImageRef && !d.sourceImage) { |
||||
|
sourceImage = await loadImageById(d.sourceImageRef, workflowPath, loadedImages, "inputs"); |
||||
|
} |
||||
|
if (d.outputImageRef && !d.outputImage) { |
||||
|
outputImage = await loadImageById(d.outputImageRef, workflowPath, loadedImages, "inputs"); |
||||
|
} |
||||
|
|
||||
|
newData = { |
||||
|
...d, |
||||
|
sourceImage, |
||||
|
outputImage, |
||||
|
}; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "nanoBanana": { |
||||
|
const d = data as import("@/types").NanoBananaNodeData; |
||||
|
let outputImage = d.outputImage; |
||||
|
const inputImages = [...d.inputImages]; |
||||
|
|
||||
|
if (d.outputImageRef && !d.outputImage) { |
||||
|
outputImage = await loadImageById(d.outputImageRef, workflowPath, loadedImages, "generations"); |
||||
|
} |
||||
|
|
||||
|
// 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, |
||||
|
outputImage, |
||||
|
}; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "llmGenerate": { |
||||
|
const d = data as import("@/types").LLMGenerateNodeData; |
||||
|
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) { |
||||
|
const image = await loadImageById(d.imageRef, workflowPath, loadedImages, "generations"); |
||||
|
newData = { |
||||
|
...d, |
||||
|
image, |
||||
|
}; |
||||
|
} else { |
||||
|
newData = d; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "splitGrid": { |
||||
|
const d = data as import("@/types").SplitGridNodeData; |
||||
|
if (d.sourceImageRef && !d.sourceImage) { |
||||
|
const sourceImage = await loadImageById(d.sourceImageRef, workflowPath, loadedImages, "inputs"); |
||||
|
newData = { |
||||
|
...d, |
||||
|
sourceImage, |
||||
|
}; |
||||
|
} else { |
||||
|
newData = d; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
newData = data; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...node, |
||||
|
data: newData, |
||||
|
} as WorkflowNode; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Load an image by ID (with caching) |
||||
|
* @param folder - Optional hint for which folder to check first |
||||
|
*/ |
||||
|
async function loadImageById( |
||||
|
imageId: string, |
||||
|
workflowPath: string, |
||||
|
loadedImages: Map<string, string>, |
||||
|
folder?: "inputs" | "generations" |
||||
|
): Promise<string> { |
||||
|
if (loadedImages.has(imageId)) { |
||||
|
return loadedImages.get(imageId)!; |
||||
|
} |
||||
|
|
||||
|
const params = new URLSearchParams({ |
||||
|
workflowPath, |
||||
|
imageId, |
||||
|
}); |
||||
|
if (folder) { |
||||
|
params.set("folder", folder); |
||||
|
} |
||||
|
|
||||
|
const response = await fetch(`/api/workflow-images?${params.toString()}`); |
||||
|
|
||||
|
const result = await response.json(); |
||||
|
|
||||
|
if (!result.success) { |
||||
|
console.error(`Failed to load image ${imageId}: ${result.error}`); |
||||
|
return ""; // Return empty string on error to avoid breaking the workflow
|
||||
|
} |
||||
|
|
||||
|
loadedImages.set(imageId, result.image); |
||||
|
return result.image; |
||||
|
} |
||||
Loading…
Reference in new issue