Skip to content

Commit b33a9cd

Browse files
committed
added dockerfile output, improved parameter UI, etc.
1 parent 5c99e8c commit b33a9cd

File tree

4 files changed

+793
-229
lines changed

4 files changed

+793
-229
lines changed

src/components/NodeComponent.jsx

Lines changed: 165 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Handle, Position } from 'reactflow';
44
import { Modal, Form } from 'react-bootstrap';
55
import { getToolConfigSync } from '../utils/toolRegistry.js';
66
import { DOCKER_IMAGES, DOCKER_TAGS, annotationByName } from '../utils/toolAnnotations.js';
7-
import { useToast } from '../context/ToastContext.jsx';
87
import TagDropdown from './TagDropdown.jsx';
98
import { ScatterPropagationContext } from '../context/ScatterPropagationContext.jsx';
109
import { WiredInputsContext } from '../context/WiredInputsContext.jsx';
@@ -46,10 +45,8 @@ const NodeComponent = ({ data, id }) => {
4645
const wiredContext = useContext(WiredInputsContext);
4746
const wiredInputs = wiredContext.get(id) || new Map();
4847

49-
const { showError, dismissMessage } = useToast();
50-
const JSON_ERROR_MSG = 'Invalid JSON entered. Please ensure entry is formatted appropriately.';
5148
const [showModal, setShowModal] = useState(false);
52-
const [textInput, setTextInput] = useState(data.parameters || '');
49+
const [paramValues, setParamValues] = useState({});
5350
const [dockerVersion, setDockerVersion] = useState(data.dockerVersion || 'latest');
5451
const [versionValid, setVersionValid] = useState(true);
5552
const [versionWarning, setVersionWarning] = useState('');
@@ -60,19 +57,20 @@ const NodeComponent = ({ data, id }) => {
6057
const [infoTooltipPos, setInfoTooltipPos] = useState({ top: 0, left: 0 });
6158
const infoIconRef = useRef(null);
6259

63-
// Get tool definition and optional inputs
60+
// Get tool definition
6461
const tool = getToolConfigSync(data.label);
65-
const optionalInputs = tool?.optionalInputs || {};
66-
const hasDefinedTool = !!tool;
6762
const dockerImage = tool?.dockerImage || null;
6863

69-
// Required File/Directory inputs (shown as wired/unwired in modal)
70-
const requiredFileInputs = useMemo(() => {
71-
if (!tool?.requiredInputs) return {};
72-
return Object.fromEntries(
73-
Object.entries(tool.requiredInputs)
74-
.filter(([_, def]) => def.type === 'File' || def.type === 'Directory')
75-
);
64+
// All parameters split into required and optional
65+
const allParams = useMemo(() => {
66+
if (!tool) return { required: [], optional: [] };
67+
const required = Object.entries(tool.requiredInputs || {})
68+
.filter(([_, def]) => def.type !== 'record')
69+
.map(([name, def]) => ({ name, ...def }));
70+
const optional = Object.entries(tool.optionalInputs || {})
71+
.filter(([_, def]) => def.type !== 'record')
72+
.map(([name, def]) => ({ name, ...def }));
73+
return { required, optional };
7674
}, [tool]);
7775

7876
// Get known tags for this tool's docker image
@@ -106,68 +104,105 @@ const NodeComponent = ({ data, id }) => {
106104
return annotationByName.get(data.label) || null;
107105
}, [data.label]);
108106

109-
// Generate a helpful default JSON showing available optional parameters
110-
const defaultJson = useMemo(() => {
111-
if (!hasDefinedTool || Object.keys(optionalInputs).length === 0) {
112-
return '{\n \n}';
113-
}
107+
// Update a single parameter value
108+
const updateParam = (name, value) => {
109+
setParamValues(prev => ({ ...prev, [name]: value }));
110+
};
111+
112+
// Clamp numeric value to bounds on blur
113+
const clampToBounds = (name, param) => {
114+
const val = paramValues[name];
115+
if (val === null || val === undefined || !param.bounds) return;
116+
const [min, max] = param.bounds;
117+
if (val < min) updateParam(name, min);
118+
else if (val > max) updateParam(name, max);
119+
};
114120

115-
const exampleParams = {};
116-
Object.entries(optionalInputs).forEach(([name, def]) => {
117-
// Skip record types in example
118-
if (def.type === 'record') return;
119-
120-
// Generate example value based on type
121-
switch (def.type) {
122-
case 'boolean':
123-
exampleParams[name] = false;
124-
break;
125-
case 'int':
126-
exampleParams[name] = def.bounds ? def.bounds[0] : 0;
127-
break;
128-
case 'double':
129-
exampleParams[name] = def.bounds ? def.bounds[0] : 0.0;
130-
break;
131-
case 'string':
132-
exampleParams[name] = '';
133-
break;
134-
default:
135-
exampleParams[name] = null;
136-
}
137-
});
138-
139-
return JSON.stringify(exampleParams, null, 4);
140-
}, [hasDefinedTool, optionalInputs]);
141-
142-
// Generate help text showing available options
143-
const optionsHelpText = useMemo(() => {
144-
if (!hasDefinedTool || Object.keys(optionalInputs).length === 0) {
145-
return 'No optional parameters defined for this tool.';
121+
// Shared renderer for param inline controls (used by both required and optional sections)
122+
const renderParamControl = (param, wiredInfo, isRequired) => {
123+
const isFileType = param.type === 'File' || param.type === 'Directory';
124+
125+
if (isFileType) {
126+
// File/Directory: show wired source or runtime placeholder
127+
const content = wiredInfo ? (
128+
<span className="input-source">
129+
from {wiredInfo.sourceNodeLabel} / {wiredInfo.sourceOutput}
130+
</span>
131+
) : (
132+
<span className="input-runtime">runtime input</span>
133+
);
134+
// Required file types render inline (no wrapper div); optional get param-control wrapper
135+
return isRequired ? content : <div className="param-control">{content}</div>;
146136
}
147137

148-
return Object.entries(optionalInputs)
149-
.filter(([_, def]) => def.type !== 'record')
150-
.map(([name, def]) => `• ${name} (${def.type}): ${def.label}`)
151-
.join('\n');
152-
}, [hasDefinedTool, optionalInputs]);
138+
// Scalar types: render editable control
139+
const control = param.type === 'boolean' ? (
140+
<Form.Check
141+
type="switch"
142+
id={`param-${id}-${param.name}`}
143+
checked={paramValues[param.name] === true}
144+
onChange={(e) => updateParam(param.name, e.target.checked)}
145+
className="param-switch"
146+
/>
147+
) : param.options ? (
148+
<Form.Select
149+
size="sm"
150+
className="param-select"
151+
value={paramValues[param.name] ?? ''}
152+
onChange={(e) => updateParam(param.name, e.target.value || null)}
153+
>
154+
<option value="">-- default --</option>
155+
{param.options.map((opt) => (
156+
<option key={opt} value={opt}>{opt}</option>
157+
))}
158+
</Form.Select>
159+
) : (param.type === 'int' || param.type === 'double' || param.type === 'float' || param.type === 'long') ? (
160+
<Form.Control
161+
type="number"
162+
size="sm"
163+
className="param-number"
164+
step={param.type === 'int' || param.type === 'long' ? 1 : 0.01}
165+
min={param.bounds ? param.bounds[0] : undefined}
166+
max={param.bounds ? param.bounds[1] : undefined}
167+
placeholder={param.bounds ? `${param.bounds[0]}..${param.bounds[1]}` : ''}
168+
value={paramValues[param.name] ?? ''}
169+
onChange={(e) => {
170+
const val = e.target.value;
171+
if (val === '') {
172+
updateParam(param.name, null);
173+
} else {
174+
updateParam(param.name, param.type === 'int' || param.type === 'long' ? parseInt(val, 10) : parseFloat(val));
175+
}
176+
}}
177+
onBlur={() => clampToBounds(param.name, param)}
178+
/>
179+
) : (
180+
<Form.Control
181+
type="text"
182+
size="sm"
183+
className="param-text"
184+
value={paramValues[param.name] ?? ''}
185+
onChange={(e) => updateParam(param.name, e.target.value || null)}
186+
/>
187+
);
188+
189+
return <div className="param-control">{control}</div>;
190+
};
153191

154192
const handleOpenModal = () => {
155193
// Auto-enable scatter toggle if inherited from upstream (non-source node)
156194
if (!isSourceNode && isScatterInherited && !scatterEnabled) {
157195
setScatterEnabled(true);
158196
}
159197

160-
let inputValue = textInput;
161-
162-
// Ensure inputValue is always a string before calling trim()
163-
if (typeof inputValue !== 'string') {
164-
inputValue = JSON.stringify(inputValue, null, 4);
165-
}
166-
167-
if (!inputValue.trim()) {
168-
setTextInput(defaultJson);
198+
// Initialize paramValues from saved data (object or legacy JSON string)
199+
const existing = data.parameters;
200+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
201+
setParamValues({ ...existing });
202+
} else if (typeof existing === 'string' && existing.trim()) {
203+
try { setParamValues(JSON.parse(existing)); } catch { setParamValues({}); }
169204
} else {
170-
setTextInput(inputValue);
205+
setParamValues({});
171206
}
172207

173208
setShowModal(true);
@@ -180,19 +215,9 @@ const NodeComponent = ({ data, id }) => {
180215
setDockerVersion(finalDockerVersion);
181216
}
182217

183-
// Validate JSON before allowing close
184218
if (typeof data.onSaveParameters === 'function') {
185-
let parsed;
186-
try {
187-
parsed = JSON.parse(textInput);
188-
} catch (err) {
189-
showError(JSON_ERROR_MSG, 4000);
190-
return; // Keep modal open
191-
}
192-
193-
dismissMessage(JSON_ERROR_MSG);
194219
data.onSaveParameters({
195-
params: parsed,
220+
params: paramValues,
196221
dockerVersion: finalDockerVersion,
197222
scatterEnabled: scatterEnabled
198223
});
@@ -201,31 +226,6 @@ const NodeComponent = ({ data, id }) => {
201226
setShowModal(false);
202227
};
203228

204-
const handleInputChange = (e) => {
205-
setTextInput(e.target.value);
206-
dismissMessage(JSON_ERROR_MSG);
207-
};
208-
209-
const handleKeyDown = (e) => {
210-
if (e.key === 'Tab') {
211-
e.preventDefault();
212-
const tabSpaces = ' '; // Insert 4 spaces
213-
const { selectionStart, selectionEnd } = e.target;
214-
const newValue =
215-
textInput.substring(0, selectionStart) +
216-
tabSpaces +
217-
textInput.substring(selectionEnd);
218-
219-
setTextInput(newValue);
220-
221-
// Move cursor forward
222-
setTimeout(() => {
223-
e.target.selectionStart = e.target.selectionEnd =
224-
selectionStart + tabSpaces.length;
225-
}, 0);
226-
}
227-
};
228-
229229
// Info icon hover handlers (simple tooltip, no click persistence)
230230
const handleInfoMouseEnter = () => {
231231
if (infoIconRef.current && toolInfo) {
@@ -399,65 +399,70 @@ const NodeComponent = ({ data, id }) => {
399399
</div>
400400
</Form.Group>
401401

402-
{/* Required Inputs (File/Directory) */}
403-
{Object.keys(requiredFileInputs).length > 0 && (
404-
<Form.Group className="required-inputs-section">
405-
<Form.Label className="modal-label">Inputs</Form.Label>
406-
{Object.entries(requiredFileInputs).map(([name, def]) => {
407-
const wiredInfo = wiredInputs.get(name);
408-
return (
409-
<div key={name} className={`input-row ${wiredInfo ? 'input-wired' : 'input-unwired'}`}>
410-
<span className="input-name">{def.label || name}</span>
411-
<span className="input-type-badge">{def.type}</span>
412-
{wiredInfo ? (
413-
<span className="input-source">
414-
from {wiredInfo.sourceNodeLabel} / {wiredInfo.sourceOutput}
415-
</span>
416-
) : (
417-
<span className="input-runtime">supplied at runtime</span>
418-
)}
419-
</div>
420-
);
421-
})}
422-
</Form.Group>
423-
)}
424-
425-
<Form.Group className="mb-3">
426-
<Form.Label className="modal-label">
427-
Configure optional parameters as JSON.
428-
{!hasDefinedTool && ' (Tool not fully defined - using generic parameters)'}
429-
</Form.Label>
430-
<Form.Control
431-
as="textarea"
432-
rows={8}
433-
value={textInput}
434-
onChange={handleInputChange}
435-
onKeyDown={handleKeyDown}
436-
className="code-input"
437-
spellCheck="false"
438-
autoCorrect="off"
439-
autoCapitalize="off"
440-
/>
441-
</Form.Group>
442-
{hasDefinedTool && Object.keys(optionalInputs).length > 0 && (
443-
<Form.Group>
444-
<Form.Label className="modal-label" style={{ fontSize: '0.8rem', color: '#808080' }}>
445-
Available options:
446-
</Form.Label>
447-
<pre style={{
448-
fontSize: '0.75rem',
449-
color: '#a0a0a0',
450-
backgroundColor: '#1a1a1a',
451-
padding: '8px',
452-
borderRadius: '4px',
453-
maxHeight: '150px',
454-
overflow: 'auto',
455-
whiteSpace: 'pre-wrap'
456-
}}>
457-
{optionsHelpText}
458-
</pre>
459-
</Form.Group>
460-
)}
402+
{/* Unified Parameter Pane */}
403+
<div className="params-scroll">
404+
{/* Required Parameters */}
405+
{allParams.required.length > 0 && (
406+
<div className="param-section">
407+
<div className="param-section-header">Required</div>
408+
{allParams.required.map((param) => {
409+
const wiredInfo = wiredInputs.get(param.name);
410+
const isFileType = param.type === 'File' || param.type === 'Directory';
411+
return (
412+
<div key={param.name} className={`param-card ${isFileType ? (wiredInfo ? 'input-wired' : 'input-unwired') : ''}`}>
413+
<div className="param-card-header">
414+
<span className="param-name">{param.name}</span>
415+
<span className="param-type-badge">{param.type}</span>
416+
{renderParamControl(param, wiredInfo, true)}
417+
</div>
418+
{param.label && (
419+
<div className="param-description">{param.label}</div>
420+
)}
421+
{param.bounds && (
422+
<div className="param-bounds">bounds: {param.bounds[0]}{param.bounds[1]}</div>
423+
)}
424+
</div>
425+
);
426+
})}
427+
</div>
428+
)}
429+
430+
{/* Optional Parameters */}
431+
{allParams.optional.length > 0 && (
432+
<div className="param-section">
433+
<div className="param-section-header">Optional</div>
434+
{allParams.optional.map((param) => {
435+
const wiredInfo = wiredInputs.get(param.name);
436+
const isFileType = param.type === 'File' || param.type === 'Directory';
437+
return (
438+
<div key={param.name} className={`param-card ${isFileType && wiredInfo ? 'input-wired' : ''}`}>
439+
<div className="param-card-header">
440+
<span className="param-name">{param.name}</span>
441+
<span className="param-type-badge">{param.type}</span>
442+
{renderParamControl(param, wiredInfo, false)}
443+
</div>
444+
{param.label && (
445+
<div className="param-description">{param.label}</div>
446+
)}
447+
{param.bounds && (
448+
<div className="param-bounds">bounds: {param.bounds[0]}{param.bounds[1]}</div>
449+
)}
450+
</div>
451+
);
452+
})}
453+
</div>
454+
)}
455+
456+
{/* Fallback for unknown tools */}
457+
{!tool && (
458+
<div className="param-section">
459+
<div className="param-section-header">Parameters</div>
460+
<div className="param-description" style={{ padding: '8px 0' }}>
461+
Tool not fully defined — parameters unavailable.
462+
</div>
463+
</div>
464+
)}
465+
</div>
461466
</Form>
462467
</Modal.Body>
463468
</Modal>

0 commit comments

Comments
 (0)