Compare commits

...
14 Commits
Author SHA1 Message Date
KarrixLee 94210021e3 tweaj 2025-06-28 00:41:05 +08:00
KarrixLee 6d2b918ef1 tweak 2025-06-28 00:27:00 +08:00
KarrixLee d8b1e88c03 hi 2025-06-28 00:23:25 +08:00
KarrixLee 22458a1cd6 Merge branch 'local-flow-2' 2025-06-27 14:10:10 +08:00
Tristan-mc-qandtristan22mc 0a58eba554 feat: Add deployable EXR saver node (#98)
Co-authored-by: tristan22mc <tristan22mc@gmail.com>
2025-06-26 14:07:48 -07:00
KarrixLee def54df9c2 tweak 2025-06-17 18:37:02 +08:00
KarrixLee 32a950afe8 tweak 2025-06-17 18:07:43 +08:00
KarrixLee 8130779d94 Enhance configuration saving and workflow list management
- Updated the save method in ConfigDialog to be asynchronous, allowing for smoother handling of configuration saves.
- Added a new function to refresh the workflow list if the sidebar is open, ensuring the UI reflects the latest data after configuration changes.
- Made workflowsState globally accessible for improved state management across components.
- Adjusted the height of the workflows list for better UI layout.

These changes improve the user experience by ensuring that the workflow list is up-to-date and enhancing the overall responsiveness of the configuration dialog.
2025-06-16 21:25:33 +08:00
KarrixLee 4cbd2a8225 Add workflow retrieval functionality and enhance UI interaction
- Introduced a new endpoint in custom_routes.py for fetching workflows by ID, including authorization checks and error handling.
- Updated workflow-list.js to support fetching and displaying workflow data upon user interaction, including loading indicators and error handling.
- Enhanced the createWorkflowItem function to accept additional parameters for improved data handling and user feedback.

These changes improve the user experience by enabling seamless workflow retrieval and interaction within the application.
2025-06-16 21:01:43 +08:00
KarrixLee d6fb2daeff Add workflow list management and search functionality
- Introduced a new workflow-list.js file to manage workflows, including fetching, displaying, and searching workflows.
- Enhanced the custom_routes.py file with a new endpoint for retrieving workflows, ensuring proper authorization and query parameter handling.
- Updated index.js to initialize the workflows list and integrate search functionality within the UI.

These changes improve the user experience by allowing efficient management and retrieval of workflows in the application.
2025-06-16 18:47:18 +08:00
KarrixLee a91effe3c8 Add workflow management endpoints and enhance deployment logic
- Introduced new endpoints for creating workflows and versions in the custom_routes.py file.
- Updated the deployWorkflow function in index.js to include apiUrl in the request body and handle workflow versioning.
- Improved error handling for API requests and ensured required fields are validated before processing.
- Enhanced user feedback during deployment with updated success messages.

These changes streamline workflow management and improve the overall deployment process within the application.
2025-06-16 16:32:23 +08:00
KarrixLee c015b710fe Enhance authentication flow and improve API integration
- Added a new endpoint for handling authentication responses in the UploadQueue class.
- Updated the deployWorkflow function to include apiUrl in the configuration checks.
- Refactored API calls to use the new auth-response endpoint, ensuring proper request handling.
- Improved logging for better debugging during workflow deployment.

These changes streamline the authentication process and enhance the overall API interaction within the application.
2025-06-15 13:55:59 +08:00
KarrixLee 2738d1913a tweak 2025-06-14 22:04:53 +08:00
KarrixLee 3f4c11e3f1 Refactor event dispatching and improve code readability in index.js
- Standardized formatting for CustomEvent dispatches to enhance consistency.
- Simplified async function prompts and improved filtering logic for existing input IDs.
- Enhanced readability by restructuring multiline statements and ensuring consistent indentation.
- Added error handling for deployment processes and improved dialog display methods.

These changes aim to improve maintainability and clarity of the codebase.
2025-06-14 21:49:53 +08:00
5 changed files with 996 additions and 156 deletions
+78
View File
@@ -0,0 +1,78 @@
# In file: comfyui-deploy/comfy-nodes/output_exr.py
import os
import numpy as np
import folder_paths
# Try to set up OpenCV for EXR writing.
try:
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
import cv2
OPENCV_AVAILABLE = True
except ImportError:
print("Warning: OpenCV not found for ComfyDeployOutputEXR. Please add opencv-python-headless to requirements.txt")
OPENCV_AVAILABLE = False
# ALIGNED: Renamed class to match project conventions
class ComfyDeployOutputEXR:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", ),
"filename_prefix": ("STRING", {"default": "ComfyDeploy_EXR"})
},
# ADDED: Optional output_id for consistency with other ComfyDeploy nodes
"optional": {
"output_id": ("STRING", {"multiline": False, "default": "output_exr"}),
},
}
RETURN_TYPES = ()
# ALIGNED: Changed function name to 'run'
FUNCTION = "run"
OUTPUT_NODE = True
# ALIGNED: Matched the category name
CATEGORY = "🔗ComfyDeploy"
DESCRIPTION = "Saves the input images as EXR (HDR) files."
def run(self, images, filename_prefix="ComfyDeploy_EXR", output_id="output_exr"):
if not OPENCV_AVAILABLE:
raise ImportError("OpenCV is required to save EXR files. Please ensure opencv-python-headless is in requirements.txt.")
full_output_folder, filename, counter, subfolder, filename_prefix = (
folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
)
)
results = list()
for image in images:
image_np = image.cpu().numpy()
if image_np.dtype != np.float32:
image_np = image_np.astype(np.float32)
file = f"{filename}_{counter:05}.exr"
file_path = os.path.join(full_output_folder, file)
image_np_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
cv2.imwrite(file_path, image_np_bgr)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type,
"output_id": output_id, # ADDED
})
counter += 1
return {"ui": {"images": results}}
# ALIGNED: Mappings are defined at the bottom of the node file in this project
NODE_CLASS_MAPPINGS = {"ComfyDeployOutputEXR": ComfyDeployOutputEXR}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyDeployOutputEXR": "EXR Output (ComfyDeploy)"}
+1 -1
View File
@@ -92,7 +92,7 @@ class ComfyDeployOutputText:
}
)
return {"ui": {"text_file": results}}
return {"ui": {"text_file": results, "text": [text]}}
NODE_CLASS_MAPPINGS = {"ComfyDeployOutputText": ComfyDeployOutputText}
+178 -3
View File
@@ -31,6 +31,7 @@ import torch
import psutil
from collections import OrderedDict
import io
from urllib.parse import urlencode
# Global session
client_session = None
@@ -2769,9 +2770,13 @@ class UploadQueue:
del self.node_uploads[prompt_id][node_id]
if prompt_id in self.node_output_data:
if node_id in self.node_output_data[prompt_id]:
if self.node_output_data[prompt_id][node_id]["data"]:
if self.node_output_data[prompt_id][node_id][
"data"
]:
# Send final node data to API before cleanup
if prompt_metadata[prompt_id].status_endpoint:
if prompt_metadata[
prompt_id
].status_endpoint:
body = {
"run_id": prompt_id,
"output_data": self.node_output_data[
@@ -2785,7 +2790,9 @@ class UploadQueue:
prompt_metadata[
prompt_id
].status_endpoint,
token=prompt_metadata[prompt_id].token,
token=prompt_metadata[
prompt_id
].token,
json=body,
)
except Exception as e:
@@ -2885,3 +2892,171 @@ def format_execution_timeline(execution_times):
current_time += duration
return format_table(headers, rows)
@server.PromptServer.instance.routes.get("/comfyui-deploy/auth-response")
async def auth_response_proxy(request):
request_id = request.rel_url.query.get("request_id")
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
if not request_id:
return web.json_response({"error": "request_id is required"}, status=400)
target_url = f"{api_url}/api/platform/comfyui/auth-response?request_id={request_id}"
try:
await ensure_client_session()
async with client_session.get(target_url) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.post("/comfyui-deploy/workflow")
async def create_workflow_proxy(request):
data = await request.json()
name = data.get("name")
workflow_json = data.get("workflow_json")
workflow_api = data.get("workflow_api")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
if not name or not workflow_json or not workflow_api:
return web.json_response(
{"error": "name, workflow_json, workflow_api are required"}, status=400
)
target_url = f"{api_url}/api/workflow"
request_body = {
"name": name,
"workflow_json": json.dumps(workflow_json),
"workflow_api": json.dumps(workflow_api),
}
try:
await ensure_client_session()
async with client_session.post(
target_url,
json=request_body,
headers={
"Content-Type": "application/json",
"Authorization": auth_header,
},
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.post("/comfyui-deploy/workflow/version")
async def create_workflow_version_proxy(request):
data = await request.json()
workflow_id = data.get("workflow_id")
workflow = data.get("workflow")
workflow_api = data.get("workflow_api")
comment = data.get("comment", "")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/workflow/{workflow_id}/version"
request_body = {
"workflow": workflow,
"workflow_api": workflow_api,
"comment": comment,
}
try:
await ensure_client_session()
async with client_session.post(
target_url,
json=request_body,
headers={
"Content-Type": "application/json",
"Authorization": auth_header,
},
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflows")
async def get_workflows_proxy(request):
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
search = request.rel_url.query.get("search", "")
limit = request.rel_url.query.get("limit", 10)
offset = request.rel_url.query.get("offset", 0)
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
# Build query parameters properly
params = {}
if search:
params["search"] = search
if limit:
params["limit"] = limit
if offset:
params["offset"] = offset
target_url = f"{api_url}/api/workflows"
if params:
target_url += f"?{urlencode(params)}"
try:
await ensure_client_session()
async with client_session.get(
target_url,
headers={
"Content-Type": "application/json",
"Authorization": auth_header,
},
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# for getting a workflow by id
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflow")
async def get_workflow_proxy(request):
workflow_id = request.rel_url.query.get("workflow_id")
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/workflow/{workflow_id}"
try:
await ensure_client_session()
async with client_session.get(
target_url, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+352 -152
View File
@@ -3,6 +3,7 @@ import { api } from "../../scripts/api.js";
// import { LGraphNode } from "../../scripts/widgets.js";
LGraphNode = LiteGraph.LGraphNode;
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { initializeWorkflowsList, addWorkflowSearch } from "./workflow-list.js";
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25";
// import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
@@ -96,7 +97,7 @@ function dispatchAPIEventData(data) {
break;
case "executing":
api.dispatchEvent(
new CustomEvent("executing", { detail: msg.data.node }),
new CustomEvent("executing", { detail: msg.data.node })
);
break;
case "executed":
@@ -104,17 +105,17 @@ function dispatchAPIEventData(data) {
break;
case "execution_start":
api.dispatchEvent(
new CustomEvent("execution_start", { detail: msg.data }),
new CustomEvent("execution_start", { detail: msg.data })
);
break;
case "execution_error":
api.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data }),
new CustomEvent("execution_error", { detail: msg.data })
);
break;
case "execution_cached":
api.dispatchEvent(
new CustomEvent("execution_cached", { detail: msg.data }),
new CustomEvent("execution_cached", { detail: msg.data })
);
break;
default:
@@ -217,28 +218,26 @@ async function convertToInput(node, widget, config) {
console.log(node, widget, config);
const result = await app.extensionManager.dialog.prompt(
{
title: "Convert " + widget.name + " to external input",
message: "Input name",
defaultValue: widget.name,
}
);
const result = await app.extensionManager.dialog.prompt({
title: "Convert " + widget.name + " to external input",
message: "Input name",
defaultValue: widget.name,
});
if (!result) return;
// Check for duplicate input IDs across existing external input nodes
const existingInputIds = Object.values(app.graph.nodes)
.filter(n => n.type.startsWith("ComfyUIDeployExternal"))
.map(n => n.widgets_values?.[0])
.filter((n) => n.type.startsWith("ComfyUIDeployExternal"))
.map((n) => n.widgets_values?.[0])
.filter(Boolean);
if (existingInputIds.includes(result)) {
app.extensionManager.toast.add({
severity: 'error',
summary: 'Input ID already exists',
detail: 'Please choose a different name.',
life: 3000
severity: "error",
summary: "Input ID already exists",
detail: "Please choose a different name.",
life: 3000,
});
return;
}
@@ -273,7 +272,7 @@ async function convertToInput(node, widget, config) {
console.log(links[link]);
setTimeout(
() => inputNode.connect(0, llink.target_id, llink.target_slot),
100,
100
);
}
@@ -309,7 +308,10 @@ async function convertToInput(node, widget, config) {
node.convertWidgetToInput(widget);
var inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId);
var inputNode = LiteGraph.createNode(
externalNode,
"External Input: " + inputId
);
// if (type === "COMBO") {
// inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId, {
@@ -342,7 +344,8 @@ async function convertToInput(node, widget, config) {
if (type === "COMBO") {
console.log(inputNode);
console.log(options);
inputNode.widgets.find((x) => x.name == "default_value").options.values = options;
inputNode.widgets.find((x) => x.name == "default_value").options.values =
options;
}
app.graph.add(inputNode);
@@ -365,7 +368,11 @@ function getConfig(widgetName) {
function isConvertibleWidget(node, widget, config) {
// console.log(config);
if (node.type === "LoadImage" && widget.type === "combo" && widget.name == "image") {
if (
node.type === "LoadImage" &&
widget.type === "combo" &&
widget.name == "image"
) {
return true;
}
@@ -447,7 +454,7 @@ const ext = {
} else {
loadingDialog.showLoading(
"Loading workflow from " + org_display,
"Please wait...",
"Please wait..."
);
fetch(endpoint + "/api/workflow-version/" + workflow_version_id, {
method: "GET",
@@ -483,13 +490,63 @@ const ext = {
window.history.replaceState(
{},
document.title,
window.location.pathname,
window.location.pathname
);
});
}
},
async beforeRegisterNodeDef(nodeType, nodeData, app2) {
// Add text display functionality for ComfyDeployOutputText
if (nodeData.name === "ComfyDeployOutputText") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
// Create a multiline text widget to display the output (similar to rgthree)
this.textDisplayWidget = app.widgets.STRING(
this,
"displayed_text",
["STRING", { multiline: true }],
app2
).widget;
// Style the widget to look like a proper text display box
if (this.textDisplayWidget.inputEl) {
this.textDisplayWidget.inputEl.readOnly = true;
this.textDisplayWidget.inputEl.style.backgroundColor =
"var(--comfy-input-bg)";
this.textDisplayWidget.inputEl.style.border =
"1px solid var(--border-color)";
this.textDisplayWidget.inputEl.style.color = "var(--input-text)";
this.textDisplayWidget.inputEl.style.fontFamily = "monospace";
this.textDisplayWidget.inputEl.style.fontSize = "12px";
this.textDisplayWidget.inputEl.style.minHeight = "100px";
this.textDisplayWidget.inputEl.style.resize = "vertical";
this.textDisplayWidget.inputEl.style.padding = "8px";
this.textDisplayWidget.inputEl.style.whiteSpace = "pre-wrap";
this.textDisplayWidget.inputEl.style.wordWrap = "break-word";
}
// Override the serialize function to not save the display value
this.textDisplayWidget.serializeValue = () => "";
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
// Display the text output on the node
if (message.text && message.text.length > 0 && this.textDisplayWidget) {
this.textDisplayWidget.value = message.text[0];
// Resize the node to fit the content
this.setSize(this.computeSize());
app.graph.setDirtyCanvas(true);
}
};
}
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
const r = origGetExtraMenuOptions
@@ -507,7 +564,7 @@ const ext = {
content: `Convert ${w.name} to widget`,
callback: /* @__PURE__ */ __name(
() => convertToWidget(this, w),
"callback",
"callback"
),
});
} else {
@@ -520,7 +577,7 @@ const ext = {
content: `Convert ${w.name} to external input`,
callback: /* @__PURE__ */ __name(
async () => convertToInput(this, w, config),
"callback",
"callback"
),
className: "comfydeploy-menu-item",
});
@@ -545,7 +602,7 @@ const ext = {
// className: "comfydeploy-menu-item"
// },
...toInput,
null,
null
);
} else {
options.push(...toInput, null);
@@ -574,9 +631,7 @@ const ext = {
console.log(nodeData.input.optional.default_value_url);
}
if (
nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true
) {
if (nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true) {
nodeData.input.optional.default_value = ["DYNAMIC_ENUM"];
// console.log(nodeData.input.optional.default_value);
}
@@ -642,7 +697,7 @@ const ext = {
(v) => {
this.properties.workflow_name = v;
},
{ multiline: false },
{ multiline: false }
);
this.addWidget(
@@ -652,7 +707,7 @@ const ext = {
(v) => {
this.properties.workflow_id = v;
},
{ multiline: false },
{ multiline: false }
);
this.addWidget(
@@ -662,7 +717,7 @@ const ext = {
(v) => {
this.properties.version = v;
},
{ multiline: false },
{ multiline: false }
);
this.widgets_start_y = 10;
@@ -688,8 +743,19 @@ const ext = {
onConfigure(o) {
// This method is called when the node is being configured (e.g., when loading a saved graph)
// Ensure all necessary data is restored
if (o.properties) {
// ComfyUI stores widget values in widgets_values array, not properties
if (o.widgets_values && o.widgets_values.length >= 3) {
// Set the widget values directly
this.widgets[0].value = o.widgets_values[0] || "";
this.widgets[1].value = o.widgets_values[1] || "";
this.widgets[2].value = o.widgets_values[2] || "1";
// Also update properties to stay in sync
this.properties.workflow_name = o.widgets_values[0] || "";
this.properties.workflow_id = o.widgets_values[1] || "";
this.properties.version = o.widgets_values[2] || "1";
} else if (o.properties) {
// Fallback to properties if widgets_values is not available
this.properties = { ...this.properties, ...o.properties };
this.widgets[0].value = this.properties.workflow_name || "";
this.widgets[1].value = this.properties.workflow_id || "";
@@ -705,7 +771,7 @@ const ext = {
title: "Comfy Deploy",
title_mode: LiteGraph.NORMAL_TITLE,
collapsable: true,
}),
})
);
ComfyDeploy.category = "deploy";
@@ -719,8 +785,8 @@ const ext = {
"string",
inputName,
/* value=*/ "",
() => { },
{ serialize: true },
() => {},
{ serialize: true }
);
const buttonWidget = node.addWidget(
@@ -734,7 +800,7 @@ const ext = {
});
// console.log("load image");
},
{ serialize: false },
{ serialize: false }
);
console.log(node.widgets);
@@ -783,12 +849,10 @@ const ext = {
DYNAMIC_ENUM(node, inputName, inputData) {
// console.log("DYNAMIC_ENUM", JSON.parse(JSON.stringify(node)), inputName, inputData);
const enumWidget = node.addWidget(
"combo",
inputName,
"",
{ serialize: true, values: [] },
);
const enumWidget = node.addWidget("combo", inputName, "", {
serialize: true,
values: [],
});
return { widget: enumWidget };
},
@@ -796,10 +860,14 @@ const ext = {
},
async afterConfigureGraph() {
app.graph.nodes.forEach(node => {
app.graph.nodes.forEach((node) => {
if (node.type === "ComfyUIDeployExternalEnum") {
const default_value_index = node.widgets.findIndex(x => x.name === "default_value");
const options_index = node.widgets.findIndex(x => x.name === "options");
const default_value_index = node.widgets.findIndex(
(x) => x.name === "default_value"
);
const options_index = node.widgets.findIndex(
(x) => x.name === "options"
);
var dynamic_enum_options = [node.widgets[default_value_index].value];
if (node.widgets[options_index].value) {
@@ -828,12 +896,12 @@ const ext = {
try {
await window["app"].ui.settings.setSettingValueAsync(
"Comfy.Validation.Workflows",
true,
true
);
} catch (error) {
console.warning(
"Error setting validation to false, is fine to ignore this",
error,
error
);
}
console.log("loadGraphData");
@@ -975,11 +1043,11 @@ const ext = {
await app.ui.settings.setSettingValueAsync("Comfy.UseNewMenu", "Top");
await app.ui.settings.setSettingValueAsync(
"Comfy.Sidebar.Size",
"small",
"small"
);
await app.ui.settings.setSettingValueAsync(
"Comfy.Sidebar.Location",
"left",
"left"
);
// localStorage.setItem("Comfy.MenuPosition.Docked", "true");
console.log("native mode manmanman");
@@ -998,7 +1066,6 @@ const ext = {
}
})(app.graph.onAfterChange);
sendEventToCD("cd_plugin_setup");
},
};
@@ -1010,7 +1077,7 @@ const ext = {
function showError(title, message) {
infoDialog.show(
`<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `,
`<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `
);
}
@@ -1033,10 +1100,10 @@ function createDynamicUIHtml(data) {
<h3 style="font-size: 14px; font-weight: semibold; margin-bottom: 8px;">Missing Nodes</h3>
<p style="font-size: 12px;">These nodes are not found with any matching custom_nodes in the ComfyUI Manager Database</p>
${data.missing_nodes
.map((node) => {
return `<p style="font-size: 14px; color: #d69e2e;">${node}</p>`;
})
.join("")}
.map((node) => {
return `<p style="font-size: 14px; color: #d69e2e;">${node}</p>`;
})
.join("")}
</div>
`;
}
@@ -1044,14 +1111,17 @@ function createDynamicUIHtml(data) {
Object.values(data.custom_nodes).forEach((node) => {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 16px;">
<a href="${node.url
}" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${node.name
}</a>
<a href="${
node.url
}" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${
node.name
}</a>
<p style="font-size: 14px; color: #4b5563;">${node.hash}</p>
${node.warning
? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
: ""
}
${
node.warning
? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
: ""
}
</div>
`;
});
@@ -1065,8 +1135,9 @@ function createDynamicUIHtml(data) {
Object.entries(data.models).forEach(([section, items]) => {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`;
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`;
items.forEach((item) => {
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
});
@@ -1082,8 +1153,9 @@ function createDynamicUIHtml(data) {
Object.entries(data.files).forEach(([section, items]) => {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`;
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`;
items.forEach((item) => {
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
});
@@ -1102,9 +1174,9 @@ async function deployWorkflow() {
/** @type {LGraph} */
const graph = app.graph;
let { endpoint, apiKey, displayName } = getData();
let { endpoint, apiKey, apiUrl, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
if (!endpoint || !apiKey || apiKey === "" || endpoint === "" || !apiUrl) {
configDialog.show();
return;
}
@@ -1114,7 +1186,7 @@ async function deployWorkflow() {
if (deployMeta.length == 0) {
const text = await inputDialog.input(
"Create your deployment",
"Workflow name",
"Workflow name"
);
if (!text) return;
console.log(text);
@@ -1148,62 +1220,111 @@ async function deployWorkflow() {
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
<input id="include-deps" type="checkbox">Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
`
);
if (!ok) return;
const includeDeps = document.getElementById("include-deps").checked;
const reuseHash = document.getElementById("reuse-hash").checked;
const prompt = await app.graphToPrompt();
let deps = undefined;
console.log(prompt);
if (workflow_id.trim() !== "") {
const text = await inputDialog.input("Save changes", "Comment");
if (!text) return;
try {
loadingDialog.showLoading("Saving changes");
const body = {
api_url: apiUrl,
workflow: prompt.workflow,
workflow_id: workflow_id,
workflow_api: prompt.output,
comment: text,
};
let data = await fetch("/comfyui-deploy/workflow/version", {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
if (data.status !== 200) {
throw new Error(await data.text());
} else {
data = await data.json();
console.log(data);
infoDialog.show(
`<span style="color:green;">New version created!</span> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`
);
deployMetaNode.widgets[2].value = data.version;
graph.change();
}
} catch (e) {
infoDialog.showError("Error", e.message);
return;
} finally {
loadingDialog.close();
return;
}
}
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
// console.log(snapshot);
console.log(snapshot);
loadingDialog.close();
if (!snapshot) {
showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager",
"Unable to generate snapshot, please install ComfyUI Manager"
);
return;
}
const title = deploy.querySelector("#button-title");
const prompt = await app.graphToPrompt();
let deps = undefined;
console.log(prompt);
if (includeDeps) {
loadingDialog.showLoading("Fetching existing version");
const existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
apiUrl + "/api/workflow/" + workflow_id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
}
)
.then((x) => x.json())
.catch(() => {
return {};
});
console.log("workflow", existing_workflow);
loadingDialog.close();
loadingDialog.showLoading("Generating dependency graph");
@@ -1216,7 +1337,7 @@ async function deployWorkflow() {
// Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models,
existing_workflow?.dependencies?.models
).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({
...x,
@@ -1238,7 +1359,7 @@ async function deployWorkflow() {
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`,
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
@@ -1287,6 +1408,7 @@ async function deployWorkflow() {
};
loadingDialog.close();
console.log(deps);
const depsOk = await confirmDialog.confirm(
"Check dependencies",
@@ -1305,13 +1427,12 @@ async function deployWorkflow() {
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
JSON.stringify(deps)
)}" />`
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
if (!depsOk) return;
}
loadingDialog.showLoading("Deploying...");
@@ -1319,28 +1440,15 @@ async function deployWorkflow() {
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
name: workflow_name,
workflow_json: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
api_url: apiUrl,
};
console.log(body);
let data = await fetch(apiRoute, {
let data = await fetch("/comfyui-deploy/workflow", {
method: "POST",
body: JSON.stringify(body),
headers: {
@@ -1350,11 +1458,11 @@ async function deployWorkflow() {
});
console.log(data);
if (data.status !== 200) {
throw new Error(await data.text());
} else {
data = await data.json();
await refreshWorkflowListIfOpen();
}
loadingDialog.close();
@@ -1363,20 +1471,37 @@ async function deployWorkflow() {
title.style.color = "green";
deployMetaNode.widgets[1].value = data.workflow_id;
deployMetaNode.widgets[2].value = data.version;
deployMetaNode.widgets[2].value = 2;
graph.change();
infoDialog.show(
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`,
);
if (data.workflow_id) {
const prompt_with_workflow_id = await app.graphToPrompt();
// // Refresh the workflows list in the sidebar
// const sidebarEl = document.querySelector(
// '.comfy-sidebar-tab[data-id="search"]',
// );
// if (sidebarEl) {
// refreshWorkflowsList(sidebarEl);
// }
const body = {
api_url: apiUrl,
workflow: prompt_with_workflow_id.workflow,
workflow_id: data.workflow_id,
workflow_api: prompt_with_workflow_id.output,
comment: "chore: apply workflow id",
};
let new_version_data = await fetch("/comfyui-deploy/workflow/version", {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
if (new_version_data.status !== 200) {
throw new Error(await new_version_data.text());
}
}
infoDialog.show(
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: 2 <br/>`
);
setTimeout(() => {
title.textContent = "Deploy";
@@ -1489,6 +1614,24 @@ export class InfoDialog extends ComfyDialog {
</div>
`);
}
showError(title, message) {
this.show(`
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; color: #ff4444; display: flex; align-items: center; gap: 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
${title}
</h3>
<div style="padding: 12px;">
${message}
</div>
</div>
`);
// Set higher z-index to appear above config dialog
this.element.style.zIndex = 1010;
}
}
export class LoadingDialog extends ComfyDialog {
@@ -1524,12 +1667,14 @@ export class LoadingDialog extends ComfyDialog {
showLoading(title, message) {
this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${this.loadingIcon
}</h3>
${message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: ""
}
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
this.loadingIcon
}</h3>
${
message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: ""
}
</div>
`);
}
@@ -1578,7 +1723,7 @@ export class InputDialog extends InfoDialog {
}
},
}),
],
]
),
];
}
@@ -1639,7 +1784,7 @@ export class ConfirmDialog extends InfoDialog {
this.close();
},
}),
],
]
),
];
}
@@ -1674,12 +1819,14 @@ function getData(environment) {
if (!data) {
if (deployOption == "cloud")
return {
endpoint: "https://www.comfydeploy.com",
endpoint: "https://app.comfydeploy.com",
apiUrl: "https://api.comfydeploy.com",
apiKey: "",
};
else
return {
endpoint: "http://localhost:3000",
endpoint: "http://localhost:3001",
apiUrl: "http://localhost:3011",
apiKey: "",
};
}
@@ -1696,7 +1843,7 @@ function getData(environment) {
function saveData(data) {
localStorage.setItem(
"comfy_deploy_env_data_" + data.environment,
JSON.stringify(data),
JSON.stringify(data)
);
}
@@ -1745,7 +1892,7 @@ export class ConfigDialog extends ComfyDialog {
this.close();
},
}),
],
]
),
];
}
@@ -1756,7 +1903,7 @@ export class ConfigDialog extends ComfyDialog {
clearTimeout(this.timeout);
}
save(api_key, displayName) {
async save(api_key, displayName) {
const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption);
@@ -1773,10 +1920,14 @@ export class ConfigDialog extends ComfyDialog {
saveData({
endpoint,
apiUrl: getData(deployOption).apiUrl,
apiKey,
displayName,
environment: deployOption,
});
// Refresh workflow list after saving configuration
await refreshWorkflowListIfOpen();
}
show() {
@@ -1789,23 +1940,33 @@ export class ConfigDialog extends ComfyDialog {
<h3 style="margin: 0px;">Comfy Deploy Config</h3>
<label style="color: white; width: 100%;">
<select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" >
<option value="cloud" ${data.environment === "cloud" ? "selected" : ""}>Cloud</option>
<option value="local" ${data.environment === "local" ? "selected" : ""}>Local</option>
<option value="cloud" ${
data.environment === "cloud" ? "selected" : ""
}>Cloud</option>
<option value="local" ${
data.environment === "local" ? "selected" : ""
}>Local</option>
</select>
</label>
<label style="color: white; width: 100%;">
Endpoint:
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${data.endpoint
}">
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${
data.endpoint
}">
</label>
<div style="color: white;">
API Key: User / Org <button style="font-size: 18px;">${data.displayName ?? ""
}</button>
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${data.apiKey
}">
API Key: User / Org ${
data.displayName
? `<button style="font-size: 18px;">${data.displayName}</button>`
: ""
}
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
data.apiKey
}">
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
${data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
}
${
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
}
</button>
</div>
</div>
@@ -1816,21 +1977,23 @@ export class ConfigDialog extends ComfyDialog {
this.save();
const data = getData();
const uuid =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
window.open(data.endpoint + "/auth-request/" + uuid, "_blank");
const uuid = crypto.randomUUID();
window.open(data.endpoint + "/auth/request/" + uuid, "_blank");
this.timeout = setTimeout(() => {
clearInterval(poll);
infoDialog.showMessage(
clearInterval(this.poll);
infoDialog.showError(
"Timeout",
"Wait too long for the response, please try re-login",
"Wait too long for the response, please try re-login"
);
}, 30000); // Stop polling after 30 seconds
this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid)
fetch(
`/comfyui-deploy/auth-response?request_id=${uuid}&api_url=${encodeURIComponent(
data.apiUrl
)}`
)
.then((response) => response.json())
.then(async (json) => {
if (json.api_key) {
@@ -1843,8 +2006,9 @@ export class ConfigDialog extends ComfyDialog {
// Refresh dialog
const a = await confirmDialog.confirm(
"Authenticated",
`<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`,
`<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`
);
configDialog.show();
}
})
@@ -1899,7 +2063,7 @@ if (isLocalhost) {
title: "Deploy",
tooltip: "Deploy and Configure",
type: "custom",
render: (el) => {
render: async (el) => {
el.innerHTML = `
<div style="padding: 20px;">
<h3>Comfy Deploy</h3>
@@ -1964,7 +2128,12 @@ if (isLocalhost) {
const workflowsList = el.querySelector("#workflows-list");
const workflowsLoading = el.querySelector("#workflows-loading");
// refreshWorkflowsList(el);
// Initialize workflows list and search
const data = getData();
if (data.apiKey) {
addWorkflowSearch(el, getData, getTimeAgo);
await initializeWorkflowsList(el, getData, getTimeAgo);
}
},
});
}
@@ -2250,7 +2419,7 @@ function addMenuButtons(options) {
// Remove any existing CD buttons
const existingButtons = document.querySelectorAll(
`[id^="${buttonIdPrefix}"]`,
`[id^="${buttonIdPrefix}"]`
);
existingButtons.forEach((button) => button.remove());
@@ -2347,3 +2516,34 @@ function addMenuRightButtons(buttonConfigs) {
containerStyle: {},
});
}
// Function to refresh workflow list if sidebar is open
async function refreshWorkflowListIfOpen() {
try {
const workflowsContainer = document.querySelector("#workflows-container");
const workflowsList = document.querySelector("#workflows-list");
// Only refresh if workflows container is visible and has been initialized
if (!workflowsContainer || !workflowsList) {
return;
}
// Import the functions we need
const { initializeWorkflowsList } = await import("./workflow-list.js");
// Clear existing workflows and reset state safely
workflowsList.innerHTML = "";
if (window.workflowsState) {
window.workflowsState.initialized = false;
window.workflowsState.workflows = [];
window.workflowsState.offset = 0;
window.workflowsState.hasMore = true;
}
// Reinitialize the workflow list
await initializeWorkflowsList(document, getData, getTimeAgo);
console.log("Workflow list refreshed successfully");
} catch (error) {
console.error("Error refreshing workflow list:", error);
}
}
+387
View File
@@ -0,0 +1,387 @@
// Workflow list management
let workflowsState = {
workflows: [],
offset: 0,
limit: 20,
loading: false,
hasMore: true,
initialized: false,
currentSearch: "",
};
// Make workflowsState accessible globally
window.workflowsState = workflowsState;
async function fetchWorkflows(getData, offset = 0, limit = 20, search = "") {
try {
const data = getData();
if (!data.apiKey) {
throw new Error("API key not configured");
}
const params = new URLSearchParams({
offset: offset.toString(),
limit: limit.toString(),
api_url: data.apiUrl || "https://api.comfydeploy.com",
...(search && { search }),
});
const response = await fetch(`/comfyui-deploy/workflows?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch workflows: ${response.status}`);
}
const result = await response.json();
console.log("result", result);
return Array.isArray(result) ? result : [];
} catch (error) {
console.error("Error fetching workflows:", error);
return [];
}
}
function createWorkflowItem(workflow, getTimeAgo, getData) {
const li = document.createElement("li");
li.style.cssText = `
border-bottom: 1px solid #444;
background: transparent;
transition: all 0.2s ease;
cursor: pointer;
`;
li.addEventListener("mouseenter", () => {
li.style.background = "#333";
});
li.addEventListener("mouseleave", () => {
li.style.background = "transparent";
});
// Add click handler to fetch and load workflow data
li.addEventListener("click", async () => {
try {
const data = getData();
if (!data.apiKey) {
console.error("No API key configured");
return;
}
// Show loading toast
const loadingToast = window.app.extensionManager.toast.add({
severity: "info",
summary: "Loading workflow...",
detail: `Loading "${workflow.name}"`,
life: 3000,
});
const params = new URLSearchParams({
workflow_id: workflow.id,
api_url: data.apiUrl || "https://api.comfydeploy.com",
});
const response = await fetch(`/comfyui-deploy/workflow?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch workflow: ${response.status}`);
}
const workflowData = await response.json();
console.log("Workflow data:", workflowData);
// Load the workflow into the graph
if (workflowData.versions && workflowData.versions.length > 0) {
const latestVersion = workflowData.versions[0];
if (latestVersion.workflow && window.app) {
// Load the workflow
window.app.loadGraphData(latestVersion.workflow);
// Show success toast
window.app.extensionManager.toast.add({
severity: "success",
summary: "Workflow loaded successfully",
detail: `Loaded "${workflow.name}" v${latestVersion.version}`,
life: 3000,
});
}
}
} catch (error) {
console.error("Error loading workflow:", error);
// Show error toast
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to load workflow",
detail: error.message,
life: 5000,
});
} finally {
loadingToast.close();
}
});
const updatedDate = new Date(workflow.updated_at);
const timeAgo = getTimeAgo(updatedDate);
li.innerHTML = `
<div style="padding: 12px 16px;">
<div style="display: flex; align-items: flex-start; gap: 12px;">
${
workflow.cover_image
? `<img src="${workflow.cover_image}"
style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0;"
onerror="this.style.display='none'">`
: `<div style="width: 40px; height: 40px; border-radius: 4px; background: #444; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #888;">
${workflow.name.charAt(0).toUpperCase()}
</div>`
}
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<h4 style="margin: 0; font-size: 14px; font-weight: 400; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${workflow.name}
</h4>
${
workflow.pinned
? `<span style="color: #ffd700; font-size: 12px;">📌</span>`
: ""
}
</div>
${
workflow.description
? `<p style="margin: 0 0 8px 0; font-size: 12px; color: #bbb; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
${workflow.description}
</p>`
: ""
}
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<img src="${workflow.user_icon}"
style="width: 16px; height: 16px; border-radius: 50%;"
onerror="this.style.display='none'">
<span style="font-size: 11px; color: #888;">
${workflow.user_name} • Updated ${timeAgo}
</span>
</div>
</div>
</div>
</div>
`;
return li;
}
async function loadMoreWorkflows(element, getData, getTimeAgo) {
if (workflowsState.loading || !workflowsState.hasMore) return;
workflowsState.loading = true;
const workflowsList = element.querySelector("#workflows-list");
const workflowsLoading = element.querySelector("#workflows-loading");
// Show loading indicator
workflowsLoading.style.display = "flex";
try {
const newWorkflows = await fetchWorkflows(
getData,
workflowsState.offset,
workflowsState.limit,
workflowsState.currentSearch
);
if (newWorkflows.length === 0) {
workflowsState.hasMore = false;
} else {
workflowsState.workflows.push(...newWorkflows);
workflowsState.offset += newWorkflows.length;
// Render new workflow items
newWorkflows.forEach((workflow) => {
const workflowItem = createWorkflowItem(workflow, getTimeAgo, getData);
workflowsList.appendChild(workflowItem);
});
}
} catch (error) {
console.error("Error loading more workflows:", error);
} finally {
workflowsState.loading = false;
workflowsLoading.style.display = "none";
}
}
function setupInfiniteScroll(container, element, getData, getTimeAgo) {
let isScrolling = false;
container.addEventListener("scroll", () => {
if (isScrolling) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// Load more when scrolled to bottom (with 100px threshold)
if (scrollTop + clientHeight >= scrollHeight - 100) {
isScrolling = true;
loadMoreWorkflows(element, getData, getTimeAgo).finally(() => {
isScrolling = false;
});
}
});
}
async function initializeWorkflowsList(element, getData, getTimeAgo) {
const workflowsContainer = element.querySelector("#workflows-container");
const workflowsList = element.querySelector("#workflows-list");
const workflowsLoading = element.querySelector("#workflows-loading");
// Check if already initialized AND the DOM elements still exist
if (
workflowsState.initialized &&
workflowsList &&
workflowsList.children.length > 0
)
return;
try {
// Reset state (always reset when reinitializing)
workflowsState = {
workflows: [],
offset: 0,
limit: 20,
loading: false,
hasMore: true,
initialized: true,
currentSearch: "",
};
// Clear existing content in case of reinitialization
if (workflowsList) {
workflowsList.innerHTML = "";
}
// Show container and loading
workflowsContainer.style.display = "block";
workflowsLoading.style.display = "flex";
// Style the workflows list for full height scrolling
workflowsList.style.cssText = `
list-style-type: none;
padding: 0;
margin: 0;
height: calc(100vh - 350px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #666 transparent;
border-top: 1px solid #444;
`;
// Add webkit scrollbar styles
const style = document.createElement("style");
style.textContent = `
#workflows-list::-webkit-scrollbar {
width: 6px;
}
#workflows-list::-webkit-scrollbar-track {
background: transparent;
}
#workflows-list::-webkit-scrollbar-thumb {
background: #666;
border-radius: 3px;
}
#workflows-list::-webkit-scrollbar-thumb:hover {
background: #777;
}
`;
document.head.appendChild(style);
// Setup infinite scroll
setupInfiniteScroll(workflowsList, element, getData, getTimeAgo);
// Load initial workflows
await loadMoreWorkflows(element, getData, getTimeAgo);
// Show the list
workflowsList.style.display = "block";
} catch (error) {
console.error("Error initializing workflows list:", error);
workflowsLoading.innerHTML = `
<div style="text-align: center; color: #e74c3c; font-size: 12px; padding: 20px;">
<div>Failed to load workflows</div>
<button onclick="initializeWorkflowsList(this.closest('.comfy-menu'), getData, getTimeAgo)"
style="margin-top: 8px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
Retry
</button>
</div>
`;
}
}
// Search functionality
function addWorkflowSearch(element, getData, getTimeAgo) {
const workflowsContainer = element.querySelector("#workflows-container");
const h4 = workflowsContainer.querySelector("h4");
const searchContainer = document.createElement("div");
searchContainer.style.cssText = "margin-bottom: 12px;";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Search workflows...";
searchInput.style.cssText = `
width: 100%;
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
font-size: 12px;
box-sizing: border-box;
background: #333;
color: #fff;
`;
let searchTimeout;
searchInput.addEventListener("input", (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const searchTerm = e.target.value.trim();
// Update the tracked search term
workflowsState.currentSearch = searchTerm;
// Reset state for new search
workflowsState.workflows = [];
workflowsState.offset = 0;
workflowsState.hasMore = true;
// Clear current list
const workflowsList = element.querySelector("#workflows-list");
workflowsList.innerHTML = "";
// Load with search term
workflowsState.loading = false;
await loadMoreWorkflows(element, getData, getTimeAgo);
}, 300);
});
searchContainer.appendChild(searchInput);
h4.after(searchContainer);
}
// Export the functions
export {
initializeWorkflowsList,
addWorkflowSearch,
workflowsState,
fetchWorkflows,
loadMoreWorkflows,
};