diff --git a/custom_routes.py b/custom_routes.py index 70d19e0..849fc92 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -31,6 +31,7 @@ import torch import psutil from collections import OrderedDict import io +from urllib.parse import urlencode # Global session client_session = None @@ -2993,3 +2994,44 @@ async def create_workflow_version_proxy(request): 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) diff --git a/web-plugin/index.js b/web-plugin/index.js index cb17bde..7351552 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -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"; @@ -1973,7 +1974,7 @@ if (isLocalhost) { title: "Deploy", tooltip: "Deploy and Configure", type: "custom", - render: (el) => { + render: async (el) => { el.innerHTML = `

Comfy Deploy

@@ -2038,7 +2039,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); + } }, }); } diff --git a/web-plugin/workflow-list.js b/web-plugin/workflow-list.js new file mode 100644 index 0000000..46ce5a2 --- /dev/null +++ b/web-plugin/workflow-list.js @@ -0,0 +1,305 @@ +// Workflow list management +let workflowsState = { + workflows: [], + offset: 0, + limit: 20, + loading: false, + hasMore: true, + initialized: false, + currentSearch: "", +}; + +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) { + 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"; + }); + + const updatedDate = new Date(workflow.updated_at); + const timeAgo = getTimeAgo(updatedDate); + + li.innerHTML = ` +
+
+ ${ + workflow.cover_image + ? ` +
+ Workflow cover +
+ 📄 +
+
+ ` + : ` +
+ 📄 +
+ ` + } + +
+
+
+ ${workflow.name} +
+ ${ + workflow.pinned + ? '📌' + : "" + } +
+ +
+ + Updated ${timeAgo} + +
+
+
+
+ `; + + 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); + 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"); + + if (workflowsState.initialized) return; + + try { + // Reset state + workflowsState = { + workflows: [], + offset: 0, + limit: 20, + loading: false, + hasMore: true, + initialized: true, + currentSearch: "", + }; + + // 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 - 400px); + 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 = ` +
+
Failed to load workflows
+ +
+ `; + } +} + +// 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, +};