diff --git a/js/button/actions.js b/js/button/actions.js index 1d7c4ba..0b9e162 100644 --- a/js/button/actions.js +++ b/js/button/actions.js @@ -24,63 +24,10 @@ import { nimbus, local } from '../resource/index.js'; import { endpoint } from '../constants.js'; import { ComfyCloudDialog, ComfyCloudPopover } from '../comfy/ui.js'; -import van from '../lib/van.js'; -import { Await } from '../lib/van-ui.js'; -const {video, h2, img, a, button, div, b, span, source } = van.tags +import { taskId, Progress } from '../ui/uploadProgress.js'; -const ProgressBar = (progress) => { - const progressPercentage = 100 - (progress.value/progress.max * 100) - return () => div({style: `width: ${progressPercentage}%; height: 24px; background-color: #1D4AFF; transition: all .2s;`}, `${progressPercentage.toFixed(2)}%`) -} - -const taskId = van.state(null) -const Progress = (dialogInstance) => { - - const activeTab = van.state(0) - const data = van.state(local.pollUploadStatus(taskId.val)) // workflowRun data - - const start = () => dialogInstance.poll = dialogInstance.poll || setInterval(async () => { - const res = await local.pollUploadStatus(taskId.val) - - if(!(res.status == "Task started" || res.status == "Task uploading" || res.status == "Task hashing")) { - // Stop poll - clearInterval(dialogInstance.poll) - dialogInstance.close() - dialogInstance, dialogInstance.poll = 0; - } - - data.val = Promise.resolve(res) - }, 1000) - - start() - - return () => van.tags.div({style: 'width: 320px'}, - div( - h2("Uploading dependencies"), - ), - Await({ - value: data.val, - container: span, - Loading: () => "Loading", - Error: () => "Request failed.", - }, data => div( - div( - b("Status:"), - data.status - ), - Object.keys(data.progress).length === 0 ? "Loading" : - Object.entries(data.progress).map(([key, val]) => { - return () => div( - b(key), - ProgressBar({value: val.value, max: val.max}) - ) - })) - )) -} - -export const progressDialog = new ComfyCloudPopover(Progress) -//progressDialog.show(); +export const progressDialog = new ComfyCloudPopover(Progress, "Uploading dependencies...") export async function onGeneration() { try { diff --git a/js/button/dependencies.js b/js/button/dependencies.js index dd39629..e056fcb 100644 --- a/js/button/dependencies.js +++ b/js/button/dependencies.js @@ -119,10 +119,18 @@ export const resolveDependencies = async (diff) => { } } + +export const pollSyncDependenciesStatus = { + STARTED: 'Started', + COMPLETED: 'Completed', + HASHING: 'Hashing', + UPLOADING: 'Uploading', + ERROR: 'Failed', +} export const pollSyncDependencies = async (taskId) => { let status = ''; let statusMessage = ''; - while (status !== 'Task completed' && status !== 'Task failed') { + while (status !== pollSyncDependenciesStatus.COMPLETED && status !== pollSyncDependenciesStatus.ERROR) { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second before the next poll try { @@ -136,7 +144,7 @@ export const pollSyncDependencies = async (taskId) => { } } - if (status == "Task failed") { + if (status == pollSyncDependenciesStatus.ERROR) { throw new Error(statusMessage || "Failed to upload") } } diff --git a/js/comfy/ui.js b/js/comfy/ui.js index 832e7f1..c6fe222 100644 --- a/js/comfy/ui.js +++ b/js/comfy/ui.js @@ -1,46 +1,73 @@ import { ComfyDialog, $el } from './comfy.js'; import { getData, setData } from '../store.js'; -import { headerHtml } from '../ui/html.js'; +import { headerHtml, chevronUpIcon } from '../ui/html.js'; import van from '../lib/van.js'; export class ComfyCloudPopover extends ComfyDialog { container = null; - constructor(ui) { + constructor(ui, title = "") { super(); this.element.classList.add("comfy-normal-modal"); this.element.style.padding = "16px"; this.element.style.overflowY = "auto"; - - this.container = document.createElement("div"); - this.container.style.color = "white"; - - this.header = document.createElement("div"); - this.header.innerHTML = headerHtml - this.element.querySelector(".comfy-modal-content").prepend(this.container); - //this.element.querySelector(".comfy-modal-content").prepend(this.header); this.element.style.top = "24px"; this.element.style.left = "14px"; this.element.style.transform = "none"; + // hotfix: set all p elements inside this.element to have 0 margin + this.element.querySelectorAll("p").forEach((p) => { + p.style.margin = "0"; + }); + + + // Title + this.title = document.createElement("div"); + this.title.style.fontSize = "18px"; + this.title.style.fontWeight = "bold"; + this.title.style.color = "white"; + this.title.innerHTML = title; + + // Collapse button + this.collapsed = false; + this.collapseButton = document.createElement("button"); + this.collapseButton.style.width = "36px"; + this.collapseButton.innerHTML = chevronUpIcon; + this.collapseButton.onclick = () => { + this.collapsed = !this.collapsed; + this.component.style.height = this.collapsed ? "0" : "auto"; + this.collapseButton.style.transform = this.collapsed ? "rotate(180deg)" : "rotate(0deg)"; + } + + // Title + collapse button wrapper + this.header = document.createElement("div"); + this.header.style.display = "flex"; + this.header.style.alignItems = "center"; + this.header.style.justifyContent = "space-between"; + this.header.style.gap = "8px"; + this.header.append(this.title); + this.header.append(this.collapseButton); + + + // VanJS content + this.container = document.createElement("div"); + this.container.style.color = "white"; + this.container.style.overflow = "hidden"; + this.component = null; this.ui = ui + + + this.element.querySelector(".comfy-modal-content").prepend(this.container); + this.element.querySelector(".comfy-modal-content").prepend(this.header); } createButtons() { return [ $el( "div", - [ - /* - $el("button", { - type: "button", - textContent: "Close", - onclick: () => this.close(), - }), - */ - ], + [], ), ]; } diff --git a/js/ui/html.js b/js/ui/html.js index 52f6e50..6f21f25 100644 --- a/js/ui/html.js +++ b/js/ui/html.js @@ -18,6 +18,13 @@ export const cloudIconWhite = ` ` +export const fileIcon = ` + +` + +export const chevronUpIcon = ` + +` export const headerHtml = `
diff --git a/js/ui/uploadProgress.js b/js/ui/uploadProgress.js new file mode 100644 index 0000000..20d66fa --- /dev/null +++ b/js/ui/uploadProgress.js @@ -0,0 +1,87 @@ +import van from '../lib/van.js'; +import { Await } from '../lib/van-ui.js'; +import { nimbus, local } from '../resource/index.js'; +import { pollSyncDependenciesStatus } from '../button/dependencies.js'; +import { fileIcon } from './html.js'; + +const { h2, img, a, button, div, b, span, source } = van.tags +const { path, svg} = van.tags("http://www.w3.org/2000/svg") + +// Show the first 4 characters, and last 18 characters of this string +function cropString(str) { + const firstThree = str.substring(0, 4); + const lastTen = str.substring(str.length - 36); + const middlePart = '...'; + return firstThree + middlePart + lastTen; +} + + +const ProgressBar = (progress) => { + const progressPercentage = 100 - (progress.value/progress.max * 100) + return () => div({style: "height: 18px; width: 100%; background: rgba(255,255,255,.1); transition: all .2s; border-radius: 4px; overflow: hidden;"}, + div({style: `width: ${progressPercentage}%; background-color: #1D4AFF; border-radius: 4px;`}, + span({style: "font-size: 12px"}, `${progressPercentage.toFixed(2)}%`) + ) + ) +} + +export const taskId = van.state(null) +export const Progress = (dialogInstance) => { + + const activeTab = van.state(0) + const data = van.state(local.pollUploadStatus(taskId.val)) // workflowRun data + + const start = () => dialogInstance.poll = dialogInstance.poll || setInterval(async () => { + const res = await local.pollUploadStatus(taskId.val) + + if(!( + res.status == pollSyncDependenciesStatus.STARTED || + res.status == pollSyncDependenciesStatus.UPLOADING || + res.status == pollSyncDependenciesStatus.HASHING) + ) { + // Stop poll + clearInterval(dialogInstance.poll) + dialogInstance.close() + dialogInstance, dialogInstance.poll = 0; + } + + data.val = Promise.resolve(res) + }, 1000) + + start() + + return () => van.tags.div({style: 'width: 360px'}, + Await({ + value: data.val, + container: span, + Loading: () => "Loading", + Error: () => "Request failed.", + }, data => div( + + div({style: "margin-bottom: 16px"}, + b("Status:"), + data.status + ), + + Object.entries(data.progress).map(([key, val]) => { + return () => div({style: "margin-bottom: 16px;"}, + div({style: "display: flex; align-items: center; gap: 8px;"}, + div({style: 'width: 12px; color: white;'}, + svg({xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 384 512"}, + path({"d": "M320 464c8.8 0 16-7.2 16-16V160H256c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H320zM0 64C0 28.7 28.7 0 64 0H229.5c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64z", "fill": "currentColor"}), + ) + ), + span({style: "font-size: 12px"}, b(cropString(key))), + ), + + div({style: "width: 100%"}, + ProgressBar({value: val.value, max: val.max}) + ) + ) + }) + + ) + )) +} + + diff --git a/python/custom_routes.py b/python/custom_routes.py index 1f66715..6a8926d 100644 --- a/python/custom_routes.py +++ b/python/custom_routes.py @@ -7,7 +7,6 @@ import uuid import time import traceback import random -import re import requests import importlib import sys @@ -21,11 +20,13 @@ import folder_paths from .upload import upload_file_specs from .upload.spec import FileSpecContextManager -from .upload.progress import progress_dict +from .upload.progress import progress_dict, reset_progress from .utils.paths import build_paths from .utils.task import task_create, task_set_status, task_set_progress, task_get_by_id, task_serialize, TaskStatus +from .utils.requirements import update_requirements + @server.PromptServer.instance.routes.post("/comfy-cloud/validate-input-path") async def validate_input_path(request): @@ -148,44 +149,6 @@ def load_custom_node(module_path, ignore=set()): print(f"Cannot import {module_path} module for custom nodes:", e) return (module_name, []) -def update_dependencies(): - """ - Formats requirements.txt - """ - - base = folder_paths.base_path - custom_nodes_dir = os.path.join(base, "custom_nodes") - - # Loop over the files in the specified directory - for custom_node in os.listdir(custom_nodes_dir): - filepath = os.path.join(custom_nodes_dir, custom_node, "requirements.txt") - - # Check if the file is a requirements.txt file - if os.path.exists(filepath): - - # Read the content of the requirements.txt file - with open(filepath, 'r') as file: - lines = file.readlines() - - # Update dependencies in the file content - updated_lines = [] - for line in lines: - # Check if the line contains a git dependency without the specified format - match_git = re.match(r'^\s*git\+https://.*?/([^/]+)\.git', line) - match_git_plus = re.match(r'^\s*git\+https://.*?/([^/]+)$', line) - - if match_git: - package_name = match_git.group(1) - updated_lines.append(f"{package_name} @ {line.strip()}\n") - elif match_git_plus: - package_name = match_git_plus.group(1).strip() - updated_lines.append(f"{package_name} @ {line.strip()}\n") - else: - updated_lines.append(line) - - # Write the updated content back to the requirements.txt file - with open(filepath, 'w') as file: - file.writelines(updated_lines) async def upload_task_execution(task_id, file_specs, workflow_id): try: @@ -202,6 +165,9 @@ async def upload_task_execution(task_id, file_specs, workflow_id): print(e) task_set_status(task_id, TaskStatus.ERROR) + finally: + reset_progress(progress_dict) + @server.PromptServer.instance.routes.post("/comfy-cloud/upload") async def upload_dependencies(request): @@ -229,7 +195,7 @@ async def upload_dependencies(request): # Our server uses a custom dependency manager # that requires a specific format for requirements.txt. # Loop through all dependent custom nodes and patch. - update_dependencies() + update_requirements() # Get dependency paths paths = build_paths(paths_to_upload, dep_lists, workflow_id) diff --git a/python/upload/progress.py b/python/upload/progress.py index 6d798f3..d49763f 100644 --- a/python/upload/progress.py +++ b/python/upload/progress.py @@ -17,6 +17,8 @@ def progress_update(filename, value, max): progress_dict[filename] = progress +def reset_progress(progress_dict): + progress_dict = {} diff --git a/python/utils/requirements.py b/python/utils/requirements.py new file mode 100644 index 0000000..b3008a6 --- /dev/null +++ b/python/utils/requirements.py @@ -0,0 +1,42 @@ +import os +import re +import folder_paths + +def update_requirements(): + """ + Formats requirements.txt + """ + + base = folder_paths.base_path + custom_nodes_dir = os.path.join(base, "custom_nodes") + + # Loop over the files in the specified directory + for custom_node in os.listdir(custom_nodes_dir): + filepath = os.path.join(custom_nodes_dir, custom_node, "requirements.txt") + + # Check if the file is a requirements.txt file + if os.path.exists(filepath): + + # Read the content of the requirements.txt file + with open(filepath, 'r') as file: + lines = file.readlines() + + # Update dependencies in the file content + updated_lines = [] + for line in lines: + # Check if the line contains a git dependency without the specified format + match_git = re.match(r'^\s*git\+https://.*?/([^/]+)\.git', line) + match_git_plus = re.match(r'^\s*git\+https://.*?/([^/]+)$', line) + + if match_git: + package_name = match_git.group(1) + updated_lines.append(f"{package_name} @ {line.strip()}\n") + elif match_git_plus: + package_name = match_git_plus.group(1).strip() + updated_lines.append(f"{package_name} @ {line.strip()}\n") + else: + updated_lines.append(line) + + # Write the updated content back to the requirements.txt file + with open(filepath, 'w') as file: + file.writelines(updated_lines) diff --git a/python/utils/task.py b/python/utils/task.py index 7e6e934..87c5579 100644 --- a/python/utils/task.py +++ b/python/utils/task.py @@ -4,11 +4,11 @@ from enum import Enum from typing import Any, Dict class TaskStatus(Enum): - STARTED = "Task started" - COMPLETED = "Task completed" - HASHING = "Task hashing" - UPLOADING = "Task uploading" - ERROR = "Task failed" + STARTED = "Started" + COMPLETED = "Completed" + HASHING = "Hashing" + UPLOADING = "Uploading" + ERROR = "Failed" @dataclasses.dataclass class Task: