import { app } from "../../scripts/app.js"; let globalEnabled = false; let schedules = []; let workflows = []; let settingsContainer = null; // Detect if we're in dark mode function isDarkMode() { // Check ComfyUI's theme const body = document.body; const computedStyle = getComputedStyle(body); const bgColor = computedStyle.backgroundColor; // If background is dark, we're in dark mode if (bgColor) { const rgb = bgColor.match(/\d+/g); if (rgb && rgb.length >= 3) { const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; return brightness < 128; } } // Fallback: check for dark class or default to false return body.classList.contains('dark') || body.classList.contains('dark-theme'); } // Get theme colors function getThemeColors() { const darkMode = isDarkMode(); return { background: darkMode ? '#2b2b2b' : '#ffffff', surface: darkMode ? '#383838' : '#fafafa', border: darkMode ? '#555555' : '#e0e0e0', text: darkMode ? '#ffffff' : '#333333', textSecondary: darkMode ? '#cccccc' : '#666666', input: darkMode ? '#404040' : '#ffffff', inputBorder: darkMode ? '#666666' : '#cccccc', overlay: darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.7)' }; } // Save workflow as task async function saveWorkflowAsTask() { try { // Check if there are nodes in the current graph if (!app.graph || !app.graph._nodes || app.graph._nodes.length === 0) { showNotification("No workflow to save!", "error"); return; } // Show naming dialog const workflowName = await showNamingDialog(); if (!workflowName) { return; // User cancelled } // Get pure API format (without _meta and other workflow metadata) let apiWorkflow; try { // Use ComfyUI's graphToPrompt to get the API format const promptData = await app.graphToPrompt(); // promptData.output contains the API format we need if (promptData.output) { apiWorkflow = promptData.output; } else if (promptData.workflow) { // If output is not available, clean the workflow format apiWorkflow = cleanWorkflowToAPI(promptData.workflow); } else { throw new Error("No valid prompt data generated"); } } catch (error) { console.log("graphToPrompt failed, trying manual conversion:", error); try { // Manual conversion as fallback const workflow = app.graph.serialize(); apiWorkflow = convertToAPIFormat(workflow); } catch (conversionError) { console.error("Manual conversion failed:", conversionError); throw new Error("Failed to convert workflow to API format"); } } if (!apiWorkflow || Object.keys(apiWorkflow).length === 0) { throw new Error("Generated API workflow is empty"); } console.log("Generated API workflow:", apiWorkflow); // Save workflow const response = await fetch('/scheduledtask/save_workflow', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: workflowName, workflow: apiWorkflow }) }); if (response.ok) { showNotification(`Workflow saved as "${workflowName}.json"`, "success"); // Refresh workflow list if settings is open if (settingsContainer) { workflows = await getWorkflowList(); // Re-render the settings if they're currently displayed const newContent = await createScheduleSettings(); settingsContainer.innerHTML = ''; settingsContainer.appendChild(newContent); } } else { const errorData = await response.json(); throw new Error(errorData.error || `HTTP ${response.status}`); } } catch (error) { console.error("Failed to save workflow:", error); showNotification("Failed to save workflow: " + error.message, "error"); } } // Clean workflow format to pure API format function cleanWorkflowToAPI(workflow) { const apiWorkflow = {}; Object.keys(workflow).forEach(nodeId => { const node = workflow[nodeId]; // Create clean API node (remove _meta and other metadata) apiWorkflow[nodeId] = { class_type: node.class_type, inputs: { ...node.inputs } // Copy inputs without metadata }; }); return apiWorkflow; } // Show naming dialog function showNamingDialog() { return new Promise((resolve) => { const colors = getThemeColors(); // Create overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${colors.overlay}; z-index: 10000; display: flex; justify-content: center; align-items: center; `; // Create dialog const dialog = document.createElement('div'); dialog.style.cssText = ` background: ${colors.background}; color: ${colors.text}; border-radius: 8px; padding: 25px; min-width: 400px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); border: 1px solid ${colors.border}; `; // Title const title = document.createElement('h3'); title.textContent = '💾 Save Workflow as Task'; title.style.cssText = `margin: 0 0 20px 0; color: ${colors.text}; text-align: center;`; // Input container const inputContainer = document.createElement('div'); inputContainer.style.cssText = 'margin-bottom: 20px;'; const label = document.createElement('label'); label.textContent = 'Workflow Name:'; label.style.cssText = `display: block; margin-bottom: 8px; font-weight: bold; color: ${colors.text};`; const input = document.createElement('input'); input.type = 'text'; input.placeholder = 'Enter workflow name (without .json extension)'; input.style.cssText = ` width: 100%; padding: 10px; border: 2px solid ${colors.inputBorder}; border-radius: 4px; font-size: 14px; box-sizing: border-box; background: ${colors.input}; color: ${colors.text}; `; // Auto-generate name based on current time const now = new Date(); const defaultName = `workflow_${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}_${now.getHours().toString().padStart(2,'0')}${now.getMinutes().toString().padStart(2,'0')}`; input.value = defaultName; input.select(); inputContainer.appendChild(label); inputContainer.appendChild(input); // Button container const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;'; // Save button const saveButton = document.createElement('button'); saveButton.textContent = '💾 Save'; saveButton.style.cssText = ` padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; `; // Cancel button const cancelButton = document.createElement('button'); cancelButton.textContent = '❌ Cancel'; cancelButton.style.cssText = ` padding: 10px 20px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; `; // Event handlers function handleSave() { const name = input.value.trim(); if (!name) { input.style.borderColor = '#f44336'; input.focus(); return; } // Validate filename const invalidChars = /[<>:"/\\|?*]/g; if (invalidChars.test(name)) { showNotification("Invalid characters in filename!", "error"); input.style.borderColor = '#f44336'; input.focus(); return; } document.body.removeChild(overlay); resolve(name); } function handleCancel() { document.body.removeChild(overlay); resolve(null); } saveButton.onclick = handleSave; cancelButton.onclick = handleCancel; // Enter key to save input.onkeydown = (e) => { if (e.key === 'Enter') { handleSave(); } else if (e.key === 'Escape') { handleCancel(); } }; // Click overlay to cancel overlay.onclick = (e) => { if (e.target === overlay) { handleCancel(); } }; buttonContainer.appendChild(saveButton); buttonContainer.appendChild(cancelButton); dialog.appendChild(title); dialog.appendChild(inputContainer); dialog.appendChild(buttonContainer); overlay.appendChild(dialog); document.body.appendChild(overlay); // Focus input setTimeout(() => input.focus(), 100); }); } // Convert workflow to API format function convertToAPIFormat(workflow) { try { const apiWorkflow = {}; // Convert nodes to API format if (workflow.nodes) { workflow.nodes.forEach(node => { if (node.id !== undefined) { apiWorkflow[node.id.toString()] = { class_type: node.type, inputs: {} }; // Process node properties to get input values const nodeInputs = apiWorkflow[node.id.toString()].inputs; // Handle widgets (standalone values) if (node.widgets_values && node.widgets_values.length > 0) { // Get widget names from node definition const nodeClass = LiteGraph.registered_node_types[node.type]; if (nodeClass && nodeClass.prototype) { const tempNode = new nodeClass(); if (tempNode.widgets) { tempNode.widgets.forEach((widget, index) => { if (index < node.widgets_values.length) { nodeInputs[widget.name] = node.widgets_values[index]; } }); } } } // Handle connections (linked inputs) if (node.inputs && workflow.links) { node.inputs.forEach((input, inputIndex) => { if (input.link !== null && input.link !== undefined) { // Find the link const link = workflow.links.find(l => l && l[0] === input.link); if (link) { const sourceNodeId = link[1]; const sourceOutputIndex = link[2]; nodeInputs[input.name] = [ sourceNodeId.toString(), sourceOutputIndex ]; } } }); } // Fallback: try to get default values from properties if (node.properties) { Object.keys(node.properties).forEach(key => { if (!(key in nodeInputs)) { nodeInputs[key] = node.properties[key]; } }); } } }); } return apiWorkflow; } catch (error) { console.error("Error converting workflow to API format:", error); // If conversion fails, try a simpler approach try { console.log("Trying alternative conversion method..."); // Use app.graphToPrompt if available if (app && app.graphToPrompt) { const prompt = app.graphToPrompt(); if (prompt && prompt.workflow) { return prompt.workflow; } } // Last resort: return the original workflow if it's already in API format if (workflow && typeof workflow === 'object' && !workflow.nodes) { console.log("Using original workflow (appears to be API format)"); return workflow; } throw new Error("All conversion methods failed"); } catch (fallbackError) { console.error("Fallback conversion also failed:", fallbackError); throw new Error("Failed to convert workflow format"); } } } // Get workflow list async function getWorkflowList() { try { const response = await fetch('/scheduledtask/get_workflows'); if (response.ok) { const data = await response.json(); return data.workflows || []; } } catch (error) { console.error("Failed to get workflow list:", error); } return []; } // Save schedule settings async function saveSchedules() { try { // If globally disabled, clear all schedules const finalSchedules = globalEnabled ? schedules.filter(s => s.time && s.workflow && s.enabled ) : []; const response = await fetch('/scheduledtask/save_schedules', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ schedules: finalSchedules, globalEnabled: globalEnabled }) }); if (response.ok) { console.log("Schedule settings saved and applied"); showNotification("Schedule settings saved and applied!", "success"); return true; } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error("Save failed:", error); showNotification("Save failed: " + error.message, "error"); } return false; } // Load existing schedule settings async function loadSchedules() { try { const response = await fetch('/scheduledtask/get_schedules'); if (response.ok) { const data = await response.json(); globalEnabled = data.globalEnabled || false; return data.schedules || []; } } catch (error) { console.error("Failed to load schedule settings:", error); } return []; } // Show notification function showNotification(message, type = "info") { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 4px; color: white; font-weight: bold; z-index: 10000; background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3'}; box-shadow: 0 4px 12px rgba(0,0,0,0.15); `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, 3000); } // Create schedule settings interface async function createScheduleSettings() { const colors = getThemeColors(); // Load data try { const [loadedSchedules, loadedWorkflows] = await Promise.all([ loadSchedules(), getWorkflowList() ]); schedules = loadedSchedules; workflows = loadedWorkflows; // Ensure at least 3 schedule slots while (schedules.length < 3) { schedules.push({ time: '', workflow: '', enabled: false }); } } catch (error) { console.error('Failed to load schedule data:', error); schedules = [{time: '', workflow: '', enabled: false}, {time: '', workflow: '', enabled: false}, {time: '', workflow: '', enabled: false}]; workflows = []; } // Create container const container = document.createElement('div'); container.style.cssText = ` padding: 15px; border: 2px solid ${colors.border}; border-radius: 8px; background: ${colors.surface}; color: ${colors.text}; margin: 8px 0; max-width: 100%; `; // Description const description = document.createElement('div'); description.innerHTML = `
ComfyUI-ScheduledTask/Workflow/ folder or rightclick save as task.