421 lines
14 KiB
JavaScript
421 lines
14 KiB
JavaScript
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 = '<h3 style="margin: 0 0 15px 0; color: var(--content-fg);">Save API Workflow</h3>' +
|
|
'<div style="margin-bottom: 15px;">' +
|
|
'<label style="display: block; margin-bottom: 5px; color: var(--content-fg);">Workflow name:</label>' +
|
|
'<input type="text" id="workflow-name" placeholder="Enter workflow name" style="width: 100%; padding: 8px; box-sizing: border-box;" required>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label style="display: flex; align-items: center; color: var(--content-fg);">' +
|
|
'<input type="checkbox" id="overwrite-checkbox" style="margin-right: 8px;">' +
|
|
'Overwrite if exists' +
|
|
'</label>' +
|
|
'</div>';
|
|
|
|
// 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 = '<h3 style="margin: 0 0 15px 0; color: ' + color + '; text-align: center;">' + title + '</h3>' +
|
|
'<p style="margin: 0; text-align: center; color: var(--content-fg);">' + message + '</p>';
|
|
|
|
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) =>
|
|
'<div style="margin-bottom: 8px;">' +
|
|
'<label style="display: flex; align-items: center; color: var(--content-fg); cursor: pointer;">' +
|
|
'<input type="radio" name="field-selection" value="' + name + '" style="margin-right: 8px;">' +
|
|
name +
|
|
'</label>' +
|
|
'</div>'
|
|
).join('');
|
|
|
|
const html = '<h3 style="margin: 0 0 15px 0; color: var(--content-fg);">Select Input Field</h3>' +
|
|
'<div id="radio-container">' +
|
|
options +
|
|
'</div>';
|
|
|
|
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 = '<h3 style="margin: 0 0 15px 0; color: var(--content-fg);">Set Variable Name for `' + fieldName + '`</h3>' +
|
|
'<div>' +
|
|
'<input type="text" id="var-name" placeholder="Enter variable name" style="width: 100%; padding: 8px; box-sizing: border-box;" required>' +
|
|
'</div>';
|
|
|
|
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);
|
|
},
|
|
})
|