Initial refactoring changes
- Moved to web folder - Splitting into individual files
This commit is contained in:
parent
3637e19eff
commit
5e25c77074
|
@ -0,0 +1,476 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="lib/litegraph.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
|
<script type="text/javascript" src="lib/litegraph.core.js"></script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
await app.setup();
|
||||||
|
window.app = app;
|
||||||
|
window.graph = app.graph;
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
return;
|
||||||
|
|
||||||
|
function postPrompt(number) {
|
||||||
|
let prompt = graphToPrompt();
|
||||||
|
let full_data = {client_id: clientId, prompt: prompt, extra_data: {extra_pnginfo: {workflow: graph.serialize()}}};
|
||||||
|
if (number == -1) {
|
||||||
|
full_data.front = true;
|
||||||
|
} else
|
||||||
|
if (number != 0) {
|
||||||
|
full_data.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/prompt', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(full_data)
|
||||||
|
})
|
||||||
|
.then(data => promptPosted(data))
|
||||||
|
.catch(error => console.error(error))
|
||||||
|
|
||||||
|
// console.log(JSON.stringify(prompt));
|
||||||
|
// console.log(JSON.stringify(graph.serialize()));
|
||||||
|
|
||||||
|
// restore initial values replaced by dynamic prompting
|
||||||
|
for (let x in graph._nodes_by_id) {
|
||||||
|
let n = graph._nodes_by_id[x];
|
||||||
|
for (let w in n.widgets) {
|
||||||
|
let wid = n.widgets[w];
|
||||||
|
if (wid.dynamic_prompt && wid.dynamic_prompt === true)
|
||||||
|
wid.value = wid.value_initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prompt_file_load(file)
|
||||||
|
{
|
||||||
|
if (file.type === 'image/png') {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
// Get the PNG data as a Uint8Array
|
||||||
|
const pngData = new Uint8Array(event.target.result);
|
||||||
|
const dataView = new DataView(pngData.buffer);
|
||||||
|
|
||||||
|
// Check that the PNG signature is present
|
||||||
|
if (dataView.getUint32(0) !== 0x89504e47) {
|
||||||
|
console.error('Not a valid PNG file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start searching for chunks after the PNG signature
|
||||||
|
let offset = 8;
|
||||||
|
let txt_chunks = {}
|
||||||
|
// Loop through the chunks in the PNG file
|
||||||
|
while (offset < pngData.length) {
|
||||||
|
// Get the length of the chunk
|
||||||
|
const length = dataView.getUint32(offset);
|
||||||
|
// Get the chunk type
|
||||||
|
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
|
||||||
|
if (type === 'tEXt') {
|
||||||
|
// Get the keyword
|
||||||
|
let keyword_end = offset + 8;
|
||||||
|
while (pngData[keyword_end] !== 0) {
|
||||||
|
keyword_end++;
|
||||||
|
}
|
||||||
|
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
|
||||||
|
// Get the text
|
||||||
|
const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
|
||||||
|
txt_chunks[keyword] = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next chunk
|
||||||
|
offset += 12 + length;
|
||||||
|
}
|
||||||
|
console.log(txt_chunks);
|
||||||
|
// console.log(JSON.parse(txt_chunks["prompt"]));
|
||||||
|
loadGraphData(graph, JSON.parse(txt_chunks["workflow"]));
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function() {
|
||||||
|
console.log(reader.result);
|
||||||
|
var jsonData = JSON.parse(reader.result);
|
||||||
|
loadGraphData(graph, jsonData);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get prompt from dropped PNG or json
|
||||||
|
document.addEventListener('drop', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
console.log(file.type);
|
||||||
|
prompt_file_load(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
let runningNodeId = null;
|
||||||
|
let progress = null;
|
||||||
|
let clientId = null;
|
||||||
|
const orig = LGraphCanvas.prototype.drawNodeShape;
|
||||||
|
LGraphCanvas.prototype.drawNodeShape = function(node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
|
||||||
|
const res = orig.apply(this, arguments);
|
||||||
|
|
||||||
|
if(node.id + "" === runningNodeId) {
|
||||||
|
const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.globalAlpha = 0.8;
|
||||||
|
ctx.beginPath();
|
||||||
|
if( shape == LiteGraph.BOX_SHAPE )
|
||||||
|
ctx.rect(-6,-6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0]+1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT );
|
||||||
|
else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) )
|
||||||
|
ctx.roundRect(-6,-6 - LiteGraph.NODE_TITLE_HEIGHT, 12 +size[0]+1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT , this.round_radius * 2);
|
||||||
|
else if (shape == LiteGraph.CARD_SHAPE)
|
||||||
|
ctx.roundRect(-6,-6 + LiteGraph.NODE_TITLE_HEIGHT, 12 +size[0]+1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT , this.round_radius * 2, 2);
|
||||||
|
else if (shape == LiteGraph.CIRCLE_SHAPE)
|
||||||
|
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI*2);
|
||||||
|
ctx.strokeStyle = "#0f0"
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.strokeStyle = fgcolor;
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
|
if(progress) {
|
||||||
|
ctx.fillStyle = "green";
|
||||||
|
ctx.fillRect(0, 0, size[0] * (progress.value / progress.max), 6);
|
||||||
|
ctx.fillStyle = bgcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeProgress(v) {
|
||||||
|
progress = v;
|
||||||
|
graph.setDirtyCanvas(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRunningNode(id) {
|
||||||
|
progress = null;
|
||||||
|
runningNodeId = id;
|
||||||
|
graph.setDirtyCanvas(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
function updateStatus(data) {
|
||||||
|
document.getElementById("queuesize").innerHTML = "Queue size: " + (data ? data.exec_info.queue_remaining : "ERR");
|
||||||
|
}
|
||||||
|
|
||||||
|
//fix for colab and other things that don't support websockets.
|
||||||
|
function manually_fetch_queue() {
|
||||||
|
fetch('/prompt')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateStatus(data);
|
||||||
|
}).catch((response) => {updateStatus(null)});
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws;
|
||||||
|
function createSocket(isReconnect) {
|
||||||
|
if(ws) return;
|
||||||
|
|
||||||
|
let opened = false;
|
||||||
|
ws = new WebSocket(`ws${window.location.protocol === "https:"? "s" : ""}://${location.host}/ws`);
|
||||||
|
|
||||||
|
ws.addEventListener("open", () => {
|
||||||
|
opened = true;
|
||||||
|
if(isReconnect) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("error", () => {
|
||||||
|
if(ws) ws.close();
|
||||||
|
manually_fetch_queue();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("close", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
ws = null;
|
||||||
|
createSocket(true);
|
||||||
|
}, 300);
|
||||||
|
if(opened) {
|
||||||
|
updateStatus(null);
|
||||||
|
showModal("Reconnecting...");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("message", (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
switch(msg.type) {
|
||||||
|
case "status":
|
||||||
|
if(msg.data.sid) {
|
||||||
|
clientId = msg.data.sid;
|
||||||
|
}
|
||||||
|
updateStatus(msg.data.status);
|
||||||
|
break;
|
||||||
|
case "progress":
|
||||||
|
updateNodeProgress(msg.data)
|
||||||
|
break;
|
||||||
|
case "executing":
|
||||||
|
setRunningNode(msg.data.node);
|
||||||
|
break;
|
||||||
|
case "executed":
|
||||||
|
nodeOutputs[msg.data.node] = msg.data.output;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown message type")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unhandled message:", event.data)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
createSocket();
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function clearGraph() {
|
||||||
|
graph.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTxt2Img() {
|
||||||
|
loadGraphData(graph, default_graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGraph() {
|
||||||
|
var json = JSON.stringify(graph.serialize()); // convert the data to a JSON string
|
||||||
|
var blob = new Blob([json], {type: "application/json"});
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.style = "display: none";
|
||||||
|
a.href = url;
|
||||||
|
a.download = "workflow.json";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setTimeout(function() {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = document.createElement("input");
|
||||||
|
input.setAttribute("type", "file");
|
||||||
|
input.setAttribute("accept", ".json,image/png");
|
||||||
|
input.style.display = "none";
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
var file = input.files[0];
|
||||||
|
prompt_file_load(file);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadGraph() {
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('paste', e=>{
|
||||||
|
let data = (e.clipboardData || window.clipboardData).getData('text/plain');
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = data.slice(data.indexOf('{'));
|
||||||
|
j = JSON.parse(data);
|
||||||
|
} catch(err) {
|
||||||
|
data = data.slice(data.indexOf('workflow\n'));
|
||||||
|
data = data.slice(data.indexOf('{'));
|
||||||
|
j = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (Object.hasOwn(j, 'version') && Object.hasOwn(j, 'nodes') && Object.hasOwn(j, 'extra')) {
|
||||||
|
loadGraphData(graph, j);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteQueueElement(type, delete_id, then) {
|
||||||
|
fetch('/' + type, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({"delete":[delete_id]})
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
then();
|
||||||
|
})
|
||||||
|
.catch(error => console.error(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadQueue() {
|
||||||
|
loadItems("queue")
|
||||||
|
}
|
||||||
|
function loadHistory() {
|
||||||
|
loadItems("history")
|
||||||
|
}
|
||||||
|
function loadItems(type) {
|
||||||
|
fetch('/' + type)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
var queue_div = document.getElementById(type + "button-content");
|
||||||
|
queue_div.style.display = 'block';
|
||||||
|
var see_queue_button = document.getElementById("see" + type + "button");
|
||||||
|
let old_w = see_queue_button.style.width;
|
||||||
|
see_queue_button.innerHTML = "Close";
|
||||||
|
|
||||||
|
let runningcontents;
|
||||||
|
if(type === "queue") {
|
||||||
|
runningcontents = document.getElementById("runningcontents");
|
||||||
|
runningcontents.innerHTML = '';
|
||||||
|
}
|
||||||
|
let queuecontents = document.getElementById(type + "contents");
|
||||||
|
queuecontents.innerHTML = '';
|
||||||
|
function append_to_list(list_element, append_to_element, append_delete, state) {
|
||||||
|
let number = list_element[0];
|
||||||
|
let id = list_element[1];
|
||||||
|
let prompt = list_element[2];
|
||||||
|
let workflow = list_element[3].extra_pnginfo.workflow;
|
||||||
|
let a = document.createElement("a");
|
||||||
|
a.innerHTML = number + ": ";
|
||||||
|
append_to_element.appendChild(a);
|
||||||
|
let button = document.createElement("button");
|
||||||
|
button.innerHTML = "Load";
|
||||||
|
button.style.fontSize = "10px";
|
||||||
|
button.workflow = workflow;
|
||||||
|
button.onclick = function(event) {
|
||||||
|
loadGraphData(graph, event.target.workflow);
|
||||||
|
if(state) {
|
||||||
|
nodeOutputs = state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
append_to_element.appendChild(button);
|
||||||
|
if (append_delete) {
|
||||||
|
let button = document.createElement("button");
|
||||||
|
button.innerHTML = "Delete";
|
||||||
|
button.style.fontSize = "10px";
|
||||||
|
button.delete_id = id;
|
||||||
|
button.onclick = function(event) {
|
||||||
|
deleteQueueElement(type, event.target.delete_id, () => loadItems(type));
|
||||||
|
};
|
||||||
|
append_to_element.appendChild(button);
|
||||||
|
}
|
||||||
|
append_to_element.appendChild(document.createElement("br"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(runningcontents) {
|
||||||
|
for (let x in data.queue_running) {
|
||||||
|
append_to_list(data.queue_running[x], runningcontents, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let items;
|
||||||
|
if(type === "queue") {
|
||||||
|
items = data.queue_pending;
|
||||||
|
} else {
|
||||||
|
items = Object.values(data);
|
||||||
|
}
|
||||||
|
items.sort((a, b) => a[0] - b[0]);
|
||||||
|
for (let i of items) {
|
||||||
|
append_to_list(type === "queue" ? i : i.prompt, queuecontents, true, i.outputs);
|
||||||
|
}
|
||||||
|
}).catch((response) => {console.log(response)});
|
||||||
|
}
|
||||||
|
|
||||||
|
function seeItems(type) {
|
||||||
|
var queue_div = document.getElementById(type + "button-content");
|
||||||
|
if (queue_div.style.display == 'block') {
|
||||||
|
closeItems(type)
|
||||||
|
} else {
|
||||||
|
loadItems(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function seeQueue() {
|
||||||
|
closeItems("history")
|
||||||
|
seeItems("queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
function seeHistory() {
|
||||||
|
closeItems("queue")
|
||||||
|
seeItems("history")
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeItems(type) {
|
||||||
|
var queue_div = document.getElementById(type + "button-content");
|
||||||
|
queue_div.style.display = 'none';
|
||||||
|
var see_queue_button = document.getElementById("see" + type + "button");
|
||||||
|
see_queue_button.innerHTML = "See " + type[0].toUpperCase() + type.substr(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearItems(type) {
|
||||||
|
fetch('/' + type, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({"clear":true})
|
||||||
|
}).then(data => {
|
||||||
|
loadItems(type);
|
||||||
|
})
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span id="menu" style="font-size: 15px;position: absolute; top: 50%; right: 0%; background-color: white; text-align: center; z-index: 100;width:170px">
|
||||||
|
<span id="queuesize">Queue size: X</span><br>
|
||||||
|
<button style="font-size: 20px;width: 100%;" id="queuebutton" onclick="postPrompt(0)">Queue Prompt</button><br>
|
||||||
|
<span style="left: 0%;">
|
||||||
|
<button style="font-size: 10px;" id="queuebutton" onclick="postPrompt(-1)">Queue Front</button>
|
||||||
|
<button style="font-size: 10px; width: 50%;" id="seequeuebutton" onclick="seeQueue()">See Queue</button>
|
||||||
|
<button style="font-size: 10px; width: 50%;" id="seehistorybutton" onclick="seeHistory()">See History</button>
|
||||||
|
<br>
|
||||||
|
</span>
|
||||||
|
<div id="queuebutton-content" style="background-color: #e1e1e1;min-width: 160px;display: none;z-index: 101;">
|
||||||
|
<span style="width:100%;padding: 3px;display:inline-block;">Running:</span>
|
||||||
|
<div id="runningcontents" style="background-color: #d0d0d0; padding: 5px;">
|
||||||
|
<a>1</a>
|
||||||
|
<button style="font-size: 10px;">Load</button>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<span style="left: 0%;padding: 3px;display:inline-block;">Queued:</span>
|
||||||
|
<div id="queuecontents" style="overflow-y: scroll;height: 100px;background-color: #d0d0d0;padding: 5px;">
|
||||||
|
<a>1</a>
|
||||||
|
<button style="font-size: 10px;">Load</button>
|
||||||
|
<button style="font-size: 10px;">Delete</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<span style="padding: 5px;display:inline-block;">
|
||||||
|
<button style="font-size: 12px;" onclick="clearItems('queue')">Clear Queue</button>
|
||||||
|
<button style="font-size: 12px;" onclick="loadQueue()">Refresh</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="historybutton-content" style="background-color: #e1e1e1;min-width: 160px;display: none;z-index: 101;">
|
||||||
|
<span style="width:100%;padding: 3px;display:inline-block;">History:</span>
|
||||||
|
<div id="historycontents" style="overflow-y: scroll;height: 100px;background-color: #d0d0d0;padding: 5px;">
|
||||||
|
</div>
|
||||||
|
<span style="padding: 5px;display:inline-block;">
|
||||||
|
<button style="font-size: 12px;" onclick="clearItems('history')">Clear History</button>
|
||||||
|
<button style="font-size: 12px;" onclick="loadHistory()">Refresh</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button style="font-size: 20px;" onclick="saveGraph()">Save</button><br>
|
||||||
|
<button style="font-size: 20px;" onclick="loadGraph()">Load</button>
|
||||||
|
<br>
|
||||||
|
<button style="font-size: 20px;" onclick="clearGraph()">Clear</button><br>
|
||||||
|
<button style="font-size: 20px;" onclick="loadTxt2Img()">Load Default</button><br>
|
||||||
|
</span>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,36 @@
|
||||||
|
class ComfyApi {
|
||||||
|
async getNodeDefs() {
|
||||||
|
const resp = await fetch("object_info", { cache: "no-store" });
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async queuePrompt(number, { output, workflow }) {
|
||||||
|
const body = {
|
||||||
|
client_id: this.clientId,
|
||||||
|
prompt: output,
|
||||||
|
extra_data: { extra_pnginfo: { workflow } },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (number === -1) {
|
||||||
|
body.front = true;
|
||||||
|
} else if (number != 0) {
|
||||||
|
body.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/prompt", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw {
|
||||||
|
response: await res.text(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = new ComfyApi();
|
|
@ -0,0 +1,569 @@
|
||||||
|
import { ComfyWidgets } from "./widgets.js";
|
||||||
|
import { api } from "./api.js";
|
||||||
|
import { defaultGraph } from "./defaultGraph.js";
|
||||||
|
|
||||||
|
class ComfyDialog {
|
||||||
|
constructor() {
|
||||||
|
this.element = document.createElement("div");
|
||||||
|
this.element.classList.add("comfy-modal");
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.classList.add("comfy-modal-content");
|
||||||
|
this.textElement = document.createElement("p");
|
||||||
|
content.append(this.textElement);
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("button");
|
||||||
|
closeBtn.type = "button";
|
||||||
|
closeBtn.textContent = "CLOSE";
|
||||||
|
content.append(closeBtn);
|
||||||
|
closeBtn.onclick = () => this.close();
|
||||||
|
|
||||||
|
this.element.append(content);
|
||||||
|
document.body.append(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.element.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
show(html) {
|
||||||
|
this.textElement.innerHTML = html;
|
||||||
|
this.element.style.display = "flex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComfyQueue {
|
||||||
|
constructor() {
|
||||||
|
this.element = document.createElement("div");
|
||||||
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
if (this.element.style.display !== "none") {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async show() {
|
||||||
|
this.element.style.display = "block";
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const queue = await api.getQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.element.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ComfyUI {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
this.menuContainer = document.createElement("div");
|
||||||
|
this.menuContainer.classList.add("comfy-menu");
|
||||||
|
document.body.append(this.menuContainer);
|
||||||
|
|
||||||
|
this.dialog = new ComfyDialog();
|
||||||
|
this.queue = new ComfyQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComfyApp {
|
||||||
|
constructor() {
|
||||||
|
this.ui = new ComfyUI(this);
|
||||||
|
this.nodeOutputs = {};
|
||||||
|
this.extensions = [
|
||||||
|
{
|
||||||
|
name: "TestExtension",
|
||||||
|
init(app) {
|
||||||
|
console.log("[ext:init]", app);
|
||||||
|
},
|
||||||
|
setup(app) {
|
||||||
|
console.log("[ext:setup]", app);
|
||||||
|
},
|
||||||
|
addCustomNodeDefs(defs, app) {
|
||||||
|
console.log("[ext:addCustomNodeDefs]", defs, app);
|
||||||
|
},
|
||||||
|
loadedGraphNode(node, app) {
|
||||||
|
// console.log("[ext:loadedGraphNode]", node, app);
|
||||||
|
},
|
||||||
|
getCustomWidgets(app) {
|
||||||
|
console.log("[ext:getCustomWidgets]", app);
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
beforeRegisterNode(nodeType, nodeData, app) {
|
||||||
|
// console.log("[ext:beforeRegisterNode]", nodeType, nodeData, app);
|
||||||
|
},
|
||||||
|
registerCustomNodes(app) {
|
||||||
|
console.log("[ext:registerCustomNodes]", app);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#log(message, ...other) {
|
||||||
|
console.log("[comfy]", message, ...other);
|
||||||
|
}
|
||||||
|
|
||||||
|
#error(message, ...other) {
|
||||||
|
console.error("[comfy]", message, ...other);
|
||||||
|
}
|
||||||
|
|
||||||
|
#invokeExtensions(method, ...args) {
|
||||||
|
let results = [];
|
||||||
|
for (const ext of this.extensions) {
|
||||||
|
if (method in ext) {
|
||||||
|
try {
|
||||||
|
results.push(ext[method](...args, this));
|
||||||
|
} catch (error) {
|
||||||
|
this.#error(
|
||||||
|
`Error calling extension '${ext.name}' method '${method}'`,
|
||||||
|
{ error },
|
||||||
|
{ extension: ext },
|
||||||
|
{ args }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #invokeExtensionsAsync(method, ...args) {
|
||||||
|
return await Promise.all(
|
||||||
|
this.extensions.map(async (ext) => {
|
||||||
|
if (method in ext) {
|
||||||
|
try {
|
||||||
|
return await ext[method](...args, this);
|
||||||
|
} catch (error) {
|
||||||
|
this.#error(
|
||||||
|
`Error calling extension '${ext.name}' method '${method}'`,
|
||||||
|
{ error },
|
||||||
|
{ extension: ext },
|
||||||
|
{ args }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#addNodeContextMenuHandler(node) {
|
||||||
|
node.prototype.getExtraMenuOptions = function (_, options) {
|
||||||
|
if (this.imgs) {
|
||||||
|
// If this node has images then we add an open in new tab item
|
||||||
|
let img;
|
||||||
|
if (this.imageIndex != null) {
|
||||||
|
// An image is selected so select that
|
||||||
|
img = this.imgs[this.imageIndex];
|
||||||
|
} else if (this.overIndex != null) {
|
||||||
|
// No image is selected but one is hovered
|
||||||
|
img = this.imgs[this.overIndex];
|
||||||
|
}
|
||||||
|
if (img) {
|
||||||
|
options.unshift({
|
||||||
|
content: "Open Image",
|
||||||
|
callback: () => window.open(img.src, "_blank"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#addDrawBackgroundHandler(node) {
|
||||||
|
const app = this;
|
||||||
|
node.prototype.onDrawBackground = function (ctx) {
|
||||||
|
if (!this.flags.collapsed) {
|
||||||
|
const output = app.nodeOutputs[this.id + ""];
|
||||||
|
if (output && output.images) {
|
||||||
|
if (this.images !== output.images) {
|
||||||
|
this.images = output.images;
|
||||||
|
this.imgs = null;
|
||||||
|
this.imageIndex = null;
|
||||||
|
Promise.all(
|
||||||
|
output.images.map((src) => {
|
||||||
|
return new Promise((r) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => r(img);
|
||||||
|
img.onerror = () => r(null);
|
||||||
|
img.src = "/view/" + src;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
).then((imgs) => {
|
||||||
|
if (this.images === output.images) {
|
||||||
|
this.imgs = imgs.filter(Boolean);
|
||||||
|
if (this.size[1] < 100) {
|
||||||
|
this.size[1] = 250;
|
||||||
|
}
|
||||||
|
app.graph.setDirtyCanvas(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.imgs) {
|
||||||
|
const canvas = graph.list_of_graphcanvas[0];
|
||||||
|
const mouse = canvas.graph_mouse;
|
||||||
|
if (!canvas.pointer_is_down && this.pointerDown) {
|
||||||
|
if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) {
|
||||||
|
this.imageIndex = this.pointerDown.index;
|
||||||
|
}
|
||||||
|
this.pointerDown = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = this.imgs[0].naturalWidth;
|
||||||
|
let h = this.imgs[0].naturalHeight;
|
||||||
|
let imageIndex = this.imageIndex;
|
||||||
|
const numImages = this.imgs.length;
|
||||||
|
if (numImages === 1 && !imageIndex) {
|
||||||
|
this.imageIndex = imageIndex = 0;
|
||||||
|
}
|
||||||
|
let shiftY = this.type === "SaveImage" ? 55 : 0;
|
||||||
|
let dw = this.size[0];
|
||||||
|
let dh = this.size[1];
|
||||||
|
dh -= shiftY;
|
||||||
|
|
||||||
|
if (imageIndex == null) {
|
||||||
|
let best = 0;
|
||||||
|
let cellWidth;
|
||||||
|
let cellHeight;
|
||||||
|
let cols = 0;
|
||||||
|
let shiftX = 0;
|
||||||
|
for (let c = 1; c <= numImages; c++) {
|
||||||
|
const rows = Math.ceil(numImages / c);
|
||||||
|
const cW = dw / c;
|
||||||
|
const cH = dh / rows;
|
||||||
|
const scaleX = cW / w;
|
||||||
|
const scaleY = cH / h;
|
||||||
|
|
||||||
|
const scale = Math.min(scaleX, scaleY, 1);
|
||||||
|
const imageW = w * scale;
|
||||||
|
const imageH = h * scale;
|
||||||
|
const area = imageW * imageH * numImages;
|
||||||
|
|
||||||
|
if (area > best) {
|
||||||
|
best = area;
|
||||||
|
cellWidth = imageW;
|
||||||
|
cellHeight = imageH;
|
||||||
|
cols = c;
|
||||||
|
shiftX = c * ((cW - imageW) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let anyHovered = false;
|
||||||
|
this.imageRects = [];
|
||||||
|
for (let i = 0; i < numImages; i++) {
|
||||||
|
const img = this.imgs[i];
|
||||||
|
const row = Math.floor(i / cols);
|
||||||
|
const col = i % cols;
|
||||||
|
const x = col * cellWidth + shiftX;
|
||||||
|
const y = row * cellHeight + shiftY;
|
||||||
|
if (!anyHovered) {
|
||||||
|
anyHovered = LiteGraph.isInsideRectangle(
|
||||||
|
mouse[0],
|
||||||
|
mouse[1],
|
||||||
|
x + this.pos[0],
|
||||||
|
y + this.pos[1],
|
||||||
|
cellWidth,
|
||||||
|
cellHeight
|
||||||
|
);
|
||||||
|
if (anyHovered) {
|
||||||
|
this.overIndex = i;
|
||||||
|
let value = 110;
|
||||||
|
if (canvas.pointer_is_down) {
|
||||||
|
if (!this.pointerDown || this.pointerDown.index !== i) {
|
||||||
|
this.pointerDown = { index: i, pos: [...mouse] };
|
||||||
|
}
|
||||||
|
value = 125;
|
||||||
|
}
|
||||||
|
ctx.filter = `contrast(${value}%) brightness(${value}%)`;
|
||||||
|
canvas.canvas.style.cursor = "pointer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.imageRects.push([x, y, cellWidth, cellHeight]);
|
||||||
|
ctx.drawImage(img, x, y, cellWidth, cellHeight);
|
||||||
|
ctx.filter = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyHovered) {
|
||||||
|
this.pointerDown = null;
|
||||||
|
this.overIndex = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw individual
|
||||||
|
const scaleX = dw / w;
|
||||||
|
const scaleY = dh / h;
|
||||||
|
const scale = Math.min(scaleX, scaleY, 1);
|
||||||
|
|
||||||
|
w *= scale;
|
||||||
|
h *= scale;
|
||||||
|
|
||||||
|
let x = (dw - w) / 2;
|
||||||
|
let y = (dh - h) / 2 + shiftY;
|
||||||
|
ctx.drawImage(this.imgs[imageIndex], x, y, w, h);
|
||||||
|
|
||||||
|
const drawButton = (x, y, sz, text) => {
|
||||||
|
const hovered = LiteGraph.isInsideRectangle(
|
||||||
|
mouse[0],
|
||||||
|
mouse[1],
|
||||||
|
x + this.pos[0],
|
||||||
|
y + this.pos[1],
|
||||||
|
sz,
|
||||||
|
sz
|
||||||
|
);
|
||||||
|
let fill = "#333";
|
||||||
|
let textFill = "#fff";
|
||||||
|
let isClicking = false;
|
||||||
|
if (hovered) {
|
||||||
|
canvas.canvas.style.cursor = "pointer";
|
||||||
|
if (canvas.pointer_is_down) {
|
||||||
|
fill = "#1e90ff";
|
||||||
|
isClicking = true;
|
||||||
|
} else {
|
||||||
|
fill = "#eee";
|
||||||
|
textFill = "#000";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.pointerWasDown = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(x, y, sz, sz, [4]);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = textFill;
|
||||||
|
ctx.font = "12px Arial";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(text, x + 15, y + 20);
|
||||||
|
|
||||||
|
return isClicking;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (numImages > 1) {
|
||||||
|
if (drawButton(x + w - 35, y + h - 35, 30, `${this.imageIndex + 1}/${numImages}`)) {
|
||||||
|
let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1;
|
||||||
|
if (!this.pointerDown || !this.pointerDown.index === i) {
|
||||||
|
this.pointerDown = { index: i, pos: [...mouse] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawButton(x + w - 35, y + 5, 30, `x`)) {
|
||||||
|
if (!this.pointerDown || !this.pointerDown.index === null) {
|
||||||
|
this.pointerDown = { index: null, pos: [...mouse] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the app on the page
|
||||||
|
*/
|
||||||
|
async setup() {
|
||||||
|
// Create and mount the LiteGraph in the DOM
|
||||||
|
const canvasEl = Object.assign(document.createElement("canvas"), { id: "graph-canvas" });
|
||||||
|
document.body.prepend(canvasEl);
|
||||||
|
|
||||||
|
this.graph = new LGraph();
|
||||||
|
const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph));
|
||||||
|
this.ctx = canvasEl.getContext("2d");
|
||||||
|
|
||||||
|
this.graph.start();
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
canvasEl.width = canvasEl.offsetWidth;
|
||||||
|
canvasEl.height = canvasEl.offsetHeight;
|
||||||
|
canvas.draw(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the canvas fills the window
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
|
||||||
|
await this.#invokeExtensionsAsync("init");
|
||||||
|
await this.registerNodes();
|
||||||
|
|
||||||
|
// Load previous workflow
|
||||||
|
let restored = false;
|
||||||
|
try {
|
||||||
|
const json = localStorage.getItem("workflow");
|
||||||
|
if (json) {
|
||||||
|
const workflow = JSON.parse(json);
|
||||||
|
this.loadGraphData(workflow);
|
||||||
|
restored = true;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
// We failed to restore a workflow so load the default
|
||||||
|
if (!restored) {
|
||||||
|
this.loadGraphData(defaultGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current workflow automatically
|
||||||
|
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000);
|
||||||
|
|
||||||
|
await this.#invokeExtensionsAsync("setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerNodes() {
|
||||||
|
const app = this;
|
||||||
|
// Load node definitions from the backend
|
||||||
|
const defs = await api.getNodeDefs();
|
||||||
|
await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
|
||||||
|
|
||||||
|
// Generate list of known widgets
|
||||||
|
const widgets = Object.assign(
|
||||||
|
{},
|
||||||
|
ComfyWidgets,
|
||||||
|
...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register a node for each definition
|
||||||
|
for (const nodeId in defs) {
|
||||||
|
const nodeData = defs[nodeId];
|
||||||
|
const node = Object.assign(
|
||||||
|
function ComfyNode() {
|
||||||
|
const inputs = nodeData["input"]["required"];
|
||||||
|
const config = { minWidth: 1, minHeight: 1 };
|
||||||
|
for (const inputName in inputs) {
|
||||||
|
const inputData = inputs[inputName];
|
||||||
|
const type = inputData[0];
|
||||||
|
|
||||||
|
if (Array.isArray(type)) {
|
||||||
|
// Enums e.g. latent rotation
|
||||||
|
this.addWidget("combo", inputName, type[0], () => {}, { values: type });
|
||||||
|
} else if (`${type}:${inputName}` in widgets) {
|
||||||
|
// Support custom widgets by Type:Name
|
||||||
|
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
|
||||||
|
} else if (type in widgets) {
|
||||||
|
// Standard type widgets
|
||||||
|
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
|
||||||
|
} else {
|
||||||
|
// Node connection inputs
|
||||||
|
this.addInput(inputName, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = this.computeSize();
|
||||||
|
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
||||||
|
s[1] = Math.max(config.minHeight, s[1]);
|
||||||
|
this.size = s;
|
||||||
|
this.serialize_widgets = true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: nodeData.name,
|
||||||
|
comfyClass: nodeData.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
node.prototype.comfyClass = nodeData.name;
|
||||||
|
|
||||||
|
this.#addNodeContextMenuHandler(node);
|
||||||
|
this.#addDrawBackgroundHandler(node, app);
|
||||||
|
|
||||||
|
await this.#invokeExtensionsAsync("beforeRegisterNode", node, nodeData);
|
||||||
|
LiteGraph.registerNodeType(nodeId, node);
|
||||||
|
node.category = nodeData.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#invokeExtensionsAsync("registerCustomNodes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the graph with the specified workflow data
|
||||||
|
* @param {*} graphData A serialized graph object
|
||||||
|
*/
|
||||||
|
loadGraphData(graphData) {
|
||||||
|
this.graph.configure(graphData);
|
||||||
|
|
||||||
|
for (const node of this.graph._nodes) {
|
||||||
|
const size = node.computeSize();
|
||||||
|
size[0] = Math.max(node.size[0], size[0]);
|
||||||
|
size[1] = Math.max(node.size[1], size[1]);
|
||||||
|
node.size = size;
|
||||||
|
|
||||||
|
if (node.widgets) {
|
||||||
|
// If you break something in the backend and want to patch workflows in the frontend
|
||||||
|
// This is the place to do this
|
||||||
|
for (let widget of node.widgets) {
|
||||||
|
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
|
||||||
|
if (widget.name == "sampler_name") {
|
||||||
|
if (widget.value.startsWith("sample_")) {
|
||||||
|
wid.value = widget.value.slice(7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#invokeExtensions("loadedGraphNode", node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
graphToPrompt() {
|
||||||
|
// TODO: Implement dynamic prompts
|
||||||
|
const workflow = this.graph.serialize();
|
||||||
|
const output = {};
|
||||||
|
for (const n of workflow.nodes) {
|
||||||
|
const inputs = {};
|
||||||
|
const node = this.graph.getNodeById(n.id);
|
||||||
|
const widgets = node.widgets;
|
||||||
|
|
||||||
|
// Store all widget values
|
||||||
|
if (widgets) {
|
||||||
|
for (const widget of widgets) {
|
||||||
|
if (widget.options.serialize !== false) {
|
||||||
|
inputs[widget.name] = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store all node links
|
||||||
|
for (let i in node.inputs) {
|
||||||
|
const link = node.getInputLink(i);
|
||||||
|
if (link) {
|
||||||
|
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output[String(node.id)] = {
|
||||||
|
inputs,
|
||||||
|
class_type: node.comfyClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { workflow, output };
|
||||||
|
}
|
||||||
|
|
||||||
|
async queuePrompt(number) {
|
||||||
|
const p = this.graphToPrompt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.queuePrompt(number, p);
|
||||||
|
} catch (error) {
|
||||||
|
this.ui.dialog.show(error.response || error.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of p.workflow.nodes) {
|
||||||
|
const node = graph.getNodeById(n.id);
|
||||||
|
if (node.widgets) {
|
||||||
|
for (const widget of node.widgets) {
|
||||||
|
// Allow widgets to run callbacks after a prompt has been queued
|
||||||
|
// e.g. random seed after every gen
|
||||||
|
if (widget.afterQueued) {
|
||||||
|
widget.afterQueued();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas.draw(true, true);
|
||||||
|
await this.ui.queue.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const app = new ComfyApp();
|
|
@ -0,0 +1,119 @@
|
||||||
|
export const defaultGraph = {
|
||||||
|
last_node_id: 9,
|
||||||
|
last_link_id: 9,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
type: "CLIPTextEncode",
|
||||||
|
pos: [413, 389],
|
||||||
|
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
|
||||||
|
flags: {},
|
||||||
|
order: 3,
|
||||||
|
mode: 0,
|
||||||
|
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
|
||||||
|
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
|
||||||
|
properties: {},
|
||||||
|
widgets_values: ["bad hands"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
type: "CLIPTextEncode",
|
||||||
|
pos: [415, 186],
|
||||||
|
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
|
||||||
|
flags: {},
|
||||||
|
order: 2,
|
||||||
|
mode: 0,
|
||||||
|
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
|
||||||
|
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
|
||||||
|
properties: {},
|
||||||
|
widgets_values: ["masterpiece best quality girl"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
type: "EmptyLatentImage",
|
||||||
|
pos: [473, 609],
|
||||||
|
size: { 0: 315, 1: 106 },
|
||||||
|
flags: {},
|
||||||
|
order: 1,
|
||||||
|
mode: 0,
|
||||||
|
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
|
||||||
|
properties: {},
|
||||||
|
widgets_values: [512, 512, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: "KSampler",
|
||||||
|
pos: [863, 186],
|
||||||
|
size: { 0: 315, 1: 262 },
|
||||||
|
flags: {},
|
||||||
|
order: 4,
|
||||||
|
mode: 0,
|
||||||
|
inputs: [
|
||||||
|
{ name: "model", type: "MODEL", link: 1 },
|
||||||
|
{ name: "positive", type: "CONDITIONING", link: 4 },
|
||||||
|
{ name: "negative", type: "CONDITIONING", link: 6 },
|
||||||
|
{ name: "latent_image", type: "LATENT", link: 2 },
|
||||||
|
],
|
||||||
|
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
|
||||||
|
properties: {},
|
||||||
|
widgets_values: [8566257, true, 20, 8, "euler", "normal", 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
type: "VAEDecode",
|
||||||
|
pos: [1209, 188],
|
||||||
|
size: { 0: 210, 1: 46 },
|
||||||
|
flags: {},
|
||||||
|
order: 5,
|
||||||
|
mode: 0,
|
||||||
|
inputs: [
|
||||||
|
{ name: "samples", type: "LATENT", link: 7 },
|
||||||
|
{ name: "vae", type: "VAE", link: 8 },
|
||||||
|
],
|
||||||
|
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
type: "SaveImage",
|
||||||
|
pos: [1451, 189],
|
||||||
|
size: { 0: 210, 1: 26 },
|
||||||
|
flags: {},
|
||||||
|
order: 6,
|
||||||
|
mode: 0,
|
||||||
|
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
type: "CheckpointLoader",
|
||||||
|
pos: [26, 474],
|
||||||
|
size: { 0: 315, 1: 122 },
|
||||||
|
flags: {},
|
||||||
|
order: 0,
|
||||||
|
mode: 0,
|
||||||
|
outputs: [
|
||||||
|
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
|
||||||
|
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
|
||||||
|
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
|
||||||
|
],
|
||||||
|
properties: {},
|
||||||
|
widgets_values: ["v1-inference.yaml", "v1-5-pruned-emaonly.ckpt"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
[1, 4, 0, 3, 0, "MODEL"],
|
||||||
|
[2, 5, 0, 3, 3, "LATENT"],
|
||||||
|
[3, 4, 1, 6, 0, "CLIP"],
|
||||||
|
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||||
|
[5, 4, 1, 7, 0, "CLIP"],
|
||||||
|
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||||
|
[7, 3, 0, 8, 0, "LATENT"],
|
||||||
|
[8, 4, 2, 8, 1, "VAE"],
|
||||||
|
[9, 8, 0, 9, 0, "IMAGE"],
|
||||||
|
],
|
||||||
|
groups: [],
|
||||||
|
config: {},
|
||||||
|
extra: {},
|
||||||
|
version: 0.4,
|
||||||
|
};
|
|
@ -0,0 +1,118 @@
|
||||||
|
function getNumberDefaults(inputData, defaultStep) {
|
||||||
|
let defaultVal = inputData[1]["default"];
|
||||||
|
let { min, max, step } = inputData[1];
|
||||||
|
|
||||||
|
if (defaultVal == undefined) defaultVal = 0;
|
||||||
|
if (min == undefined) min = 0;
|
||||||
|
if (max == undefined) max = 2048;
|
||||||
|
if (step == undefined) step = defaultStep;
|
||||||
|
|
||||||
|
return { val: defaultVal, config: { min, max, step: 10.0 * step } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedWidget(node, inputName, inputData) {
|
||||||
|
const seed = ComfyWidgets.INT(node, inputName, inputData);
|
||||||
|
const randomize = node.addWidget("toggle", "Random seed after every gen", true, function (v) {}, {
|
||||||
|
on: "enabled",
|
||||||
|
off: "disabled",
|
||||||
|
serialize: false, // Don't include this in prompt.
|
||||||
|
});
|
||||||
|
|
||||||
|
randomize.afterQueued = () => {
|
||||||
|
if (randomize.value) {
|
||||||
|
seed.widget.value = Math.floor(Math.random() * 1125899906842624);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { widget: seed, randomize };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMultilineWidget(node, name, defaultVal, dynamicPrompt, app) {
|
||||||
|
const widget = {
|
||||||
|
type: "customtext",
|
||||||
|
name,
|
||||||
|
get value() {
|
||||||
|
return this.inputEl.value;
|
||||||
|
},
|
||||||
|
set value(x) {
|
||||||
|
this.inputEl.value = x;
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
dynamicPrompt,
|
||||||
|
},
|
||||||
|
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
|
||||||
|
const visible = app.canvas.ds.scale > 0.5;
|
||||||
|
const t = ctx.getTransform();
|
||||||
|
const margin = 10;
|
||||||
|
console.log("back you go")
|
||||||
|
Object.assign(this.inputEl.style, {
|
||||||
|
left: `${t.a * margin + t.e}px`,
|
||||||
|
top: `${t.d * (y + widgetHeight - margin) + t.f}px`,
|
||||||
|
width: `${(widgetWidth - margin * 2 - 3) * t.a}px`,
|
||||||
|
height: `${(this.parent.size[1] - (y + widgetHeight) - 3) * t.d}px`,
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 1,
|
||||||
|
fontSize: `${t.d * 10.0}px`,
|
||||||
|
});
|
||||||
|
this.inputEl.hidden = !visible;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
widget.inputEl = document.createElement("textarea");
|
||||||
|
widget.inputEl.className = "comfy-multiline-input";
|
||||||
|
widget.inputEl.value = defaultVal;
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
if (!widget.inputEl.contains(event.target)) {
|
||||||
|
widget.inputEl.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
widget.parent = node;
|
||||||
|
document.body.appendChild(widget.inputEl);
|
||||||
|
|
||||||
|
node.addCustomWidget(widget);
|
||||||
|
|
||||||
|
node.onRemoved = function () {
|
||||||
|
// When removing this node we need to remove the input from the DOM
|
||||||
|
for (let y in this.widgets) {
|
||||||
|
if (this.widgets[y].inputEl) {
|
||||||
|
this.widgets[y].inputEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { minWidth: 400, minHeight: 200, widget };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComfyWidgets = {
|
||||||
|
"INT:seed": seedWidget,
|
||||||
|
"INT:noise_seed": seedWidget,
|
||||||
|
FLOAT(node, inputName, inputData) {
|
||||||
|
const { val, config } = getNumberDefaults(inputData, 0.5);
|
||||||
|
return { widget: node.addWidget("number", inputName, val, () => {}, config) };
|
||||||
|
},
|
||||||
|
INT(node, inputName, inputData) {
|
||||||
|
const { val, config } = getNumberDefaults(inputData, 1);
|
||||||
|
return {
|
||||||
|
widget: node.addWidget(
|
||||||
|
"number",
|
||||||
|
inputName,
|
||||||
|
val,
|
||||||
|
function (v) {
|
||||||
|
const s = this.options.step / 10;
|
||||||
|
this.value = Math.round(v / s) * s;
|
||||||
|
},
|
||||||
|
config
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
STRING(node, inputName, inputData, app) {
|
||||||
|
const defaultVal = inputData[1].default || "";
|
||||||
|
const multiline = !!inputData[1].multiline;
|
||||||
|
const dynamicPrompt = !!inputData[1].dynamic_prompt;
|
||||||
|
|
||||||
|
if (multiline) {
|
||||||
|
return addMultilineWidget(node, inputName, defaultVal, dynamicPrompt, app);
|
||||||
|
} else {
|
||||||
|
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, { dynamicPrompt }) };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,81 @@
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-multiline-input {
|
||||||
|
background-color: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2px;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-modal {
|
||||||
|
display: none; /* Hidden by default */
|
||||||
|
position: fixed; /* Stay in place */
|
||||||
|
z-index: 100; /* Sit on top */
|
||||||
|
padding: 30px 30px 10px 30px;
|
||||||
|
background-color: #ff0000; /* Modal background */
|
||||||
|
box-shadow: 0px 0px 20px #888888;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
max-width: 80vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-modal p {
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-line; /* This will respect line breaks */
|
||||||
|
margin-bottom: 20px; /* Add some margin between the text and the close button*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-modal button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aaaaaa;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-modal button:hover,
|
||||||
|
.comfy-modal button:focus {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #202020;
|
||||||
|
}
|
||||||
|
.comfy-multiline-input {
|
||||||
|
background-color: #202020;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-height: 850px) {
|
||||||
|
#menu {
|
||||||
|
margin-top: -70px;
|
||||||
|
}
|
||||||
|
}
|
1117
webshit/index.html
1117
webshit/index.html
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue