import React, { useState } from 'react';
import { Handle, Position } from 'reactflow';
import api from './api'; // Import the centralized api
import './CustomNode.css';
// A dedicated component for the slider to manage its own state
const SliderInput = ({ id, label, props }) => {
const [value, setValue] = useState(props.value || props.minimum);
return (
{label} ({value})
setValue(e.target.value)}
/>
);
};
// This component renders a single input row, including a target handle if applicable
const InputRow = ({ node_id, input, isConnected, isConnectable }) => {
const { type, label, props = {} } = input;
const controlId = `node-${node_id}-input-${input.id}`;
const isPrimary = ['textbox', 'image', 'audio', 'video', 'file'].includes(type);
const [uploadStatus, setUploadStatus] = useState('idle');
const [filePath, setFilePath] = useState('');
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
setUploadStatus('uploading');
try {
const response = await api.post('/upload/', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
setFilePath(response.data.path);
setUploadStatus('success');
} catch (error) {
console.error('File upload failed:', error);
setUploadStatus('error');
}
};
if (isConnected) {
return (
{isPrimary && }
{label}
Connected via edge
);
}
const renderControl = () => {
switch (type) {
case 'textbox':
return ;
case 'number':
return ;
case 'slider':
return ;
case 'checkbox':
return (
{label}
);
case 'dropdown':
return (
{props.choices?.map((choice, index) => (
{choice}
))}
);
case 'image':
case 'audio':
case 'video':
case 'file':
return (
{/* Hidden input to store the server path */}
{uploadStatus === 'uploading' && Uploading... }
{uploadStatus === 'success' && ✅ Uploaded }
{uploadStatus === 'error' && ⚠️ Error }
);
default:
return Unsupported type: {type} ;
}
};
return (
{isPrimary && }
{type !== 'checkbox' && {label} }
{renderControl()}
);
};
// Simple JSON output selector with pre-defined common keys
const JsonOutputSelector = ({ selectedPath, onPathChange }) => {
// Common JSON keys that APIs typically return
const commonJsonKeys = [
'(entire object)',
'response',
'message',
'result',
'data',
'content',
'text',
'output',
'value',
'answer',
'generated_text',
'choices[0].text',
'choices[0].message.content',
'results[0]',
'items[0]'
];
return (
onPathChange(e.target.value)}
className="json-path-select"
>
{commonJsonKeys.map((key) => (
{key}
))}
);
};
const CustomNode = ({ id, data, isConnectable }) => {
const [isAdvancedVisible, setAdvancedVisible] = useState(false);
const primaryTypes = ['textbox', 'image', 'audio', 'video', 'file'];
const primaryInputs = data.inputs?.filter(input => primaryTypes.includes(input.type));
const advancedInputs = data.inputs?.filter(input => !primaryTypes.includes(input.type));
const handleSelectChange = (event) => {
const newApiName = event.target.value;
if (data.onEndpointChange) {
data.onEndpointChange(id, newApiName);
}
};
const handleJsonPathChange = (outputId, path) => {
if (data.onOutputPathChange) {
data.onOutputPathChange(id, outputId, path);
}
};
// Determine if an output is JSON type (case insensitive)
const isJsonType = (type) => {
return typeof type === 'string' && type.toLowerCase() === 'json';
};
return (
{data.label}
{data.endpoints && data.endpoints.length > 1 ? (
{data.endpoints.map(endpoint => (
{`${endpoint.api_name} ${endpoint.is_preferred ? '(preferred main)' : ''}`}
))}
) : (
/{data.apiName}
)}
Inputs
{primaryInputs?.map(input => (
))}
{advancedInputs && advancedInputs.length > 0 && (
setAdvancedVisible(!isAdvancedVisible)}
>
{isAdvancedVisible ? '▼' : '►'} Advanced Settings
{isAdvancedVisible && (
{advancedInputs.map(input => (
))}
)}
)}
{data.outputs && data.outputs.length > 0 && (
Outputs
{data.outputs.map(output => (
{output.label} ({output.type})
{/* JSON Path Selector - Only show for JSON type outputs */}
{isJsonType(output.type) && (
handleJsonPathChange(output.id, path)}
/>
)}
))}
)}
);
};
export default CustomNode;