Files
0velia-ComfyUI-Dynamic-Drop…/generator/generator.html
T
2026-07-10 23:29:28 +07:00

268 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI Dynamic Dropdowns Node Generator</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 0; padding: 20px; }
.container { max-width: 850px; margin: 0 auto; background: #252538; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
h1, h2 { color: #89b4fa; margin-top: 0; }
.meta-group { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 25px; }
.form-group { margin-bottom: 15px; flex-direction: column; display: flex; }
label { margin-bottom: 5px; font-weight: bold; font-size: 0.9em; color: #a6adc8; }
input { padding: 10px; background: #313244; border: 1px solid #45475a; color: #cdd6f4; border-radius: 4px; font-size: 1rem; }
input:focus { border-color: #89b4fa; outline: none; }
.dropdown-item { background: #11111b; padding: 15px; border-radius: 6px; margin-bottom: 15px; border-left: 4px solid #f38ba8; position: relative; }
.dropdown-item .remove-btn { position: absolute; right: 15px; top: 15px; background: #f38ba8; color: #11111b; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-weight: bold; }
.upload-row { display: flex; gap: 10px; align-items: center; }
.upload-row input[type="text"] { flex-grow: 1; }
.file-label { display: inline-block; padding: 10px 15px; background: #cdd6f4; color: #11111b; font-weight: bold; border-radius: 4px; cursor: pointer; text-align: center; }
.file-input { display: none; }
.btn { padding: 12px 20px; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; font-size: 1rem; transition: background 0.2s; }
.btn-add { background: #a6e3a1; color: #11111b; margin-bottom: 20px; }
.btn-build { background: #89b4fa; color: #11111b; width: 100%; font-size: 1.1em; margin-top: 10px; margin-bottom: 20px; }
.btn-action { background: #a6e3a1; color: #11111b; margin-right: 10px; }
.btn-download { background: #abe9b3; color: #11111b; }
.btn:hover { opacity: 0.9; }
.action-group { display: none; margin-top: 15px; }
pre { background: #11111b; padding: 15px; border-radius: 6px; overflow-x: auto; max-height: 400px; border: 1px solid #45475a; margin-top: 10px; }
code { font-family: 'Courier New', Courier, monospace; color: #a6e3a1; }
/* Help Instructions Styles */
.help-panel { background: #181825; border: 1px solid #45475a; border-radius: 6px; padding: 20px; margin-bottom: 30px; }
.help-panel ul { padding-left: 20px; margin: 5px 0; }
.help-panel li { margin-bottom: 8px; color: #cdd6f4; }
.example-box { background: #313244; padding: 10px; border-radius: 4px; font-family: monospace; color: #f9e2af; margin-top: 5px; white-space: pre-line; }
</style>
</head>
<body>
<div class="container">
<h1>Universal Dropdown Node Generator</h1>
<div class="help-panel">
<h2 style="color: #f9e2af; font-size: 1.2rem; margin-bottom: 10px;">📋 Self-Contained Folder Structure & Formatting</h2>
<p style="font-size: 0.95em; color: #a6adc8;">The generated node aggregates all dropdown choices into a single comma-separated output. The node scans a subfolder named <code>lists</code> inside your custom node directory:</p>
<div class="example-box" style="color: #a6e3a1;">ComfyUI/
└── custom_nodes/
└── ComfyUI-Dynamic-Dropdowns/
├── your_node_file.py
└── lists/
├── hair_length.txt
├── hair_color.txt
└── hairstyle.txt</div>
<ul style="margin-top: 15px;">
<li><strong>Formatting Rule:</strong> Your text files must feature exactly <strong>one option per line</strong>.</li>
<li><strong>Restrictions:</strong> No empty lines, no double quotes (<code>"</code>), and no backslashes (<code>\</code>). Validated on upload.</li>
</ul>
</div>
<div class="meta-group">
<div class="form-group">
<label for="displayName">UI Display Name</label>
<input type="text" id="displayName" value="Universal Text Selector" oninput="autoGenerateClassName(this.value)">
</div>
<div class="form-group">
<label for="nodeName">Python Class Name (Automatically Generated)</label>
<input type="text" id="nodeName" value="UniversalTextSelectorNode" readonly style="background: #2b2b3c; color: #b4befe; cursor: not-allowed;">
</div>
</div>
<h2>Dropdown Elements</h2>
<div id="dropdownsContainer"></div>
<button class="btn btn-add" onclick="addDropdown()">+ Add Dropdown Menu</button>
<button class="btn btn-build" onclick="generateNodeCode()">Build Custom Node</button>
<div id="resultSection" class="action-group">
<h2>Generated Code</h2>
<button class="btn btn-action" onclick="copyCode()">Copy Code</button>
<button class="btn btn-action btn-download" onclick="downloadCode()">Download Code</button>
<pre><code id="codeOutput"></code></pre>
</div>
</div>
<script>
let dropdownCount = 0;
function autoGenerateClassName(val) {
let sanitized = val.replace(/[^a-zA-Z0-9 ]/g, '');
let camelCase = sanitized.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('');
if(!camelCase.endsWith('Node')) {
camelCase += 'Node';
}
document.getElementById('nodeName').value = camelCase || "MyDropdownNode";
}
function addDropdown() {
dropdownCount++;
const container = document.getElementById('dropdownsContainer');
const div = document.createElement('div');
div.className = 'dropdown-item';
div.id = `item_${dropdownCount}`;
div.innerHTML = `
<button class="remove-btn" onclick="removeDropdown(${dropdownCount})">X</button>
<div class="form-group" style="margin-bottom: 10px;">
<label>Dropdown Label Name (e.g., hair_length, hair_color)</label>
<input type="text" class="input-label" value="attribute_${dropdownCount}" oninput="updateFileName(this)">
</div>
<div class="form-group">
<label>Associated Text File Name (Must match your file in custom_node/lists/)</label>
<div class="upload-row">
<input type="text" class="input-file" value="attribute_${dropdownCount}.txt">
<label class="file-label">
Upload
<input type="file" accept=".txt" class="file-input" onchange="handleFileUpload(this, ${dropdownCount})">
</label>
</div>
</div>
`;
container.appendChild(div);
}
function removeDropdown(id) {
document.getElementById(`item_${id}`).remove();
}
function updateFileName(inputElement) {
const item = inputElement.closest('.dropdown-item');
const fileInput = item.querySelector('.input-file');
const sanitized = inputElement.value.toLowerCase().replace(/[^a-z0-9_]/g, '_');
fileInput.value = sanitized ? `${sanitized}.txt` : '';
}
function handleFileUpload(fileInput, id) {
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const textContent = e.target.result;
const lines = textContent.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const currentLine = lines[i].trim();
if (lines[i] === "" && i < lines.length - 1) {
alert(`❌ File Rejected! Empty line detected on line ${i + 1}.`);
fileInput.value = "";
return;
}
if (currentLine.includes('"') || currentLine.includes('\\')) {
alert(`❌ File Rejected! Illegal character (quotes or backslash) on line ${i + 1}.`);
fileInput.value = "";
return;
}
}
const fileName = file.name;
const item = document.getElementById(`item_${id}`);
item.querySelector('.input-file').value = fileName;
const baseName = fileName.replace(/\.[^/.]+$/, "");
const cleanLabel = baseName.toLowerCase().replace(/[^a-z0-9_]/g, '_');
item.querySelector('.input-label').value = cleanLabel;
};
reader.readAsText(file);
}
}
function generateNodeCode() {
const nodeName = document.getElementById('nodeName').value;
const displayName = document.getElementById('displayName').value;
const items = document.querySelectorAll('.dropdown-item');
if(items.length === 0) {
alert("Please add at least one dropdown element before building.");
return;
}
let configLines = [];
let readLines = [' "clip": ("CLIP",),'];
let params = ['clip'];
let variableNames = [];
items.forEach(item => {
const label = item.querySelector('.input-label').value.trim().replace(/[^a-zA-Z0-9_]/g, '_');
const file = item.querySelector('.input-file').value.trim();
configLines.push(` # Setup list for ${label}\n ${label}_list = ["default_val"]\n ${label}_path = os.path.join(lists_dir, "${file}")\n if os.path.exists(${label}_path):\n with open(${label}_path, "r", encoding="utf-8") as f:\n lines = [l.strip() for l in f.readlines() if l.strip()]\n if lines: ${label}_list = lines`);
readLines.push(` "${label}": (${label}_list, {"default": ${label}_list[0]}),`);
params.push(label);
variableNames.push(label);
});
const pythonCode = `import os
class ${nodeName}:
@classmethod
def INPUT_TYPES(cls):
current_dir = os.path.dirname(os.path.realpath(__file__))
lists_dir = os.path.join(current_dir, "lists")
if not os.path.exists(lists_dir):
os.makedirs(lists_dir, exist_ok=True)
${configLines.join('\n\n')}
return {
"required": {
${readLines.join('\n')}
}
}
RETURN_TYPES = ("STRING", "CONDITIONING",)
RETURN_NAMES = ("PROMPT_STRING", "CONDITIONING",)
FUNCTION = "process_selection"
CATEGORY = "Custom Selection"
def process_selection(self, ${params.join(', ')}):
selections = [${variableNames.join(', ')}]
combined_text = ", ".join([str(item) for item in selections if item])
tokens = clip.tokenize(combined_text)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
conditioning_output = [[cond, {"pooled_output": pooled}]]
return (combined_text, conditioning_output,)
NODE_CLASS_MAPPINGS = {
"${nodeName}": ${nodeName}
}
NODE_DISPLAY_NAME_MAPPINGS = {
"${nodeName}": "${displayName}"
}
`;
document.getElementById('codeOutput').textContent = pythonCode;
document.getElementById('resultSection').style.display = 'block';
}
function copyCode() {
const codeText = document.getElementById('codeOutput').textContent;
navigator.clipboard.writeText(codeText).then(() => {
alert("Code copied to clipboard!");
});
}
function downloadCode() {
const codeText = document.getElementById('codeOutput').textContent;
const nodeName = document.getElementById('nodeName').value;
const blob = new Blob([codeText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${nodeName.toLowerCase()}.py`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
addDropdown();
</script>
</body>
</html>