parent
785a40ff9d
commit
6093acc813
|
@ -1,15 +1,21 @@
|
|||
import React, { useState, useEffect, FC, memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import './customnode.css';
|
||||
import ModalComponent from './ModalComponent';
|
||||
|
||||
type Schema = {
|
||||
type: string;
|
||||
properties: { [key: string]: any };
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ data }) => {
|
||||
const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
const [modalValue, setModalValue] = useState<string>('');
|
||||
|
||||
// Automatically open properties when output_data or status is updated
|
||||
useEffect(() => {
|
||||
if (data.output_data || data.status) {
|
||||
setIsPropertiesOpen(true);
|
||||
|
@ -24,7 +30,7 @@ const CustomNode: FC<NodeProps> = ({ data }) => {
|
|||
if (!schema?.properties) return null;
|
||||
const keys = Object.keys(schema.properties);
|
||||
return keys.map((key) => (
|
||||
<div key={key} style={{ display: 'flex', alignItems: 'center', position: 'relative', marginBottom: '5px' }}>
|
||||
<div key={key} className="handle-container">
|
||||
{type === 'target' && (
|
||||
<>
|
||||
<Handle
|
||||
|
@ -33,12 +39,12 @@ const CustomNode: FC<NodeProps> = ({ data }) => {
|
|||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span style={{ color: '#e0e0e0', marginLeft: '10px' }}>{key}</span>
|
||||
<span className="handle-label">{key}</span>
|
||||
</>
|
||||
)}
|
||||
{type === 'source' && (
|
||||
<>
|
||||
<span style={{ color: '#e0e0e0', marginRight: '10px' }}>{key}</span>
|
||||
<span className="handle-label">{key}</span>
|
||||
<Handle
|
||||
type={type}
|
||||
position={Position.Right}
|
||||
|
@ -57,57 +63,160 @@ const CustomNode: FC<NodeProps> = ({ data }) => {
|
|||
};
|
||||
|
||||
const isHandleConnected = (key: string) => {
|
||||
return data.connections.some((conn: string) => {
|
||||
const [, target] = conn.split(' -> ');
|
||||
return data.connections && data.connections.some((conn: string) => {
|
||||
const [source, target] = conn.split(' -> ');
|
||||
return target.includes(key) && target.includes(data.title);
|
||||
});
|
||||
};
|
||||
|
||||
const hasDisconnectedHandle = (key: string) => {
|
||||
return !isHandleConnected(key);
|
||||
const handleInputClick = (key: string) => {
|
||||
setActiveKey(key);
|
||||
setModalValue(data.hardcodedValues[key] || '');
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalSave = (value: string) => {
|
||||
if (activeKey) {
|
||||
handleInputChange(activeKey, value);
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setActiveKey(null);
|
||||
};
|
||||
|
||||
const renderInputField = (key: string, schema: any) => {
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
return schema.enum ? (
|
||||
<div key={key} className="input-container">
|
||||
<select
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="select-input"
|
||||
>
|
||||
{schema.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div key={key} className="input-container">
|
||||
<div className="clickable-input" onClick={() => handleInputClick(key)}>
|
||||
{data.hardcodedValues[key] || `Enter ${key}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
<label className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
value="true"
|
||||
checked={data.hardcodedValues[key] === true}
|
||||
onChange={() => handleInputChange(key, true)}
|
||||
/>
|
||||
True
|
||||
</label>
|
||||
<label className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
value="false"
|
||||
checked={data.hardcodedValues[key] === false}
|
||||
onChange={() => handleInputChange(key, false)}
|
||||
/>
|
||||
False
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
<input
|
||||
type="number"
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, parseFloat(e.target.value))}
|
||||
className="number-input"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'array':
|
||||
if (schema.items && schema.items.type === 'string' && schema.items.enum) {
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
<select
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="select-input"
|
||||
>
|
||||
{schema.items.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', border: '2px solid #fff', borderRadius: '20px', background: '#333', color: '#e0e0e0', width: '250px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>
|
||||
{data?.title.replace(/\d+/g, '')}
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleProperties}
|
||||
style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: '#e0e0e0' }}
|
||||
>
|
||||
<div className="custom-node">
|
||||
<div className="node-header">
|
||||
<div className="node-title">{data?.title.replace(/\d+/g, '')}</div>
|
||||
<button onClick={toggleProperties} className="toggle-button">
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', gap: '20px' }}>
|
||||
<div>
|
||||
{data.inputSchema && generateHandles(data.inputSchema, 'target')}
|
||||
{data.inputSchema && Object.keys(data.inputSchema.properties).map(key => (
|
||||
hasDisconnectedHandle(key) && (
|
||||
<div key={key} style={{ marginBottom: '5px' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Enter ${key}`}
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
style={{ width: '100%', padding: '5px', borderRadius: '4px', border: '1px solid #555', background: '#444', color: '#e0e0e0' }}
|
||||
/>
|
||||
<div className="node-content">
|
||||
<div className="input-section">
|
||||
{data.inputSchema &&
|
||||
Object.keys(data.inputSchema.properties).map((key) => (
|
||||
<div key={key}>
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
</div>
|
||||
{!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])}
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="output-section">
|
||||
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
|
||||
</div>
|
||||
</div>
|
||||
{isPropertiesOpen && (
|
||||
<div style={{ marginTop: '10px', background: '#444', padding: '10px', borderRadius: '10px' }}>
|
||||
<div className="node-properties">
|
||||
<h4>Node Output</h4>
|
||||
<p><strong>Status:</strong> {typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}</p>
|
||||
<p><strong>Output Data:</strong> {typeof data.output_data === 'object' ? JSON.stringify(data.output_data) : data.output_data || 'N/A'}</p>
|
||||
<p>
|
||||
<strong>Status:</strong>{' '}
|
||||
{typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Output Data:</strong>{' '}
|
||||
{typeof data.output_data === 'object'
|
||||
? JSON.stringify(data.output_data)
|
||||
: data.output_data || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<ModalComponent
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSave={handleModalSave}
|
||||
value={modalValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import React, { FC } from 'react';
|
||||
import './modal.css';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
|
||||
const [tempValue, setTempValue] = React.useState(value);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(tempValue);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<textarea
|
||||
className="modal-textarea"
|
||||
value={tempValue}
|
||||
onChange={(e) => setTempValue(e.target.value)}
|
||||
/>
|
||||
<div className="modal-actions">
|
||||
<button onClick={onClose}>Cancel</button>
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalComponent;
|
|
@ -0,0 +1,143 @@
|
|||
.custom-node {
|
||||
padding: 20px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 12px;
|
||||
background: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
width: 260px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #e0e0e0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-section, .output-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.handle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.handle-label {
|
||||
color: #e0e0e0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.clickable-input {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #555;
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.clickable-input:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.radio-label input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.node-properties {
|
||||
margin-top: 20px;
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.node-properties h4 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.node-properties p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.custom-node {
|
||||
width: 90%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clickable-input {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
Loading…
Reference in New Issue