Sfoglia il codice sorgente

feat: a numeric setting can take a value from another node

Width, height, steps, seed and every other numeric setting rendered as a number
box, which cannot hold {{ }}. A width that comes from an earlier node had
nowhere to go: dragging a field in did nothing and typing an expression was
rejected by the input itself.

Each numeric setting now carries a link - "Use a value from another node" -
that swaps it for an expression field with the usual drag-and-drop, and "Use a
number instead" to swap back. A setting already holding {{ }} opens in that mode
on its own, so a workflow saved this way reopens the way it was left. Switching
back clears the expression rather than leaving text in a numeric setting, where
it would reach the node as a string.

Fixed while in there: clearing a number box wrote NaN. It now leaves the setting
unset, so a node that falls back to its own default - which for the generation
nodes means the architecture preset - actually can.

Verified that a numeric setting given an expression reaches the node as a
NUMBER, not the string form of one: a Wait node with duration {{data.result.ms}}
where ms is 250 reports waitedMs 250. Eight numeric settings on SD.cpp Text to
Image offer the link, and switching Width over gives an expression field.
Full suite 59/59.
fszontagh 1 mese fa
parent
commit
55a065acc2

+ 18 - 0
tests/nodes/numeric-field-from-expression.json

@@ -0,0 +1,18 @@
+{
+  "name": "verify-numeric-field-from-expression",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "src", "name": "Src", "type": "code", "position": {"x": 0, "y": 100},
+     "config": {"code": "return { ms: 250 };"}},
+    {"id": "w", "name": "Wait", "type": "wait", "position": {"x": 0, "y": 200},
+     "config": {"duration": "{{data.result.ms}}", "unit": "milliseconds"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "src", "targetInput": "data"},
+    {"sourceNodeId": "src", "sourceOutput": "main", "targetNodeId": "w", "targetInput": "data"}
+  ],
+  "expectStatus": "completed",
+  "expect": {
+    "w": {"status": "completed", "output": {"waitedMs": 250}}
+  }
+}

+ 79 - 10
webui/src/components/workflow/NodeConfigModal.tsx

@@ -115,6 +115,19 @@ export function NodeConfigModal({
     { busy: false, message: '', error: false }
   )
 
+  // Numeric settings that are being given an expression instead of a number.
+  // A value that already holds {{ }} is one by definition; this covers the
+  // moment after switching, when the box is still empty.
+  const [asExpression, setAsExpression] = useState<Set<string>>(new Set())
+  const toggleExpression = (key: string, on: boolean) => {
+    setAsExpression((prev) => {
+      const next = new Set(prev)
+      if (on) next.add(key)
+      else next.delete(key)
+      return next
+    })
+  }
+
   const [importing, setImporting] = useState<{
     field: string
     source: string
@@ -578,16 +591,72 @@ export function NodeConfigModal({
                         <span className="text-sm text-gray-600 dark:text-gray-400">{prop.description}</span>
                       </label>
                     ) : prop.type === 'number' || prop.type === 'integer' ? (
-                      <input
-                        type="number"
-                        value={editingConfig[key] ?? prop.default ?? ''}
-                        onChange={(e) =>
-                          onConfigChange({ ...editingConfig, [key]: prop.type === 'integer' ? parseInt(e.target.value, 10) : Number(e.target.value) })
-                        }
-                        step={prop.type === 'integer' ? 1 : undefined}
-                        placeholder={prop.description}
-                        className="w-full px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
-                      />
+                      (() => {
+                        const current = editingConfig[key]
+                        // A number box cannot hold {{ }}, so a width that comes
+                        // from an earlier node had nowhere to go. Either form is
+                        // allowed and the field remembers which it is in.
+                        const usingExpression =
+                          asExpression.has(key) ||
+                          (typeof current === 'string' && current.includes('{{'))
+                        return (
+                          <div className="space-y-1">
+                            {usingExpression ? (
+                              <ExpressionInput
+                                value={String(current ?? '')}
+                                onChange={(value) =>
+                                  onConfigChange({ ...editingConfig, [key]: value })
+                                }
+                                placeholder={`An expression giving a number, such as {{data.result.width}}`}
+                                availableFields={getUpstreamOutputFields(selectedNodeData.id).map((f) => ({
+                                  path: f.path,
+                                  type: f.type,
+                                  nodeName: f.nodeName,
+                                  nodeId: f.nodeId,
+                                  displayName: f.displayName,
+                                }))}
+                              />
+                            ) : (
+                              <input
+                                type="number"
+                                value={current ?? prop.default ?? ''}
+                                onChange={(e) =>
+                                  onConfigChange({
+                                    ...editingConfig,
+                                    // An empty box is not zero. Clearing it has
+                                    // to leave the setting unset, or a node that
+                                    // falls back to its own default never can.
+                                    [key]: e.target.value === ''
+                                      ? ''
+                                      : prop.type === 'integer'
+                                        ? parseInt(e.target.value, 10)
+                                        : Number(e.target.value),
+                                  })
+                                }
+                                step={prop.type === 'integer' ? 1 : undefined}
+                                placeholder={prop.description}
+                                className="w-full px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
+                              />
+                            )}
+                            <button
+                              type="button"
+                              onClick={() => {
+                                const goingToExpression = !usingExpression
+                                toggleExpression(key, goingToExpression)
+                                // Switching back drops an expression rather than
+                                // leaving a string in a numeric setting, where it
+                                // would reach the node as text.
+                                if (!goingToExpression && typeof current === 'string') {
+                                  onConfigChange({ ...editingConfig, [key]: '' })
+                                }
+                              }}
+                              className="text-[11px] text-primary-600 dark:text-primary-400 hover:underline"
+                            >
+                              {usingExpression ? 'Use a number instead' : 'Use a value from another node'}
+                            </button>
+                          </div>
+                        )
+                      })()
                     ) : prop.dynamicOptions?.source === 'workflows' ? (
                       <select
                         value={editingConfig[key] ?? ''}