Spaces:
Sleeping
Sleeping
| 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; |