1099 lines
45 KiB
JavaScript
1099 lines
45 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
// ── Visual constants ──────────────────────────────────────────────────
|
|
const STAR_FILLED = "★";
|
|
const STAR_EMPTY = "☆";
|
|
const STAR_COUNT = 5;
|
|
const GOLD = "#FFD700";
|
|
const GREY = "#555";
|
|
const CARD_BG = "#1c1c1c";
|
|
const CARD_BORDER = "#333333";
|
|
const TEXT_COLOR = "#e0e0e0";
|
|
const LABEL_COLOR = "#94a3b8";
|
|
const CONCEPT_CLR = "#7dd3fc";
|
|
const NOTES_BG = "#222222";
|
|
const NOTES_BORDER = "#444444";
|
|
const BTN_SAVE_BG = "#2563eb";
|
|
const BTN_SAVE_HV = "#3b82f6";
|
|
const BTN_WAIT_BG = "#d97706";
|
|
const BTN_WAIT_HV = "#f59e0b";
|
|
const BTN_DONE_BG = "#16a34a";
|
|
const AVOID_CLR = "#f87171";
|
|
const REPLICATE_CLR = "#4ade80";
|
|
const NODE_MIN_W = 400;
|
|
const THUMB_MAX_H = 150;
|
|
const THUMB_GAP = 8;
|
|
const NOTES_H = 62;
|
|
const PROMPT_FONT = "12px 'Courier New', Consolas, monospace";
|
|
const PROMPT_LH = 17;
|
|
|
|
const RATING_LABELS = {
|
|
0: "", 1: "Poor — AVOID", 2: "Weak — AVOID",
|
|
3: "Neutral", 4: "Good — REPLICATE", 5: "Excellent — REPLICATE",
|
|
};
|
|
|
|
// Word-wrap helper: returns array of display lines respecting maxW in canvas pixels
|
|
function wrapText(ctx, text, maxW) {
|
|
const out = [];
|
|
for (const para of text.split("\n")) {
|
|
if (!para) { out.push(""); continue; }
|
|
const words = para.split(" ");
|
|
let cur = "";
|
|
for (const w of words) {
|
|
const test = cur ? cur + " " + w : w;
|
|
if (ctx.measureText(test).width > maxW && cur) {
|
|
out.push(cur); cur = w;
|
|
} else { cur = test; }
|
|
}
|
|
if (cur) out.push(cur);
|
|
}
|
|
return out.length ? out : [""];
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "session.FeedbackEditor",
|
|
|
|
async setup() {
|
|
api.addEventListener("session_feedback_update", (event) => {
|
|
const data = event.detail;
|
|
for (const node of app.graph._nodes) {
|
|
if (String(node.id) === String(data.node_id) && node._fb_onUpdate) {
|
|
node._fb_onUpdate(data);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Global keydown to handle Delete/Backspace on selected thumbnails
|
|
window.addEventListener("keydown", (e) => {
|
|
const key = e.key.toLowerCase();
|
|
const tag = document.activeElement?.tagName;
|
|
if (tag === "INPUT" || tag === "TEXTAREA" || document.activeElement?.isContentEditable) return;
|
|
|
|
if (key === "delete" || key === "backspace") {
|
|
let targetNode = null;
|
|
// Find if a SessionFeedbackEditor node has selected thumbnail images
|
|
for (const n of app.graph._nodes) {
|
|
if (n.type === "SessionFeedbackEditor" || n.comfyClass === "SessionFeedbackEditor") {
|
|
if (n._fb && n._fb.selectedImages && n._fb.selectedImages.size > 0) {
|
|
// Ensure the node itself is selected in the graph to avoid accidental deletions across multiple UI panels
|
|
const isSel = n.is_selected || (app.canvas?.selected_nodes && app.canvas.selected_nodes[n.id] !== undefined);
|
|
if (isSel) {
|
|
targetNode = n;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targetNode) {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
// Move selected items to pendingDeletes queue
|
|
for (const path of targetNode._fb.selectedImages) {
|
|
targetNode._fb.pendingDeletes.add(path);
|
|
}
|
|
targetNode._fb.selectedImages.clear();
|
|
targetNode._fb.lastSelectedIdx = null;
|
|
targetNode.setDirtyCanvas(true, true);
|
|
}
|
|
}
|
|
}, { capture: true });
|
|
},
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
// ── Session Feedback Editor ───────────────────────────────────────
|
|
if (nodeData.name === "SessionFeedbackEditor") {
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (onNodeCreated) onNodeCreated.apply(this, arguments);
|
|
|
|
this._fb = {
|
|
sessionId: "default", runCount: 0, selectedRun: 0,
|
|
prompts: [], ratings: {}, notes: {},
|
|
waiting: false, saveFlash: 0,
|
|
hitAreas: null, hoverBtn: false,
|
|
thumbnails: {}, thumbsLoading: 0,
|
|
collapsed: {},
|
|
// text editing: visible textarea overlay
|
|
_editingPrompt: null, _editingTA: null,
|
|
_notesScreenRects: {}, // populated during draw
|
|
};
|
|
|
|
if (this.size[0] < NODE_MIN_W) this.size[0] = NODE_MIN_W;
|
|
|
|
const node = this;
|
|
this.addCustomWidget({
|
|
name: "feedback_panel",
|
|
type: "custom",
|
|
value: "",
|
|
computeSize: () => {
|
|
const n = node._fb.prompts.length;
|
|
const W = node.size[0]; // preserve user width
|
|
if (n === 0) return [W, 80];
|
|
const cardW = W - 20;
|
|
const availW = cardW - 20;
|
|
let totalH = 72;
|
|
for (let i = 0; i < n; i++) {
|
|
const pNum = String(node._fb.prompts[i].number);
|
|
const collapsed = node._fb.collapsed[pNum] !== false;
|
|
const thumbs = (node._fb.thumbnails[pNum]) || [];
|
|
let cardH = 34; // header
|
|
// Full prompt text (only when expanded)
|
|
if (!collapsed) {
|
|
const ft = node._fb.prompts[i].fullText || "";
|
|
if (ft) {
|
|
const cpl = Math.max(10, Math.floor(availW / 7));
|
|
cardH += 10 + Math.ceil(ft.length / cpl) * PROMPT_LH + 8;
|
|
}
|
|
}
|
|
// Thumbnails: always visible
|
|
if (thumbs.length > 0) {
|
|
// Estimate rows: pack thumbs side by side at THUMB_MAX_H
|
|
let rowW = 0, rows = 1;
|
|
for (const img of thumbs) {
|
|
const tw = img.complete && img.naturalHeight > 0
|
|
? Math.round(img.naturalWidth * (THUMB_MAX_H / img.naturalHeight))
|
|
: 120;
|
|
if (rowW + tw + THUMB_GAP > availW && rowW > 0) { rows++; rowW = tw + THUMB_GAP; }
|
|
else rowW += tw + THUMB_GAP;
|
|
}
|
|
cardH += rows * (THUMB_MAX_H + THUMB_GAP) + 8;
|
|
}
|
|
cardH += 30 + NOTES_H + 10; // stars + notes + gap
|
|
totalH += cardH + 10;
|
|
}
|
|
totalH += 40;
|
|
return [W, totalH];
|
|
},
|
|
draw: (ctx, _n, width, posY) => node._fb_drawPanel(ctx, width, posY),
|
|
mouse: (event, pos) => node._fb_handleMouse(event, pos),
|
|
serialize: false,
|
|
});
|
|
|
|
setTimeout(() => this._fb_loadRun(), 400);
|
|
|
|
this.onRemoved = () => this._fb_stopEditing();
|
|
};
|
|
|
|
const onWidgetChanged = nodeType.prototype.onWidgetChanged;
|
|
nodeType.prototype.onWidgetChanged = function (widget) {
|
|
if (onWidgetChanged) onWidgetChanged.apply(this, arguments);
|
|
if (widget.name === "session_id") {
|
|
this._fb.sessionId = widget.value;
|
|
this._fb_loadRun();
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._fb_onUpdate = function (data) {
|
|
this._fb.sessionId = data.session_id;
|
|
this._fb.runCount = data.run_count;
|
|
this._fb.selectedRun = data.selected_run;
|
|
this._fb.prompts = data.prompts || [];
|
|
this._fb.waiting = data.mode === "blocking";
|
|
this._fb.ratings = {};
|
|
this._fb.notes = {};
|
|
for (const [k, v] of Object.entries(data.feedback || {})) {
|
|
this._fb.ratings[k] = v.rating || 0;
|
|
this._fb.notes[k] = v.notes || "";
|
|
}
|
|
this._fb_loadThumbnails();
|
|
this.setDirtyCanvas(true);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
nodeType.prototype._fb_resolveSessionId = function() {
|
|
// Evaluate input link if session_id is wired to another node
|
|
const input = this.inputs?.find(i => i.name === "session_id");
|
|
if (input && input.link) {
|
|
const l = app.graph.links[input.link];
|
|
if (l) {
|
|
const srcNode = app.graph.getNodeById(l.origin_id);
|
|
if (srcNode) {
|
|
if (
|
|
srcNode.type === "SessionIDSelector" || srcNode.comfyClass === "SessionIDSelector" ||
|
|
srcNode.type === "SessionMemoryReader" || srcNode.comfyClass === "SessionMemoryReader"
|
|
) {
|
|
const ex = srcNode.widgets?.find(w => w.name === "existing_session")?.value;
|
|
const nw = srcNode.widgets?.find(w => w.name === "new_session_name")?.value;
|
|
if (ex === "[CREATE NEW]") return nw?.trim() || "default";
|
|
return ex || "default";
|
|
}
|
|
if (srcNode.type === "PrimitiveNode" || (srcNode.type && srcNode.type.includes("String"))) {
|
|
return srcNode.widgets?.[0]?.value || "default";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Fallback to local widget
|
|
const w = this.widgets?.find(w => w.name === "session_id");
|
|
return w?.value || this._fb.sessionId || "default";
|
|
};
|
|
|
|
nodeType.prototype._fb_loadRun = function (runNumber) {
|
|
const sid = this._fb_resolveSessionId();
|
|
this._fb.sessionId = sid;
|
|
const rp = runNumber ? `&run_number=${runNumber}` : "";
|
|
fetch(`/session_feedback/load?session_id=${encodeURIComponent(sid)}${rp}`)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
this._fb.runCount = data.run_count || 0;
|
|
this._fb.selectedRun = data.selected_run || 0;
|
|
this._fb.prompts = data.prompts || [];
|
|
this._fb.ratings = {};
|
|
this._fb.notes = {};
|
|
for (const [k, v] of Object.entries(data.feedback || {})) {
|
|
this._fb.ratings[k] = v.rating || 0;
|
|
this._fb.notes[k] = v.notes || "";
|
|
}
|
|
this._fb_loadThumbnails();
|
|
this.setDirtyCanvas(true);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
})
|
|
.catch(err => console.error("[Feedback] Load error:", err));
|
|
};
|
|
|
|
nodeType.prototype._fb_loadThumbnails = function () {
|
|
const sid = this._fb.sessionId;
|
|
const run = this._fb.selectedRun;
|
|
if (!run) { this._fb.thumbnails = {}; return; }
|
|
fetch(`/session_feedback/thumbnails?session_id=${encodeURIComponent(sid)}&run_number=${run}`)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
this._fb.thumbnails = {};
|
|
this._fb.thumbsLoading = 0;
|
|
this._fb.selectedImages = new Set();
|
|
this._fb.pendingDeletes = new Set();
|
|
this._fb.lastSelectedIdx = -1;
|
|
let globalIdx = 0;
|
|
for (const [pNum, urls] of Object.entries(data)) {
|
|
this._fb.thumbnails[pNum] = urls.map(url => {
|
|
const img = new Image();
|
|
img._urlPath = url.split("?path=")[1] || url;
|
|
img._globalIdx = globalIdx++;
|
|
this._fb.thumbsLoading++;
|
|
img.onload = () => {
|
|
this._fb.thumbsLoading--;
|
|
if (this._fb.thumbsLoading <= 0) {
|
|
requestAnimationFrame(() => {
|
|
this.setSize([this.size[0], this.computeSize()[1]]);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
});
|
|
} else { this.setDirtyCanvas(true); }
|
|
};
|
|
img.onerror = () => { this._fb.thumbsLoading--; };
|
|
img.src = url;
|
|
return img;
|
|
});
|
|
|
|
if (!this._fb.prompts.find((p) => String(p.number) === pNum)) {
|
|
this._fb.prompts.push({ number: parseInt(pNum, 10), concept: `Result ${pNum}`, fullText: "" });
|
|
}
|
|
}
|
|
this._fb.prompts.sort((a, b) => a.number - b.number);
|
|
this.setSize([this.size[0], this.computeSize()[1]]);
|
|
this.setDirtyCanvas(true);
|
|
})
|
|
.catch(err => console.error("[Feedback] Thumbnail load error:", err));
|
|
};
|
|
|
|
// ── Visible textarea overlay ────────────────────────────────────
|
|
// Position computed from ctx.getTransform() captured during draw.
|
|
nodeType.prototype._fb_stopEditing = function () {
|
|
const fb = this._fb;
|
|
if (fb._editingTA) {
|
|
fb.notes[fb._editingPrompt] = fb._editingTA.value;
|
|
fb._editingTA.remove();
|
|
fb._editingTA = null;
|
|
}
|
|
if (fb._editingClickOut) {
|
|
document.removeEventListener("pointerdown", fb._editingClickOut, true);
|
|
fb._editingClickOut = null;
|
|
}
|
|
fb._editingPrompt = null;
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype._fb_startEditing = function (promptNum) {
|
|
this._fb_stopEditing();
|
|
const fb = this._fb;
|
|
const sr = fb._notesScreenRects[promptNum];
|
|
if (!sr) return;
|
|
|
|
const ta = document.createElement("textarea");
|
|
ta.value = fb.notes[promptNum] || "";
|
|
ta.placeholder = "Add notes…";
|
|
Object.assign(ta.style, {
|
|
position: "fixed",
|
|
left: `${sr.x}px`, top: `${sr.y}px`,
|
|
width: `${sr.w}px`, height: `${sr.h}px`,
|
|
background: "#222222",
|
|
color: "#e0e0e0",
|
|
caretColor: "#7dd3fc",
|
|
border: "1.5px solid #2563eb",
|
|
borderRadius: "5px",
|
|
padding: "6px 8px",
|
|
font: "11px Inter, Arial, sans-serif",
|
|
lineHeight: "1.4",
|
|
resize: "none",
|
|
zIndex: "999999",
|
|
boxSizing: "border-box",
|
|
outline: "none",
|
|
overflow: "auto",
|
|
});
|
|
|
|
const node = this;
|
|
|
|
// Close when user clicks OUTSIDE the textarea (capture phase, runs first)
|
|
const onClickOut = (e) => {
|
|
if (!ta.contains(e.target)) {
|
|
node._fb_stopEditing();
|
|
}
|
|
};
|
|
document.addEventListener("pointerdown", onClickOut, true);
|
|
fb._editingClickOut = onClickOut;
|
|
|
|
ta.addEventListener("input", () => {
|
|
fb.notes[promptNum] = ta.value;
|
|
node._fb_autosave();
|
|
});
|
|
ta.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") { node._fb_stopEditing(); return; }
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
});
|
|
ta.addEventListener("keyup", (e) => { e.stopPropagation(); e.stopImmediatePropagation(); });
|
|
ta.addEventListener("keypress",(e) => { e.stopPropagation(); e.stopImmediatePropagation(); });
|
|
// Refocus only if LiteGraph steals focus without a user click
|
|
ta.addEventListener("blur", () => {
|
|
setTimeout(() => {
|
|
if (fb._editingTA === ta && document.activeElement !== ta) {
|
|
ta.focus();
|
|
}
|
|
}, 30);
|
|
});
|
|
// Prevent canvas drag when clicking inside textarea
|
|
for (const ev of ["pointerdown","mousedown","pointermove","mousemove","pointerup","mouseup","click","dblclick","wheel"])
|
|
ta.addEventListener(ev, (e) => e.stopPropagation());
|
|
|
|
document.body.appendChild(ta);
|
|
fb._editingTA = ta;
|
|
fb._editingPrompt = promptNum;
|
|
ta.focus();
|
|
ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
};
|
|
|
|
// ── Save / Autosave ─────────────────────────────────────────────
|
|
nodeType.prototype._fb_autosave = function () {
|
|
if (this._autosaveTimer) clearTimeout(this._autosaveTimer);
|
|
this._autosaveTimer = setTimeout(() => {
|
|
this._fb_saveFeedback(false);
|
|
}, 800);
|
|
};
|
|
|
|
nodeType.prototype._fb_saveFeedback = function (includeDeletions = true) {
|
|
if (this._fb.selectedRun === 0) return;
|
|
if (includeDeletions) this._fb_stopEditing();
|
|
|
|
const prompts = {};
|
|
for (const p of this._fb.prompts) {
|
|
const k = String(p.number);
|
|
prompts[k] = { rating: this._fb.ratings[k] || 0, notes: this._fb.notes[k] || "" };
|
|
}
|
|
|
|
fetch("/session_feedback/save", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
session_id: this._fb.sessionId,
|
|
run_number: this._fb.selectedRun,
|
|
prompts,
|
|
deleted_thumbs: includeDeletions ? Array.from(this._fb.pendingDeletes || []) : []
|
|
}),
|
|
})
|
|
.then(() => {
|
|
this._fb.saveFlash = Date.now();
|
|
if (includeDeletions) {
|
|
if (this._fb.pendingDeletes) this._fb.pendingDeletes.clear();
|
|
if (this._fb.selectedImages) this._fb.selectedImages.clear();
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
|
|
if (this._fb.waiting && includeDeletions) {
|
|
fetch("/session_feedback/resume", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ node_id: String(this.id) }),
|
|
}).then(() => { this._fb.waiting = false; this.setDirtyCanvas(true); });
|
|
}
|
|
|
|
if (includeDeletions) this._fb_loadThumbnails();
|
|
});
|
|
};
|
|
|
|
nodeType.prototype._fb_resumeWorkflow = function () {
|
|
if (!this._fb.waiting) return;
|
|
fetch("/session_feedback/resume", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ node_id: String(this.id) }),
|
|
}).then(() => { this._fb.waiting = false; this.setDirtyCanvas(true); });
|
|
};
|
|
|
|
nodeType.prototype._fb_deleteRun = function () {
|
|
if (this._fb.selectedRun === 0) return;
|
|
this._fb_stopEditing();
|
|
fetch("/session_feedback/delete_run", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ session_id: this._fb.sessionId, run_number: this._fb.selectedRun }),
|
|
})
|
|
.then(() => {
|
|
// Unblock workflow if waiting
|
|
if (this._fb.waiting) {
|
|
fetch("/session_feedback/resume", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ node_id: String(this.id) }),
|
|
}).then(() => { this._fb.waiting = false; });
|
|
}
|
|
this._fb.selectedRun = 0;
|
|
this._fb_loadRun(0);
|
|
})
|
|
.catch(err => console.error("[Feedback] Delete error:", err));
|
|
};
|
|
|
|
// ── Draw panel ──────────────────────────────────────────────────
|
|
nodeType.prototype._fb_drawPanel = function (ctx, width, posY) {
|
|
const pad = 14;
|
|
const inW = width - pad * 2;
|
|
let curY = posY + 6;
|
|
const fb = this._fb;
|
|
|
|
ctx.save();
|
|
|
|
if (fb.runCount === 0) {
|
|
ctx.fillStyle = LABEL_COLOR;
|
|
ctx.font = "bold 12px Inter, Arial, sans-serif";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("Session:", pad, curY + 14);
|
|
|
|
ctx.fillStyle = "#fff";
|
|
ctx.font = "bold 13px Inter, Arial, sans-serif";
|
|
ctx.fillText(fb.sessionId || "default", pad + 55, curY + 14);
|
|
|
|
ctx.fillStyle = GREY;
|
|
ctx.font = "italic 12px Inter, Arial, sans-serif";
|
|
ctx.fillText("No runs in this session.", pad, curY + 40);
|
|
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
// Blocking bar
|
|
if (fb.waiting) {
|
|
const barH = 26;
|
|
ctx.fillStyle = "#451a03";
|
|
ctx.beginPath(); ctx.roundRect(pad - 4, curY, inW + 8, barH, 5); ctx.fill();
|
|
ctx.globalAlpha = 0.6 + 0.4 * Math.sin(Date.now() / 300);
|
|
ctx.fillStyle = BTN_WAIT_BG;
|
|
ctx.font = "bold 11px Inter, Arial, sans-serif";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("⏸ Workflow paused — add feedback and click Resume Workflow", pad + 6, curY + 13);
|
|
ctx.globalAlpha = 1;
|
|
curY += barH + 4;
|
|
requestAnimationFrame(() => this.setDirtyCanvas(true));
|
|
}
|
|
|
|
fb.hitAreas = { leftArrow: null, rightArrow: null, stars: [], headers: [], notes: [], saveBtn: null, thumbnails: [], trashBtn: null, undoBtn: null, confirmBtn: null };
|
|
|
|
// --- ROW 1: Session Info & Load/Nuke Buttons ---
|
|
ctx.fillStyle = LABEL_COLOR;
|
|
ctx.font = "bold 12px Inter, Arial, sans-serif";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("Session:", pad, curY + 14);
|
|
|
|
ctx.fillStyle = "#fff";
|
|
ctx.font = "bold 13px Inter, Arial, sans-serif";
|
|
ctx.fillText(fb.sessionId || "default", pad + 55, curY + 14);
|
|
|
|
curY += 28; // move to row 2
|
|
|
|
// --- ROW 2: Run Info & Delete Button ---
|
|
ctx.fillStyle = LABEL_COLOR;
|
|
ctx.font = "bold 12px Inter, Arial, sans-serif";
|
|
ctx.fillText("Run:", pad, curY + 14);
|
|
|
|
const leftX = pad + 35, labelX = leftX + 26, rightX = labelX + 80;
|
|
ctx.fillStyle = fb.selectedRun > 1 ? TEXT_COLOR : "#333";
|
|
ctx.font = "bold 16px Inter, Arial, sans-serif";
|
|
ctx.fillText("◀", leftX + 2, curY + 14);
|
|
fb.hitAreas.leftArrow = { x: leftX, y: curY + 4, w: 24, h: 22 };
|
|
|
|
ctx.fillStyle = "#fff";
|
|
ctx.font = "bold 13px Inter, Arial, sans-serif";
|
|
ctx.fillText(`Run ${fb.selectedRun} / ${fb.runCount}`, labelX + 2, curY + 14);
|
|
|
|
ctx.fillStyle = fb.selectedRun < fb.runCount ? TEXT_COLOR : "#333";
|
|
ctx.font = "bold 16px Inter, Arial, sans-serif";
|
|
ctx.fillText("▶", rightX + 2, curY + 14);
|
|
fb.hitAreas.rightArrow = { x: rightX, y: curY + 4, w: 24, h: 22 };
|
|
|
|
// Draw "Delete" (run) right-aligned with the right arrow for tidiness, or just fixed offset
|
|
const delX = rightX + 30;
|
|
const borderColor = fb.hoverDelete ? "#fca5a5" : "#f87171"; // lighter red/salmon
|
|
const bgColor = fb.hoverDelete ? "#451a1a" : "#2c1414"; // dark red tint
|
|
|
|
ctx.fillStyle = bgColor;
|
|
ctx.beginPath();
|
|
ctx.roundRect(delX, curY + 3, 100, 22, 4);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = borderColor;
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = borderColor;
|
|
ctx.font = "12px Inter, Arial, sans-serif";
|
|
ctx.fillText("✕ Delete Run", delX + 10, curY + 14);
|
|
fb.hitAreas.deleteBtn = { x: delX, y: curY + 3, w: 100, h: 22 };
|
|
|
|
let dynX = delX + 110;
|
|
|
|
// Trash Button
|
|
if (fb.selectedImages && fb.selectedImages.size > 0) {
|
|
const trashBorder = fb.hoverTrash ? "#93c5fd" : "#3b82f6";
|
|
const trashBg = fb.hoverTrash ? "#1e3a8a" : "#1e40af";
|
|
const txt = `🗑 Delete ${fb.selectedImages.size}`;
|
|
const textW = ctx.measureText(txt).width + 24;
|
|
|
|
ctx.fillStyle = trashBg;
|
|
ctx.beginPath(); ctx.roundRect(dynX, curY + 3, textW, 22, 4); ctx.fill();
|
|
ctx.strokeStyle = trashBorder; ctx.lineWidth = 1; ctx.stroke();
|
|
|
|
ctx.fillStyle = trashBorder;
|
|
ctx.font = "12px Inter, Arial, sans-serif";
|
|
ctx.fillText(txt, dynX + 10, curY + 14);
|
|
fb.hitAreas.trashBtn = { x: dynX, y: curY + 3, w: textW, h: 22 };
|
|
dynX += textW + 10;
|
|
}
|
|
|
|
// Undo & Confirm Deletion
|
|
if (fb.pendingDeletes && fb.pendingDeletes.size > 0) {
|
|
const undoBorder = fb.hoverUndo ? "#fcd34d" : "#f59e0b";
|
|
const undoBg = fb.hoverUndo ? "#78350f" : "#451a03";
|
|
const uTxt = `↩️ Undo (${fb.pendingDeletes.size})`;
|
|
const uTextW = ctx.measureText(uTxt).width + 24;
|
|
|
|
ctx.fillStyle = undoBg;
|
|
ctx.beginPath(); ctx.roundRect(dynX, curY + 3, uTextW, 22, 4); ctx.fill();
|
|
ctx.strokeStyle = undoBorder; ctx.lineWidth = 1; ctx.stroke();
|
|
|
|
ctx.fillStyle = undoBorder;
|
|
ctx.font = "12px Inter, Arial, sans-serif";
|
|
ctx.fillText(uTxt, dynX + 10, curY + 14);
|
|
fb.hitAreas.undoBtn = { x: dynX, y: curY + 3, w: uTextW, h: 22 };
|
|
dynX += uTextW + 10;
|
|
|
|
const confirmBorder = fb.hoverConfirm ? "#fca5a5" : "#ef4444";
|
|
const confirmBg = fb.hoverConfirm ? "#7f1d1d" : "#450a0a";
|
|
const cTxt = `⚠️ Confirm Deletion`;
|
|
const cTextW = ctx.measureText(cTxt).width + 24;
|
|
|
|
ctx.fillStyle = confirmBg;
|
|
ctx.beginPath(); ctx.roundRect(dynX, curY + 3, cTextW, 22, 4); ctx.fill();
|
|
ctx.strokeStyle = confirmBorder; ctx.lineWidth = 1; ctx.stroke();
|
|
|
|
ctx.fillStyle = confirmBorder;
|
|
ctx.font = "bold 12px Inter, Arial, sans-serif";
|
|
ctx.fillText(cTxt, dynX + 10, curY + 14);
|
|
fb.hitAreas.confirmBtn = { x: dynX, y: curY + 3, w: cTextW, h: 22 };
|
|
dynX += cTextW + 10;
|
|
}
|
|
|
|
curY += 38;
|
|
|
|
// Prompt cards
|
|
for (let i = 0; i < fb.prompts.length; i++) {
|
|
const prompt = fb.prompts[i];
|
|
const pNum = String(prompt.number);
|
|
const rating = fb.ratings[pNum] || 0;
|
|
const notes = fb.notes[pNum] || "";
|
|
const thumbs = (fb.thumbnails[pNum] || []).filter(img => !fb.pendingDeletes.has(img._urlPath));
|
|
const collapsed = fb.collapsed[pNum] !== false;
|
|
|
|
const cX = pad - 4;
|
|
const cW = inW + 8;
|
|
const availW = cW - 20;
|
|
|
|
// Measure prompt text lines (only when expanded)
|
|
let promptLines = [];
|
|
if (!collapsed && prompt.fullText) {
|
|
const words = prompt.fullText.split(/(\s+)/);
|
|
ctx.font = PROMPT_FONT;
|
|
let line = "";
|
|
for (const tok of words) {
|
|
const test = line + tok;
|
|
if (ctx.measureText(test).width > availW - 4 && line.trim()) {
|
|
promptLines.push(line.trimEnd());
|
|
line = tok.trimStart();
|
|
} else { line = test; }
|
|
}
|
|
if (line.trim()) promptLines.push(line.trimEnd());
|
|
}
|
|
|
|
// Layout thumb rows (always visible)
|
|
const thumbRows = [];
|
|
if (thumbs.length > 0) {
|
|
let row = [], rowW = 0;
|
|
for (const img of thumbs) {
|
|
const tw = img.complete && img.naturalHeight > 0
|
|
? Math.round(img.naturalWidth * (THUMB_MAX_H / img.naturalHeight))
|
|
: 120;
|
|
if (row.length > 0 && rowW + tw + THUMB_GAP > availW) {
|
|
thumbRows.push(row);
|
|
row = [img]; rowW = tw + THUMB_GAP;
|
|
} else { row.push(img); rowW += tw + THUMB_GAP; }
|
|
}
|
|
if (row.length) thumbRows.push(row);
|
|
}
|
|
|
|
// Compute card height
|
|
let cardH = 34; // header
|
|
if (promptLines.length > 0) cardH += 10 + promptLines.length * PROMPT_LH + 8;
|
|
if (thumbRows.length > 0) cardH += thumbRows.length * (THUMB_MAX_H + THUMB_GAP) + 8;
|
|
cardH += 30 + NOTES_H + 10;
|
|
|
|
// Card bg
|
|
ctx.fillStyle = CARD_BG;
|
|
ctx.strokeStyle = CARD_BORDER;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.roundRect(cX, curY, cW, cardH, 6); ctx.fill(); ctx.stroke();
|
|
|
|
// Header (toggle)
|
|
const chevron = collapsed ? "▶" : "▼";
|
|
ctx.font = "bold 11px Inter, Arial, sans-serif";
|
|
ctx.fillStyle = CONCEPT_CLR;
|
|
ctx.textBaseline = "middle";
|
|
let headerLabel = `${chevron} 📋 Prompt ${pNum}: ${prompt.concept || ""}`;
|
|
while (ctx.measureText(headerLabel).width > cW - 22 && headerLabel.length > 20)
|
|
headerLabel = headerLabel.slice(0, -4) + "…";
|
|
ctx.fillText(headerLabel, cX + 10, curY + 17);
|
|
fb.hitAreas.headers[i] = { x: cX, y: curY, w: cW, h: 34, promptNum: pNum };
|
|
|
|
// Separator
|
|
ctx.strokeStyle = "#1e3a5f"; ctx.lineWidth = 0.5;
|
|
ctx.beginPath(); ctx.moveTo(cX + 8, curY + 34); ctx.lineTo(cX + cW - 8, curY + 34); ctx.stroke();
|
|
|
|
let innerY = curY + 38;
|
|
|
|
// Full prompt text (only when expanded)
|
|
if (promptLines.length > 0) {
|
|
ctx.font = PROMPT_FONT;
|
|
ctx.fillStyle = "#94a3b8";
|
|
ctx.textBaseline = "top";
|
|
for (let li = 0; li < promptLines.length; li++)
|
|
ctx.fillText(promptLines[li], cX + 10, innerY + li * PROMPT_LH);
|
|
innerY += promptLines.length * PROMPT_LH + 10;
|
|
ctx.textBaseline = "middle";
|
|
}
|
|
|
|
// Thumbnails — ALWAYS VISIBLE
|
|
if (thumbRows.length > 0) {
|
|
let tY = innerY;
|
|
for (const row of thumbRows) {
|
|
let tX = cX + 10;
|
|
for (const img of row) {
|
|
if (!img.complete || img.naturalWidth === 0) {
|
|
// Placeholder
|
|
ctx.fillStyle = "#0d1117";
|
|
ctx.beginPath(); ctx.roundRect(tX, tY, 120, THUMB_MAX_H, 5); ctx.fill();
|
|
const phase = (Date.now() / 400) % 3;
|
|
for (let d = 0; d < 3; d++) {
|
|
ctx.fillStyle = d === Math.floor(phase) ? "#475569" : "#1e293b";
|
|
ctx.beginPath(); ctx.arc(tX + 60 - 10 + d * 10, tY + THUMB_MAX_H / 2, 2.5, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
tX += 120 + THUMB_GAP;
|
|
continue;
|
|
}
|
|
const tw = Math.round(img.naturalWidth * (THUMB_MAX_H / img.naturalHeight));
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.roundRect(tX, tY, tw, THUMB_MAX_H, 5); ctx.clip();
|
|
ctx.drawImage(img, tX, tY, tw, THUMB_MAX_H);
|
|
ctx.restore();
|
|
|
|
const isSelected = fb.selectedImages && fb.selectedImages.has(img._urlPath);
|
|
|
|
if (isSelected) {
|
|
ctx.fillStyle = "rgba(59,130,246,0.25)";
|
|
ctx.beginPath(); ctx.roundRect(tX, tY, tw, THUMB_MAX_H, 5); ctx.fill();
|
|
ctx.strokeStyle = "#60a5fa";
|
|
ctx.lineWidth = 2.5;
|
|
} else {
|
|
ctx.strokeStyle = "#334155";
|
|
ctx.lineWidth = 1;
|
|
}
|
|
|
|
ctx.beginPath(); ctx.roundRect(tX, tY, tw, THUMB_MAX_H, 5); ctx.stroke();
|
|
|
|
fb.hitAreas.thumbnails.push({ x: tX, y: tY, w: tw, h: THUMB_MAX_H, path: img._urlPath, globalIdx: img._globalIdx });
|
|
tX += tw + THUMB_GAP;
|
|
}
|
|
tY += THUMB_MAX_H + THUMB_GAP;
|
|
}
|
|
innerY = tY + 4;
|
|
if (fb.thumbsLoading > 0) requestAnimationFrame(() => this.setDirtyCanvas(true));
|
|
}
|
|
|
|
// Stars
|
|
const sX = cX + 10, sY = innerY;
|
|
const sSpc = 23, sSize = 18;
|
|
fb.hitAreas.stars[i] = [];
|
|
for (let s = 0; s < STAR_COUNT; s++) {
|
|
ctx.fillStyle = s < rating ? GOLD : GREY;
|
|
ctx.font = `${sSize}px Inter, Arial, sans-serif`;
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(s < rating ? STAR_FILLED : STAR_EMPTY, sX + s * sSpc, sY + 14);
|
|
fb.hitAreas.stars[i][s] = { x: sX + s * sSpc - 2, y: sY, w: sSpc, h: 22, promptNum: pNum, starIdx: s };
|
|
}
|
|
if (rating > 0) {
|
|
ctx.fillStyle = rating <= 2 ? AVOID_CLR : rating >= 4 ? REPLICATE_CLR : LABEL_COLOR;
|
|
ctx.font = "10px Inter, Arial, sans-serif";
|
|
ctx.fillText(RATING_LABELS[rating], sX + STAR_COUNT * sSpc + 6, sY + 12);
|
|
}
|
|
|
|
// Notes area
|
|
const isEditing = fb._editingPrompt === pNum;
|
|
const nY = sY + 26, nW = cW - 20;
|
|
ctx.fillStyle = NOTES_BG;
|
|
ctx.strokeStyle = isEditing ? "#2563eb" : NOTES_BORDER;
|
|
ctx.lineWidth = isEditing ? 1.5 : 0.5;
|
|
ctx.beginPath(); ctx.roundRect(cX + 10, nY, nW, NOTES_H, 5); ctx.fill(); ctx.stroke();
|
|
|
|
// Capture screen rect for textarea positioning via ctx transform
|
|
const t = ctx.getTransform();
|
|
const canvasEl = app.canvas.canvas;
|
|
const cRect = canvasEl.getBoundingClientRect();
|
|
const ratio = cRect.width / canvasEl.width;
|
|
fb._notesScreenRects[pNum] = {
|
|
x: cRect.left + (t.a * (cX + 10) + t.e) * ratio,
|
|
y: cRect.top + (t.d * nY + t.f) * ratio,
|
|
w: nW * t.a * ratio,
|
|
h: NOTES_H * t.d * ratio,
|
|
};
|
|
|
|
// Only draw text on canvas when NOT editing (textarea is on top)
|
|
if (!isEditing) {
|
|
ctx.font = "11px Inter, Arial, sans-serif";
|
|
ctx.textBaseline = "top";
|
|
if (notes) {
|
|
ctx.fillStyle = TEXT_COLOR;
|
|
const wrapped = wrapText(ctx, notes, nW - 16);
|
|
for (let nl = 0; nl < Math.min(wrapped.length, 4); nl++)
|
|
ctx.fillText(wrapped[nl], cX + 16, nY + 8 + nl * 14);
|
|
} else {
|
|
ctx.fillStyle = "#475569";
|
|
ctx.fillText("Add notes\u2026", cX + 14, nY + 8);
|
|
}
|
|
ctx.textBaseline = "middle";
|
|
} else if (fb._editingTA) {
|
|
// Reposition textarea on every frame (handles pan/zoom)
|
|
const sr = fb._notesScreenRects[pNum];
|
|
fb._editingTA.style.left = `${sr.x}px`;
|
|
fb._editingTA.style.top = `${sr.y}px`;
|
|
fb._editingTA.style.width = `${sr.w}px`;
|
|
fb._editingTA.style.height = `${sr.h}px`;
|
|
}
|
|
|
|
fb.hitAreas.notes[i] = { x: cX + 10, y: nY, w: nW, h: NOTES_H, promptNum: pNum };
|
|
curY += cardH + 10;
|
|
}
|
|
|
|
// Bottom button: Only "Resume Option" if blocking, else just a tiny flash text if just saved
|
|
const btnX = pad - 4, btnY = curY + 2, btnW = inW + 8;
|
|
const flash = (Date.now() - fb.saveFlash) < 1500;
|
|
|
|
if (fb.waiting) {
|
|
const btnH = 34;
|
|
const btnBg = fb.hoverBtn ? BTN_WAIT_HV : BTN_WAIT_BG;
|
|
const btnText = "▶ Resume Workflow";
|
|
|
|
ctx.shadowColor = "rgba(0,0,0,0.3)"; ctx.shadowBlur = 6; ctx.shadowOffsetY = 2;
|
|
ctx.fillStyle = btnBg;
|
|
ctx.beginPath(); ctx.roundRect(btnX, btnY, btnW, btnH, 8); ctx.fill();
|
|
ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; ctx.shadowOffsetY = 0;
|
|
|
|
ctx.fillStyle = "#fff";
|
|
ctx.font = "bold 13px Inter, Arial, sans-serif";
|
|
ctx.textBaseline = "middle";
|
|
const tw = ctx.measureText(btnText).width;
|
|
ctx.fillText(btnText, btnX + (btnW - tw) / 2, btnY + btnH / 2);
|
|
fb.hitAreas.saveBtn = { x: btnX, y: btnY, w: btnW, h: btnH };
|
|
} else if (flash) {
|
|
ctx.fillStyle = "#4ade80";
|
|
ctx.font = "italic 11px Inter, Arial, sans-serif";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("✅ Autosaved", btnX + 4, btnY + 12);
|
|
requestAnimationFrame(() => this.setDirtyCanvas(true));
|
|
}
|
|
|
|
ctx.restore();
|
|
};
|
|
|
|
// ── Mouse handler ───────────────────────────────────────────────
|
|
nodeType.prototype._fb_handleMouse = function (event, pos) {
|
|
const fb = this._fb;
|
|
if (!fb.hitAreas) return false;
|
|
const [mx, my] = pos;
|
|
const hit = (r) => r && mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h;
|
|
|
|
if (event.type === "pointermove" || event.type === "mousemove") {
|
|
const wasH = fb.hoverBtn, wasD = fb.hoverDelete;
|
|
const wasT = fb.hoverTrash, wasU = fb.hoverUndo, wasC = fb.hoverConfirm;
|
|
fb.hoverBtn = hit(fb.hitAreas.saveBtn);
|
|
fb.hoverDelete = hit(fb.hitAreas.deleteBtn);
|
|
fb.hoverTrash = hit(fb.hitAreas.trashBtn);
|
|
fb.hoverUndo = hit(fb.hitAreas.undoBtn);
|
|
fb.hoverConfirm = hit(fb.hitAreas.confirmBtn);
|
|
if (wasH !== fb.hoverBtn || wasD !== fb.hoverDelete || wasT !== fb.hoverTrash || wasU !== fb.hoverUndo || wasC !== fb.hoverConfirm) this.setDirtyCanvas(true);
|
|
return false;
|
|
}
|
|
|
|
const isClick = event.type === "pointerdown" ||
|
|
event.type === (LiteGraph.pointerevents_method + "down");
|
|
if (!isClick) return false;
|
|
|
|
|
|
|
|
if (hit(fb.hitAreas.leftArrow) && fb.selectedRun > 1) {
|
|
this._fb_stopEditing(); this._fb_loadRun(fb.selectedRun - 1); return true;
|
|
}
|
|
if (hit(fb.hitAreas.rightArrow) && fb.selectedRun < fb.runCount) {
|
|
this._fb_stopEditing(); this._fb_loadRun(fb.selectedRun + 1); return true;
|
|
}
|
|
|
|
if (hit(fb.hitAreas.deleteBtn) && fb.selectedRun > 0) {
|
|
this._fb_stopEditing();
|
|
if (window.confirm("🗑️ Completely delete Run " + fb.selectedRun + "?\n\nThis will remove it from the session history along with all its thumbnails. This action cannot be undone.")) {
|
|
this._fb_deleteRun();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (hit(fb.hitAreas.trashBtn)) {
|
|
for (const path of fb.selectedImages) fb.pendingDeletes.add(path);
|
|
fb.selectedImages.clear();
|
|
fb.lastSelectedIdx = -1;
|
|
this._fb_stopEditing();
|
|
requestAnimationFrame(() => {
|
|
this.setSize([this.size[0], this.computeSize()[1]]);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (hit(fb.hitAreas.undoBtn)) {
|
|
fb.pendingDeletes.clear();
|
|
this._fb_stopEditing();
|
|
requestAnimationFrame(() => {
|
|
this.setSize([this.size[0], this.computeSize()[1]]);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Thumbnails selection
|
|
for (const t of (fb.hitAreas.thumbnails || [])) {
|
|
if (!hit(t)) continue;
|
|
|
|
this._fb_stopEditing();
|
|
|
|
if (event.shiftKey && fb.lastSelectedIdx >= 0) {
|
|
const start = Math.min(fb.lastSelectedIdx, t.globalIdx);
|
|
const end = Math.max(fb.lastSelectedIdx, t.globalIdx);
|
|
for (const ht of fb.hitAreas.thumbnails) {
|
|
if (ht.globalIdx >= start && ht.globalIdx <= end) {
|
|
if (!fb.pendingDeletes.has(ht.path)) {
|
|
fb.selectedImages.add(ht.path);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Additive Selection default
|
|
if (fb.selectedImages.has(t.path)) {
|
|
fb.selectedImages.delete(t.path);
|
|
} else {
|
|
fb.selectedImages.add(t.path);
|
|
fb.lastSelectedIdx = t.globalIdx;
|
|
}
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
return true;
|
|
}
|
|
|
|
// Header toggle
|
|
for (const h of (fb.hitAreas.headers || [])) {
|
|
if (!h || !hit(h)) continue;
|
|
fb.collapsed[h.promptNum] = !(fb.collapsed[h.promptNum] !== false);
|
|
this._fb_stopEditing();
|
|
requestAnimationFrame(() => {
|
|
this.setSize([this.size[0], this.computeSize()[1]]);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Stars
|
|
for (const stars of (fb.hitAreas.stars || [])) {
|
|
if (!stars) continue;
|
|
for (const s of stars) {
|
|
if (!s || !hit(s)) continue;
|
|
const nr = s.starIdx + 1;
|
|
fb.ratings[s.promptNum] = fb.ratings[s.promptNum] === nr ? 0 : nr;
|
|
this._fb_autosave();
|
|
this.setDirtyCanvas(true);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Notes → open textarea overlay
|
|
for (const n of (fb.hitAreas.notes || [])) {
|
|
if (!n || !hit(n)) continue;
|
|
this._fb_startEditing(n.promptNum);
|
|
this.setDirtyCanvas(true);
|
|
return true;
|
|
}
|
|
|
|
// Confirm explicit Deletions
|
|
if (hit(fb.hitAreas.confirmBtn)) { this._fb_saveFeedback(true); return true; }
|
|
|
|
// Resume workflow explicit button (when paused/blocking)
|
|
if (hit(fb.hitAreas.saveBtn) && fb.waiting) {
|
|
this._fb_saveFeedback(false);
|
|
this._fb_resumeWorkflow();
|
|
return true;
|
|
}
|
|
|
|
// Click elsewhere closes textarea
|
|
this._fb_stopEditing();
|
|
return false;
|
|
};
|
|
}
|
|
|
|
// ── Session Memory Reader & Selector — dynamic load and custom buttons ──
|
|
if (nodeData.name === "SessionMemoryReader" || nodeData.name === "SessionIDSelector") {
|
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (origOnNodeCreated) origOnNodeCreated.apply(this, arguments);
|
|
|
|
// Native Buttons for Rename and Nuke
|
|
this.addWidget("button", "✏️ Rename Session", "rename_session", () => {
|
|
const w = this.widgets.find(x => x.name === "existing_session");
|
|
const oldName = w ? w.value : null;
|
|
|
|
if (!oldName || oldName === "[CREATE NEW]") return alert("Please select an active session to rename.");
|
|
|
|
const newName = window.prompt("Enter new session name:", oldName);
|
|
if (!newName || newName === oldName || newName.trim() === "") return;
|
|
|
|
fetch("/session_feedback/rename_session", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ old_session_id: oldName, new_session_id: newName.trim() })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.error) return alert("Error renaming: " + data.error);
|
|
|
|
// Rename in all dropdowns
|
|
app.graph._nodes.forEach(n => {
|
|
if (n.type === "SessionMemoryReader" || n.comfyClass === "SessionMemoryReader" ||
|
|
n.type === "SessionIDSelector" || n.comfyClass === "SessionIDSelector") {
|
|
const combo = n.widgets?.find(x => x.name === "existing_session");
|
|
if (combo && combo.options && combo.options.values) {
|
|
const idx = combo.options.values.indexOf(oldName);
|
|
if (idx !== -1) {
|
|
combo.options.values[idx] = newName.trim();
|
|
} else {
|
|
combo.options.values.push(newName.trim()); // Fallback if missing
|
|
}
|
|
if (combo.value === oldName) combo.value = newName.trim();
|
|
}
|
|
}
|
|
});
|
|
// Refresh all Feedback Editor nodes
|
|
app.graph._nodes.forEach(n => {
|
|
if (n.type === "SessionFeedbackEditor" || n.comfyClass === "SessionFeedbackEditor") {
|
|
if (n._fb_loadRun) n._fb_loadRun(0);
|
|
}
|
|
});
|
|
}).catch(err => console.error("[Feedback] Rename session error:", err));
|
|
});
|
|
|
|
this.addWidget("button", "☢️ Nuke Session", "nuke_session", () => {
|
|
const w = this.widgets.find(x => x.name === "existing_session");
|
|
const sessionName = w ? w.value : null;
|
|
|
|
if (!sessionName || sessionName === "[CREATE NEW]") return alert("Please select a session to Nuke.");
|
|
|
|
if (window.confirm(`⚠️ WARNING ⚠️\n\nYou are about to DELETE the entire session '${sessionName}'.\nThis removes all runs, prompts, and thumbnails permanently.\n\nAre you sure you want to proceed?`)) {
|
|
fetch("/session_feedback/delete_session", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ session_id: sessionName })
|
|
}).then(() => {
|
|
// Remove from all dropdowns
|
|
app.graph._nodes.forEach(n => {
|
|
if (n.type === "SessionMemoryReader" || n.comfyClass === "SessionMemoryReader" ||
|
|
n.type === "SessionIDSelector" || n.comfyClass === "SessionIDSelector") {
|
|
const combo = n.widgets?.find(x => x.name === "existing_session");
|
|
if (combo && combo.options && combo.options.values) {
|
|
combo.options.values = combo.options.values.filter(v => v !== sessionName);
|
|
if (combo.value === sessionName) combo.value = combo.options.values[0] || "[CREATE NEW]";
|
|
}
|
|
}
|
|
});
|
|
// Refresh all Feedback Editor nodes
|
|
app.graph._nodes.forEach(n => {
|
|
if (n.type === "SessionFeedbackEditor" || n.comfyClass === "SessionFeedbackEditor") {
|
|
if (n._fb_loadRun) n._fb_loadRun(0);
|
|
}
|
|
});
|
|
}).catch(err => console.error("[Feedback] Delete session error:", err));
|
|
}
|
|
});
|
|
|
|
// We wrap this in a small timeout to let ComfyUI finish building native widgets
|
|
setTimeout(() => {
|
|
if (!this.widgets) return;
|
|
// Filter out the old reset_session widget if it still exists from cached versions
|
|
const rsIdx = this.widgets.findIndex(w => w.name === "reset_session");
|
|
if (rsIdx !== -1) this.widgets.splice(rsIdx, 1);
|
|
|
|
for (const w of this.widgets) {
|
|
const origCb = w.callback;
|
|
w.callback = function (v, canvas, node, pos, event) {
|
|
const res = origCb ? origCb.apply(this, arguments) : v;
|
|
|
|
if (w.name === "existing_session" || w.name === "new_session_name") {
|
|
// Find node by referencing 'this' from the outer scope, or 'node' argument
|
|
const parentNode = node || app.graph.getNodeById(this?.id) || {};
|
|
clearTimeout(parentNode._fb_loadTimeout);
|
|
parentNode._fb_loadTimeout = setTimeout(() => {
|
|
app.graph._nodes.forEach(n => {
|
|
if (n.type === "SessionFeedbackEditor" || n.comfyClass === "SessionFeedbackEditor") {
|
|
if (n._fb_loadRun) n._fb_loadRun(0);
|
|
}
|
|
});
|
|
}, 300);
|
|
}
|
|
|
|
return res;
|
|
};
|
|
}
|
|
}, 100);
|
|
};
|
|
}
|
|
},
|
|
});
|