mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
Update project configuration and enhance workflow execution
- Added .env to .gitignore to prevent sensitive data exposure. - Updated package.json and package-lock.json to include OpenAI SDK version 6.3.0. - Enhanced NodeConfiguration component to support additional model options for AI tasks. - Modified TestingPanel to accept test input for workflow execution. - Improved WorkflowToolbar with OpenAI configuration button and modal. - Refactored workflow execution logic to handle test inputs and added processing functions for various node types.
This commit is contained in:
+2
-1
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
node_modules
|
||||
.env
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"id": "c69ca19c-4174-4ec5-b2f8-6f941d1cd796",
|
||||
"name": "Workflow 1760487469928",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "57861f51-a1ed-4efe-8646-3564de2cac10",
|
||||
"type": "dataInput",
|
||||
"position": {
|
||||
"x": 372,
|
||||
"y": 87
|
||||
},
|
||||
"data": {
|
||||
"id": "960e2857-f602-46a9-af43-ec932d77153e",
|
||||
"type": "dataInput",
|
||||
"label": "dataInput Node",
|
||||
"status": "success",
|
||||
"config": {
|
||||
"defaultValue": "Hello World!",
|
||||
"dataType": "text"
|
||||
},
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"output": "What is today's date?"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "d11a7455-c591-4606-9e7f-11b002e8c20d",
|
||||
"type": "llmTask",
|
||||
"position": {
|
||||
"x": 436.75,
|
||||
"y": 118.75
|
||||
},
|
||||
"data": {
|
||||
"id": "d52f145f-1fa6-415c-aca7-ca11895f3991",
|
||||
"type": "llmTask",
|
||||
"label": "llmTask Node",
|
||||
"status": "success",
|
||||
"config": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"temperature": 0.7,
|
||||
"prompt": "Analyze the following text and provide insights: {{input}}"
|
||||
},
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"output": "LLM Response (gpt-3.5-turbo): Analyze the following text and provide insights: What is today's date?",
|
||||
"prompt": "Analyze the following text and provide insights: What is today's date?",
|
||||
"model": "gpt-3.5-turbo"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "40d139ed-8290-4d14-8c1a-940c2927105a",
|
||||
"type": "dataOutput",
|
||||
"position": {
|
||||
"x": 504.5575564762356,
|
||||
"y": 309.2556953849361
|
||||
},
|
||||
"data": {
|
||||
"id": "9b38825f-ae74-491f-b088-36969d87385d",
|
||||
"type": "dataOutput",
|
||||
"label": "dataOutput Node",
|
||||
"status": "success",
|
||||
"config": {
|
||||
"format": "text",
|
||||
"filename": "analysis.txt"
|
||||
},
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"output": "What is today's date?",
|
||||
"format": "text",
|
||||
"filename": "analysis.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "0e841dba-cdb5-4738-8311-2ec1c426c0a5",
|
||||
"source": "57861f51-a1ed-4efe-8646-3564de2cac10",
|
||||
"target": "d11a7455-c591-4606-9e7f-11b002e8c20d"
|
||||
},
|
||||
{
|
||||
"id": "5447100b-4949-406f-a3e8-9f8dd336eeef",
|
||||
"source": "d11a7455-c591-4606-9e7f-11b002e8c20d",
|
||||
"target": "40d139ed-8290-4d14-8c1a-940c2927105a"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-10-15T00:17:49.928Z",
|
||||
"updatedAt": "2025-10-15T00:20:17.205Z",
|
||||
"exportedAt": "2025-10-15T00:22:18.078Z",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
Generated
+48
-26
@@ -15,6 +15,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"clsx": "^1.2.1",
|
||||
"lucide-react": "^0.263.1",
|
||||
"openai": "^6.3.0",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -11318,6 +11319,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -12259,6 +12281,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.3.0.tgz",
|
||||
"integrity": "sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -17212,27 +17255,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-manifest-plugin": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz",
|
||||
@@ -17859,16 +17881,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
||||
+12
-12
@@ -7,20 +7,21 @@
|
||||
"@types/node": "^16.18.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"clsx": "^1.2.1",
|
||||
"lucide-react": "^0.263.1",
|
||||
"openai": "^6.3.0",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.10.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.0",
|
||||
"lucide-react": "^0.263.1",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.24",
|
||||
"clsx": "^1.2.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"zustand": "^4.4.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"reactflow": "^11.10.1",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^4.9.0",
|
||||
"uuid": "^9.0.0",
|
||||
"@types/uuid": "^9.0.2"
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -45,6 +46,5 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,13 @@ const nodeTypeConfigs = {
|
||||
key: "model",
|
||||
label: "Model",
|
||||
type: "select",
|
||||
options: ["gpt-3.5-turbo", "gpt-4", "claude-3"],
|
||||
options: [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-32k",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "temperature",
|
||||
@@ -114,7 +120,13 @@ const nodeTypeConfigs = {
|
||||
key: "model",
|
||||
label: "Model",
|
||||
type: "select",
|
||||
options: ["gpt-3.5-turbo", "gpt-4", "claude-3"],
|
||||
options: [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-32k",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import React, { useState } from "react";
|
||||
import { Key, AlertCircle, CheckCircle } from "lucide-react";
|
||||
|
||||
interface OpenAIConfigProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const OpenAIConfig: React.FC<OpenAIConfigProps> = ({ onClose }) => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<
|
||||
"idle" | "success" | "error"
|
||||
>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
setErrorMessage("Please enter an API key");
|
||||
setValidationStatus("error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
setValidationStatus("idle");
|
||||
|
||||
try {
|
||||
// In a real app, you'd validate the API key with a backend service
|
||||
// For now, we'll just check if it looks like a valid OpenAI API key format
|
||||
if (apiKey.startsWith("sk-") && apiKey.length > 20) {
|
||||
// Save to localStorage for this session
|
||||
localStorage.setItem("openai_api_key", apiKey);
|
||||
setValidationStatus("success");
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
window.location.reload(); // Reload to pick up the new API key
|
||||
}, 1500);
|
||||
} else {
|
||||
setErrorMessage(
|
||||
"Invalid API key format. OpenAI API keys should start with 'sk-'"
|
||||
);
|
||||
setValidationStatus("error");
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage("Failed to validate API key");
|
||||
setValidationStatus("error");
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
setErrorMessage("Please enter an API key first");
|
||||
setValidationStatus("error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
setValidationStatus("idle");
|
||||
|
||||
try {
|
||||
// Test the API key by making a simple call
|
||||
const response = await fetch("https://api.openai.com/v1/models", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setValidationStatus("success");
|
||||
setErrorMessage("");
|
||||
} else {
|
||||
setErrorMessage("API key is invalid or has insufficient permissions");
|
||||
setValidationStatus("error");
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(
|
||||
"Failed to test API key. Please check your internet connection."
|
||||
);
|
||||
setValidationStatus("error");
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Key className="w-5 h-5 text-gray-600" />
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
OpenAI API Configuration
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
||||
<p className="text-sm text-blue-800">
|
||||
To use real AI responses, you need to configure your OpenAI API
|
||||
key. Get your API key from{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
OpenAI Platform
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
OpenAI API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="sk-..."
|
||||
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{validationStatus === "error" && (
|
||||
<div className="flex items-center space-x-2 text-red-600">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span className="text-sm">{errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validationStatus === "success" && (
|
||||
<div className="flex items-center space-x-2 text-green-600">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span className="text-sm">API key is valid!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={isValidating || !apiKey.trim()}
|
||||
className="flex-1 px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isValidating ? "Testing..." : "Test Key"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isValidating || !apiKey.trim()}
|
||||
className="flex-1 px-3 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isValidating ? "Saving..." : "Save & Use"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
<p>• Your API key is stored locally in your browser</p>
|
||||
<p>• It's only used to make requests to OpenAI's API</p>
|
||||
<p>• You can change it anytime by reopening this dialog</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -35,7 +35,7 @@ export const TestingPanel: React.FC = () => {
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
setTestResults(null);
|
||||
await executeWorkflow();
|
||||
await executeWorkflow(testInput);
|
||||
|
||||
// Collect all results
|
||||
const results = currentWorkflow.nodes.map((node) => ({
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import React, { useRef } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useWorkflowStore } from "../store/workflowStore";
|
||||
import { Download, Upload, Save, Play, Square, RotateCcw } from "lucide-react";
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
Save,
|
||||
Play,
|
||||
Square,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
downloadWorkflow,
|
||||
loadWorkflowFromFile,
|
||||
} from "../utils/workflowSerialization";
|
||||
import { OpenAIConfig } from "./OpenAIConfig";
|
||||
|
||||
export const WorkflowToolbar: React.FC = () => {
|
||||
const {
|
||||
@@ -16,6 +25,7 @@ export const WorkflowToolbar: React.FC = () => {
|
||||
} = useWorkflowStore();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [showOpenAIConfig, setShowOpenAIConfig] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
if (currentWorkflow) {
|
||||
@@ -101,6 +111,15 @@ export const WorkflowToolbar: React.FC = () => {
|
||||
<span>Import</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowOpenAIConfig(true)}
|
||||
className="flex items-center space-x-2 px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
|
||||
title="Configure OpenAI API"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>AI Config</span>
|
||||
</button>
|
||||
|
||||
<div className="w-px h-6 bg-gray-300" />
|
||||
|
||||
<button
|
||||
@@ -139,6 +158,11 @@ export const WorkflowToolbar: React.FC = () => {
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* OpenAI Configuration Modal */}
|
||||
{showOpenAIConfig && (
|
||||
<OpenAIConfig onClose={() => setShowOpenAIConfig(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import OpenAI from "openai";
|
||||
|
||||
// Get API key from environment or localStorage
|
||||
const getApiKey = (): string => {
|
||||
return (
|
||||
localStorage.getItem("openai_api_key") ||
|
||||
process.env.REACT_APP_OPENAI_API_KEY ||
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize OpenAI client
|
||||
const openai = new OpenAI({
|
||||
apiKey: getApiKey(),
|
||||
dangerouslyAllowBrowser: true, // Only for development - in production, use a backend
|
||||
});
|
||||
|
||||
export interface OpenAIConfig {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export interface OpenAIResponse {
|
||||
content: string;
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const callOpenAI = async (
|
||||
prompt: string,
|
||||
config: OpenAIConfig
|
||||
): Promise<OpenAIResponse> => {
|
||||
try {
|
||||
// Check if API key is configured
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey || apiKey === "your_openai_api_key_here") {
|
||||
throw new Error(
|
||||
"OpenAI API key not configured. Please configure your API key in the settings."
|
||||
);
|
||||
}
|
||||
|
||||
// Update the OpenAI client with the current API key
|
||||
openai.apiKey = apiKey;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: config.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
temperature: config.temperature,
|
||||
max_tokens: config.maxTokens || 1000,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content || "";
|
||||
const usage = response.usage;
|
||||
|
||||
return {
|
||||
content,
|
||||
usage: usage
|
||||
? {
|
||||
promptTokens: usage.prompt_tokens,
|
||||
completionTokens: usage.completion_tokens,
|
||||
totalTokens: usage.total_tokens,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("OpenAI API Error:", error);
|
||||
|
||||
// Return a fallback response if API fails
|
||||
return {
|
||||
content: `Error calling OpenAI API: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}. Please check your API key and try again.`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getAvailableModels = (): string[] => {
|
||||
return [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-32k",
|
||||
];
|
||||
};
|
||||
+197
-9
@@ -7,6 +7,7 @@ import {
|
||||
NodeStatus,
|
||||
} from "../types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { callOpenAI, OpenAIConfig } from "../services/openaiService";
|
||||
|
||||
interface WorkflowStore {
|
||||
workflows: Workflow[];
|
||||
@@ -37,7 +38,7 @@ interface WorkflowStore {
|
||||
deleteEdge: (edgeId: string) => void;
|
||||
|
||||
// Execution
|
||||
executeWorkflow: () => Promise<void>;
|
||||
executeWorkflow: (testInput?: string) => Promise<void>;
|
||||
updateNodeStatus: (
|
||||
nodeId: string,
|
||||
status: NodeStatus,
|
||||
@@ -212,23 +213,68 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
||||
}));
|
||||
},
|
||||
|
||||
executeWorkflow: async () => {
|
||||
executeWorkflow: async (testInput?: string) => {
|
||||
const { currentWorkflow } = get();
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
set({ isExecuting: true, executionResults: {} });
|
||||
|
||||
try {
|
||||
// Simple execution logic - in a real app, this would call your backend
|
||||
for (const node of currentWorkflow.nodes) {
|
||||
// Find the first data input node to start with
|
||||
const dataInputNode = currentWorkflow.nodes.find(
|
||||
(node) => node.data.type === "dataInput"
|
||||
);
|
||||
|
||||
if (!dataInputNode) {
|
||||
console.error("No data input node found in workflow");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with the data input node
|
||||
get().updateNodeStatus(dataInputNode.id, "running");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Use test input if provided, otherwise use the node's default value
|
||||
const inputData =
|
||||
testInput || dataInputNode.data.config.defaultValue || "";
|
||||
const dataInputResult = { output: inputData };
|
||||
get().updateNodeStatus(dataInputNode.id, "success", dataInputResult);
|
||||
|
||||
// Process remaining nodes in order
|
||||
const remainingNodes = currentWorkflow.nodes.filter(
|
||||
(node) => node.id !== dataInputNode.id
|
||||
);
|
||||
|
||||
for (const node of remainingNodes) {
|
||||
get().updateNodeStatus(node.id, "running");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Simulate processing time
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Process node based on type
|
||||
let result;
|
||||
switch (node.data.type) {
|
||||
case "llmTask":
|
||||
result = await processLLMNode(node, inputData);
|
||||
break;
|
||||
case "dataOutput":
|
||||
result = await processDataOutputNode(node, inputData);
|
||||
break;
|
||||
case "webScraping":
|
||||
result = await processWebScrapingNode(node, inputData);
|
||||
break;
|
||||
case "embeddingGenerator":
|
||||
result = await processEmbeddingNode(node, inputData);
|
||||
break;
|
||||
case "similaritySearch":
|
||||
result = await processSimilaritySearchNode(node, inputData);
|
||||
break;
|
||||
case "structuredOutput":
|
||||
result = await processStructuredOutputNode(node, inputData);
|
||||
break;
|
||||
default:
|
||||
result = { output: `Processed by ${node.data.type} node` };
|
||||
}
|
||||
|
||||
// Mock execution result
|
||||
const mockResult = { output: `Processed by ${node.data.type} node` };
|
||||
get().updateNodeStatus(node.id, "success", mockResult);
|
||||
get().updateNodeStatus(node.id, "success", result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Workflow execution failed:", error);
|
||||
@@ -261,3 +307,145 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
||||
set({ executionResults: {}, isExecuting: false });
|
||||
},
|
||||
}));
|
||||
|
||||
// Node processing functions
|
||||
const processLLMNode = async (node: WorkflowNode, inputData: string) => {
|
||||
const config = node.data.config;
|
||||
const prompt = config.prompt || "Process the following input: {{input}}";
|
||||
const model = config.model || "gpt-3.5-turbo";
|
||||
const temperature = config.temperature || 0.7;
|
||||
const maxTokens = config.maxTokens || 1000;
|
||||
|
||||
// Replace {{input}} placeholder with actual input data
|
||||
const processedPrompt = prompt.replace(/\{\{input\}\}/g, inputData);
|
||||
|
||||
try {
|
||||
// Call OpenAI API
|
||||
const openaiConfig: OpenAIConfig = {
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
};
|
||||
|
||||
const response = await callOpenAI(processedPrompt, openaiConfig);
|
||||
|
||||
return {
|
||||
output: response.content,
|
||||
prompt: processedPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
usage: response.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing LLM node:", error);
|
||||
|
||||
// Fallback to mock response if API fails
|
||||
const fallbackResponse = `Error calling OpenAI API: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}. Please check your API key and try again.`;
|
||||
|
||||
return {
|
||||
output: fallbackResponse,
|
||||
prompt: processedPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const processDataOutputNode = async (node: WorkflowNode, inputData: string) => {
|
||||
const config = node.data.config;
|
||||
const format = config.format || "text";
|
||||
const filename = config.filename || "output.txt";
|
||||
|
||||
let output;
|
||||
switch (format) {
|
||||
case "json":
|
||||
output = JSON.stringify(
|
||||
{ data: inputData, timestamp: new Date().toISOString() },
|
||||
null,
|
||||
2
|
||||
);
|
||||
break;
|
||||
case "csv":
|
||||
output = `data\n"${inputData}"`;
|
||||
break;
|
||||
default:
|
||||
output = inputData;
|
||||
}
|
||||
|
||||
return { output, format, filename };
|
||||
};
|
||||
|
||||
const processWebScrapingNode = async (
|
||||
node: WorkflowNode,
|
||||
inputData: string
|
||||
) => {
|
||||
const config = node.data.config;
|
||||
const url = config.url || inputData;
|
||||
const selector = config.selector || "body";
|
||||
const maxLength = config.maxLength || 1000;
|
||||
|
||||
// In a real app, this would perform actual web scraping
|
||||
const mockScrapedContent = `Scraped content from ${url} using selector "${selector}": ${inputData.substring(
|
||||
0,
|
||||
maxLength
|
||||
)}...`;
|
||||
|
||||
return { output: mockScrapedContent, url, selector };
|
||||
};
|
||||
|
||||
const processEmbeddingNode = async (node: WorkflowNode, inputData: string) => {
|
||||
const config = node.data.config;
|
||||
const model = config.model || "text-embedding-ada-002";
|
||||
const dimensions = config.dimensions || 1536;
|
||||
|
||||
// In a real app, this would generate actual embeddings
|
||||
const mockEmbedding = Array.from({ length: dimensions }, () => Math.random());
|
||||
|
||||
return { output: mockEmbedding, model, dimensions };
|
||||
};
|
||||
|
||||
const processSimilaritySearchNode = async (
|
||||
node: WorkflowNode,
|
||||
inputData: string
|
||||
) => {
|
||||
const config = node.data.config;
|
||||
const vectorStore = config.vectorStore || "pinecone";
|
||||
const topK = config.topK || 5;
|
||||
const threshold = config.threshold || 0.8;
|
||||
|
||||
// In a real app, this would perform actual similarity search
|
||||
const mockResults = Array.from({ length: topK }, (_, i) => ({
|
||||
id: `result_${i + 1}`,
|
||||
content: `Similar content ${i + 1} for: ${inputData}`,
|
||||
similarity: threshold + Math.random() * (1 - threshold),
|
||||
}));
|
||||
|
||||
return { output: mockResults, vectorStore, topK, threshold };
|
||||
};
|
||||
|
||||
const processStructuredOutputNode = async (
|
||||
node: WorkflowNode,
|
||||
inputData: string
|
||||
) => {
|
||||
const config = node.data.config;
|
||||
const schema = config.schema || '{"type": "object"}';
|
||||
const model = config.model || "gpt-3.5-turbo";
|
||||
|
||||
// In a real app, this would use an LLM to structure the data according to the schema
|
||||
const mockStructuredOutput = {
|
||||
input: inputData,
|
||||
structured: {
|
||||
text: inputData,
|
||||
length: inputData.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
schema: JSON.parse(schema),
|
||||
};
|
||||
|
||||
return { output: mockStructuredOutput, model, schema };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user