feat(ui): edit objective/constraint source in panel + UNSET wiring
This commit is contained in:
@@ -560,9 +560,10 @@ function SpecRendererInner({
|
||||
// clear the target's source to avoid stale runnable config.
|
||||
if (sourceType === 'extractor' && (targetType === 'objective' || targetType === 'constraint')) {
|
||||
updateNode(edge.target, {
|
||||
// Setting to an empty object would violate schema; clear to placeholder
|
||||
// and let validation catch missing wiring.
|
||||
source: { extractor_id: '', output_name: '' },
|
||||
// Objective/constraint.source is required by schema.
|
||||
// Use explicit UNSET placeholders so validation can catch it
|
||||
// without risking accidental execution.
|
||||
source: { extractor_id: '__UNSET__', output_name: '__UNSET__' },
|
||||
}).catch((err) => {
|
||||
console.error('Failed to clear source on node:', err);
|
||||
setError(err.message);
|
||||
|
||||
@@ -820,6 +820,34 @@ interface ObjectiveNodeConfigProps {
|
||||
}
|
||||
|
||||
function ObjectiveNodeConfig({ node, onChange }: ObjectiveNodeConfigProps) {
|
||||
const spec = useSpec();
|
||||
const extractors = spec?.extractors || [];
|
||||
|
||||
const currentExtractorId = node.source?.extractor_id || '__UNSET__';
|
||||
const currentOutputName = node.source?.output_name || '__UNSET__';
|
||||
|
||||
const selectedExtractor = extractors.find((e) => e.id === currentExtractorId);
|
||||
const outputOptions = selectedExtractor?.outputs?.map((o) => o.name) || [];
|
||||
|
||||
const handleExtractorChange = (extractorId: string) => {
|
||||
// Reset output_name to a sensible default when extractor changes
|
||||
const ext = extractors.find((e) => e.id === extractorId);
|
||||
const outs = ext?.outputs?.map((o) => o.name) || [];
|
||||
const preferred = outs.includes('value') ? 'value' : outs[0] || '__UNSET__';
|
||||
|
||||
onChange('source', {
|
||||
extractor_id: extractorId,
|
||||
output_name: preferred,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOutputChange = (outputName: string) => {
|
||||
onChange('source', {
|
||||
extractor_id: currentExtractorId,
|
||||
output_name: outputName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
@@ -831,6 +859,45 @@ function ObjectiveNodeConfig({ node, onChange }: ObjectiveNodeConfigProps) {
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Source Extractor</label>
|
||||
<select
|
||||
value={currentExtractorId}
|
||||
onChange={(e) => handleExtractorChange(e.target.value)}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="__UNSET__">(not connected)</option>
|
||||
{extractors.map((ext) => (
|
||||
<option key={ext.id} value={ext.id}>
|
||||
{ext.id} — {ext.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Source Output</label>
|
||||
<select
|
||||
value={currentOutputName}
|
||||
onChange={(e) => handleOutputChange(e.target.value)}
|
||||
className={selectClass}
|
||||
disabled={currentExtractorId === '__UNSET__'}
|
||||
>
|
||||
{currentExtractorId === '__UNSET__' ? (
|
||||
<option value="__UNSET__">(select an extractor)</option>
|
||||
) : (
|
||||
outputOptions.map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
<p className="text-xs text-dark-500 mt-1">
|
||||
This drives execution. Canvas wires are just a visual check.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Direction</label>
|
||||
@@ -877,6 +944,33 @@ interface ConstraintNodeConfigProps {
|
||||
}
|
||||
|
||||
function ConstraintNodeConfig({ node, onChange }: ConstraintNodeConfigProps) {
|
||||
const spec = useSpec();
|
||||
const extractors = spec?.extractors || [];
|
||||
|
||||
const currentExtractorId = node.source?.extractor_id || '__UNSET__';
|
||||
const currentOutputName = node.source?.output_name || '__UNSET__';
|
||||
|
||||
const selectedExtractor = extractors.find((e) => e.id === currentExtractorId);
|
||||
const outputOptions = selectedExtractor?.outputs?.map((o) => o.name) || [];
|
||||
|
||||
const handleExtractorChange = (extractorId: string) => {
|
||||
const ext = extractors.find((e) => e.id === extractorId);
|
||||
const outs = ext?.outputs?.map((o) => o.name) || [];
|
||||
const preferred = outs.includes('value') ? 'value' : outs[0] || '__UNSET__';
|
||||
|
||||
onChange('source', {
|
||||
extractor_id: extractorId,
|
||||
output_name: preferred,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOutputChange = (outputName: string) => {
|
||||
onChange('source', {
|
||||
extractor_id: currentExtractorId,
|
||||
output_name: outputName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
@@ -888,6 +982,45 @@ function ConstraintNodeConfig({ node, onChange }: ConstraintNodeConfigProps) {
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Source Extractor</label>
|
||||
<select
|
||||
value={currentExtractorId}
|
||||
onChange={(e) => handleExtractorChange(e.target.value)}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="__UNSET__">(not connected)</option>
|
||||
{extractors.map((ext) => (
|
||||
<option key={ext.id} value={ext.id}>
|
||||
{ext.id} — {ext.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Source Output</label>
|
||||
<select
|
||||
value={currentOutputName}
|
||||
onChange={(e) => handleOutputChange(e.target.value)}
|
||||
className={selectClass}
|
||||
disabled={currentExtractorId === '__UNSET__'}
|
||||
>
|
||||
{currentExtractorId === '__UNSET__' ? (
|
||||
<option value="__UNSET__">(select an extractor)</option>
|
||||
) : (
|
||||
outputOptions.map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
<p className="text-xs text-dark-500 mt-1">
|
||||
This drives execution. Canvas wires are just a visual check.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
@@ -897,24 +1030,37 @@ function ConstraintNodeConfig({ node, onChange }: ConstraintNodeConfigProps) {
|
||||
onChange={(e) => onChange('type', e.target.value)}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="less_than">< Less than</option>
|
||||
<option value="less_equal"><= Less or equal</option>
|
||||
<option value="greater_than">> Greater than</option>
|
||||
<option value="greater_equal">>= Greater or equal</option>
|
||||
<option value="equal">= Equal</option>
|
||||
<option value="hard">Hard</option>
|
||||
<option value="soft">Soft</option>
|
||||
</select>
|
||||
<p className="text-xs text-dark-500 mt-1">Spec type (hard/soft). Operator is set below.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Threshold</label>
|
||||
<input
|
||||
type="number"
|
||||
value={node.threshold}
|
||||
onChange={(e) => onChange('threshold', parseFloat(e.target.value))}
|
||||
className={inputClass}
|
||||
/>
|
||||
<label className={labelClass}>Operator</label>
|
||||
<select
|
||||
value={node.operator}
|
||||
onChange={(e) => onChange('operator', e.target.value)}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="<="><=</option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value=">">></option>
|
||||
<option value="==">==</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Threshold</label>
|
||||
<input
|
||||
type="number"
|
||||
value={node.threshold}
|
||||
onChange={(e) => onChange('threshold', parseFloat(e.target.value))}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,9 +178,14 @@ const validationRules: ValidationRule[] = [
|
||||
edge => edge.target === obj.id && edge.source.startsWith('ext_')
|
||||
);
|
||||
|
||||
// Also check if source.extractor_id is set
|
||||
const hasDirectSource = obj.source?.extractor_id &&
|
||||
spec.extractors.some(e => e.id === obj.source.extractor_id);
|
||||
// Also check if source.extractor_id is set (and not UNSET placeholders)
|
||||
const extractorId = obj.source?.extractor_id;
|
||||
const outputName = obj.source?.output_name;
|
||||
const hasDirectSource = Boolean(extractorId) &&
|
||||
extractorId !== '__UNSET__' &&
|
||||
Boolean(outputName) &&
|
||||
outputName !== '__UNSET__' &&
|
||||
spec.extractors.some(e => e.id === extractorId);
|
||||
|
||||
if (!hasSource && !hasDirectSource) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user