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 { |
import type { ProviderEnvStatus } from "@/types"; |
||||
return true; |
|
||||
|
export function isSmartGenerateUploadEnabled(providerEnvStatus: ProviderEnvStatus | null): boolean { |
||||
|
return providerEnvStatus?.smartGenerateUpload !== false; |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue