UI: Refine session feedback UI bottom margin and replace blue panel colors with standard dark grey
This commit is contained in:
+234
-17
@@ -91,6 +91,20 @@ def _parse_prompts_from_entry(entry_text: str) -> list[dict]:
|
||||
"concept": concept,
|
||||
"fullText": full_text,
|
||||
})
|
||||
|
||||
# Fallback: if no PROMPT X markers, treat the whole entry as Prompt 1
|
||||
if not prompts and entry_text.strip():
|
||||
full_text = entry_text.strip()
|
||||
trail = _SUMMARY_MARKERS.search(full_text)
|
||||
if trail:
|
||||
full_text = full_text[:trail.start()]
|
||||
|
||||
prompts.append({
|
||||
"number": 1,
|
||||
"concept": "Result",
|
||||
"fullText": full_text.strip()
|
||||
})
|
||||
|
||||
return prompts
|
||||
|
||||
|
||||
@@ -160,6 +174,9 @@ def _save_thumbnails(session_id, run_number, num_prompts, batch_count,
|
||||
img_np = (img_t.cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
|
||||
pil_img = PILImage.fromarray(img_np)
|
||||
|
||||
if pil_img.mode != "RGB":
|
||||
pil_img = pil_img.convert("RGB")
|
||||
|
||||
if (new_h, new_w) != (h, w):
|
||||
pil_img = pil_img.resize((new_w, new_h), PILImage.LANCZOS)
|
||||
|
||||
@@ -309,6 +326,24 @@ async def _api_save_feedback(request):
|
||||
session["feedback"] = feedback
|
||||
_save(session_id, session)
|
||||
|
||||
deleted_thumbs = body.get("deleted_thumbs", [])
|
||||
if deleted_thumbs:
|
||||
del_count = 0
|
||||
for thumb_rel in deleted_thumbs:
|
||||
# Reconstruct absolute paths safely
|
||||
safe_rel = thumb_rel.lstrip("/\\")
|
||||
if ".." in safe_rel:
|
||||
continue
|
||||
abs_path = os.path.join(SESSIONS_DIR, safe_rel)
|
||||
if os.path.isfile(abs_path):
|
||||
try:
|
||||
os.remove(abs_path)
|
||||
del_count += 1
|
||||
except OSError as e:
|
||||
logger.warning(f"[Session Feedback] Could not delete thumb {thumb_rel}: {e}")
|
||||
if del_count > 0:
|
||||
logger.info(f"[Session Feedback] Permanently deleted {del_count} selected thumbnails.")
|
||||
|
||||
logger.info(
|
||||
f"[Session Feedback] Saved feedback for Run {run_number} "
|
||||
f"in session '{session_id}' ({len(prompts_fb)} prompts)."
|
||||
@@ -334,6 +369,146 @@ async def _api_resume_feedback(request):
|
||||
return web.json_response({"status": "not_found"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/session_feedback/delete_run")
|
||||
async def _api_delete_run(request):
|
||||
"""Delete a specific run and its thumbnails."""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
|
||||
session_id = body.get("session_id", "default")
|
||||
run_number = str(body.get("run_number", ""))
|
||||
|
||||
if not run_number:
|
||||
return web.json_response({"error": "run_number required"}, status=400)
|
||||
|
||||
session = _load(session_id)
|
||||
|
||||
del_run_int = int(run_number)
|
||||
entries = session.get("entries", [])
|
||||
run_count = session.get("run_count", len(entries))
|
||||
idx = del_run_int - 1
|
||||
|
||||
# 1. Update entries
|
||||
if 0 <= idx < len(entries):
|
||||
entries.pop(idx)
|
||||
session["run_count"] = max(0, run_count - 1)
|
||||
session["entries"] = entries
|
||||
|
||||
# 2. Update feedback keys
|
||||
feedback = session.get("feedback", {})
|
||||
new_feedback = {}
|
||||
for k_str, v in feedback.items():
|
||||
try:
|
||||
k_int = int(k_str)
|
||||
if k_int < del_run_int:
|
||||
new_feedback[str(k_int)] = v
|
||||
elif k_int > del_run_int:
|
||||
new_feedback[str(k_int - 1)] = v
|
||||
except ValueError:
|
||||
pass
|
||||
session["feedback"] = new_feedback
|
||||
_save(session_id, session)
|
||||
|
||||
# 3. Delete the target thumbnail folder
|
||||
safe_sid = _safe_id(session_id)
|
||||
del_dir = os.path.join(SESSIONS_DIR, safe_sid, f"run_{del_run_int}")
|
||||
if os.path.isdir(del_dir):
|
||||
shutil.rmtree(del_dir, ignore_errors=True)
|
||||
|
||||
# 4. Shift subsequent thumbnail folders down by 1
|
||||
for k in range(del_run_int + 1, run_count + 1):
|
||||
old_dir = os.path.join(SESSIONS_DIR, safe_sid, f"run_{k}")
|
||||
new_dir = os.path.join(SESSIONS_DIR, safe_sid, f"run_{k-1}")
|
||||
if os.path.isdir(old_dir):
|
||||
try:
|
||||
os.rename(old_dir, new_dir)
|
||||
except OSError as e:
|
||||
logger.error(f"[Session Feedback] Rename failed: {e}")
|
||||
|
||||
logger.info(f"[Session Feedback] Deleted Run {del_run_int} in session '{session_id}' and shifted remaining runs.")
|
||||
return web.json_response({"status": "deleted"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/session_feedback/delete_session")
|
||||
async def _api_delete_session(request):
|
||||
"""Completely delete a session JSON and all its image assets."""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
|
||||
session_id = body.get("session_id", "default")
|
||||
if not session_id:
|
||||
return web.json_response({"error": "Session ID required"}, status=400)
|
||||
|
||||
safe_sid = _safe_id(session_id)
|
||||
|
||||
# 1. Delete the JSON file
|
||||
json_path = os.path.join(SESSIONS_DIR, f"{safe_sid}.json")
|
||||
if os.path.isfile(json_path):
|
||||
os.remove(json_path)
|
||||
|
||||
# 2. Delete the entire thumbnails folder
|
||||
img_dir = os.path.join(SESSIONS_DIR, safe_sid)
|
||||
if os.path.isdir(img_dir):
|
||||
shutil.rmtree(img_dir, ignore_errors=True)
|
||||
|
||||
logger.warning(f"[Session Feedback] NUKED session '{session_id}' entirely.")
|
||||
return web.json_response({"status": "deleted"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/session_feedback/rename_session")
|
||||
async def _api_rename_session(request):
|
||||
"""Rename a session JSON and its thumbnail folder."""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
|
||||
old_id = body.get("old_session_id", "")
|
||||
new_id = body.get("new_session_id", "")
|
||||
|
||||
if not old_id or not new_id:
|
||||
return web.json_response({"error": "old_session_id and new_session_id required"}, status=400)
|
||||
|
||||
safe_old = _safe_id(old_id)
|
||||
safe_new = _safe_id(new_id)
|
||||
|
||||
if safe_old == safe_new:
|
||||
return web.json_response({"status": "renamed"}) # Nothing to do
|
||||
|
||||
old_json = os.path.join(SESSIONS_DIR, f"{safe_old}.json")
|
||||
new_json = os.path.join(SESSIONS_DIR, f"{safe_new}.json")
|
||||
|
||||
if os.path.exists(new_json):
|
||||
return web.json_response({"error": f"Session '{new_id}' already exists."}, status=400)
|
||||
|
||||
# Rename JSON
|
||||
if os.path.isfile(old_json):
|
||||
try:
|
||||
os.rename(old_json, new_json)
|
||||
except OSError as e:
|
||||
return web.json_response({"error": f"Could not rename file: {e}"}, status=500)
|
||||
else:
|
||||
# Create an empty session if it didn't exist before renaming
|
||||
_save(new_id, {"run_count": 0, "entries": [], "feedback": {}})
|
||||
|
||||
# Rename images directory
|
||||
old_dir = os.path.join(SESSIONS_DIR, safe_old)
|
||||
new_dir = os.path.join(SESSIONS_DIR, safe_new)
|
||||
|
||||
if os.path.isdir(old_dir):
|
||||
try:
|
||||
os.rename(old_dir, new_dir)
|
||||
except OSError as e:
|
||||
pass # Non-critical if it fails or doesn't exist
|
||||
|
||||
logger.info(f"[Session Feedback] Renamed session from '{old_id}' to '{new_id}'.")
|
||||
return web.json_response({"status": "renamed"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/session_feedback/thumbnails")
|
||||
async def _api_list_thumbnails(request):
|
||||
"""List available thumbnail images for a specific run."""
|
||||
@@ -531,17 +706,16 @@ class SessionMemoryReaderNode:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
sessions = _get_available_sessions()
|
||||
return {
|
||||
"required": {
|
||||
"session_id": ("STRING", {
|
||||
"default": "default",
|
||||
"tooltip": "Session identifier. Must match Writer/Feedback nodes.",
|
||||
"existing_session": (["[CREATE NEW]"] + sessions, {
|
||||
"default": sessions[-1] if sessions else "[CREATE NEW]",
|
||||
"tooltip": "Select an existing session, or [CREATE NEW] to use the text box below."
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"reset_session": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Clear all memory, feedback, AND thumbnail images.",
|
||||
"new_session_name": ("STRING", {
|
||||
"default": "new_session",
|
||||
"tooltip": "If [CREATE NEW] is selected above, this will be your new session_id."
|
||||
}),
|
||||
},
|
||||
}
|
||||
@@ -555,15 +729,14 @@ class SessionMemoryReaderNode:
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return float("NaN")
|
||||
|
||||
def read(self, session_id: str = "default", reset_session: bool = False):
|
||||
if reset_session:
|
||||
_save(session_id, {"run_count": 0, "entries": [], "feedback": {}})
|
||||
deleted = _delete_session_images(session_id)
|
||||
logger.info(
|
||||
f"[Session Memory Reader] Session '{session_id}' reset "
|
||||
f"({deleted} thumbnail images removed)."
|
||||
)
|
||||
return ("", 0, session_id)
|
||||
def read(self, existing_session="[CREATE NEW]", new_session_name="new_session"):
|
||||
if existing_session == "[CREATE NEW]":
|
||||
session_id = new_session_name.strip()
|
||||
else:
|
||||
session_id = existing_session
|
||||
|
||||
if not session_id:
|
||||
session_id = "unnamed_session"
|
||||
|
||||
session = _load(session_id)
|
||||
entries = session.get("entries", [])
|
||||
@@ -670,14 +843,58 @@ class SessionMemoryWriterNode:
|
||||
return (run_summary, session_id)
|
||||
|
||||
|
||||
def _get_available_sessions():
|
||||
os.makedirs(SESSIONS_DIR, exist_ok=True)
|
||||
sessions = []
|
||||
for f in os.listdir(SESSIONS_DIR):
|
||||
if f.endswith(".json"):
|
||||
sessions.append(f[:-5])
|
||||
return sorted(sessions) if sessions else ["default"]
|
||||
|
||||
|
||||
class SessionIDSelectorNode:
|
||||
"""
|
||||
Select an existing session id from a list, or type a new one to start fresh.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
sessions = _get_available_sessions()
|
||||
return {
|
||||
"required": {
|
||||
"existing_session": (["[CREATE NEW]"] + sessions, {
|
||||
"default": sessions[-1] if sessions else "[CREATE NEW]",
|
||||
"tooltip": "Select an existing session, or [CREATE NEW] to use the text box below."
|
||||
}),
|
||||
"new_session_name": ("STRING", {
|
||||
"default": "new_session",
|
||||
"tooltip": "If [CREATE NEW] is selected above, this will be your new session_id."
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("session_id",)
|
||||
FUNCTION = "select_id"
|
||||
CATEGORY = "🧠 Memory"
|
||||
|
||||
def select_id(self, existing_session, new_session_name):
|
||||
if existing_session == "[CREATE NEW]":
|
||||
final_id = new_session_name.strip()
|
||||
else:
|
||||
final_id = existing_session
|
||||
return (final_id,)
|
||||
|
||||
|
||||
# ── Registration ───────────────────────────────────────────────────────
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SessionIDSelector": SessionIDSelectorNode,
|
||||
"SessionFeedbackEditor": SessionFeedbackEditorNode,
|
||||
"SessionMemoryReader": SessionMemoryReaderNode,
|
||||
"SessionMemoryWriter": SessionMemoryWriterNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SessionIDSelector": "🗑️ (DEPRECATED) Session ID Selector",
|
||||
"SessionFeedbackEditor": "\u2b50 Session Feedback Editor",
|
||||
"SessionMemoryReader": "\U0001f9e0 Session Memory Reader",
|
||||
"SessionMemoryWriter": "\U0001f4be Session Memory Writer",
|
||||
|
||||
+468
-52
@@ -7,13 +7,13 @@ const STAR_EMPTY = "☆";
|
||||
const STAR_COUNT = 5;
|
||||
const GOLD = "#FFD700";
|
||||
const GREY = "#555";
|
||||
const CARD_BG = "#16213e";
|
||||
const CARD_BORDER = "#0f3460";
|
||||
const CARD_BG = "#1c1c1c";
|
||||
const CARD_BORDER = "#333333";
|
||||
const TEXT_COLOR = "#e0e0e0";
|
||||
const LABEL_COLOR = "#94a3b8";
|
||||
const CONCEPT_CLR = "#7dd3fc";
|
||||
const NOTES_BG = "#1e293b";
|
||||
const NOTES_BORDER = "#334155";
|
||||
const NOTES_BG = "#222222";
|
||||
const NOTES_BORDER = "#444444";
|
||||
const BTN_SAVE_BG = "#2563eb";
|
||||
const BTN_SAVE_HV = "#3b82f6";
|
||||
const BTN_WAIT_BG = "#d97706";
|
||||
@@ -64,6 +64,42 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
@@ -99,7 +135,7 @@ app.registerExtension({
|
||||
if (n === 0) return [W, 80];
|
||||
const cardW = W - 20;
|
||||
const availW = cardW - 20;
|
||||
let totalH = 48;
|
||||
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;
|
||||
@@ -129,7 +165,7 @@ app.registerExtension({
|
||||
cardH += 30 + NOTES_H + 10; // stars + notes + gap
|
||||
totalH += cardH + 10;
|
||||
}
|
||||
totalH += 56;
|
||||
totalH += 40;
|
||||
return [W, totalH];
|
||||
},
|
||||
draw: (ctx, _n, width, posY) => node._fb_drawPanel(ctx, width, posY),
|
||||
@@ -168,9 +204,36 @@ app.registerExtension({
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
nodeType.prototype._fb_loadRun = function (runNumber) {
|
||||
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");
|
||||
const sid = w?.value || "default";
|
||||
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}`)
|
||||
@@ -201,9 +264,15 @@ app.registerExtension({
|
||||
.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--;
|
||||
@@ -218,7 +287,12 @@ app.registerExtension({
|
||||
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);
|
||||
})
|
||||
@@ -255,7 +329,7 @@ app.registerExtension({
|
||||
position: "fixed",
|
||||
left: `${sr.x}px`, top: `${sr.y}px`,
|
||||
width: `${sr.w}px`, height: `${sr.h}px`,
|
||||
background: "#1e293b",
|
||||
background: "#222222",
|
||||
color: "#e0e0e0",
|
||||
caretColor: "#7dd3fc",
|
||||
border: "1.5px solid #2563eb",
|
||||
@@ -283,6 +357,7 @@ app.registerExtension({
|
||||
|
||||
ta.addEventListener("input", () => {
|
||||
fb.notes[promptNum] = ta.value;
|
||||
node._fb_autosave();
|
||||
});
|
||||
ta.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") { node._fb_stopEditing(); return; }
|
||||
@@ -310,33 +385,86 @@ app.registerExtension({
|
||||
ta.setSelectionRange(ta.value.length, ta.value.length);
|
||||
};
|
||||
|
||||
// ── Save ────────────────────────────────────────────────────────
|
||||
nodeType.prototype._fb_saveFeedback = function () {
|
||||
// ── 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;
|
||||
this._fb_stopEditing();
|
||||
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 }),
|
||||
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) {
|
||||
|
||||
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;
|
||||
@@ -347,10 +475,19 @@ app.registerExtension({
|
||||
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.textBaseline = "middle";
|
||||
ctx.fillText("No runs in this session.", pad, curY + 30);
|
||||
ctx.fillText("No runs in this session.", pad, curY + 40);
|
||||
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
@@ -364,18 +501,29 @@ app.registerExtension({
|
||||
ctx.fillStyle = BTN_WAIT_BG;
|
||||
ctx.font = "bold 11px Inter, Arial, sans-serif";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("⏸ Workflow paused — add feedback and click Save", pad + 6, curY + 13);
|
||||
ctx.fillText("⏸ Workflow paused — add feedback and click Resume Workflow", pad + 6, curY + 13);
|
||||
ctx.globalAlpha = 1;
|
||||
curY += barH + 4;
|
||||
requestAnimationFrame(() => this.setDirtyCanvas(true));
|
||||
}
|
||||
|
||||
// Run navigator
|
||||
fb.hitAreas = { leftArrow: null, rightArrow: null, stars: [], headers: [], notes: [], saveBtn: null };
|
||||
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;
|
||||
@@ -392,6 +540,79 @@ app.registerExtension({
|
||||
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
|
||||
@@ -400,7 +621,7 @@ app.registerExtension({
|
||||
const pNum = String(prompt.number);
|
||||
const rating = fb.ratings[pNum] || 0;
|
||||
const notes = fb.notes[pNum] || "";
|
||||
const thumbs = fb.thumbnails[pNum] || [];
|
||||
const thumbs = (fb.thumbnails[pNum] || []).filter(img => !fb.pendingDeletes.has(img._urlPath));
|
||||
const collapsed = fb.collapsed[pNum] !== false;
|
||||
|
||||
const cX = pad - 4;
|
||||
@@ -502,8 +723,22 @@ app.registerExtension({
|
||||
ctx.beginPath(); ctx.roundRect(tX, tY, tw, THUMB_MAX_H, 5); ctx.clip();
|
||||
ctx.drawImage(img, tX, tY, tw, THUMB_MAX_H);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = "#334155"; ctx.lineWidth = 1;
|
||||
|
||||
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;
|
||||
@@ -576,25 +811,34 @@ app.registerExtension({
|
||||
curY += cardH + 10;
|
||||
}
|
||||
|
||||
// Save button
|
||||
const btnW = inW + 8, btnH = 34, btnX = pad - 4, btnY = curY + 2;
|
||||
// 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;
|
||||
const btnBg = flash ? BTN_DONE_BG : (fb.waiting ? (fb.hoverBtn ? BTN_WAIT_HV : BTN_WAIT_BG) : (fb.hoverBtn ? BTN_SAVE_HV : BTN_SAVE_BG));
|
||||
const btnText = flash ? "✅ Saved" : (fb.waiting ? "💾 Save & Continue ▶" : "💾 Save Feedback");
|
||||
|
||||
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;
|
||||
if (fb.waiting) {
|
||||
const btnH = 34;
|
||||
const btnBg = fb.hoverBtn ? BTN_WAIT_HV : BTN_WAIT_BG;
|
||||
const btnText = "▶ Resume Workflow";
|
||||
|
||||
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 };
|
||||
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));
|
||||
}
|
||||
|
||||
if (flash) requestAnimationFrame(() => this.setDirtyCanvas(true));
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
@@ -606,9 +850,14 @@ app.registerExtension({
|
||||
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 was = fb.hoverBtn;
|
||||
const wasH = fb.hoverBtn, wasD = fb.hoverDelete;
|
||||
const wasT = fb.hoverTrash, wasU = fb.hoverUndo, wasC = fb.hoverConfirm;
|
||||
fb.hoverBtn = hit(fb.hitAreas.saveBtn);
|
||||
if (was !== fb.hoverBtn) this.setDirtyCanvas(true);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -616,6 +865,8 @@ app.registerExtension({
|
||||
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;
|
||||
}
|
||||
@@ -623,6 +874,65 @@ app.registerExtension({
|
||||
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;
|
||||
@@ -642,6 +952,7 @@ app.registerExtension({
|
||||
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;
|
||||
}
|
||||
@@ -655,8 +966,15 @@ app.registerExtension({
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save
|
||||
if (hit(fb.hitAreas.saveBtn)) { this._fb_saveFeedback(); 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();
|
||||
@@ -664,18 +982,116 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
|
||||
// ── Session Memory Reader — reset confirmation ────────────────────
|
||||
if (nodeData.name === "SessionMemoryReader") {
|
||||
const orig = nodeType.prototype.onWidgetChanged;
|
||||
nodeType.prototype.onWidgetChanged = function (widget) {
|
||||
if (widget?.name === "reset_session" && widget.value === true) {
|
||||
const ok = window.confirm(
|
||||
"⚠️ Reset session?\n\nALL data will be permanently deleted:\n" +
|
||||
"• Run history\n• Feedback & ratings\n• All thumbnail images\n\nThis action cannot be undone."
|
||||
);
|
||||
if (!ok) { widget.value = false; this.setDirtyCanvas(true); return; }
|
||||
}
|
||||
if (orig) orig.apply(this, arguments);
|
||||
// ── 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);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user