Files
BennyKok-comfyui-deploy/web-plugin/workflow-list.js
T
KarrixLee a91edf6371 Fix serverless machine (#106)
* refresh the machine list automatically

* update ui

* sync non local workflow

* Bump CD ver to 2.3.2 in Manager
2025-08-26 17:38:34 +08:00

418 lines
13 KiB
JavaScript

// 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");
let loadingToast = null;
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
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);
// Wait a bit for the graph to fully load before checking for ComfyDeploy node
await new Promise((resolve) => setTimeout(resolve, 100));
// Check if ComfyDeploy node exists, if not add it back
const graph = window.app.graph;
let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length === 0) {
// Add ComfyDeploy node with workflow metadata
graph.beforeChange();
const node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [
workflow.name, // workflow_name
workflow.id, // workflow_id
latestVersion.version, // version
],
});
node.pos = [0, 0];
graph.add(node);
graph.afterChange();
console.log(
`Added ComfyDeploy node with: name="${workflow.name}", id="${workflow.id}", version="${latestVersion.version}"`
);
}
// 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 {
if (loadingToast) {
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 - 550px);
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,
};