@manny-est/node-red-flowpilot 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +22 -13
- package/flowpilot-core.css +187 -2
- package/flowpilot.js +462 -33
- package/lib/build-system-prompt.js +26 -4
- package/lib/core/apply-review.js +494 -64
- package/lib/core/init.js +67 -132
- package/lib/core/main.js +160 -7
- package/lib/core/modes.js +876 -79
- package/lib/core/selection-context.js +28 -1
- package/lib/default-system-prompt.js +13 -9
- package/lib/document-system-prompt.js +19 -30
- package/lib/generation-system-prompt.js +24 -40
- package/lib/modify-system-prompt.js +98 -70
- package/lib/prompt-fragments.js +45 -0
- package/lib/provider-anthropic.js +385 -0
- package/lib/provider-openai-compatible.js +122 -16
- package/lib/storage.js +43 -2
- package/lib/validator.js +238 -0
- package/package.json +2 -2
package/lib/core/apply-review.js
CHANGED
|
@@ -28,7 +28,21 @@
|
|
|
28
28
|
// throwing) AND on disk (each string serializes with id
|
|
29
29
|
// undefined -> null, confirmed via a live flows.json). Group
|
|
30
30
|
// membership must only ever change through applyGroupChanges().
|
|
31
|
-
nodes: 1
|
|
31
|
+
nodes: 1,
|
|
32
|
+
// Node appearance/documentation fields and node-type-specific
|
|
33
|
+
// internal defaults that models hallucinate sentinel placeholders
|
|
34
|
+
// for during build-loop reviews. None of these are meaningfully
|
|
35
|
+
// settable via a Modify patch — they're either Appearance-tab
|
|
36
|
+
// metadata or type-internal runtime flags. Adding them here
|
|
37
|
+
// prevents false "redaction" blocks with no actionable remediation.
|
|
38
|
+
// Appearance/docs (all node types):
|
|
39
|
+
info: 1, inputLabels: 1, outputLabels: 1, icon: 1,
|
|
40
|
+
// debug node internal display flags:
|
|
41
|
+
console: 1, tostatus: 1, targetType: 1, statusVal: 1, statusType: 1,
|
|
42
|
+
// function node internal flags:
|
|
43
|
+
noerr: 1, initialize: 1, finalize: 1,
|
|
44
|
+
// mqtt in/out retain handling:
|
|
45
|
+
rh: 1
|
|
32
46
|
};
|
|
33
47
|
|
|
34
48
|
// Sentinel strings written by sanitizeNode (and redactDebugValue) for
|
|
@@ -113,7 +127,7 @@
|
|
|
113
127
|
// internally for t:"add" events) and pushes one compound history entry so
|
|
114
128
|
// a single Ctrl+Z removes both the new nodes and their wires together.
|
|
115
129
|
function applyInsertions(newNodes, newWires, contextNodeIds) {
|
|
116
|
-
if (!newNodes || !newNodes.length) { return; }
|
|
130
|
+
if ((!newNodes || !newNodes.length) && (!newWires || !newWires.length)) { return; }
|
|
117
131
|
|
|
118
132
|
// Determine z (flow-tab id) from the active workspace.
|
|
119
133
|
var z = "";
|
|
@@ -246,6 +260,57 @@
|
|
|
246
260
|
});
|
|
247
261
|
});
|
|
248
262
|
|
|
263
|
+
// Collision-avoidance stagger: independent components that happen to
|
|
264
|
+
// share the same anchor (e.g. several standalone new nodes each
|
|
265
|
+
// wired from/to the SAME existing node — "add 5 debug taps after
|
|
266
|
+
// this http request node") compute identical target coordinates
|
|
267
|
+
// above and land exactly on top of each other. Detect a component
|
|
268
|
+
// whose origin collides with an earlier one and nudge the WHOLE
|
|
269
|
+
// component (preserving its own internal layout) into a small grid
|
|
270
|
+
// offset from the first occupant, so multi-node insertions never
|
|
271
|
+
// pile invisibly at one point.
|
|
272
|
+
var occupiedOrigins = [];
|
|
273
|
+
function collidesWithOccupied(x, y) {
|
|
274
|
+
return occupiedOrigins.some(function (p) {
|
|
275
|
+
return Math.abs(p.x - x) < 20 && Math.abs(p.y - y) < 20;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
Object.keys(componentAnchors).forEach(function (c) {
|
|
279
|
+
var members = laid.filter(function (n) { return componentOf[n.id] === c; });
|
|
280
|
+
if (!members.length) { return; }
|
|
281
|
+
var baseX = members.reduce(function (m, n) { return Math.min(m, n.x); }, members[0].x);
|
|
282
|
+
var baseY = members.reduce(function (m, n) { return Math.min(m, n.y); }, members[0].y);
|
|
283
|
+
// Walk a single-column grid outward from the natural origin
|
|
284
|
+
// until we find a slot nothing else already occupies — one
|
|
285
|
+
// node per row, never two at the same y (CLAUDE-012: Manny's
|
|
286
|
+
// original spec, which the earlier 2-wide grid still violated
|
|
287
|
+
// by putting a pair at the same vertical level). After 5 nodes
|
|
288
|
+
// in a column, wrap to a new column further right than the
|
|
289
|
+
// first node in the previous one, so wires stay visually
|
|
290
|
+
// distinguishable instead of stacking on top of each other.
|
|
291
|
+
var slot = 0;
|
|
292
|
+
var x = baseX, y = baseY;
|
|
293
|
+
while (collidesWithOccupied(x, y) && slot < 200) {
|
|
294
|
+
slot++;
|
|
295
|
+
x = baseX + Math.floor(slot / 5) * 250;
|
|
296
|
+
y = baseY + (slot % 5) * 90;
|
|
297
|
+
}
|
|
298
|
+
if (slot > 0) {
|
|
299
|
+
var dx = x - baseX, dy = y - baseY;
|
|
300
|
+
members.forEach(function (n) { n.x += dx; n.y += dy; });
|
|
301
|
+
}
|
|
302
|
+
occupiedOrigins.push({ x: x, y: y });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// B3: snapshot placeholder→type before ids are overwritten, so wire
|
|
306
|
+
// failure messages can name the placeholder id and type rather than the
|
|
307
|
+
// opaque real hex id the node was never given (node not inserted = id
|
|
308
|
+
// exists in idMap but never landed in the live graph).
|
|
309
|
+
var placeholderToType = {};
|
|
310
|
+
laid.forEach(function (n) {
|
|
311
|
+
if (placeholderIds[n.id]) { placeholderToType[n.id] = n.type || "?"; }
|
|
312
|
+
});
|
|
313
|
+
|
|
249
314
|
// Build placeholder-id → real-id map and assign real ids + z.
|
|
250
315
|
var idMap = {};
|
|
251
316
|
laid.forEach(function (n) {
|
|
@@ -338,6 +403,15 @@
|
|
|
338
403
|
? typeDef.outputs
|
|
339
404
|
: (Array.isArray(n.wires) ? n.wires.length : 0);
|
|
340
405
|
}
|
|
406
|
+
// The model's "wires" array can disagree with the node type's
|
|
407
|
+
// real port count (e.g. a hallucinated single empty port on a
|
|
408
|
+
// 0-output type like http response) — the editor draws one
|
|
409
|
+
// anchor per n.wires entry regardless of n.outputs, so a
|
|
410
|
+
// mismatch renders a phantom port. Reconcile the array length
|
|
411
|
+
// to the authoritative n.outputs count now that it's resolved.
|
|
412
|
+
if (!Array.isArray(n.wires)) { n.wires = []; }
|
|
413
|
+
while (n.wires.length < n.outputs) { n.wires.push([]); }
|
|
414
|
+
if (n.wires.length > n.outputs) { n.wires.length = n.outputs; }
|
|
341
415
|
// Apply type-definition defaults for any property the model omitted.
|
|
342
416
|
// This covers required fields (e.g. statusVal/statusType on debug)
|
|
343
417
|
// that oneditsave would normally set, preventing a spurious triangle.
|
|
@@ -396,7 +470,16 @@
|
|
|
396
470
|
var fromNode = findLiveNode(fromId);
|
|
397
471
|
var toNode = findLiveNode(toId);
|
|
398
472
|
if (!fromNode || !toNode) {
|
|
399
|
-
|
|
473
|
+
var _failRef = !fromNode ? wire.from : wire.to;
|
|
474
|
+
var _failMsg;
|
|
475
|
+
if (placeholderIds[_failRef]) {
|
|
476
|
+
_failMsg = "Cannot wire — referenced node was not inserted (" +
|
|
477
|
+
_failRef + " / type: " + (placeholderToType[_failRef] || "?") + ")";
|
|
478
|
+
} else {
|
|
479
|
+
_failMsg = "Cannot wire — node not found in editor: " +
|
|
480
|
+
(!fromNode ? fromId : toId);
|
|
481
|
+
}
|
|
482
|
+
addMessage("error", _failMsg);
|
|
400
483
|
return;
|
|
401
484
|
}
|
|
402
485
|
var fromPort = wire.fromPort || 0;
|
|
@@ -524,12 +607,13 @@
|
|
|
524
607
|
var propertyChanges = [];
|
|
525
608
|
var wiresChanged = false;
|
|
526
609
|
var redactionSkips = 0;
|
|
610
|
+
var redactedKeys = [];
|
|
527
611
|
Object.keys(modNode).forEach(function (k) {
|
|
528
612
|
if (DIFF_SKIP[k]) { return; }
|
|
529
613
|
var newRaw = modNode[k];
|
|
530
614
|
// If the model echoed a sanitizer sentinel, the field is opaque —
|
|
531
615
|
// we can't meaningfully compare or apply it, so skip entirely.
|
|
532
|
-
if (isSanitizeSentinel(newRaw)) { redactionSkips++; return; }
|
|
616
|
+
if (isSanitizeSentinel(newRaw)) { redactionSkips++; redactedKeys.push(k); return; }
|
|
533
617
|
var oldRaw = liveNode ? liveNode[k] : undefined;
|
|
534
618
|
var oldStr, newStr;
|
|
535
619
|
try { oldStr = JSON.stringify(oldRaw); } catch (e) { oldStr = String(oldRaw); }
|
|
@@ -538,7 +622,7 @@
|
|
|
538
622
|
if (k === "wires") { wiresChanged = true; return; }
|
|
539
623
|
propertyChanges.push({ key: k, oldVal: oldRaw, newVal: newRaw });
|
|
540
624
|
});
|
|
541
|
-
return { propertyChanges: propertyChanges, wiresChanged: wiresChanged, redactionSkips: redactionSkips };
|
|
625
|
+
return { propertyChanges: propertyChanges, wiresChanged: wiresChanged, redactionSkips: redactionSkips, redactedKeys: redactedKeys };
|
|
542
626
|
}
|
|
543
627
|
|
|
544
628
|
// Diff outgoing wires for an existing node: compare what's live in the graph
|
|
@@ -642,6 +726,7 @@
|
|
|
642
726
|
propertyChanges: diff.propertyChanges,
|
|
643
727
|
wiresChanged: diff.wiresChanged,
|
|
644
728
|
redactionSkips: diff.redactionSkips,
|
|
729
|
+
redactedKeys: diff.redactedKeys,
|
|
645
730
|
wiresDiff: wiresDiff,
|
|
646
731
|
name: modNode.name || (liveNode && liveNode.name) || "",
|
|
647
732
|
type: modNode.type || (liveNode && liveNode.type) || ""
|
|
@@ -662,7 +747,7 @@
|
|
|
662
747
|
var hasNewNodes = newNodes.length > 0;
|
|
663
748
|
var hasRemoveNodes = removeNodes.length > 0;
|
|
664
749
|
var hasNewGroups = newGroups.length > 0;
|
|
665
|
-
var hasAnyChanges = hasPropChanges || hasWireChanges || hasNewNodes || hasRemoveNodes || hasNewGroups;
|
|
750
|
+
var hasAnyChanges = hasPropChanges || hasWireChanges || hasNewNodes || hasRemoveNodes || hasNewGroups || (newWires && newWires.length > 0);
|
|
666
751
|
var totalRedactionSkips = nodeDiffs.reduce(function (s, d) { return s + (d.redactionSkips || 0); }, 0);
|
|
667
752
|
|
|
668
753
|
var $msg = $("<div>").addClass("fp-message fp-review");
|
|
@@ -715,15 +800,27 @@
|
|
|
715
800
|
});
|
|
716
801
|
if (redactionOnlyDiffs.length > 0) {
|
|
717
802
|
var redSectionTop = hasPropChanges ? "12px" : "0";
|
|
718
|
-
|
|
719
|
-
.
|
|
803
|
+
var totalRedFields = redactionOnlyDiffs.reduce(function (s, d) {
|
|
804
|
+
return s + (Array.isArray(d.redactedKeys) && d.redactedKeys.length ? d.redactedKeys.length : 1);
|
|
805
|
+
}, 0);
|
|
806
|
+
var $redToggle = $("<div>").addClass("fp-review-count")
|
|
807
|
+
.css({ "margin-top": redSectionTop, "cursor": "pointer" })
|
|
808
|
+
.text("▶ Not applied — FlowPilot can’t change these directly (" + totalRedFields + " field(s))")
|
|
720
809
|
.appendTo($summaryPanel);
|
|
810
|
+
var $redContent = $("<div>").addClass("fp-hidden").appendTo($summaryPanel);
|
|
811
|
+
$redToggle.on("click", function () {
|
|
812
|
+
var collapsed = $redContent.hasClass("fp-hidden");
|
|
813
|
+
$redContent.toggleClass("fp-hidden", !collapsed);
|
|
814
|
+
$redToggle.text((collapsed ? "▼" : "▶") + " Not applied — FlowPilot can’t change these directly (" + totalRedFields + " field(s))");
|
|
815
|
+
});
|
|
721
816
|
redactionOnlyDiffs.forEach(function (d) {
|
|
722
|
-
var $section = $("<div>").addClass("fp-diff-node").appendTo($
|
|
817
|
+
var $section = $("<div>").addClass("fp-diff-node").appendTo($redContent);
|
|
723
818
|
var title = d.type + (d.name ? " — \"" + d.name + "\"" : "");
|
|
724
819
|
$("<div>").addClass("fp-diff-node-title").text(title).appendTo($section);
|
|
820
|
+
var keys = Array.isArray(d.redactedKeys) && d.redactedKeys.length ? d.redactedKeys : [];
|
|
821
|
+
var fieldList = keys.length ? keys.join(", ") : "one or more fields";
|
|
725
822
|
$("<div>").addClass("fp-diff-warn")
|
|
726
|
-
.text("
|
|
823
|
+
.text("Cannot change “" + fieldList + "” — FlowPilot cannot read the live value for this field (internal node config). Edit it directly in the node editor if needed.")
|
|
727
824
|
.appendTo($section);
|
|
728
825
|
});
|
|
729
826
|
}
|
|
@@ -845,7 +942,7 @@
|
|
|
845
942
|
if (!hasAnyChanges) {
|
|
846
943
|
if (totalRedactionSkips > 0) {
|
|
847
944
|
$("<div>").addClass("fp-warning")
|
|
848
|
-
.text("
|
|
945
|
+
.text("None of the proposed changes could be applied — all targeted fields are internal node config that FlowPilot cannot read or modify directly. See the field names above and edit them in the node editor if needed.")
|
|
849
946
|
.appendTo($actions);
|
|
850
947
|
} else {
|
|
851
948
|
$("<div>").addClass("fp-review-hint")
|
|
@@ -863,17 +960,11 @@
|
|
|
863
960
|
: hasNewNodes ? "Insert Nodes"
|
|
864
961
|
: "Apply Changes"; // covers a request that ONLY creates/updates a group
|
|
865
962
|
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
// re-fetches
|
|
871
|
-
// lost). A plain Modify call (applyCallback is the bare
|
|
872
|
-
// applyModifications reference) gets "data-fp-apply-modify"; a
|
|
873
|
-
// /build loop fix (buildFixInfo set — see applyBuildLoopFix)
|
|
874
|
-
// gets "data-fp-apply-build-fix" instead, carrying capReached
|
|
875
|
-
// too since the relayed click needs to run the SAME loop
|
|
876
|
-
// bookkeeping a local click would, not just applyModifications.
|
|
963
|
+
// sharedApplyData is stored in the review record (Phase 10 0B)
|
|
964
|
+
// so the pop-out can relay an applyByRecordId intent to the
|
|
965
|
+
// parent without serializing payload into DOM attributes.
|
|
966
|
+
// nodeDiffs is re-serialized without liveNode (a RED node object,
|
|
967
|
+
// not JSON-safe; applyModifications re-fetches via findLiveNode).
|
|
877
968
|
var sharedApplyData = {
|
|
878
969
|
nodeDiffs: nodeDiffs.map(function (d) {
|
|
879
970
|
return {
|
|
@@ -892,40 +983,57 @@
|
|
|
892
983
|
existingNodeIds: nodes.map(function (n) { return n.id; }),
|
|
893
984
|
hasMutations: hasMutations
|
|
894
985
|
};
|
|
895
|
-
if (
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
sharedApplyData
|
|
899
|
-
|
|
900
|
-
}
|
|
986
|
+
if (buildFixInfo) { sharedApplyData.capReached = !!buildFixInfo.capReached; }
|
|
987
|
+
var _modRecord = addRecord("review", {
|
|
988
|
+
subkind: buildFixInfo ? "build-fix" : "modify",
|
|
989
|
+
sharedApplyData: sharedApplyData,
|
|
990
|
+
state: "pending"
|
|
991
|
+
});
|
|
992
|
+
$msg.attr("data-fp-record-id", _modRecord.id);
|
|
901
993
|
|
|
994
|
+
var hintParts = [];
|
|
995
|
+
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
996
|
+
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
997
|
+
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
998
|
+
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
999
|
+
if (!hasNewNodes && newWires && newWires.length > 0) { hintParts.push("connections added"); }
|
|
1000
|
+
var applySub = hintParts.length ? "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo." : "";
|
|
902
1001
|
var $applyBtn = $("<button>")
|
|
903
|
-
.addClass("
|
|
1002
|
+
.addClass("fp-chip fp-chip-card")
|
|
904
1003
|
.attr("type", "button")
|
|
905
|
-
.text(btnLabel)
|
|
906
1004
|
.on("click", function () {
|
|
907
|
-
$applyBtn.prop("disabled", true)
|
|
1005
|
+
$applyBtn.prop("disabled", true);
|
|
1006
|
+
$applyTitle.text("Applying…");
|
|
908
1007
|
// Insertions run FIRST so their placeholder→real-id map is
|
|
909
1008
|
// available to applyModifications/applyGroupChanges — an
|
|
910
1009
|
// existing node's rewired "wires" (Tier 3) or a new
|
|
911
1010
|
// group's membership may point at a node being inserted
|
|
912
1011
|
// in this same response.
|
|
913
1012
|
var idMap = {};
|
|
914
|
-
|
|
1013
|
+
var hasNewWires = newWires && newWires.length > 0;
|
|
1014
|
+
if (hasNewNodes || hasNewWires) {
|
|
915
1015
|
idMap = applyInsertions(newNodes, newWires, nodes.map(function (n) { return n.id; })) || {};
|
|
916
1016
|
}
|
|
917
|
-
|
|
1017
|
+
// W4 Phase 2: applyCallback also runs the verify read-back,
|
|
1018
|
+
// so it must fire for insertion-only Modifies too (no
|
|
1019
|
+
// property/wire change on an EXISTING node), not just when
|
|
1020
|
+
// hasMutations is true — otherwise "exists"/"wire" checks on
|
|
1021
|
+
// newly inserted nodes would never run.
|
|
1022
|
+
if ((hasMutations || hasNewNodes || hasNewWires) && applyCallback) {
|
|
1023
|
+
applyCallback(nodeDiffs, removeNodes, null, idMap);
|
|
1024
|
+
}
|
|
918
1025
|
if (hasNewGroups) { applyGroupChanges(newGroups, idMap); }
|
|
919
|
-
|
|
1026
|
+
if (_modRecord) { _modRecord.state = "applied"; }
|
|
1027
|
+
$applyTitle.text("Done ✓");
|
|
920
1028
|
});
|
|
1029
|
+
$("<span>").addClass("fp-chip-icon")
|
|
1030
|
+
.append($("<i>").addClass(buildFixInfo ? "fa fa-wrench" : "fa fa-check-circle"))
|
|
1031
|
+
.appendTo($applyBtn);
|
|
1032
|
+
var $applyBody = $("<span>").addClass("fp-chip-body").appendTo($applyBtn);
|
|
1033
|
+
var $applyTitle = $("<span>").addClass("fp-chip-title").text(buildFixInfo ? "Apply Fix" : btnLabel).appendTo($applyBody);
|
|
1034
|
+
$("<span>").addClass("fp-chip-sub").text(buildFixInfo && buildFixInfo.capReached ? "Apply final fix and stop the loop" : applySub).appendTo($applyBody);
|
|
1035
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($applyBtn);
|
|
921
1036
|
$actions.append($applyBtn);
|
|
922
|
-
var hintParts = [];
|
|
923
|
-
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
924
|
-
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
925
|
-
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
926
|
-
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
927
|
-
var hintText = "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo.";
|
|
928
|
-
$("<span>").addClass("fp-review-hint").text(hintText).appendTo($actions);
|
|
929
1037
|
}
|
|
930
1038
|
|
|
931
1039
|
$box.append($msg);
|
|
@@ -953,7 +1061,17 @@
|
|
|
953
1061
|
|
|
954
1062
|
var oldValues = {};
|
|
955
1063
|
d.propertyChanges.forEach(function (c) { oldValues[c.key] = liveNode[c.key]; });
|
|
956
|
-
d.propertyChanges.forEach(function (c) {
|
|
1064
|
+
d.propertyChanges.forEach(function (c) {
|
|
1065
|
+
// Rewrite placeholder ids that point at newly-inserted config
|
|
1066
|
+
// nodes (e.g. an MQTT out node's broker: "fp-new-0" referring
|
|
1067
|
+
// to a new mqtt-broker in the same response's newNodes).
|
|
1068
|
+
// applyInsertions returns idMap and it flows to us via the
|
|
1069
|
+
// applyCallback chain — idMap[placeholder] is the real id.
|
|
1070
|
+
var val = (idMap && typeof c.newVal === "string" && idMap[c.newVal])
|
|
1071
|
+
? idMap[c.newVal]
|
|
1072
|
+
: c.newVal;
|
|
1073
|
+
liveNode[c.key] = val;
|
|
1074
|
+
});
|
|
957
1075
|
|
|
958
1076
|
// Switch nodes derive their port count from rules.length (the edit
|
|
959
1077
|
// dialog's "outputCount" map assigns each rule its own output and
|
|
@@ -1567,18 +1685,10 @@
|
|
|
1567
1685
|
var v = validateGeneratedFlow(nodes);
|
|
1568
1686
|
|
|
1569
1687
|
var $msg = $("<div>").addClass("fp-message fp-review");
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
// to
|
|
1573
|
-
//
|
|
1574
|
-
// importing it also needs to start the loop (startBuildLoop),
|
|
1575
|
-
// which the parent does itself once it gets the relayed intent;
|
|
1576
|
-
// see the "applyBuild" handler in initMainWindow.
|
|
1577
|
-
if (!onImported) {
|
|
1578
|
-
$msg.attr("data-fp-apply-flow", JSON.stringify(nodes));
|
|
1579
|
-
} else if (buildGoal) {
|
|
1580
|
-
$msg.attr("data-fp-apply-build", JSON.stringify({ flow: nodes, goal: buildGoal }));
|
|
1581
|
-
}
|
|
1688
|
+
// The review record (created in the action row when nodes are valid)
|
|
1689
|
+
// carries data-fp-record-id; the pop-out uses it to relay
|
|
1690
|
+
// applyByRecordId to the parent — see bindReviewApplyButtons/
|
|
1691
|
+
// applyByRecordId in initMainWindow (Phase 10 0B).
|
|
1582
1692
|
$("<div>").addClass("fp-label").text("GENERATED FLOW — REVIEW").appendTo($msg);
|
|
1583
1693
|
|
|
1584
1694
|
var $tabSummary = $("<button>").addClass("fp-tab fp-tab-active").attr("type", "button").text("Summary");
|
|
@@ -1656,22 +1766,342 @@
|
|
|
1656
1766
|
} else if (!nodes.length) {
|
|
1657
1767
|
$("<div>").addClass("fp-warning").text("No nodes were generated — nothing to add.").appendTo($actions);
|
|
1658
1768
|
} else {
|
|
1659
|
-
var
|
|
1660
|
-
|
|
1769
|
+
var _genRecord = addRecord("review", {
|
|
1770
|
+
subkind: onImported ? "build-generate" : "generate",
|
|
1771
|
+
flow: nodes,
|
|
1772
|
+
buildGoal: buildGoal || null,
|
|
1773
|
+
onImported: onImported || null,
|
|
1774
|
+
state: "pending"
|
|
1775
|
+
});
|
|
1776
|
+
$msg.attr("data-fp-record-id", _genRecord.id);
|
|
1777
|
+
if (onImported && buildGoal) {
|
|
1778
|
+
// B1: primary deploy-and-verify chip (accent, full-width) + secondary
|
|
1779
|
+
// plain escape hatch. Only when buildGoal is set — that signals this
|
|
1780
|
+
// is a build-loop context, not just a plain post-import callback.
|
|
1781
|
+
var $primaryChip = $("<button>")
|
|
1782
|
+
.addClass("fp-chip fp-chip-card")
|
|
1783
|
+
.attr("type", "button")
|
|
1784
|
+
.on("click", function () {
|
|
1785
|
+
$primaryChip.prop("disabled", true);
|
|
1786
|
+
$secondaryAddBtn.prop("disabled", true);
|
|
1787
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1788
|
+
importGeneratedFlow(nodes, onImported);
|
|
1789
|
+
});
|
|
1790
|
+
$("<span>").addClass("fp-chip-icon")
|
|
1791
|
+
.append($("<i>").addClass("fa fa-check-circle"))
|
|
1792
|
+
.appendTo($primaryChip);
|
|
1793
|
+
var $chipBody = $("<span>").addClass("fp-chip-body").appendTo($primaryChip);
|
|
1794
|
+
$("<span>").addClass("fp-chip-title").text("Deploy & verify this works →").appendTo($chipBody);
|
|
1795
|
+
$("<span>").addClass("fp-chip-sub").text("Add to canvas, deploy, then run the verification loop").appendTo($chipBody);
|
|
1796
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($primaryChip);
|
|
1797
|
+
$actions.append($primaryChip);
|
|
1798
|
+
var $secondaryAddBtn = $("<button>")
|
|
1799
|
+
.addClass("fp-chip fp-chip-card fp-chip-card-alt")
|
|
1800
|
+
.attr("type", "button")
|
|
1801
|
+
.on("click", function () {
|
|
1802
|
+
$secondaryAddBtn.prop("disabled", true);
|
|
1803
|
+
$primaryChip.prop("disabled", true);
|
|
1804
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1805
|
+
importGeneratedFlow(nodes, null);
|
|
1806
|
+
});
|
|
1807
|
+
var $altBody = $("<span>").addClass("fp-chip-body").appendTo($secondaryAddBtn);
|
|
1808
|
+
$("<span>").addClass("fp-chip-title").text("Just add to canvas").appendTo($altBody);
|
|
1809
|
+
$("<span>").addClass("fp-chip-sub").text("Skips deploy & verification").appendTo($altBody);
|
|
1810
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($secondaryAddBtn);
|
|
1811
|
+
$actions.append($secondaryAddBtn);
|
|
1812
|
+
} else if (onImported) {
|
|
1813
|
+
// Has a post-import callback (e.g. verifyImportedNodes) but no build
|
|
1814
|
+
// loop — chip button that still fires the callback on import.
|
|
1815
|
+
var $addBtn = $("<button>")
|
|
1816
|
+
.addClass("fp-chip fp-chip-card")
|
|
1817
|
+
.attr("type", "button")
|
|
1818
|
+
.on("click", function () {
|
|
1819
|
+
$addBtn.prop("disabled", true);
|
|
1820
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1821
|
+
importGeneratedFlow(nodes, onImported);
|
|
1822
|
+
});
|
|
1823
|
+
var $addBody = $("<span>").addClass("fp-chip-body").appendTo($addBtn);
|
|
1824
|
+
$("<span>").addClass("fp-chip-title").text("Add to canvas").appendTo($addBody);
|
|
1825
|
+
$("<span>").addClass("fp-chip-sub").text("Click the canvas to place the nodes").appendTo($addBody);
|
|
1826
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($addBtn);
|
|
1827
|
+
$actions.append($addBtn);
|
|
1828
|
+
} else {
|
|
1829
|
+
var $addBtn = $("<button>")
|
|
1830
|
+
.addClass("fp-chip fp-chip-card")
|
|
1831
|
+
.attr("type", "button")
|
|
1832
|
+
.on("click", function () {
|
|
1833
|
+
$addBtn.prop("disabled", true);
|
|
1834
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1835
|
+
importGeneratedFlow(nodes, null);
|
|
1836
|
+
});
|
|
1837
|
+
var $addBody = $("<span>").addClass("fp-chip-body").appendTo($addBtn);
|
|
1838
|
+
$("<span>").addClass("fp-chip-title").text("Add to canvas").appendTo($addBody);
|
|
1839
|
+
$("<span>").addClass("fp-chip-sub").text("Click the canvas to place the nodes").appendTo($addBody);
|
|
1840
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($addBtn);
|
|
1841
|
+
$actions.append($addBtn);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
$box.append($msg);
|
|
1846
|
+
scrollMessagesToBottom();
|
|
1847
|
+
return $msg;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// ---- Refresh-from-history re-render helpers (Phase 10, 0A) -------------
|
|
1851
|
+
// Called by rerenderRecord() in main.js during refreshView(). Reconstruct
|
|
1852
|
+
// review panels from stored record data — the sharedApplyData already
|
|
1853
|
+
// holds everything needed without re-querying the live editor.
|
|
1854
|
+
|
|
1855
|
+
function rerenderReviewRecord(rec) {
|
|
1856
|
+
if (!rec) { return; }
|
|
1857
|
+
switch (rec.subkind) {
|
|
1858
|
+
case "modify":
|
|
1859
|
+
case "build-fix":
|
|
1860
|
+
rerenderModifyReview(rec);
|
|
1861
|
+
break;
|
|
1862
|
+
case "generate":
|
|
1863
|
+
case "build-generate":
|
|
1864
|
+
rerenderGeneratedReview(rec);
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function rerenderModifyReview(rec) {
|
|
1870
|
+
var $box = el("#fp-messages");
|
|
1871
|
+
if (!$box.length) { return; }
|
|
1872
|
+
|
|
1873
|
+
var d = rec.sharedApplyData || {};
|
|
1874
|
+
var nodeDiffs = Array.isArray(d.nodeDiffs) ? d.nodeDiffs : [];
|
|
1875
|
+
var removeNodes = Array.isArray(d.removeNodes) ? d.removeNodes : [];
|
|
1876
|
+
var newNodes = Array.isArray(d.newNodes) ? d.newNodes : [];
|
|
1877
|
+
var newWires = Array.isArray(d.newWires) ? d.newWires : [];
|
|
1878
|
+
var newGroups = Array.isArray(d.newGroups) ? d.newGroups : [];
|
|
1879
|
+
var existingIds = Array.isArray(d.existingNodeIds) ? d.existingNodeIds : [];
|
|
1880
|
+
var hasMutations = !!d.hasMutations;
|
|
1881
|
+
var capReached = !!d.capReached;
|
|
1882
|
+
var isBuildFix = rec.subkind === "build-fix";
|
|
1883
|
+
|
|
1884
|
+
var hasPropChanges = nodeDiffs.some(function (nd) { return nd.propertyChanges && nd.propertyChanges.length > 0; });
|
|
1885
|
+
var hasWireChanges = nodeDiffs.some(function (nd) { return nd.wiresDiff && (nd.wiresDiff.toRemove.length > 0 || nd.wiresDiff.toAdd.length > 0); });
|
|
1886
|
+
var hasNewNodes = newNodes.length > 0;
|
|
1887
|
+
var hasRemoveNodes = removeNodes.length > 0;
|
|
1888
|
+
var hasNewGroups = newGroups.length > 0;
|
|
1889
|
+
var nodesWithChanges = nodeDiffs.filter(function (nd) { return nd.propertyChanges && nd.propertyChanges.length > 0; }).length;
|
|
1890
|
+
var missingLive = nodeDiffs.some(function (nd) { return nd.propertyChanges && nd.propertyChanges.length > 0 && !findLiveNode(nd.modNode && nd.modNode.id); });
|
|
1891
|
+
|
|
1892
|
+
// Add the record first so the Apply button's closure can reference it.
|
|
1893
|
+
var _newRec = addRecord("review", {
|
|
1894
|
+
subkind: rec.subkind,
|
|
1895
|
+
sharedApplyData: rec.sharedApplyData,
|
|
1896
|
+
state: rec.state
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
var $msg = $("<div>").addClass("fp-message fp-review");
|
|
1900
|
+
$("<div>").addClass("fp-label")
|
|
1901
|
+
.text(isBuildFix ? "BUILD LOOP — FIX REVIEW" : "MODIFY FLOW — REVIEW CHANGES")
|
|
1902
|
+
.appendTo($msg);
|
|
1903
|
+
|
|
1904
|
+
$msg.attr("data-fp-record-id", _newRec.id);
|
|
1905
|
+
|
|
1906
|
+
var $tabSummary = $("<button>").addClass("fp-tab fp-tab-active").attr("type", "button").text("Summary");
|
|
1907
|
+
var $tabJson = $("<button>").addClass("fp-tab").attr("type", "button").text("JSON");
|
|
1908
|
+
$("<div>").addClass("fp-tabs").append($tabSummary, $tabJson).appendTo($msg);
|
|
1909
|
+
|
|
1910
|
+
var $summaryPanel = $("<div>").addClass("fp-tab-panel");
|
|
1911
|
+
var $jsonPanel = $("<div>").addClass("fp-tab-panel fp-hidden");
|
|
1912
|
+
$msg.append($summaryPanel, $jsonPanel);
|
|
1913
|
+
|
|
1914
|
+
// Prop diffs
|
|
1915
|
+
if (hasPropChanges) {
|
|
1916
|
+
$("<div>").addClass("fp-review-count")
|
|
1917
|
+
.text(nodesWithChanges + " of " + nodeDiffs.length + " node(s) will change:")
|
|
1918
|
+
.appendTo($summaryPanel);
|
|
1919
|
+
nodeDiffs.forEach(function (nd) {
|
|
1920
|
+
if (!nd.propertyChanges || !nd.propertyChanges.length) { return; }
|
|
1921
|
+
var $section = $("<div>").addClass("fp-diff-node").appendTo($summaryPanel);
|
|
1922
|
+
var title = nd.type + (nd.name ? " — \"" + nd.name + "\"" : "");
|
|
1923
|
+
$("<div>").addClass("fp-diff-node-title").text(title).appendTo($section);
|
|
1924
|
+
if (!findLiveNode(nd.modNode && nd.modNode.id)) {
|
|
1925
|
+
$("<div>").addClass("fp-diff-warn")
|
|
1926
|
+
.text("⚠ Node not found in editor (id: " + (nd.modNode && nd.modNode.id) + ")")
|
|
1927
|
+
.appendTo($section);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
nd.propertyChanges.forEach(function (c) {
|
|
1931
|
+
var $row = $("<div>").addClass("fp-diff-row").appendTo($section);
|
|
1932
|
+
$("<span>").addClass("fp-diff-key").text(c.key).appendTo($row);
|
|
1933
|
+
$("<span>").addClass("fp-diff-old").text(formatDiffVal(c.oldVal)).appendTo($row);
|
|
1934
|
+
$("<span>").addClass("fp-diff-arrow").text("→").appendTo($row);
|
|
1935
|
+
$("<span>").addClass("fp-diff-new").text(formatDiffVal(c.newVal)).appendTo($row);
|
|
1936
|
+
});
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
// Wire diffs — same rendering as original addModifyReview
|
|
1940
|
+
if (hasWireChanges) {
|
|
1941
|
+
nodeDiffs.forEach(function (nd) {
|
|
1942
|
+
var wd = nd.wiresDiff;
|
|
1943
|
+
if (!wd || (!wd.toRemove.length && !wd.toAdd.length)) { return; }
|
|
1944
|
+
var $section = $("<div>").addClass("fp-diff-node").appendTo($summaryPanel);
|
|
1945
|
+
$("<div>").addClass("fp-diff-node-title")
|
|
1946
|
+
.text(nd.type + (nd.name ? " — \"" + nd.name + "\"" : ""))
|
|
1947
|
+
.appendTo($section);
|
|
1948
|
+
wd.toRemove.forEach(function (entry) {
|
|
1949
|
+
var tgt = RED.nodes.node ? RED.nodes.node(entry.targetId) : null;
|
|
1950
|
+
var tgtLabel = tgt ? (tgt.name || tgt.type || entry.targetId) : entry.targetId;
|
|
1951
|
+
var $row = $("<div>").addClass("fp-diff-row").appendTo($section);
|
|
1952
|
+
$("<span>").addClass("fp-diff-key").text("port " + entry.sourcePort).appendTo($row);
|
|
1953
|
+
$("<span>").addClass("fp-diff-old").text("→ " + tgtLabel).appendTo($row);
|
|
1954
|
+
$("<span>").addClass("fp-diff-arrow").text("✕").appendTo($row);
|
|
1955
|
+
$("<span>").addClass("fp-diff-new").text("").appendTo($row);
|
|
1956
|
+
});
|
|
1957
|
+
wd.toAdd.forEach(function (entry) {
|
|
1958
|
+
var tgt = RED.nodes.node ? RED.nodes.node(entry.targetId) : null;
|
|
1959
|
+
var tgtLabel = tgt ? (tgt.name || tgt.type || entry.targetId) : entry.targetId;
|
|
1960
|
+
var $row = $("<div>").addClass("fp-diff-row").appendTo($section);
|
|
1961
|
+
$("<span>").addClass("fp-diff-key").text("port " + entry.sourcePort).appendTo($row);
|
|
1962
|
+
$("<span>").addClass("fp-diff-old").text("").appendTo($row);
|
|
1963
|
+
$("<span>").addClass("fp-diff-arrow").text("→").appendTo($row);
|
|
1964
|
+
$("<span>").addClass("fp-diff-new").text(tgtLabel).appendTo($row);
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
// Removals
|
|
1969
|
+
if (hasRemoveNodes) {
|
|
1970
|
+
$("<div>").addClass("fp-review-count fp-diff-warn")
|
|
1971
|
+
.text(removeNodes.length + " node(s) to remove:")
|
|
1972
|
+
.appendTo($summaryPanel);
|
|
1973
|
+
var $rmList = $("<ul>").addClass("fp-review-list").appendTo($summaryPanel);
|
|
1974
|
+
removeNodes.forEach(function (id) {
|
|
1975
|
+
var lv = RED.nodes.node ? RED.nodes.node(id) : null;
|
|
1976
|
+
var label = lv ? ((lv.name || lv.type || id) + " (" + id + ")") : id + " (not found)";
|
|
1977
|
+
$("<li>").addClass("fp-diff-warn").text("✕ " + label).appendTo($rmList);
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
// New nodes
|
|
1981
|
+
if (hasNewNodes) {
|
|
1982
|
+
$("<div>").addClass("fp-review-count")
|
|
1983
|
+
.text(newNodes.length + " node(s) to insert:")
|
|
1984
|
+
.appendTo($summaryPanel);
|
|
1985
|
+
var $newList = $("<ul>").addClass("fp-review-list").appendTo($summaryPanel);
|
|
1986
|
+
newNodes.forEach(function (n) {
|
|
1987
|
+
$("<li>").text((n.type || "unknown") + (n.name ? " — \"" + n.name + "\"" : "")).appendTo($newList);
|
|
1988
|
+
});
|
|
1989
|
+
if (newWires.length > 0) {
|
|
1990
|
+
$("<div>").addClass("fp-review-count").css("margin-top", "8px")
|
|
1991
|
+
.text(newWires.length + " wire connection(s):")
|
|
1992
|
+
.appendTo($summaryPanel);
|
|
1993
|
+
var $wireList = $("<ul>").addClass("fp-review-list").appendTo($summaryPanel);
|
|
1994
|
+
newWires.forEach(function (wire) {
|
|
1995
|
+
var fromLabel = resolveWireRef(wire.from, newNodes);
|
|
1996
|
+
var toLabel = resolveWireRef(wire.to, newNodes);
|
|
1997
|
+
var portNote = (wire.fromPort && wire.fromPort > 0) ? " [port " + wire.fromPort + "]" : "";
|
|
1998
|
+
$("<li>").text(fromLabel + portNote + " → " + toLabel).appendTo($wireList);
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
// Groups
|
|
2003
|
+
if (hasNewGroups) {
|
|
2004
|
+
$("<div>").addClass("fp-review-count")
|
|
2005
|
+
.text(newGroups.length + " group(s) to create/update:")
|
|
2006
|
+
.appendTo($summaryPanel);
|
|
2007
|
+
var $grpList = $("<ul>").addClass("fp-review-list").appendTo($summaryPanel);
|
|
2008
|
+
newGroups.forEach(function (g) {
|
|
2009
|
+
$("<li>").text((g.name ? "\"" + g.name + "\"" : "(unnamed)") +
|
|
2010
|
+
" — " + (Array.isArray(g.nodes) ? g.nodes.length : 0) + " node(s)").appendTo($grpList);
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// JSON tab
|
|
2015
|
+
var jsonText = JSON.stringify(d, null, 2);
|
|
2016
|
+
var $copyBtn = $("<button>").addClass("red-ui-button red-ui-button-small").attr("type", "button").text("Copy")
|
|
2017
|
+
.on("click", function () { copyToClipboard($copyBtn, jsonText); });
|
|
2018
|
+
$("<div>").addClass("fp-json-toolbar").append($copyBtn).appendTo($jsonPanel);
|
|
2019
|
+
$("<pre>").addClass("fp-json").text(jsonText).appendTo($jsonPanel);
|
|
2020
|
+
|
|
2021
|
+
$tabSummary.on("click", function () {
|
|
2022
|
+
$tabSummary.addClass("fp-tab-active"); $tabJson.removeClass("fp-tab-active");
|
|
2023
|
+
$summaryPanel.removeClass("fp-hidden"); $jsonPanel.addClass("fp-hidden");
|
|
2024
|
+
});
|
|
2025
|
+
$tabJson.on("click", function () {
|
|
2026
|
+
$tabJson.addClass("fp-tab-active"); $tabSummary.removeClass("fp-tab-active");
|
|
2027
|
+
$jsonPanel.removeClass("fp-hidden"); $summaryPanel.addClass("fp-hidden");
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
// Action row
|
|
2031
|
+
var $actions = $("<div>").addClass("fp-review-actions").appendTo($msg);
|
|
2032
|
+
if (rec.state === "applied") {
|
|
2033
|
+
$("<button>").addClass("red-ui-button red-ui-button-primary")
|
|
2034
|
+
.attr("type", "button").prop("disabled", true).text("Applied ✓")
|
|
2035
|
+
.appendTo($actions);
|
|
2036
|
+
} else if (missingLive && (hasPropChanges || hasWireChanges)) {
|
|
2037
|
+
$("<div>").addClass("fp-warning")
|
|
2038
|
+
.text("One or more nodes could not be found in the editor. Cannot apply safely.")
|
|
2039
|
+
.appendTo($actions);
|
|
2040
|
+
} else {
|
|
2041
|
+
var btnLabel = (hasMutations && hasNewNodes) ? "Apply & Insert"
|
|
2042
|
+
: hasMutations ? "Apply Changes"
|
|
2043
|
+
: hasNewNodes ? "Insert Nodes"
|
|
2044
|
+
: "Apply Changes";
|
|
2045
|
+
var hintParts = [];
|
|
2046
|
+
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
2047
|
+
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
2048
|
+
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
2049
|
+
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
2050
|
+
if (!hasNewNodes && newWires && newWires.length > 0) { hintParts.push("connections added"); }
|
|
2051
|
+
var applySub = hintParts.length ? "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo." : "";
|
|
2052
|
+
var $applyBtn = $("<button>")
|
|
2053
|
+
.addClass("fp-chip fp-chip-card")
|
|
1661
2054
|
.attr("type", "button")
|
|
1662
|
-
.text("Add to workspace")
|
|
1663
2055
|
.on("click", function () {
|
|
1664
|
-
$
|
|
1665
|
-
|
|
2056
|
+
$applyBtn.prop("disabled", true);
|
|
2057
|
+
$applyTitle.text("Applying…");
|
|
2058
|
+
var idMap = {};
|
|
2059
|
+
if (hasNewNodes || (newWires && newWires.length > 0)) { idMap = applyInsertions(newNodes, newWires, existingIds) || {}; }
|
|
2060
|
+
if (isBuildFix) {
|
|
2061
|
+
applyBuildLoopFix(nodeDiffs, removeNodes, idMap, capReached);
|
|
2062
|
+
} else {
|
|
2063
|
+
if (hasMutations) { applyModifications(nodeDiffs, removeNodes, null, idMap); }
|
|
2064
|
+
if (hasNewGroups) { applyGroupChanges(newGroups, idMap); }
|
|
2065
|
+
}
|
|
2066
|
+
if (_newRec) { _newRec.state = "applied"; }
|
|
2067
|
+
$applyTitle.text("Done ✓");
|
|
1666
2068
|
});
|
|
1667
|
-
$
|
|
1668
|
-
|
|
1669
|
-
.
|
|
1670
|
-
|
|
2069
|
+
$("<span>").addClass("fp-chip-icon")
|
|
2070
|
+
.append($("<i>").addClass(isBuildFix ? "fa fa-wrench" : "fa fa-check-circle"))
|
|
2071
|
+
.appendTo($applyBtn);
|
|
2072
|
+
var $applyBody = $("<span>").addClass("fp-chip-body").appendTo($applyBtn);
|
|
2073
|
+
var $applyTitle = $("<span>").addClass("fp-chip-title").text(isBuildFix ? "Apply Fix" : btnLabel).appendTo($applyBody);
|
|
2074
|
+
$("<span>").addClass("fp-chip-sub").text(isBuildFix && capReached ? "Apply final fix and stop the loop" : applySub).appendTo($applyBody);
|
|
2075
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($applyBtn);
|
|
2076
|
+
$actions.append($applyBtn);
|
|
1671
2077
|
}
|
|
1672
2078
|
|
|
1673
2079
|
$box.append($msg);
|
|
1674
2080
|
scrollMessagesToBottom();
|
|
1675
|
-
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function rerenderGeneratedReview(rec) {
|
|
2084
|
+
var nodes = Array.isArray(rec.flow) ? rec.flow : [];
|
|
2085
|
+
if (rec.state === "applied") {
|
|
2086
|
+
var $box = el("#fp-messages");
|
|
2087
|
+
if (!$box.length) { return; }
|
|
2088
|
+
var $msg = $("<div>").addClass("fp-message fp-review");
|
|
2089
|
+
$("<div>").addClass("fp-label").text("GENERATED FLOW — APPLIED ✓").appendTo($msg);
|
|
2090
|
+
$("<div>").addClass("fp-review-actions")
|
|
2091
|
+
.append($("<button>").addClass("red-ui-button red-ui-button-primary")
|
|
2092
|
+
.attr("type", "button").prop("disabled", true).text("Applied ✓"))
|
|
2093
|
+
.appendTo($msg);
|
|
2094
|
+
$box.append($msg);
|
|
2095
|
+
addRecord("review", {
|
|
2096
|
+
subkind: rec.subkind, flow: rec.flow,
|
|
2097
|
+
buildGoal: rec.buildGoal, onImported: rec.onImported, state: "applied"
|
|
2098
|
+
});
|
|
2099
|
+
scrollMessagesToBottom();
|
|
2100
|
+
} else {
|
|
2101
|
+
// Re-call addGeneratedReview — it revalidates and creates a fully
|
|
2102
|
+
// interactive panel, including its own addRecord call. onImported is
|
|
2103
|
+
// a live function ref (valid within the same session).
|
|
2104
|
+
addGeneratedReview(nodes, rec.onImported || null, rec.buildGoal || null);
|
|
2105
|
+
}
|
|
1676
2106
|
}
|
|
1677
2107
|
|