Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

108 lines
4.2 KiB
JavaScript

// Viewer "Edit": quick airbrush touch-up over the image before a follow-up
// inpaint or upscale. The modal paints on a layer over the untouched source;
// Save composites them in the browser and POSTs the PNG to /promptchain/
// save-edited-image, which re-attaches the parent's embedded prompt/workflow
// chunks (so the result still anchors inpaint/upscale grafts) and records it
// with parent lineage.
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
import { sourceSavePrefixInfo } from "./upscale-from-image.js";
import { refreshEntryWorkflows } from "./history.js";
function toast(severity, detail) {
app.extensionManager?.toast?.add({ severity, summary: "Edit", detail, life: 7000 });
}
function freshWorkflowId() {
if (globalThis.crypto?.randomUUID) return crypto.randomUUID();
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
}
export async function prepareEdit(entry) {
const params = entry._browsePath != null
? `scope=${encodeURIComponent(entry._browseScope || "output")}&path=${encodeURIComponent(entry._browsePath)}`
: `hash=${encodeURIComponent(entry.hash)}`;
let data = null;
try {
const res = await api.fetchApi(`/promptchain/image-workflow?${params}`);
if (res.ok) data = await res.json();
} catch (e) {
console.error("[PromptChain] image-workflow fetch failed", e);
}
if (!data?.input_ref) {
toast("error", "Couldn't read the image on the server.");
return null;
}
const { prefix } = sourceSavePrefixInfo(data.workflow);
// No workflow-derived prefix (image dropped into a sidebar folder, no
// embedded SaveImage): default to the image's own folder instead of the
// generic edit/ bucket — but only when that folder is in the OUTPUT tree,
// since save-edited-image always writes output-side. A workflows/input
// folder name would mint a phantom output subfolder.
const sourceFolder = entry._browsePath != null
? ((entry._browseScope || "output") === "output"
? entry._browsePath.split("/").slice(0, -1).join("/")
: "")
: (entry.subfolder || "");
const defaultSavePrefix = prefix
? (prefix.endsWith("_edit") ? prefix : `${prefix}_edit`)
: sourceFolder
? `${sourceFolder}/%date:yyyy-MM-dd_hhmmss%`
: "edit/edit";
return { data, caps: { defaultSavePrefix } };
}
// Layer-stack sidecar (the "don't-flatten = keep your PSD" persistence). Keyed
// by the SAVED image's content hash; written AFTER the flat PNG save so it never
// blocks the gallery record. planes = [{ name:"L3.png", blob }]. Best-effort —
// the flat PNG is always the source of truth, a failed doc write is non-fatal.
export async function persistEditDoc(hash, manifest, planes) {
if (!hash || !manifest) return;
try {
const form = new FormData();
form.append("manifest", JSON.stringify(manifest));
for (const { name, blob } of planes) form.append(name, new File([blob], name, { type: "image/png" }));
await api.fetchApi(`/promptchain/edit-doc/${hash}`, { method: "POST", body: form });
} catch (e) {
console.warn("[PromptChain] edit-doc persist failed", e);
}
}
export async function fetchEditDoc(hash) {
if (!hash) return null;
try {
const res = await api.fetchApi(`/promptchain/edit-doc/${hash}`);
if (!res.ok) return null;
return await res.json();
} catch (e) {
return null;
}
}
export function editDocPlaneUrl(hash, file) {
return `/promptchain/edit-doc/${encodeURIComponent(hash)}/${encodeURIComponent(file)}`;
}
export async function saveEditedImage(prepared, blob, prefix) {
try {
const form = new FormData();
form.append("image", new File([blob], "edit.png", { type: "image/png" }));
form.append("prefix", prefix || "");
form.append("parent_filename", prepared?.data?.input_ref || "");
form.append("workflow_id", freshWorkflowId());
const res = await api.fetchApi("/promptchain/save-edited-image", { method: "POST", body: form });
if (!res.ok) {
const err = await res.json().catch(() => null);
toast("error", `Save failed: ${err?.error || res.status}`);
return null;
}
const entry = await res.json();
refreshEntryWorkflows(entry);
return entry;
} catch (e) {
toast("error", `Save failed: ${e.message}`);
return null;
}
}