14 changed files with 442 additions and 15 deletions
@ -0,0 +1,26 @@ |
|||
import { logGatewayRequest } from "@/app/api/_gatewayLogging"; |
|||
|
|||
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]); |
|||
|
|||
export const SKIP_GENERATION_GATEWAY_ENV = "SKIP_GENERATION_GATEWAY"; |
|||
|
|||
export function shouldSkipGenerationGateway(): boolean { |
|||
return TRUE_VALUES.has((process.env[SKIP_GENERATION_GATEWAY_ENV] || "").trim().toLowerCase()); |
|||
} |
|||
|
|||
export function logSkippedGenerationGatewayRequest( |
|||
label: string, |
|||
url: string, |
|||
init: RequestInit, |
|||
context?: Record<string, unknown> |
|||
): void { |
|||
logGatewayRequest(label, url, init, { |
|||
...(context || {}), |
|||
skipped: true, |
|||
skipEnv: SKIP_GENERATION_GATEWAY_ENV, |
|||
}); |
|||
} |
|||
|
|||
export function skippedGenerationGatewayMessage(provider: string): string { |
|||
return `${provider} generation gateway request skipped because ${SKIP_GENERATION_GATEWAY_ENV}=true. Check api.gateway logs for passed parameters.`; |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
import { beforeEach, describe, expect, it, vi } from "vitest"; |
|||
import type { WorkflowEdge, WorkflowNode } from "@/types"; |
|||
import { migrateLegacyWorkflowToSmartNodes } from "../workflowMigration"; |
|||
|
|||
const localStorageMock = (() => { |
|||
let store: Record<string, string> = {}; |
|||
return { |
|||
getItem: vi.fn((key: string) => store[key] ?? null), |
|||
setItem: vi.fn((key: string, value: string) => { |
|||
store[key] = value; |
|||
}), |
|||
clear: vi.fn(() => { |
|||
store = {}; |
|||
}), |
|||
}; |
|||
})(); |
|||
|
|||
Object.defineProperty(globalThis, "localStorage", { |
|||
value: localStorageMock, |
|||
writable: true, |
|||
}); |
|||
|
|||
function node(id: string, type: WorkflowNode["type"], data: Record<string, unknown>): WorkflowNode { |
|||
return { |
|||
id, |
|||
type, |
|||
position: { x: 0, y: 0 }, |
|||
data, |
|||
} as WorkflowNode; |
|||
} |
|||
|
|||
describe("migrateLegacyWorkflowToSmartNodes", () => { |
|||
beforeEach(() => { |
|||
localStorageMock.clear(); |
|||
vi.clearAllMocks(); |
|||
}); |
|||
|
|||
it("leaves smart nodes unchanged", () => { |
|||
const smartImage = node("smartImage-1", "smartImage", { image: "image-url" }); |
|||
const workflow = { |
|||
version: 1 as const, |
|||
name: "workflow", |
|||
nodes: [smartImage], |
|||
edges: [] as WorkflowEdge[], |
|||
edgeStyle: "angular" as const, |
|||
}; |
|||
|
|||
const result = migrateLegacyWorkflowToSmartNodes(workflow); |
|||
|
|||
expect(result.changed).toBe(false); |
|||
expect(result.workflow).toBe(workflow); |
|||
expect(result.workflow.nodes[0]).toBe(smartImage); |
|||
}); |
|||
|
|||
it("converts legacy input and generation nodes to smart nodes", () => { |
|||
const workflow = { |
|||
version: 1 as const, |
|||
name: "workflow", |
|||
nodes: [ |
|||
node("imageInput-1", "imageInput", { image: "uploaded-image", filename: "a.png", dimensions: null }), |
|||
node("nanoBanana-1", "nanoBanana", { outputImage: "generated-image", previewImg: "preview-image" }), |
|||
node("videoInput-1", "videoInput", { video: "uploaded-video", filename: "v.mp4" }), |
|||
node("generateVideo-1", "generateVideo", { outputVideo: "generated-video" }), |
|||
node("audioInput-1", "audioInput", { audioFile: "uploaded-audio", filename: "a.mp3" }), |
|||
node("generateAudio-1", "generateAudio", { outputAudio: "generated-audio" }), |
|||
node("prompt-1", "prompt", { prompt: "manual text" }), |
|||
node("llmGenerate-1", "llmGenerate", { inputPrompt: "write", outputText: "generated text" }), |
|||
], |
|||
edges: [] as WorkflowEdge[], |
|||
edgeStyle: "angular" as const, |
|||
}; |
|||
|
|||
const result = migrateLegacyWorkflowToSmartNodes(workflow); |
|||
|
|||
expect(result.changed).toBe(true); |
|||
expect(result.workflow.nodes.map((item) => item.type)).toEqual([ |
|||
"smartImage", |
|||
"smartImage", |
|||
"smartVideo", |
|||
"smartVideo", |
|||
"smartAudio", |
|||
"smartAudio", |
|||
"smartText", |
|||
"smartText", |
|||
]); |
|||
expect(result.workflow.nodes[0].data).toMatchObject({ image: "uploaded-image", filename: "a.png" }); |
|||
expect(result.workflow.nodes[1].data).toMatchObject({ image: "generated-image", outputImage: "generated-image", previewImage: "preview-image" }); |
|||
expect(result.workflow.nodes[2].data).toMatchObject({ video: "uploaded-video", filename: "v.mp4" }); |
|||
expect(result.workflow.nodes[3].data).toMatchObject({ video: "generated-video", outputVideo: "generated-video" }); |
|||
expect(result.workflow.nodes[4].data).toMatchObject({ audioFile: "uploaded-audio", filename: "a.mp3" }); |
|||
expect(result.workflow.nodes[5].data).toMatchObject({ audioFile: "generated-audio", outputAudio: "generated-audio" }); |
|||
expect(result.workflow.nodes[6].data).toMatchObject({ manualLocked: true, manualText: "manual text", outputText: null }); |
|||
expect(result.workflow.nodes[7].data).toMatchObject({ manualLocked: false, inputPrompt: "write", outputText: "generated text" }); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,179 @@ |
|||
import type { |
|||
GenerateAudioNodeData, |
|||
GenerateVideoNodeData, |
|||
AudioInputNodeData, |
|||
ImageInputNodeData, |
|||
LLMGenerateNodeData, |
|||
NanoBananaNodeData, |
|||
PromptNodeData, |
|||
VideoInputNodeData, |
|||
WorkflowNode, |
|||
} from "@/types"; |
|||
import { createDefaultNodeData } from "./nodeDefaults"; |
|||
|
|||
type WorkflowWithNodes<TNode extends WorkflowNode = WorkflowNode> = { |
|||
nodes: TNode[]; |
|||
}; |
|||
|
|||
type LegacyNanoBananaData = NanoBananaNodeData & { |
|||
image?: string | null; |
|||
previewImage?: string; |
|||
}; |
|||
|
|||
type LegacyGenerateVideoData = GenerateVideoNodeData & { |
|||
video?: string | null; |
|||
}; |
|||
|
|||
type LegacyGenerateAudioData = GenerateAudioNodeData & { |
|||
audioFile?: string | null; |
|||
}; |
|||
|
|||
function migrateLegacyNodeToSmartNode(node: WorkflowNode): { node: WorkflowNode; changed: boolean } { |
|||
switch (node.type) { |
|||
case "imageInput": |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartImage", |
|||
data: { |
|||
...createDefaultNodeData("smartImage"), |
|||
...(node.data as ImageInputNodeData), |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
|
|||
case "nanoBanana": { |
|||
const data = node.data as LegacyNanoBananaData; |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartImage", |
|||
data: { |
|||
...createDefaultNodeData("smartImage"), |
|||
...data, |
|||
image: data.image ?? data.outputImage ?? null, |
|||
previewImage: data.previewImage ?? data.previewImg, |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
} |
|||
|
|||
case "videoInput": |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartVideo", |
|||
data: { |
|||
...createDefaultNodeData("smartVideo"), |
|||
...(node.data as VideoInputNodeData), |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
|
|||
case "generateVideo": { |
|||
const data = node.data as LegacyGenerateVideoData; |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartVideo", |
|||
data: { |
|||
...createDefaultNodeData("smartVideo"), |
|||
...data, |
|||
video: data.video ?? data.outputVideo ?? null, |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
} |
|||
|
|||
case "audioInput": |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartAudio", |
|||
data: { |
|||
...createDefaultNodeData("smartAudio"), |
|||
...(node.data as AudioInputNodeData), |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
|
|||
case "generateAudio": { |
|||
const data = node.data as LegacyGenerateAudioData; |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartAudio", |
|||
data: { |
|||
...createDefaultNodeData("smartAudio"), |
|||
...data, |
|||
audioFile: data.audioFile ?? data.outputAudio ?? null, |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
} |
|||
|
|||
case "prompt": { |
|||
const data = node.data as PromptNodeData; |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartText", |
|||
data: { |
|||
...createDefaultNodeData("smartText"), |
|||
...data, |
|||
manualLocked: true, |
|||
manualText: data.prompt ?? "", |
|||
outputText: null, |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
} |
|||
|
|||
case "llmGenerate": |
|||
return { |
|||
changed: true, |
|||
node: { |
|||
...node, |
|||
type: "smartText", |
|||
data: { |
|||
...createDefaultNodeData("smartText"), |
|||
...(node.data as LLMGenerateNodeData), |
|||
manualLocked: false, |
|||
}, |
|||
} as WorkflowNode, |
|||
}; |
|||
|
|||
default: |
|||
return { node, changed: false }; |
|||
} |
|||
} |
|||
|
|||
export function migrateLegacyWorkflowToSmartNodes<TWorkflow extends WorkflowWithNodes>( |
|||
workflow: TWorkflow |
|||
): { workflow: TWorkflow; changed: boolean } { |
|||
let changed = false; |
|||
const nodes = workflow.nodes.map((node) => { |
|||
const result = migrateLegacyNodeToSmartNode(node); |
|||
if (result.changed) changed = true; |
|||
return result.node; |
|||
}); |
|||
|
|||
if (!changed) { |
|||
return { workflow, changed: false }; |
|||
} |
|||
|
|||
return { |
|||
changed: true, |
|||
workflow: { |
|||
...workflow, |
|||
nodes, |
|||
}, |
|||
}; |
|||
} |
|||
@ -1,3 +1,5 @@ |
|||
export function isSmartGenerateUploadEnabled(): boolean { |
|||
return true; |
|||
import type { ProviderEnvStatus } from "@/types"; |
|||
|
|||
export function isSmartGenerateUploadEnabled(providerEnvStatus: ProviderEnvStatus | null): boolean { |
|||
return providerEnvStatus?.smartGenerateUpload !== false; |
|||
} |
|||
|
|||
Loading…
Reference in new issue