Compare commits

...
Author SHA1 Message Date
KarrixLee 761e6db573 Refactor swizzle_execute for async and sync handling
- Enhanced the swizzle_execute function to differentiate between asynchronous and synchronous execution paths based on the origin_execute function's nature.
- Improved the structure of the swizzle_execute function to ensure proper handling of parameters and execution flow for both async and sync scenarios.
- Maintained existing error handling while ensuring consistent behavior across execution types.

These changes improve the flexibility and robustness of the execution handling in the application.
2025-07-23 22:41:18 +08:00
KarrixLee 2e527f74b5 tweak: optional 2025-07-23 21:59:13 +08:00
KarrixLee 7d8f4563af Refactor post_prompt and origin_execute for synchronous handling
- Modified the post_prompt function to call validate_prompt synchronously when a TypeError occurs, improving error handling.
- Updated the origin_execute function to execute synchronously, ensuring consistent behavior during execution.

These changes enhance the robustness of prompt validation and execution processes in the application.
2025-07-23 21:52:51 +08:00
KarrixLee fba00a3c4d Enhance error handling in post_prompt and origin_execute functions
- Added try-except blocks in post_prompt to handle TypeErrors during prompt validation, allowing for fallback to an older signature.
- Implemented similar error handling in the origin_execute function to manage potential TypeErrors, ensuring robust execution flow.
- Improved logging to capture issues with function signatures, aiding in debugging.

These changes improve the resilience of the application when dealing with prompt and execution validation.
2025-07-23 21:46:55 +08:00
KarrixLee 7f56d18599 Refactor handle_execute to support asynchronous execution
- Updated the swizzle_execute function to be asynchronous, allowing for non-blocking execution of the origin_execute function.
- Added missing parameters for pending_async_nodes in the swizzle_execute function call.
- Ensured that the result from origin_execute is awaited, improving the handling of asynchronous operations.

These changes enhance the performance and responsiveness of the execution handling in the application.
2025-07-23 12:41:15 +08:00
KarrixLee bdb2c1b85d Refactor post_prompt and send_prompt functions for async handling
- Updated the post_prompt function to be asynchronous, allowing for non-blocking execution when validating prompts.
- Adjusted calls to post_prompt in send_prompt and comfy_deploy_run to await the asynchronous execution, ensuring proper handling of prompt submissions.
- Commented out unused prompt construction code to streamline the function.

These changes enhance the performance and responsiveness of prompt handling in the application.
2025-07-23 11:58:47 +08:00
KarrixLee ee658f90b2 Refactor post_prompt function to include prompt_id handling
- Updated the post_prompt function in custom_routes.py to retrieve and validate a prompt_id from the incoming JSON data, defaulting to a new UUID if not provided.
- Adjusted the validation call to use prompt_id alongside the prompt, ensuring proper identification and processing of prompts.

These changes enhance the functionality of prompt handling within the application.
2025-07-23 11:42:16 +08:00
KarrixLee ac8779dc54 fix 2025-07-19 10:24:04 -07:00
KarrixLee 2a5223f2f0 fix 2025-06-29 15:43:12 +08:00
KarrixLee f02aef4cb3 tweak versoin 2025-06-29 13:37:24 +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 1065 additions and 238 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)"}
+295 -80
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
@@ -264,7 +265,7 @@ def clear_current_prompt(sid):
streaming_prompt_metadata[sid].running_prompt_ids.clear()
def post_prompt(json_data):
async def post_prompt(json_data):
prompt_server = server.PromptServer.instance
json_data = prompt_server.trigger_on_prompt(json_data)
@@ -280,7 +281,14 @@ def post_prompt(json_data):
if "prompt" in json_data:
prompt = json_data["prompt"]
valid = execution.validate_prompt(prompt)
prompt_id = json_data.get("prompt_id") or str(uuid.uuid4())
try:
valid = await execution.validate_prompt(prompt_id, prompt)
except TypeError as e:
logger.warning(f"Trying old validate_prompt signature: {e}")
valid = execution.validate_prompt(prompt)
extra_data = {}
if "extra_data" in json_data:
extra_data = json_data["extra_data"]
@@ -291,8 +299,6 @@ def post_prompt(json_data):
if "client_id" in json_data:
extra_data["client_id"] = json_data["client_id"]
if valid[0]:
# if the prompt id is provided
prompt_id = json_data.get("prompt_id") or str(uuid.uuid4())
outputs_to_execute = valid[2]
prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute)
@@ -499,15 +505,15 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
prompt_id = str(uuid.uuid4())
prompt = {
"prompt": workflow_api,
"client_id": sid, # "comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id,
"extra_data": {"extra_pnginfo": {"workflow": workflow}},
}
# prompt = {
# "prompt": workflow_api,
# "client_id": sid, # "comfy_deploy_instance", #api.client_id
# "prompt_id": prompt_id,
# "extra_data": {"extra_pnginfo": {"workflow": workflow}},
# }
try:
res = post_prompt(prompt)
# res = post_prompt(prompt)
inputs.running_prompt_ids.add(prompt_id)
prompt_metadata[prompt_id] = SimplePrompt(
status_endpoint=inputs.status_endpoint,
@@ -518,7 +524,7 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
except Exception as e:
error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split("\n")[-2]
stack_trace = traceback.format_exc().strip()
# stack_trace = traceback.format_exc().strip()
logger.info(f"error: {error_type}, {e}")
logger.info(f"stack trace: {stack_trace_short}")
@@ -594,7 +600,7 @@ async def comfy_deploy_run(request):
)
try:
res = post_prompt(prompt)
res = await post_prompt(prompt)
except Exception as e:
error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split("\n")[-2]
@@ -663,7 +669,7 @@ async def stream_prompt(data, token):
# log('info', "Begin prompt", prompt=prompt)
try:
res = post_prompt(prompt)
res = await post_prompt(prompt)
except Exception as e:
error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split("\n")[-2]
@@ -1265,22 +1271,11 @@ def handle_execute(class_type, last_node_id, prompt_id, server, unique_id):
try:
origin_execute = execution.execute
is_async = asyncio.iscoroutinefunction(origin_execute)
def swizzle_execute(
server,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
):
unique_id = current_item
class_type = dynprompt.get_node(unique_id)["class_type"]
last_node_id = server.last_node_id
result = origin_execute(
if is_async:
async def swizzle_execute(
server,
dynprompt,
caches,
@@ -1290,12 +1285,61 @@ try:
prompt_id,
execution_list,
pending_subgraph_results,
)
handle_execute(class_type, last_node_id, prompt_id, server, unique_id)
return result
pending_async_nodes,
):
unique_id = current_item
class_type = dynprompt.get_node(unique_id)["class_type"]
last_node_id = server.last_node_id
result = await origin_execute(
server,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
pending_async_nodes,
)
handle_execute(class_type, last_node_id, prompt_id, server, unique_id)
return result
else:
def swizzle_execute(
server,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
):
unique_id = current_item
class_type = dynprompt.get_node(unique_id)["class_type"]
last_node_id = server.last_node_id
result = origin_execute(
server,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
)
handle_execute(class_type, last_node_id, prompt_id, server, unique_id)
return result
execution.execute = swizzle_execute
except Exception as e:
except Exception:
pass
@@ -1439,10 +1483,6 @@ async def send_json_override(self, event, data, sid=None):
logger.info(format_table(headers, table_data))
# print("========================\n")
timeline = format_execution_timeline(NODE_EXECUTION_TIMES)
logger.info(f"\nNode Execution Timeline:\n{timeline}")
# Clear the execution times for the next run
# the last executing event is none, then the workflow is finished
if event == "executing" and data.get("node") is None:
mark_prompt_done(prompt_id=prompt_id)
@@ -2753,53 +2793,57 @@ class UploadQueue:
logger.error(f"Upload failed: {str(e)}")
logger.error(traceback.format_exc())
finally:
# Remove this upload from tracking
if prompt_id in self.pending_uploads:
self.pending_uploads[prompt_id].discard(upload_id)
# Remove from node tracking if applicable
if (
node_id
and prompt_id in self.node_uploads
and node_id in self.node_uploads[prompt_id]
):
self.node_uploads[prompt_id][node_id].discard(upload_id)
async with self.lock: # Acquire lock to protect shared dict access
if prompt_id in self.pending_uploads:
self.pending_uploads[prompt_id].discard(upload_id)
# If this was the last upload for this node, clean up node data
if not self.node_uploads[prompt_id][node_id]:
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"]:
# Send final node data to API before cleanup
if prompt_metadata[prompt_id].status_endpoint:
body = {
"run_id": prompt_id,
"output_data": self.node_output_data[
if (
node_id
and prompt_id in self.node_uploads
and node_id in self.node_uploads[prompt_id]
):
self.node_uploads[prompt_id][node_id].discard(upload_id)
if not self.node_uploads[prompt_id][node_id]:
del self.node_uploads[prompt_id][node_id]
if (
prompt_id in self.node_output_data
and node_id in self.node_output_data[prompt_id]
):
node_data = self.node_output_data[prompt_id][
node_id
]
if node_data["data"]:
body = {
"run_id": prompt_id,
"output_data": node_data["data"],
"node_meta": {"node_id": node_id},
}
try:
await async_request_with_retry(
"POST",
prompt_metadata[
prompt_id
][node_id]["data"],
"node_meta": {"node_id": node_id},
}
try:
await async_request_with_retry(
"POST",
prompt_metadata[
prompt_id
].status_endpoint,
token=prompt_metadata[prompt_id].token,
json=body,
)
except Exception as e:
logger.error(
f"Failed to send final node data: {str(e)}"
)
].status_endpoint,
token=prompt_metadata[
prompt_id
].token,
json=body,
)
except Exception as e:
logger.error(
f"Failed to send final node data: {str(e)}"
)
# Safe to delete now (re-check not strictly needed with lock, but harmless)
del self.node_output_data[prompt_id][node_id]
# Send status update
await self.update_queue_status(prompt_id)
# If no more pending uploads for this prompt and it's done, update status
if not self.pending_uploads[prompt_id] and is_prompt_done(
prompt_id
if (
prompt_id in self.pending_uploads
and not self.pending_uploads[prompt_id]
and is_prompt_done(prompt_id)
):
# Clean up all data for this prompt
if prompt_id in self.node_uploads:
@@ -2812,9 +2856,12 @@ class UploadQueue:
loop.create_task(update_run(prompt_id, Status.SUCCESS))
loop.create_task(send("success", {"prompt_id": prompt_id}))
# Mark task as done
# Mark task as done (outside lock to avoid holding it unnecessarily)
self.queue.task_done()
# Send status update (also outside lock)
await self.update_queue_status(prompt_id)
except Exception as e:
logger.error(f"Error in upload worker: {str(e)}")
logger.error(traceback.format_exc())
@@ -2885,3 +2932,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)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "2.1.0"
version = "2.2.1"
license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
+304 -157
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,7 +490,7 @@ const ext = {
window.history.replaceState(
{},
document.title,
window.location.pathname,
window.location.pathname
);
});
}
@@ -507,7 +514,7 @@ const ext = {
content: `Convert ${w.name} to widget`,
callback: /* @__PURE__ */ __name(
() => convertToWidget(this, w),
"callback",
"callback"
),
});
} else {
@@ -520,7 +527,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 +552,7 @@ const ext = {
// className: "comfydeploy-menu-item"
// },
...toInput,
null,
null
);
} else {
options.push(...toInput, null);
@@ -574,9 +581,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 +647,7 @@ const ext = {
(v) => {
this.properties.workflow_name = v;
},
{ multiline: false },
{ multiline: false }
);
this.addWidget(
@@ -652,7 +657,7 @@ const ext = {
(v) => {
this.properties.workflow_id = v;
},
{ multiline: false },
{ multiline: false }
);
this.addWidget(
@@ -662,7 +667,7 @@ const ext = {
(v) => {
this.properties.version = v;
},
{ multiline: false },
{ multiline: false }
);
this.widgets_start_y = 10;
@@ -688,8 +693,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 +721,7 @@ const ext = {
title: "Comfy Deploy",
title_mode: LiteGraph.NORMAL_TITLE,
collapsable: true,
}),
})
);
ComfyDeploy.category = "deploy";
@@ -719,8 +735,8 @@ const ext = {
"string",
inputName,
/* value=*/ "",
() => { },
{ serialize: true },
() => {},
{ serialize: true }
);
const buttonWidget = node.addWidget(
@@ -734,7 +750,7 @@ const ext = {
});
// console.log("load image");
},
{ serialize: false },
{ serialize: false }
);
console.log(node.widgets);
@@ -783,12 +799,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 +810,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 +846,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 +993,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 +1016,6 @@ const ext = {
}
})(app.graph.onAfterChange);
sendEventToCD("cd_plugin_setup");
},
};
@@ -1010,7 +1027,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 +1050,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 +1061,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 +1085,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 +1103,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 +1124,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 +1136,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 +1170,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 +1287,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 +1309,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 +1358,7 @@ async function deployWorkflow() {
};
loadingDialog.close();
console.log(deps);
const depsOk = await confirmDialog.confirm(
"Check dependencies",
@@ -1305,13 +1377,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 +1390,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 +1408,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 +1421,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 +1564,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 +1617,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 +1673,7 @@ export class InputDialog extends InfoDialog {
}
},
}),
],
]
),
];
}
@@ -1639,7 +1734,7 @@ export class ConfirmDialog extends InfoDialog {
this.close();
},
}),
],
]
),
];
}
@@ -1674,12 +1769,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 +1793,7 @@ function getData(environment) {
function saveData(data) {
localStorage.setItem(
"comfy_deploy_env_data_" + data.environment,
JSON.stringify(data),
JSON.stringify(data)
);
}
@@ -1745,7 +1842,7 @@ export class ConfigDialog extends ComfyDialog {
this.close();
},
}),
],
]
),
];
}
@@ -1756,7 +1853,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 +1870,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 +1890,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 +1927,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 +1956,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();
}
})
@@ -1886,20 +2000,17 @@ const currentOrigin = window.location.origin;
// serverURL: `${currentOrigin}/comfydeploy/api/`,
// });
// Check if the current URL hostname starts with localhost or 127.0.0.1
const isLocalhost =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
const isComfyDeployDashboard = currentOrigin.includes("comfydeploy.com");
// Only register the sidebar tab if we're on localhost
if (isLocalhost) {
if (!isComfyDeployDashboard) {
app.extensionManager.registerSidebarTab({
id: "search",
icon: "pi pi-cloud-upload",
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 +2075,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 +2366,7 @@ function addMenuButtons(options) {
// Remove any existing CD buttons
const existingButtons = document.querySelectorAll(
`[id^="${buttonIdPrefix}"]`,
`[id^="${buttonIdPrefix}"]`
);
existingButtons.forEach((button) => button.remove());
@@ -2347,3 +2463,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,
};