diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx index 8dc268c3b..b00f46c78 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx @@ -119,7 +119,9 @@ export default function NodeInputField({ colors={colors} setFilterEdge={setFilterEdge} showNode={showNode} - testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`} + testIdComplement={`${data?.type?.toLowerCase()}-${ + showNode ? "shownode" : "noshownode" + }`} nodeId={data.id} colorName={colorName} /> diff --git a/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx b/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx index e18ab2ad5..630522df6 100644 --- a/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx +++ b/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx @@ -8,10 +8,10 @@ import { useShallow } from "zustand/react/shallow"; import IconComponent from "@/components/common/genericIconComponent"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs-button"; +import { useIsAutoLogin } from "@/hooks/use-is-auto-login"; import useAuthStore from "@/stores/authStore"; import useFlowStore from "@/stores/flowStore"; import { useTweaksStore } from "@/stores/tweaksStore"; -import { tabsArrayType } from "@/types/tabs"; import { hasStreaming } from "@/utils/reactflowUtils"; import { getOS } from "@/utils/utils"; import { useDarkStore } from "../../../stores/darkStore"; @@ -34,7 +34,8 @@ const operatingSystemTabs = [ ]; export default function APITabsComponent() { - const [isCopied, setIsCopied] = useState(false); + const [isCopied, setIsCopied] = useState(false); + const [copiedStep, setCopiedStep] = useState(null); const endpointName = useFlowStore( useShallow((state) => state.currentFlow?.endpoint_name), ); @@ -87,18 +88,28 @@ export default function APITabsComponent() { )?.name || "macoslinux", ); + const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const isAutoLogin = useIsAutoLogin(); + const shouldDisplayApiKey = isAuthenticated && !isAutoLogin; + const tabsList = [ { title: "Python", icon: "BWPython", language: "python", - code: getNewPythonApiCode(codeOptions), + code: getNewPythonApiCode({ + ...codeOptions, + shouldDisplayApiKey, + }), }, { title: "JavaScript", icon: "javascript", language: "javascript", - code: getNewJsApiCode(codeOptions), + code: getNewJsApiCode({ + ...codeOptions, + shouldDisplayApiKey, + }), }, { title: "cURL", @@ -107,31 +118,41 @@ export default function APITabsComponent() { code: getNewCurlCode({ ...codeOptions, platform: selectedPlatform === "windows" ? "powershell" : "unix", + shouldDisplayApiKey, }), }, ]; const [selectedTab, setSelectedTab] = useState("Python"); - const copyToClipboard = () => { + const copyToClipboard = (codeText?: string, stepId?: string) => { if (!navigator.clipboard || !navigator.clipboard.writeText) { return; } const currentTab = tabsList.find((tab) => tab.title === selectedTab); - if (currentTab) { - navigator.clipboard.writeText(currentTab.code).then(() => { - setIsCopied(true); - - setTimeout(() => { - setIsCopied(false); - }, 2000); + const textToCopy = + codeText || (typeof currentTab?.code === "string" ? currentTab.code : ""); + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(() => { + if (stepId) { + setCopiedStep(stepId); + setTimeout(() => { + setCopiedStep(null); + }, 2000); + } else { + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 2000); + } }); } }; useEffect(() => { setIsCopied(false); + setCopiedStep(null); }, [selectedTab, selectedPlatform]); const currentTab = tabsList.find((tab) => tab.title === selectedTab); @@ -180,40 +201,110 @@ export default function APITabsComponent() { )} {/* Code content */} - {currentTab && ( -
-
- - - {currentTab.code} - -
-
- )} + {currentTab && + (() => { + // Work directly with structured data - no parsing needed + const codeData = currentTab.code; + const hasSteps = + typeof codeData === "object" && + codeData !== null && + "steps" in codeData; + + if (hasSteps) { + const steps = ( + codeData as { steps: { title: string; code: string }[] } + ).steps; + return ( +
+ {steps.map((step, index) => ( +
+

{step.title}

+
+ + 0} + wrapLongLines={true} + language={currentTab.language} + style={dark ? oneDark : oneLight} + className={`!mt-0 ${ + index === steps.length - 1 ? "h-full" : "" + } w-full overflow-scroll !rounded-b-md border border-border text-left !custom-scroll`} + > + {step.code} + +
+
+ ))} +
+ ); + } else { + return ( +
+
+ + + {currentTab.code} + +
+
+ ); + } + })()} ); diff --git a/src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts b/src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts new file mode 100644 index 000000000..a0f33d77d --- /dev/null +++ b/src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts @@ -0,0 +1,484 @@ +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getChatInputNodeId, + getFileNodeId, + getNonFileTypeTweaks, + hasChatInputFiles, + hasFileTweaks, +} from "../detect-file-tweaks"; +import { getNewCurlCode } from "../get-curl-code"; +import { getNewJsApiCode } from "../get-js-api-code"; +import { getNewPythonApiCode } from "../get-python-api-code"; + +describe("API Snippet Generation Utilities", () => { + describe("File Tweak Detection", () => { + it("should detect File path array", () => { + expect(hasFileTweaks({ node1: { path: [] } })).toBe(true); + }); + + it("should detect VideoFile file_path string", () => { + expect(hasFileTweaks({ node2: { file_path: "path" } })).toBe(true); + }); + + it("should detect ChatInput files string", () => { + expect(hasFileTweaks({ node3: { files: "path" } })).toBe(true); + }); + + it("should return false for no files", () => { + expect(hasFileTweaks({ node: { other: "value" } })).toBe(false); + }); + + it("should detect ChatInput files specifically", () => { + expect(hasChatInputFiles({ node: { files: "path" } })).toBe(true); + expect(hasChatInputFiles({ node: { path: [] } })).toBe(false); + }); + + it("should get single ChatInput node ID", () => { + expect(getChatInputNodeId({ node1: { files: "path" } })).toBe("node1"); + expect(getChatInputNodeId({ node1: { other: "value" } })).toBeNull(); + }); + + it("should get single File node ID", () => { + expect(getFileNodeId({ node1: { path: [] } })).toBe("node1"); + expect(getFileNodeId({ node1: { file_path: "path" } })).toBe("node1"); + expect(getFileNodeId({ node1: { files: "path" } })).toBeNull(); + }); + + it("should get all ChatInput node IDs", () => { + expect( + getAllChatInputNodeIds({ + node1: { files: "p1" }, + node2: { files: "p2" }, + node3: { other: "v" }, + }), + ).toEqual(["node1", "node2"]); + }); + + it("should get all File and VideoFile node IDs", () => { + expect( + getAllFileNodeIds({ + node1: { path: [] }, + node2: { file_path: "p" }, + node3: { files: "p" }, + node4: { other: "v" }, + }), + ).toEqual(["node1", "node2"]); + }); + + it("should filter out file-related tweaks", () => { + const tweaks = { + node1: { path: [] }, + node2: { file_path: "p" }, + node3: { files: "p" }, + node4: { other: "value" }, + node5: { param: 123 }, + }; + const result = getNonFileTypeTweaks(tweaks); + expect(result).toEqual({ + node4: { other: "value" }, + node5: { param: 123 }, + }); + }); + }); + + describe("API Code Generation", () => { + const baseOptions = { + flowId: "test-flow-id", + endpointName: "test-endpoint", + processedPayload: { + output_type: "chat", + input_type: "chat", + input_value: "Hello", + }, + shouldDisplayApiKey: true, + }; + + const noAuthOptions = { + ...baseOptions, + shouldDisplayApiKey: false, + }; + + describe("Python Code Generation", () => { + it("should generate basic Python code with API key authentication", () => { + const code = getNewPythonApiCode(baseOptions); + + // Check for required imports + expect(code).toContain("import requests"); + expect(code).toContain("import uuid"); + + // Check for API key + expect(code).toContain("api_key = 'YOUR_API_KEY_HERE'"); + expect(code).toContain('headers = {"x-api-key": api_key}'); + + // Check for session_id + expect(code).toContain('payload["session_id"] = str(uuid.uuid4())'); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate basic Python code without API key authentication", () => { + const code = getNewPythonApiCode(noAuthOptions); + + // Check for required imports + expect(code).toContain("import requests"); + expect(code).toContain("import uuid"); + + // Should not contain API key + expect(code).not.toContain("api_key = 'YOUR_API_KEY_HERE'"); + expect(code).not.toContain('headers = {"x-api-key": api_key}'); + + // Check for session_id + expect(code).toContain('payload["session_id"] = str(uuid.uuid4())'); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate multi-step code for file uploads with authentication", () => { + const optionsWithFiles = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewPythonApiCode(optionsWithFiles); + + // Check for API key in file upload sections + expect(code).toContain("api_key = 'YOUR_API_KEY_HERE'"); + expect(code).toContain('headers = {"x-api-key": api_key}'); + + // Check for file upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + expect(code).toContain("with open"); + expect(code).toContain('files={"file": f}'); + }); + + it("should generate multi-step code for file uploads without authentication", () => { + const optionsWithFiles = { + ...noAuthOptions, + processedPayload: { + ...noAuthOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewPythonApiCode(optionsWithFiles); + + // Should not contain API key + expect(code).not.toContain("api_key = 'YOUR_API_KEY_HERE'"); + + // Check for file upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + expect(code).toContain("with open"); + expect(code).toContain('files={"file": f}'); + }); + }); + + describe("JavaScript Code Generation", () => { + it("should generate basic JavaScript code with API key authentication", () => { + const code = getNewJsApiCode(baseOptions); + + // Check for API key + expect(code).toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + expect(code).toContain('"x-api-key": apiKey'); + + // Check for session_id + expect(code).toContain("session_id"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate basic JavaScript code without API key authentication", () => { + const code = getNewJsApiCode(noAuthOptions); + + // Should not contain API key + expect(code).not.toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + expect(code).not.toContain("'x-api-key': apiKey"); + + // Check for session_id + expect(code).toContain("session_id"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate multi-step code for file uploads with authentication", () => { + const optionsWithFiles = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewJsApiCode(optionsWithFiles); + + // Check for required modules + expect(code).toContain("const fs = require('fs')"); + expect(code).toContain( + "httpModule = protocol === 'https:' ? require('https') : require('http')", + ); + + // Check for API key + expect(code).toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + expect(code).toContain("const authHeaders = { 'x-api-key': apiKey }"); + + // Check for file upload functions + expect(code).toContain("createFormData"); + expect(code).toContain("makeRequest"); + + // Check for upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + }); + + it("should generate multi-step code for file uploads without authentication", () => { + const optionsWithFiles = { + ...noAuthOptions, + processedPayload: { + ...noAuthOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewJsApiCode(optionsWithFiles); + + // Check for required modules + expect(code).toContain("const fs = require('fs')"); + expect(code).toContain( + "httpModule = protocol === 'https:' ? require('https') : require('http')", + ); + + // Should not contain API key + expect(code).not.toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + + // Check for file upload functions + expect(code).toContain("createFormData"); + expect(code).toContain("makeRequest"); + + // Check for upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + }); + }); + + describe("cURL Code Generation", () => { + it("should generate Unix cURL code with API key authentication", () => { + const code = getNewCurlCode({ ...baseOptions, platform: "unix" }); + + // Check for API key + expect(code).toContain("x-api-key: YOUR_API_KEY_HERE"); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate Unix cURL code without API key authentication", () => { + const code = getNewCurlCode({ ...noAuthOptions, platform: "unix" }); + + // Should not contain API key + expect(code).not.toContain("x-api-key: YOUR_API_KEY_HERE"); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate PowerShell cURL code with API key authentication", () => { + const code = getNewCurlCode({ ...baseOptions, platform: "powershell" }); + + // Check for API key + expect(code).toContain('--header "x-api-key: YOUR_API_KEY_HERE"'); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate PowerShell cURL code without API key authentication", () => { + const code = getNewCurlCode({ + ...noAuthOptions, + platform: "powershell", + }); + + // Should not contain API key + expect(code).not.toContain('--header "x-api-key: YOUR_API_KEY_HERE"'); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate multi-step code for file uploads with authentication", () => { + const optionsWithFiles = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const result = getNewCurlCode({ + ...optionsWithFiles, + platform: "unix", + }) as { steps: { title: string; code: string }[] }; + + // Check that it returns structured steps + expect(result).toHaveProperty("steps"); + expect(Array.isArray(result.steps)).toBe(true); + expect(result.steps).toHaveLength(2); + + // Check step 1 (upload files) + expect(result.steps[0]).toHaveProperty("title"); + expect(result.steps[0].title).toContain("Upload files"); + expect(result.steps[0]).toHaveProperty("code"); + expect(result.steps[0].code).toContain("/api/v1/files/upload/"); + expect(result.steps[0].code).toContain("/api/v2/files"); + expect(result.steps[0].code).toContain('--form "file=@'); + expect(result.steps[0].code).toContain("x-api-key: YOUR_API_KEY_HERE"); + + // Check step 2 (execute flow) + expect(result.steps[1]).toHaveProperty("title"); + expect(result.steps[1].title).toContain("Execute"); + expect(result.steps[1]).toHaveProperty("code"); + expect(result.steps[1].code).toContain("/api/v1/run/test-endpoint"); + expect(result.steps[1].code).toContain("x-api-key: YOUR_API_KEY_HERE"); + }); + + it("should generate multi-step code for file uploads without authentication", () => { + const optionsWithFiles = { + ...noAuthOptions, + processedPayload: { + ...noAuthOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const result = getNewCurlCode({ + ...optionsWithFiles, + platform: "unix", + }) as { steps: { title: string; code: string }[] }; + + // Check that it returns structured steps + expect(result).toHaveProperty("steps"); + expect(Array.isArray(result.steps)).toBe(true); + expect(result.steps).toHaveLength(2); + + // Check step 1 (upload files) - should not contain API key + expect(result.steps[0]).toHaveProperty("title"); + expect(result.steps[0].title).toContain("Upload files"); + expect(result.steps[0]).toHaveProperty("code"); + expect(result.steps[0].code).toContain("/api/v1/files/upload/"); + expect(result.steps[0].code).toContain("/api/v2/files"); + expect(result.steps[0].code).toContain('--form "file=@'); + expect(result.steps[0].code).not.toContain( + "x-api-key: YOUR_API_KEY_HERE", + ); + + // Check step 2 (execute flow) - should not contain API key + expect(result.steps[1]).toHaveProperty("title"); + expect(result.steps[1].title).toContain("Execute"); + expect(result.steps[1]).toHaveProperty("code"); + expect(result.steps[1].code).toContain("/api/v1/run/test-endpoint"); + expect(result.steps[1].code).not.toContain( + "x-api-key: YOUR_API_KEY_HERE", + ); + }); + }); + + describe("Common Features", () => { + it("should conditionally include API key based on shouldDisplayApiKey parameter", () => { + const pythonCodeAuth = getNewPythonApiCode(baseOptions); + const pythonCodeNoAuth = getNewPythonApiCode(noAuthOptions); + const jsCodeAuth = getNewJsApiCode(baseOptions); + const jsCodeNoAuth = getNewJsApiCode(noAuthOptions); + const curlResultAuth = getNewCurlCode(baseOptions); + const curlResultNoAuth = getNewCurlCode(noAuthOptions); + + // With authentication + expect(pythonCodeAuth).toContain("YOUR_API_KEY_HERE"); + expect(jsCodeAuth).toContain("YOUR_API_KEY_HERE"); + expect(curlResultAuth).toContain("YOUR_API_KEY_HERE"); + + // Without authentication + expect(pythonCodeNoAuth).not.toContain("YOUR_API_KEY_HERE"); + expect(jsCodeNoAuth).not.toContain("YOUR_API_KEY_HERE"); + expect(curlResultNoAuth).not.toContain("YOUR_API_KEY_HERE"); + }); + + it("should include session_id in all generators", () => { + const pythonCode = getNewPythonApiCode(baseOptions); + const jsCode = getNewJsApiCode(baseOptions); + const curlResult = getNewCurlCode(baseOptions); + + expect(pythonCode).toContain("session_id"); + expect(jsCode).toContain("session_id"); + + // Handle both string and object return types for cURL + if (typeof curlResult === "string") { + expect(curlResult).toContain("session_id"); + } else { + // For steps object, check that at least one step contains session_id + const hasSessionId = curlResult.steps.some((step) => + step.code.includes("session_id"), + ); + expect(hasSessionId).toBe(true); + } + }); + + it("should handle empty tweaks correctly", () => { + const optionsWithEmptyTweaks = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: {}, + }, + }; + + const pythonCode = getNewPythonApiCode(optionsWithEmptyTweaks); + const jsCode = getNewJsApiCode(optionsWithEmptyTweaks); + const curlCode = getNewCurlCode(optionsWithEmptyTweaks); + + // Should not generate file upload steps + expect(pythonCode).not.toContain("Step 1:"); + expect(jsCode).not.toContain("Step 1:"); + // cURL should return string, not steps object + expect(typeof curlCode).toBe("string"); + }); + }); + }); +}); diff --git a/src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts b/src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts new file mode 100644 index 000000000..c5156c7fc --- /dev/null +++ b/src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts @@ -0,0 +1,117 @@ +/** Checks if the tweaks object contains any file-related fields (path for File, file_path for VideoFile, files for ChatInput). */ +export function hasFileTweaks(tweaks: Record): boolean { + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + // File component: { path: [...] } + if ("path" in tweak && Array.isArray(tweak.path)) return true; + + // Video File: { file_path: "..." } + if ("file_path" in tweak && typeof tweak.file_path === "string") + return true; + + // Chat Input: { files: "..." } - files is a string field + if ("files" in tweak && typeof tweak.files === "string") return true; + } + + return false; +} + +/** Checks specifically for ChatInput files field (v1 API). */ +export function hasChatInputFiles(tweaks: Record): boolean { + return Object.values(tweaks).some( + (tweak) => + tweak && + typeof tweak === "object" && + "files" in tweak && + typeof tweak.files === "string", + ); +} + +/** Gets node ID for single ChatInput with files (v1). Returns null if none. */ +export function getChatInputNodeId(tweaks: Record): string | null { + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + if ("files" in tweak && typeof tweak.files === "string") { + return nodeId; + } + } + + return null; +} + +/** Gets node ID for single File/VideoFile (v2). Returns null if none. */ +export function getFileNodeId(tweaks: Record): string | null { + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + // File component: { path: [...] } + if ("path" in tweak && Array.isArray(tweak.path)) return nodeId; + + // Video File: { file_path: "..." } + if ("file_path" in tweak && typeof tweak.file_path === "string") + return nodeId; + } + + return null; +} + +/** Gets all node IDs for ChatInputs with files (v1). */ +export function getAllChatInputNodeIds(tweaks: Record): string[] { + const nodeIds: string[] = []; + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + if ("files" in tweak && typeof tweak.files === "string") { + nodeIds.push(nodeId); + } + } + + return nodeIds; +} + +/** Gets all node IDs for File/VideoFile components (v2). */ +export function getAllFileNodeIds(tweaks: Record): string[] { + const nodeIds: string[] = []; + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + // File component: { path: [...] } + if ("path" in tweak && Array.isArray(tweak.path)) { + nodeIds.push(nodeId); + } + + // Video File: { file_path: "..." } + if ("file_path" in tweak && typeof tweak.file_path === "string") { + nodeIds.push(nodeId); + } + } + + return nodeIds; +} + +/** Filters out file-related tweaks, returning only non-file ones. */ +export function getNonFileTypeTweaks( + tweaks: Record, +): Record { + const nonFileTweaks: Record = {}; + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") { + nonFileTweaks[nodeId] = tweak; + continue; + } + + // Skip file-related tweaks + const isFileComponent = + ("path" in tweak && Array.isArray(tweak.path)) || + ("file_path" in tweak && typeof tweak.file_path === "string") || + ("files" in tweak && typeof tweak.files === "string"); + + if (!isFileComponent) { + nonFileTweaks[nodeId] = tweak; + } + } + + return nonFileTweaks; +} diff --git a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx index 1fec15953..140222693 100644 --- a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx @@ -1,6 +1,12 @@ import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags"; import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol"; -import type { GetCodeType } from "@/types/tweaks"; +import { GetCodeType } from "@/types/tweaks"; +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getNonFileTypeTweaks, + hasFileTweaks, +} from "./detect-file-tweaks"; /** * Generates a cURL command for making a POST request to a webhook endpoint. @@ -18,7 +24,9 @@ export function getCurlWebhookCode({ format = "multiline", }: GetCodeType & { format?: "multiline" | "singleline" }) { const { protocol, host } = customGetHostProtocol(); - const baseUrl = `${protocol}//${host}/api/v1/webhook/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}/api/v1/webhook/${ + endpointName || flowId + }`; const authHeader = !isAuth ? `-H 'x-api-key: '` : ""; if (format === "singleline") { @@ -38,19 +46,23 @@ export function getCurlWebhookCode({ `.trim(); } +/** Generates Curl command for API calls, handling multi-step file uploads (v1 API for ChatInput files, v2 for File/VideoFile) before execution if tweaks contain files. Supports Unix/PowerShell and optional auth. */ export function getNewCurlCode({ flowId, endpointName, processedPayload, platform, + shouldDisplayApiKey, }: { flowId: string; endpointName: string; processedPayload: any; platform?: "unix" | "powershell"; -}): string { + shouldDisplayApiKey: boolean; +}): { steps: { title: string; code: string }[] } | string { const { protocol, host } = customGetHostProtocol(); - const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}`; + const apiUrl = `${baseUrl}/api/v1/run/${endpointName || flowId}`; // Auto-detect if no platform specified const detectedPlatform = @@ -59,41 +71,196 @@ export function getNewCurlCode({ ? "powershell" : "unix"); - const singleLinePayload = JSON.stringify(processedPayload); + // Check if there are file uploads + const tweaks = processedPayload.tweaks || {}; + const hasFiles = hasFileTweaks(tweaks); - if (detectedPlatform === "powershell") { - // PowerShell with here-string (most robust for complex JSON) - return `if (-not $env:LANGFLOW_API_KEY) { - Write-Error "LANGFLOW_API_KEY environment variable not found" - exit 1 -} + // If no file uploads, use existing logic + if (!hasFiles) { + if (detectedPlatform === "powershell") { + const payloadWithSession = { + ...processedPayload, + session_id: "YOUR_SESSION_ID_HERE", + }; + const singleLinePayload = JSON.stringify(payloadWithSession); + // PowerShell with here-string (most robust for complex JSON) + const authHeader = shouldDisplayApiKey + ? ` --header "x-api-key: YOUR_API_KEY_HERE" \`` + : ""; -$jsonData = @' + return `$jsonData = @' ${singleLinePayload} '@ -curl --request POST \` +curl.exe --request POST \` --url "${apiUrl}?stream=false" \` - --header "Content-Type: application/json" \` - --header "x-api-key: $env:LANGFLOW_API_KEY" \` + --header "Content-Type: application/json" \`${ + authHeader ? "\n" + authHeader : "" + } --data $jsonData`; - } else { - // Unix-like systems (Linux, Mac, WSL2) - const unixFormattedPayload = JSON.stringify(processedPayload, null, 2) - .split("\n") - .map((line, index) => (index === 0 ? line : " " + line)) - .join("\n\t\t"); + } else { + const payloadWithSession = { + ...processedPayload, + session_id: "YOUR_SESSION_ID_HERE", + }; + // Unix-like systems (Linux, Mac, WSL2) + const unixFormattedPayload = JSON.stringify(payloadWithSession, null, 2) + .split("\n") + .map((line, index) => (index === 0 ? line : " " + line)) + .join("\n\t\t"); - return `# Get API key from environment variable -if [ -z "$LANGFLOW_API_KEY" ]; then - echo "Error: LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables." - exit 1 -fi + const authHeader = shouldDisplayApiKey + ? ` --header "x-api-key: YOUR_API_KEY_HERE" \\ ` + : ""; -curl --request POST \\ + return `curl --request POST \\ --url '${apiUrl}?stream=false' \\ - --header 'Content-Type: application/json' \\ - --header "x-api-key: $LANGFLOW_API_KEY" \\ + --header 'Content-Type: application/json' \\${ + authHeader ? "\n" + authHeader : "" + } --data '${unixFormattedPayload}'`; + } + } + + // File upload logic - handle multiple file types additively + const chatInputNodeIds = getAllChatInputNodeIds(tweaks); + const fileNodeIds = getAllFileNodeIds(tweaks); + const nonFileTweaks = getNonFileTypeTweaks(tweaks); + + // Build upload commands and tweak entries + const uploadCommands: string[] = []; + const tweakEntries: string[] = []; + let uploadCounter = 1; + + // Add ChatInput file uploads (v1 API) + chatInputNodeIds.forEach((nodeId, index) => { + if (detectedPlatform === "powershell") { + uploadCommands.push( + `curl.exe --request POST \` + --url "${baseUrl}/api/v1/files/upload/${flowId}" \` + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_image_${uploadCounter}.jpg"`, + ); + } else { + uploadCommands.push( + `curl --request POST \\ + --url "${baseUrl}/api/v1/files/upload/${flowId}" \\ + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_image_${uploadCounter}.jpg"`, + ); + } + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + modifiedTweak.files = [ + `REPLACE_WITH_FILE_PATH_FROM_UPLOAD_${uploadCounter}`, + ]; + const tweakEntry = ` "${nodeId}": ${JSON.stringify( + modifiedTweak, + null, + 6, + ) + .split("\n") + .join("\n ")}`; + tweakEntries.push(tweakEntry); + uploadCounter++; + }); + + // Add File/VideoFile uploads (v2 API) + fileNodeIds.forEach((nodeId, index) => { + if (detectedPlatform === "powershell") { + uploadCommands.push( + `curl.exe --request POST \` + --url "${baseUrl}/api/v2/files" \` + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_file_${uploadCounter}.pdf"`, + ); + } else { + uploadCommands.push( + `curl --request POST \\ + --url "${baseUrl}/api/v2/files" \\ + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_file_${uploadCounter}.pdf"`, + ); + } + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + if ("path" in originalTweak) { + modifiedTweak.path = [ + `REPLACE_WITH_FILE_PATH_FROM_UPLOAD_${uploadCounter}`, + ]; + } else if ("file_path" in originalTweak) { + modifiedTweak.file_path = `REPLACE_WITH_FILE_PATH_FROM_UPLOAD_${uploadCounter}`; + } + const tweakEntry = ` "${nodeId}": ${JSON.stringify( + modifiedTweak, + null, + 6, + ) + .split("\n") + .join("\n ")}`; + tweakEntries.push(tweakEntry); + uploadCounter++; + }); + + // Add non-file tweaks + Object.entries(nonFileTweaks).forEach(([nodeId, tweak]) => { + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(tweak, null, 6) + .split("\n") + .join("\n ")}`, + ); + }); + + const allTweaks = tweakEntries.length > 0 ? tweakEntries.join(",\n") : ""; + + if (detectedPlatform === "powershell") { + const authHeader = shouldDisplayApiKey + ? ` -H "x-api-key: YOUR_API_KEY_HERE"` + : ""; + + const uploadStep = uploadCommands.join("\n"); + const executeStep = `curl.exe -X POST "${apiUrl}" -H "Content-Type: application/json"${authHeader} -d '{ + "output_type": "${processedPayload.output_type || "chat"}", + "input_type": "${processedPayload.input_type || "chat"}", + "input_value": "${processedPayload.input_value || "Your message here"}", + "session_id": "YOUR_SESSION_ID_HERE", + "tweaks": { +${allTweaks} + } +}'`; + + // Return structured steps instead of concatenated string + return { + steps: [ + { title: "Upload files to the server", code: uploadStep }, + { title: "Execute the flow with uploaded files", code: executeStep }, + ], + }; + } else { + const authHeader = shouldDisplayApiKey + ? ` -H "x-api-key: YOUR_API_KEY_HERE"` + : ""; + + const uploadStep = uploadCommands.join("\n"); + const executeStep = `curl -X POST \\ + "${apiUrl}" \\ + -H "Content-Type: application/json"${ + authHeader ? " \\\n " + authHeader : "" + } \\ + -d '{\n "output_type": "${ + processedPayload.output_type || "chat" + }",\n "input_type": "${ + processedPayload.input_type || "chat" + }",\n "input_value": "${ + processedPayload.input_value || "Your message here" + }",\n "session_id": "YOUR_SESSION_ID_HERE",\n "tweaks": {\n${allTweaks}\n }\n }'`; + + // Return structured steps instead of concatenated string + return { + steps: [ + { title: "Upload files to the server", code: uploadStep }, + { title: "Execute the flow with uploaded files", code: executeStep }, + ], + }; } } diff --git a/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx b/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx index cc9ca17fb..8b13fe155 100644 --- a/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx @@ -1,46 +1,64 @@ import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol"; +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getChatInputNodeId, + getFileNodeId, + getNonFileTypeTweaks, + hasChatInputFiles, + hasFileTweaks, +} from "./detect-file-tweaks"; -/** - * Generates JavaScript code for making API calls to a Langflow endpoint. - * - * @param {Object} params - The parameters for generating the API code - * @param {string} params.flowId - The ID of the flow to run - * @param {string} params.endpointName - The endpoint name for the flow - * @param {Object} params.processedPayload - The pre-processed payload object - * @returns {string} Generated JavaScript code as a string - */ +/** Generates Node.js code for API calls, with multi-step file uploads (v1 for ChatInput, v2 for File/VideoFile) using http module, then flow execution. Handles auth. */ export function getNewJsApiCode({ flowId, endpointName, processedPayload, + shouldDisplayApiKey, }: { flowId: string; endpointName: string; processedPayload: any; + shouldDisplayApiKey: boolean; }): string { const { protocol, host } = customGetHostProtocol(); - const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}`; - // Add session_id to payload - const payloadWithSession = { - ...processedPayload, - session_id: "user_1", // Optional: Use session tracking if needed - }; + // Parse URL for robust hostname/port extraction + const parsedUrl = new URL(baseUrl); + const hostname = parsedUrl.hostname; + const port = + parsedUrl.port || (parsedUrl.protocol === "https:" ? "443" : "80"); - const payloadString = JSON.stringify(payloadWithSession, null, 4); + // Check if there are file uploads + const tweaks = processedPayload.tweaks || {}; + const hasFiles = hasFileTweaks(tweaks); - return `// Get API key from environment variable -if (!process.env.LANGFLOW_API_KEY) { - throw new Error('LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables.'); -} + // If no file uploads, use existing logic + if (!hasFiles) { + const apiUrl = `${baseUrl}/api/v1/run/${endpointName || flowId}`; -const payload = ${payloadString}; + const payloadString = JSON.stringify(processedPayload, null, 4); + + const authSection = shouldDisplayApiKey + ? `const crypto = require('crypto'); +const apiKey = 'YOUR_API_KEY_HERE'; +` + : ""; + + const headersSection = shouldDisplayApiKey + ? ` headers: { + 'Content-Type': 'application/json', + "x-api-key": apiKey + },` + : ""; + + return `${authSection}const payload = ${payloadString}; +payload.session_id = crypto.randomUUID(); const options = { method: 'POST', - headers: { - 'Content-Type': 'application/json',\n "x-api-key": process.env.LANGFLOW_API_KEY - }, +${headersSection} body: JSON.stringify(payload) }; @@ -48,4 +66,242 @@ fetch('${apiUrl}', options) .then(response => response.json()) .then(response => console.warn(response)) .catch(err => console.error(err));`; + } + + // File upload logic - handle multiple file types additively + const chatInputNodeIds = getAllChatInputNodeIds(tweaks); + const fileNodeIds = getAllFileNodeIds(tweaks); + const nonFileTweaks = getNonFileTypeTweaks(tweaks); + + if (chatInputNodeIds.length === 0 && fileNodeIds.length === 0) { + return getNewJsApiCode({ + flowId, + endpointName, + processedPayload: { ...processedPayload, tweaks: nonFileTweaks }, + shouldDisplayApiKey, + }); + } + + const authSection = shouldDisplayApiKey + ? `const apiKey = 'YOUR_API_KEY_HERE'; +const authHeaders = { 'x-api-key': apiKey };` + : ""; + + // Build upload steps for each file component + const uploadSteps: string[] = []; + const resultVariables: string[] = []; + const tweakEntries: string[] = []; + + // ChatInput files (v1 API) + chatInputNodeIds.forEach((nodeId, index) => { + const varName = `chatFilePath${index + 1}`; + resultVariables.push(varName); + + uploadSteps.push(` // Step ${ + uploadSteps.length + 1 + }: Upload file for ChatInput ${nodeId} + const { payload: chatPayload${index + 1}, boundary: chatBoundary${ + index + 1 + } } = createFormData('your_image_${index + 1}.jpg'); + + const chatUploadOptions${index + 1} = { + hostname: '${hostname}', + port: ${port}, + path: \`/api/v1/files/upload/\${FLOW_ID}\`, + method: 'POST', + headers: { + 'Content-Type': \`multipart/form-data; boundary=\${chatBoundary${ + index + 1 + }}\`, + 'Content-Length': chatPayload${index + 1}.length, + ...authHeaders + } + }; + + const chatUploadResult${ + index + 1 + } = await makeRequest(chatUploadOptions${index + 1}, chatPayload${ + index + 1 + }); + const ${varName} = chatUploadResult${index + 1}.file_path; + console.log('ChatInput upload ${ + index + 1 + } successful! File path:', ${varName});`); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + modifiedTweak.files = [varName]; + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(modifiedTweak, null, 12) + .split("\n") + .join("\n ")}`, + ); + }); + + // File/VideoFile components (v2 API) + fileNodeIds.forEach((nodeId, index) => { + const varName = `filePath${index + 1}`; + resultVariables.push(varName); + + uploadSteps.push(` // Step ${ + uploadSteps.length + 1 + }: Upload file for File/VideoFile ${nodeId} + const { payload: filePayload${index + 1}, boundary: fileBoundary${ + index + 1 + } } = createFormData('your_file_${index + 1}.pdf'); + + const fileUploadOptions${index + 1} = { + hostname: '${hostname}', + port: ${port}, + path: '/api/v2/files', + method: 'POST', + headers: { + 'Content-Type': \`multipart/form-data; boundary=\${fileBoundary${ + index + 1 + }}\`, + 'Content-Length': filePayload${index + 1}.length, + ...authHeaders + } + }; + + const fileUploadResult${ + index + 1 + } = await makeRequest(fileUploadOptions${index + 1}, filePayload${ + index + 1 + }); + const ${varName} = fileUploadResult${index + 1}.path; + console.log('File upload ${ + index + 1 + } successful! File path:', ${varName});`); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + if ("path" in originalTweak) { + modifiedTweak.path = [varName]; + } else if ("file_path" in originalTweak) { + modifiedTweak.file_path = varName; + } + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(modifiedTweak, null, 12) + .split("\n") + .join("\n ")}`, + ); + }); + + // Add non-file tweaks + Object.entries(nonFileTweaks).forEach(([nodeId, tweak]) => { + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(tweak, null, 12) + .split("\n") + .join("\n ")}`, + ); + }); + + const allTweaks = tweakEntries.length > 0 ? tweakEntries.join(",\n") : ""; + + return `${authSection}const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const BASE_URL = "${baseUrl}"; +const FLOW_ID = "${flowId}"; +const protocol = new URL(BASE_URL).protocol; +const httpModule = protocol === 'https:' ? require('https') : require('http'); + +// Helper function to create multipart form data +function createFormData(filePath) { + const boundary = '----FormBoundary' + Date.now(); + const filename = path.basename(filePath); + + if (!fs.existsSync(filePath)) { + throw new Error(\`File not found: \${filePath}\`); + } + + const fileData = fs.readFileSync(filePath); + + let data = ''; + data += \`--\${boundary}\\r\\n\`; + data += \`Content-Disposition: form-data; name="file"; filename="\${filename}"\\r\\n\`; + data += \`Content-Type: application/octet-stream\\r\\n\\r\\n\`; + + const payload = Buffer.concat([ + Buffer.from(data, 'utf8'), + fileData, + Buffer.from(\`\\r\\n--\${boundary}--\\r\\n\`, 'utf8') + ]); + + return { payload, boundary }; +} + +// Helper function to make HTTP requests +function makeRequest(options, data) { + return new Promise((resolve, reject) => { + const req = httpModule.request(options, (res) => { + let responseData = ''; + res.on('data', (chunk) => { responseData += chunk; }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + resolve(JSON.parse(responseData)); + } catch (e) { + resolve(responseData); + } + } else { + reject(new Error(\`Request failed with status \${res.statusCode}: \${responseData}\`)); + } + }); + }); + req.on('error', reject); + if (data) req.write(data); + req.end(); + }); +} + +async function uploadAndExecuteFlow() { + try {${ + shouldDisplayApiKey + ? ` + const apiKey = 'YOUR_API_KEY_HERE'; + const authHeaders = { 'x-api-key': apiKey };` + : ` + const authHeaders = {};` + } + +${uploadSteps.join("\n\n")} + + // Step ${uploadSteps.length + 1}: Execute flow with all file paths + const executePayload = JSON.stringify({ + "output_type": "${processedPayload.output_type || "chat"}", + "input_type": "${processedPayload.input_type || "chat"}", + "input_value": "${ + processedPayload.input_value || "Your message here" + }", + "session_id": crypto.randomUUID(), + "tweaks": { +${allTweaks} + } + }); + + const executeOptions = { + hostname: '${hostname}', + port: ${port}, + path: \`/api/v1/run/${endpointName || flowId}\`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(executePayload), + ...authHeaders + } + }; + + const result = await makeRequest(executeOptions, executePayload); + console.log('Flow execution successful!'); + console.log(result); + + } catch (error) { + console.error('Error:', error.message); + } +} + +uploadAndExecuteFlow();`; } diff --git a/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx b/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx index b76d56a8e..8c54f3b42 100644 --- a/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx @@ -1,40 +1,60 @@ import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol"; +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getChatInputNodeId, + getFileNodeId, + getNonFileTypeTweaks, + hasChatInputFiles, + hasFileTweaks, +} from "./detect-file-tweaks"; +/** Generates Python code using requests for API calls, handling multi-step file uploads (v1 for ChatInput, v2 for others) before flow execution. Supports auth. */ export function getNewPythonApiCode({ flowId, endpointName, processedPayload, + shouldDisplayApiKey, }: { flowId: string; endpointName: string; processedPayload: any; + shouldDisplayApiKey: boolean; }): string { const { protocol, host } = customGetHostProtocol(); - const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}`; - const payloadString = JSON.stringify(processedPayload, null, 4) - .replace(/true/g, "True") - .replace(/false/g, "False") - .replace(/null/g, "None"); + // Check if there are file uploads + const tweaks = processedPayload.tweaks || {}; + const hasFiles = hasFileTweaks(tweaks); - return `import requests + // If no file uploads, use existing logic + if (!hasFiles) { + const apiUrl = `${baseUrl}/api/v1/run/${endpointName || flowId}`; + const payloadString = JSON.stringify(processedPayload, null, 4) + .replace(/true/g, "True") + .replace(/false/g, "False") + .replace(/null/g, "None"); + + const authSection = shouldDisplayApiKey + ? `api_key = 'YOUR_API_KEY_HERE'` + : ""; + + const headersSection = shouldDisplayApiKey + ? `headers = {"x-api-key": api_key}` + : ""; + + return `import requests import os +import uuid -# API Configuration -try: - api_key = os.environ["LANGFLOW_API_KEY"] -except KeyError: - raise ValueError("LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables.") - -url = "${apiUrl}" # The complete API endpoint URL for this flow +${authSection}url = "${apiUrl}" # The complete API endpoint URL for this flow # Request payload configuration payload = ${payloadString} +payload["session_id"] = str(uuid.uuid4()) -# Request headers -headers = { - "Content-Type": "application/json",\n "x-api-key": api_key # Authentication key from environment variable -} +${headersSection} try: # Send API request @@ -48,4 +68,121 @@ except requests.exceptions.RequestException as e: print(f"Error making API request: {e}") except ValueError as e: print(f"Error parsing response: {e}")`; + } + + // File upload logic - handle multiple file types additively + const chatInputNodeIds = getAllChatInputNodeIds(tweaks); + const fileNodeIds = getAllFileNodeIds(tweaks); + const nonFileTweaks = getNonFileTypeTweaks(tweaks); + + if (chatInputNodeIds.length === 0 && fileNodeIds.length === 0) { + return getNewPythonApiCode({ + flowId, + endpointName, + processedPayload: { ...processedPayload, tweaks: nonFileTweaks }, + shouldDisplayApiKey, + }); + } + + const authSection = shouldDisplayApiKey + ? `api_key = 'YOUR_API_KEY_HERE'` + : ""; + + const headersSection = shouldDisplayApiKey + ? `headers = {"x-api-key": api_key}` + : ""; + + // Build upload steps for each file component + const uploadSteps: string[] = []; + const tweakAssignments: string[] = []; + + // ChatInput files (v1 API) + chatInputNodeIds.forEach((nodeId, index) => { + uploadSteps.push( + `# Step ${ + uploadSteps.length + 1 + }: Upload file for ChatInput ${nodeId}\nwith open(\"your_image_${ + index + 1 + }.jpg\", \"rb\") as f:\n response = requests.post(\n f\"{base_url}/api/v1/files/upload/{flow_id}\",\n headers=headers,\n files={\"file\": f}\n )\n response.raise_for_status()\n chat_file_path_${ + index + 1 + } = response.json()[\"file_path\"]`, + ); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + modifiedTweak.files = [`chat_file_path_${index + 1}`]; + tweakAssignments.push( + ` \"${nodeId}\": ${JSON.stringify(modifiedTweak, null, 4) + .split("\n") + .join("\n ")}`, + ); + }); + + // File/VideoFile components (v2 API) + fileNodeIds.forEach((nodeId, index) => { + uploadSteps.push( + `# Step ${ + uploadSteps.length + 1 + }: Upload file for File/VideoFile ${nodeId}\nwith open(\"your_file_${ + index + 1 + }.pdf\", \"rb\") as f:\n response = requests.post(\n f\"{base_url}/api/v2/files\",\n headers=headers,\n files={\"file\": f}\n )\n response.raise_for_status()\n file_path_${ + index + 1 + } = response.json()[\"path\"]`, + ); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + if ("path" in originalTweak) { + modifiedTweak.path = [`file_path_${index + 1}`]; + } else if ("file_path" in originalTweak) { + modifiedTweak.file_path = `file_path_${index + 1}`; + } + tweakAssignments.push( + ` \"${nodeId}\": ${JSON.stringify(modifiedTweak, null, 4) + .split("\n") + .join("\n ")}`, + ); + }); + + // Add non-file tweaks + Object.entries(nonFileTweaks).forEach(([nodeId, tweak]) => { + tweakAssignments.push( + ` \"${nodeId}\": ${JSON.stringify(tweak, null, 4) + .split("\n") + .join("\n ")}`, + ); + }); + + const allTweaks = + tweakAssignments.length > 0 ? tweakAssignments.join(",\n") : ""; + + return `import requests +import os +import uuid + +${authSection}base_url = "${baseUrl}" +flow_id = "${flowId}" + +${headersSection} + +${uploadSteps.join("\n\n")} + +# Step ${uploadSteps.length + 1}: Execute flow with all file paths +payload = { + "output_type": "${processedPayload.output_type || "chat"}", + "input_type": "${processedPayload.input_type || "chat"}", + "input_value": "${processedPayload.input_value || "Your message here"}", + "session_id": str(uuid.uuid4()), + "tweaks": { +${allTweaks} + } +} + +response = requests.post( + f"{base_url}/api/v1/run/{endpointName or flowId}", + headers={"Content-Type": "application/json", **headers}, + json=payload +) +response.raise_for_status() +print(response.json())`; }