import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { $el } from "../../scripts/ui.js"; function formatTitle(text) { return `🚀 ${text}`; } // Show save workflow dialog using ComfyUI native dialog async function showSaveWorkflowDialog() { return new Promise((resolve) => { const html = '

Save API Workflow

' + '
' + '' + '' + '
' + '
' + '' + '
'; // Use existing dialog instance const dialog = app.ui.dialog; // Show the dialog with HTML content dialog.show(html); // Replace the default buttons with our custom buttons const modalContent = dialog.element.querySelector('.comfy-modal-content'); const defaultButtons = modalContent.querySelectorAll('button'); defaultButtons.forEach(btn => btn.remove()); // Handle cancel const handleCancel = () => { dialog.close(); resolve(null); }; // Handle save const handleSave = () => { const nameInput = dialog.element.querySelector('#workflow-name'); const overwriteCheckbox = dialog.element.querySelector('#overwrite-checkbox'); const name = nameInput.value.trim(); if (!name) { alert('Please enter a workflow name'); nameInput.focus(); return; } dialog.close(); resolve({ name: name, overwrite: overwriteCheckbox.checked }); }; // Create our custom button container using $el const buttonContainer = $el('div', { style: { display: 'flex', gap: '10px', justifyContent: 'flex-end' } }, [ $el('button', { textContent: 'Cancel', style: { padding: '8px 16px' }, onclick: handleCancel }), $el('button', { textContent: 'Save', style: { padding: '8px 16px' }, onclick: handleSave }) ]); modalContent.appendChild(buttonContainer); // Get elements from the dialog const nameInput = dialog.element.querySelector('#workflow-name'); // Focus on name input setTimeout(() => nameInput?.focus(), 100); // Handle Enter key in name input nameInput?.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); handleSave(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } }); }); } // Call save API async function saveWorkflowApi(name, workflow, overwrite) { const response = await api.fetchApi('/oneapi/v1/save-api-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, workflow: workflow, overwrite: overwrite }) }); const responseData = await response.json(); return { response, responseData }; } // Show result message using ComfyUI native dialog function showResultMessage(success, message) { const title = success ? 'Save Successful' : 'Save Failed'; const color = success ? '#28a745' : '#dc3545'; const html = '

' + title + '

' + '

' + message + '

'; const dialog = app.ui.dialog; // Show the dialog dialog.show(html); // Replace the default buttons with our custom button const modalContent = dialog.element.querySelector('.comfy-modal-content'); const defaultButtons = modalContent.querySelectorAll('button'); defaultButtons.forEach(btn => btn.remove()); // Create our custom button container using $el const buttonContainer = $el('div', { style: { display: 'flex', justifyContent: 'center' } }, [ $el('button', { textContent: 'OK', style: { padding: '8px 16px' }, onclick: () => dialog.close(), $: (btn) => setTimeout(() => btn.focus(), 100) }) ]); modalContent.appendChild(buttonContainer); } // Main function to save workflow as API async function saveWorkflowAsAPI() { try { // Get workflow data const result = await app.graphToPrompt(); const workflow = result.output; // Show save dialog const formValues = await showSaveWorkflowDialog(); if (!formValues) return; const { name, overwrite } = formValues; // Call save API const { response, responseData } = await saveWorkflowApi(name, workflow, overwrite); // Show result if (response.ok) { showResultMessage(true, `Workflow saved as: ${responseData.filename}`); } else { showResultMessage(false, responseData.error || 'Unknown error'); } } catch (error) { console.error('Error saving workflow:', error); showResultMessage(false, 'Error occurred while saving workflow: ' + error.message); } } function addCanvasMenuOptions() { const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; LGraphCanvas.prototype.getCanvasMenuOptions = function () { // get the basic options const originOptions = original_getCanvasMenuOptions.apply(this, arguments); const addedOptions = [ { content: formatTitle("Save Workflow as API"), callback: saveWorkflowAsAPI }, null ] return [...addedOptions, ...originOptions]; // return originOptions; } } function addExtraMenuOptions(nodeType, nodeData, app) { const nodeTypeClass = nodeType?.comfyClass; const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { original_getExtraMenuOptions?.apply(this, arguments); options.splice(0, 0, { content: formatTitle("Set Node Input"), callback: async () => { const selectedNodes = app.canvas.selected_nodes; const selectedNodeIds = Object.keys(selectedNodes); if (selectedNodeIds.length > 1) { alert('Please select only one node'); return; } const nodeId = selectedNodeIds[0]; const node = selectedNodes[nodeId]; const fieldNames = node.widgets.map(e => e.name); // Show field selection dialog const fieldName = await showFieldSelectionDialog(fieldNames); if (!fieldName) return; // Show variable name input dialog const varName = await showVariableNameDialog(fieldName); if (!varName) return; addVar2Node(node, varName, fieldName); } }) } } // Show field selection dialog async function showFieldSelectionDialog(fieldNames) { return new Promise((resolve) => { const options = fieldNames.map((name, index) => '
' + '' + '
' ).join(''); const html = '

Select Input Field

' + '
' + options + '
'; const dialog = app.ui.dialog; // Show the dialog dialog.show(html); // Replace the default buttons with our custom buttons const modalContent = dialog.element.querySelector('.comfy-modal-content'); const defaultButtons = modalContent.querySelectorAll('button'); defaultButtons.forEach(btn => btn.remove()); const handleCancel = () => { dialog.close(); resolve(null); }; const handleOK = () => { const selectedRadio = dialog.element.querySelector('input[name="field-selection"]:checked'); if (!selectedRadio) { alert('Please select a field'); return; } dialog.close(); resolve(selectedRadio.value); }; // Create our custom button container using $el const buttonContainer = $el('div', { style: { display: 'flex', gap: '10px', justifyContent: 'flex-end' } }, [ $el('button', { textContent: 'Cancel', style: { padding: '8px 16px' }, onclick: handleCancel }), $el('button', { textContent: 'OK', style: { padding: '8px 16px' }, onclick: handleOK }) ]); modalContent.appendChild(buttonContainer); // Auto-select first option and focus on it setTimeout(() => { const firstRadio = dialog.element.querySelector('input[name="field-selection"]'); if (firstRadio) { firstRadio.checked = true; firstRadio.focus(); } }, 100); // 基于 radio 按钮父控件监听键盘事件 const radioContainer = dialog.element.querySelector('#radio-container'); radioContainer?.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); handleOK(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } }); }); } // Show variable name input dialog async function showVariableNameDialog(fieldName) { return new Promise((resolve) => { const html = '

Set Variable Name for `' + fieldName + '`

' + '
' + '' + '
'; const dialog = app.ui.dialog; // Show the dialog dialog.show(html); // Replace the default buttons with our custom buttons const modalContent = dialog.element.querySelector('.comfy-modal-content'); const defaultButtons = modalContent.querySelectorAll('button'); defaultButtons.forEach(btn => btn.remove()); const handleCancel = () => { dialog.close(); resolve(null); }; const handleOK = () => { const varNameInput = dialog.element.querySelector('#var-name'); const varName = varNameInput.value.trim(); if (!varName) { alert('Please enter a variable name'); varNameInput?.focus(); return; } dialog.close(); resolve(varName); }; // Create our custom button container using $el const buttonContainer = $el('div', { style: { display: 'flex', gap: '10px', justifyContent: 'flex-end' } }, [ $el('button', { textContent: 'Cancel', style: { padding: '8px 16px' }, onclick: handleCancel }), $el('button', { textContent: 'OK', style: { padding: '8px 16px' }, onclick: handleOK }) ]); modalContent.appendChild(buttonContainer); // Get elements const varNameInput = dialog.element.querySelector('#var-name'); setTimeout(() => varNameInput?.focus(), 100); // 基于文本输入框监听键盘事件 varNameInput?.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); handleOK(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } }); }); } function addVar2Node(node, varName, fieldName) { const newPart = '$' + varName + '.' + fieldName; // Split and remove existing items with the same fieldName let parts = node.title.split(',').map(item => item.trim()); parts = parts.filter(item => { // Match $xxx.fieldName const match = item.match(/^\$[^.]+\.(.+)$/); // Keep items that are not the current fieldName, or not in $xxx.fieldName format return !(match && match[1] === fieldName); }); // Add the new one parts.push(newPart); // Rejoin node.title = parts.filter(Boolean).join(','); } app.registerExtension({ name: "ComfyUI-OneAPI-ConvertUIToAPI", async setup() { // Custom canvas background right-click menu addCanvasMenuOptions(); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { // Custom node right-click menu addExtraMenuOptions(nodeType, nodeData, app); }, })