SuperGradio / frontend /src /CustomNode.js
hadinicknam's picture
Handling different key value of the Json
9212968
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 (
<div className="custom-node-input-row">
<label htmlFor={id}>{label} ({value})</label>
<input
id={id}
type="range"
min={props.minimum || 0}
max={props.maximum || 100}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
);
};
// 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 (
<div className="custom-node-input-row connected">
{isPrimary && <Handle type="target" position={Position.Left} id={`input-${input.id}`} isConnectable={isConnectable} />}
<label>{label}</label>
<span>Connected via edge</span>
</div>
);
}
const renderControl = () => {
switch (type) {
case 'textbox':
return <textarea id={controlId} rows={3} defaultValue={props.value || ''} />;
case 'number':
return <input id={controlId} type="number" defaultValue={props.value || 0} />;
case 'slider':
return <SliderInput id={controlId} label={label} props={props} />;
case 'checkbox':
return (
<div className="checkbox-wrapper">
<input id={controlId} type="checkbox" defaultChecked={props.value || false} />
<label htmlFor={controlId}>{label}</label>
</div>
);
case 'dropdown':
return (
<select id={controlId} defaultValue={props.value}>
{props.choices?.map((choice, index) => (
<option key={index} value={choice}>{choice}</option>
))}
</select>
);
case 'image':
case 'audio':
case 'video':
case 'file':
return (
<div className="file-input-wrapper">
<input type="file" onChange={handleFileChange} />
{/* Hidden input to store the server path */}
<input type="hidden" id={controlId} value={filePath} />
{uploadStatus === 'uploading' && <small>Uploading...</small>}
{uploadStatus === 'success' && <small>✅ Uploaded</small>}
{uploadStatus === 'error' && <small>⚠️ Error</small>}
</div>
);
default:
return <span>Unsupported type: {type}</span>;
}
};
return (
<div className="custom-node-input-row">
{isPrimary && <Handle type="target" position={Position.Left} id={`input-${input.id}`} isConnectable={isConnectable} />}
{type !== 'checkbox' && <label htmlFor={controlId}>{label}</label>}
{renderControl()}
</div>
);
};
// 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 (
<select
value={selectedPath || '(entire object)'}
onChange={(e) => onPathChange(e.target.value)}
className="json-path-select"
>
{commonJsonKeys.map((key) => (
<option key={key} value={key}>{key}</option>
))}
</select>
);
};
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 (
<div className={`custom-node ${data.status || ''}`}>
<div className="custom-node-header">
<strong>{data.label}</strong>
{data.endpoints && data.endpoints.length > 1 ? (
<select value={data.apiName} onChange={handleSelectChange} style={{ width: '100%', marginTop: '5px', padding: '4px', borderRadius: '4px', border: '1px solid #ccc' }}>
{data.endpoints.map(endpoint => (
<option key={endpoint.api_name} value={endpoint.api_name}>
{`${endpoint.api_name} ${endpoint.is_preferred ? '(preferred main)' : ''}`}
</option>
))}
</select>
) : (
<p>/{data.apiName}</p>
)}
</div>
<div className="custom-node-content">
<div className="custom-node-section">
<div className="custom-node-section-title">Inputs</div>
{primaryInputs?.map(input => (
<InputRow
key={`input-${input.id}`}
node_id={id}
input={input}
isConnected={data.connections?.targets?.includes(String(input.id))}
isConnectable={isConnectable}
/>
))}
{advancedInputs && advancedInputs.length > 0 && (
<div className="advanced-settings">
<button
className="advanced-settings-toggle"
onClick={() => setAdvancedVisible(!isAdvancedVisible)}
>
{isAdvancedVisible ? '▼' : '►'} Advanced Settings
</button>
{isAdvancedVisible && (
<div className="advanced-settings-content">
{advancedInputs.map(input => (
<InputRow
key={`input-${input.id}`}
node_id={id}
input={input}
isConnected={data.connections?.targets?.includes(String(input.id))}
isConnectable={isConnectable}
/>
))}
</div>
)}
</div>
)}
</div>
{data.outputs && data.outputs.length > 0 && (
<div className="custom-node-section">
<div className="custom-node-section-title">Outputs</div>
{data.outputs.map(output => (
<div key={`output-${output.id}`} className="custom-node-output-row">
<label>{output.label} ({output.type})</label>
{/* JSON Path Selector - Only show for JSON type outputs */}
{isJsonType(output.type) && (
<div className="json-path-selector">
<JsonOutputSelector
selectedPath={data.jsonPaths?.[output.id]}
onPathChange={(path) => handleJsonPathChange(output.id, path)}
/>
</div>
)}
<Handle
type="source"
position={Position.Right}
id={`output-${output.id}`}
isConnectable={isConnectable}
data-json-path={isJsonType(output.type) ? data.jsonPaths?.[output.id] || '' : ''}
/>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default CustomNode;