diff --git a/web/css/xyz_grid.css b/web/css/xyz_grid.css
new file mode 100644
index 0000000..cb8e78a
--- /dev/null
+++ b/web/css/xyz_grid.css
@@ -0,0 +1,247 @@
+/* XYZ Grid Styles */
+
+.xyz-value-selector {
+ position: relative;
+ margin: 5px 0;
+}
+
+.xyz-value-selector button {
+ transition: all 0.2s ease;
+}
+
+.xyz-value-selector button:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
+}
+
+.xyz-value-selector button:active {
+ transform: translateY(0);
+}
+
+/* Multi-select dialog animations */
+.xyz-multiselect-overlay {
+ animation: fadeIn 0.2s ease;
+}
+
+.xyz-multiselect-dialog {
+ animation: slideIn 0.2s ease;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateY(-20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Example helper styles */
+.xyz-example-helper {
+ background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px;
+ margin: 5px 0;
+ font-size: 11px;
+ color: #aaa;
+ position: relative;
+ overflow: hidden;
+}
+
+.xyz-example-helper::before {
+ content: "Example";
+ position: absolute;
+ top: 4px;
+ right: 8px;
+ font-size: 9px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.xyz-example-helper pre {
+ margin: 0;
+ font-family: 'Consolas', 'Monaco', monospace;
+ white-space: pre-wrap;
+ line-height: 1.4;
+}
+
+/* Value input validation styles */
+.xyz-value-input {
+ transition: border-color 0.2s ease;
+}
+
+.xyz-value-input.valid {
+ border-color: #4ecdc4 !important;
+}
+
+.xyz-value-input.invalid {
+ border-color: #ff6b6b !important;
+}
+
+/* Progress indicator */
+.xyz-progress-bar {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ height: 3px;
+ background: linear-gradient(90deg, #4ecdc4 0%, #44a3aa 100%);
+ transition: width 0.3s ease;
+ border-radius: 0 0 4px 4px;
+}
+
+/* Grid info display */
+.xyz-grid-info {
+ background: #2a2a2a;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px 12px;
+ margin: 5px 0;
+ font-size: 12px;
+ color: #ddd;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.xyz-grid-info .dimensions {
+ color: #4ecdc4;
+ font-weight: bold;
+}
+
+.xyz-grid-info .warning {
+ color: #ff6b6b;
+ font-size: 11px;
+}
+
+/* Checkbox styling in multi-select */
+.xyz-checkbox-option {
+ display: flex;
+ align-items: center;
+ padding: 6px 8px;
+ margin: 2px 0;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+}
+
+.xyz-checkbox-option:hover {
+ background-color: rgba(74, 144, 226, 0.1);
+}
+
+.xyz-checkbox-option input[type="checkbox"] {
+ margin-right: 10px;
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+}
+
+.xyz-checkbox-option label {
+ flex: 1;
+ cursor: pointer;
+ user-select: none;
+}
+
+/* Search input styling */
+.xyz-search-input {
+ width: 100%;
+ padding: 10px 12px;
+ margin-bottom: 12px;
+ background: #2a2a2a;
+ border: 1px solid #444;
+ border-radius: 4px;
+ color: #fff;
+ font-size: 14px;
+ transition: all 0.2s ease;
+}
+
+.xyz-search-input:focus {
+ outline: none;
+ border-color: #4a90e2;
+ box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
+}
+
+.xyz-search-input::placeholder {
+ color: #666;
+}
+
+/* Button group styling */
+.xyz-button-group {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.xyz-button-group button {
+ flex: 1;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.xyz-button-primary {
+ background: #4a90e2;
+ color: white;
+}
+
+.xyz-button-primary:hover {
+ background: #357abd;
+}
+
+.xyz-button-secondary {
+ background: #666;
+ color: white;
+}
+
+.xyz-button-secondary:hover {
+ background: #555;
+}
+
+/* Tooltip for parameter info */
+.xyz-param-tooltip {
+ position: absolute;
+ background: #1e1e1e;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 11px;
+ color: #ddd;
+ z-index: 10001;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ max-width: 300px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+.xyz-param-tooltip.visible {
+ opacity: 1;
+}
+
+.xyz-param-tooltip::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid #444;
+}
\ No newline at end of file
diff --git a/web/js/xyz_grid_controller.js b/web/js/xyz_grid_controller.js
index bf6575f..36348ba 100644
--- a/web/js/xyz_grid_controller.js
+++ b/web/js/xyz_grid_controller.js
@@ -1,5 +1,7 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
+import "./xyz_grid_widgets.js";
+import "./xyz_grid_ui.js";
// Helper function to trigger re-execution
async function queueNextExecution(nodeId, batchId) {
diff --git a/web/js/xyz_grid_ui.js b/web/js/xyz_grid_ui.js
new file mode 100644
index 0000000..5757148
--- /dev/null
+++ b/web/js/xyz_grid_ui.js
@@ -0,0 +1,304 @@
+import { app } from "../../scripts/app.js";
+
+// Load CSS
+const link = document.createElement("link");
+link.rel = "stylesheet";
+link.type = "text/css";
+link.href = "extensions/ComfyUI-KikoTools/css/xyz_grid.css";
+document.head.appendChild(link);
+
+// Parameter type information and examples
+export const parameterInfo = {
+ none: {
+ description: "No parameter selected",
+ example: null
+ },
+ model: {
+ description: "Checkpoint/Model files",
+ example: "Select from available models using the button below",
+ connection: "Connect to CheckpointLoaderSimple → ckpt_name",
+ outputType: "STRING"
+ },
+ vae: {
+ description: "VAE (Variational Autoencoder) models",
+ example: "Select from available VAEs or use 'Automatic'",
+ connection: "Connect to VAELoader → vae_name",
+ outputType: "STRING"
+ },
+ lora: {
+ description: "LoRA (Low-Rank Adaptation) models",
+ example: "Select LoRAs to apply (use 'None' for no LoRA)",
+ connection: "Connect to LoraLoader → lora_name",
+ outputType: "STRING"
+ },
+ sampler: {
+ description: "Sampling algorithms",
+ example: "Select from available samplers",
+ connection: "Connect to KSampler → sampler_name",
+ outputType: "STRING"
+ },
+ scheduler: {
+ description: "Noise schedulers",
+ example: "Select scheduler types",
+ connection: "Connect to KSampler → scheduler",
+ outputType: "STRING"
+ },
+ cfg_scale: {
+ description: "Classifier-Free Guidance scale",
+ example: "Examples:\n• Single values: 5, 7.5, 10, 12.5\n• Range: 5:15:2.5 (from 5 to 15, step 2.5)\n• Mixed: 1, 3, 5:10:1",
+ connection: "Connect y_float or x_float to KSampler → cfg",
+ outputType: "FLOAT",
+ range: { min: 0, max: 30, default: 7.5 }
+ },
+ steps: {
+ description: "Number of denoising steps",
+ example: "Examples:\n• List: 20, 30, 40, 50\n• Range: 10:50:10 (from 10 to 50, step 10)\n• Quick test: 10, 20, 50",
+ connection: "Connect y_int or x_int to KSampler → steps",
+ outputType: "INT",
+ range: { min: 1, max: 150, default: 20 }
+ },
+ clip_skip: {
+ description: "CLIP layers to skip from the end",
+ example: "Examples:\n• Common values: 1, 2\n• Full test: 1, 2, 3, 4\n• SDXL typically uses 1 or 2",
+ connection: "Connect to CLIPSetLastLayer → stop_at_clip_layer",
+ outputType: "INT",
+ range: { min: 1, max: 12, default: 1 }
+ },
+ seed: {
+ description: "Random seed for generation",
+ example: "Examples:\n• Specific seeds: 42, 123, 456, 789\n• Range: 0:1000:100\n• Random set: 12345, 67890, 11111",
+ connection: "Connect y_int or x_int to KSampler → seed",
+ outputType: "INT",
+ range: { min: 0, max: "0xffffffffffffffff", default: 0 }
+ },
+ denoise: {
+ description: "Denoising strength (0=no change, 1=full denoise)",
+ example: "Examples:\n• Light: 0.3, 0.5, 0.7\n• Full range: 0.2:1.0:0.2\n• img2img: 0.4, 0.6, 0.8, 1.0",
+ connection: "Connect y_float or x_float to KSampler → denoise",
+ outputType: "FLOAT",
+ range: { min: 0, max: 1, default: 1 }
+ },
+ flux_guidance: {
+ description: "Flux model guidance strength",
+ example: "Examples:\n• Low to high: 1.0, 2.0, 3.5, 5.0\n• Fine steps: 1:5:0.5\n• Recommended: 2.5, 3.5, 4.5",
+ connection: "Connect to FluxGuidance → guidance",
+ outputType: "FLOAT",
+ range: { min: 0, max: 10, default: 3.5 }
+ },
+ prompt: {
+ description: "Text prompts (one per line)",
+ example: "Enter different prompts, one per line:\n\na serene lake at sunset\na bustling city street\na mystical forest path\n\nTip: Keep prompts similar length for better grid layout",
+ connection: "Connect y_string or x_string to CLIPTextEncode → text",
+ outputType: "STRING"
+ }
+};
+
+// Create info panel for parameter type
+function createInfoPanel(node, paramType) {
+ const info = parameterInfo[paramType];
+ if (!info || paramType === 'none') return null;
+
+ const panel = document.createElement("div");
+ panel.className = "xyz-grid-info";
+ panel.innerHTML = `
+
+
${paramType.toUpperCase()}
+
${info.description}
+ ${info.connection ? `
↗ ${info.connection}
` : ''}
+
+ ${info.range ? `
+
+ Range: ${info.range.min}-${info.range.max}
+ Default: ${info.range.default}
+
+ ` : ''}
+ `;
+
+ return panel;
+}
+
+// Show example usage in a nice overlay
+function showExampleOverlay(paramType, targetElement) {
+ const info = parameterInfo[paramType];
+ if (!info || !info.example) return;
+
+ // Remove any existing overlay
+ const existing = document.querySelector('.xyz-example-overlay');
+ if (existing) existing.remove();
+
+ const overlay = document.createElement("div");
+ overlay.className = "xyz-example-overlay";
+ overlay.style.cssText = `
+ position: absolute;
+ background: #1e1e1e;
+ border: 2px solid #4a90e2;
+ border-radius: 8px;
+ padding: 15px;
+ z-index: 1000;
+ max-width: 400px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
+ `;
+
+ const title = document.createElement("h4");
+ title.style.cssText = "margin: 0 0 10px 0; color: #4a90e2;";
+ title.textContent = `${paramType.toUpperCase()} - Usage Examples`;
+
+ const content = document.createElement("pre");
+ content.style.cssText = `
+ margin: 0;
+ color: #ddd;
+ font-size: 12px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ `;
+ content.textContent = info.example;
+
+ const closeBtn = document.createElement("button");
+ closeBtn.textContent = "×";
+ closeBtn.style.cssText = `
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: none;
+ border: none;
+ color: #999;
+ font-size: 20px;
+ cursor: pointer;
+ `;
+ closeBtn.onclick = () => overlay.remove();
+
+ overlay.appendChild(title);
+ overlay.appendChild(content);
+ overlay.appendChild(closeBtn);
+
+ // Position near the target element
+ const rect = targetElement.getBoundingClientRect();
+ overlay.style.left = rect.left + "px";
+ overlay.style.top = (rect.bottom + 5) + "px";
+
+ document.body.appendChild(overlay);
+
+ // Auto-close on click outside
+ setTimeout(() => {
+ const closeOnClickOutside = (e) => {
+ if (!overlay.contains(e.target)) {
+ overlay.remove();
+ document.removeEventListener('click', closeOnClickOutside);
+ }
+ };
+ document.addEventListener('click', closeOnClickOutside);
+ }, 100);
+}
+
+// Enhance the value input widget based on parameter type
+export function enhanceValueWidget(node, widgetName, paramType) {
+ const widget = node.widgets.find(w => w.name === widgetName);
+ if (!widget || !widget.inputEl) return;
+
+ const info = parameterInfo[paramType];
+ if (!info) return;
+
+ // Add info panel above the input
+ const infoPanel = createInfoPanel(node, paramType);
+ if (infoPanel && widget.inputEl.parentNode) {
+ widget.inputEl.parentNode.insertBefore(infoPanel, widget.inputEl);
+ }
+
+ // Add example button if there's an example
+ if (info.example) {
+ const exampleBtn = document.createElement("button");
+ exampleBtn.textContent = "Show Examples";
+ exampleBtn.style.cssText = `
+ margin: 5px 0;
+ padding: 4px 10px;
+ background: #3a3a3a;
+ color: #aaa;
+ border: 1px solid #555;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 11px;
+ `;
+ exampleBtn.onclick = () => showExampleOverlay(paramType, widget.inputEl);
+
+ if (widget.inputEl.parentNode) {
+ widget.inputEl.parentNode.insertBefore(exampleBtn, widget.inputEl.nextSibling);
+ }
+ }
+
+ // Add validation for numeric types
+ if (['cfg_scale', 'steps', 'clip_skip', 'seed', 'denoise', 'flux_guidance'].includes(paramType)) {
+ widget.inputEl.addEventListener('input', () => {
+ const value = widget.inputEl.value.trim();
+ let isValid = true;
+
+ if (value) {
+ // Check for range syntax or comma-separated numbers
+ const rangePattern = /^\d+(\.\d+)?:\d+(\.\d+)?(:\d+(\.\d+)?)?$/;
+ const listPattern = /^[\d\.\s,]+$/;
+
+ isValid = rangePattern.test(value) || listPattern.test(value);
+ }
+
+ widget.inputEl.classList.toggle('xyz-value-input', true);
+ widget.inputEl.classList.toggle('valid', isValid);
+ widget.inputEl.classList.toggle('invalid', !isValid);
+ });
+ }
+}
+
+// Register UI enhancements
+app.registerExtension({
+ name: "ComfyAssets.XYZGridUI",
+
+ async nodeCreated(node) {
+ if (node.comfyClass === "XYZPlotController") {
+ setupUIEnhancements(node);
+ }
+ }
+});
+
+function setupUIEnhancements(node) {
+ // Monitor all axis type widgets
+ ['x_axis_type', 'y_axis_type', 'z_axis_type'].forEach(axisName => {
+ const widget = node.widgets.find(w => w.name === axisName);
+ if (!widget) return;
+
+ const axisPrefix = axisName.split('_')[0];
+ const valueWidgetName = `${axisPrefix}_values`;
+
+ // Enhance callback to update UI
+ const originalCallback = widget.callback;
+ widget.callback = function(value) {
+ if (originalCallback) {
+ originalCallback.call(this, value);
+ }
+
+ // Clear previous enhancements
+ const valueWidget = node.widgets.find(w => w.name === valueWidgetName);
+ if (valueWidget && valueWidget.inputEl) {
+ // Remove any info panels or buttons
+ const parent = valueWidget.inputEl.parentNode;
+ const infoPanels = parent.querySelectorAll('.xyz-grid-info');
+ const buttons = parent.querySelectorAll('button');
+ infoPanels.forEach(el => el.remove());
+ buttons.forEach(el => {
+ if (el.textContent === 'Show Examples') el.remove();
+ });
+ }
+
+ // Add new enhancements
+ if (value && value !== 'none') {
+ setTimeout(() => {
+ enhanceValueWidget(node, valueWidgetName, value);
+ }, 50);
+ }
+ };
+
+ // Trigger for initial values
+ if (widget.value && widget.value !== 'none') {
+ setTimeout(() => {
+ enhanceValueWidget(node, valueWidgetName, widget.value);
+ }, 100);
+ }
+ });
+}
\ No newline at end of file
diff --git a/web/js/xyz_grid_widgets.js b/web/js/xyz_grid_widgets.js
new file mode 100644
index 0000000..d66542e
--- /dev/null
+++ b/web/js/xyz_grid_widgets.js
@@ -0,0 +1,491 @@
+import { app } from "../../scripts/app.js";
+import { ComfyWidgets } from "../../scripts/widgets.js";
+
+// Helper to get available options for different parameter types
+const parameterHelpers = {
+ model: {
+ getOptions: async () => {
+ try {
+ const resp = await fetch('/api/models');
+ const data = await resp.json();
+ return data.checkpoints || [];
+ } catch (e) {
+ console.error("Failed to fetch models:", e);
+ return [];
+ }
+ },
+ placeholder: "Select models...",
+ multiselect: true
+ },
+
+ vae: {
+ getOptions: async () => {
+ try {
+ const resp = await fetch('/api/models');
+ const data = await resp.json();
+ return ["Automatic", ...(data.vae || [])];
+ } catch (e) {
+ return ["Automatic"];
+ }
+ },
+ placeholder: "Select VAEs...",
+ multiselect: true
+ },
+
+ lora: {
+ getOptions: async () => {
+ try {
+ const resp = await fetch('/api/models');
+ const data = await resp.json();
+ return ["None", ...(data.loras || [])];
+ } catch (e) {
+ return ["None"];
+ }
+ },
+ placeholder: "Select LoRAs...",
+ multiselect: true
+ },
+
+ sampler: {
+ getOptions: async () => {
+ // Get samplers from KSampler node definition
+ const nodeData = LiteGraph.registered_node_types["KSampler"];
+ if (nodeData && nodeData.nodeData && nodeData.nodeData.input) {
+ const samplerInput = nodeData.nodeData.input.required.sampler_name;
+ if (samplerInput && samplerInput[0]) {
+ return samplerInput[0];
+ }
+ }
+ // Fallback list
+ return ["euler", "euler_ancestral", "heun", "dpm_2", "dpm_2_ancestral",
+ "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral",
+ "dpmpp_sde", "dpmpp_2m", "dpmpp_2m_sde", "ddim", "uni_pc"];
+ },
+ placeholder: "Select samplers...",
+ multiselect: true
+ },
+
+ scheduler: {
+ getOptions: async () => {
+ const nodeData = LiteGraph.registered_node_types["KSampler"];
+ if (nodeData && nodeData.nodeData && nodeData.nodeData.input) {
+ const schedulerInput = nodeData.nodeData.input.required.scheduler;
+ if (schedulerInput && schedulerInput[0]) {
+ return schedulerInput[0];
+ }
+ }
+ return ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform"];
+ },
+ placeholder: "Select schedulers...",
+ multiselect: true
+ },
+
+ cfg_scale: {
+ example: "5, 7.5, 10, 12.5\n-- or --\n5:15:2.5",
+ placeholder: "Enter CFG values or range (start:stop:step)",
+ validate: (value) => {
+ // Check if it's a valid number list or range
+ const rangePattern = /^\d+(\.\d+)?:\d+(\.\d+)?(:\d+(\.\d+)?)?$/;
+ const listPattern = /^[\d\.\s,]+$/;
+ return rangePattern.test(value.trim()) || listPattern.test(value.trim());
+ }
+ },
+
+ steps: {
+ example: "20, 30, 40, 50\n-- or --\n10:50:10",
+ placeholder: "Enter step counts or range",
+ validate: (value) => {
+ const rangePattern = /^\d+:\d+(:\d+)?$/;
+ const listPattern = /^[\d\s,]+$/;
+ return rangePattern.test(value.trim()) || listPattern.test(value.trim());
+ }
+ },
+
+ clip_skip: {
+ example: "1, 2, 3, 4",
+ placeholder: "Enter clip skip values (typically 1-4)",
+ validate: (value) => {
+ const listPattern = /^[\d\s,]+$/;
+ return listPattern.test(value.trim());
+ }
+ },
+
+ seed: {
+ example: "42, 123, 456, 789\n-- or --\n0:1000:100",
+ placeholder: "Enter seed values or range",
+ validate: (value) => {
+ const rangePattern = /^\d+:\d+(:\d+)?$/;
+ const listPattern = /^[\d\s,]+$/;
+ return rangePattern.test(value.trim()) || listPattern.test(value.trim());
+ }
+ },
+
+ denoise: {
+ example: "0.5, 0.75, 1.0\n-- or --\n0.4:1.0:0.2",
+ placeholder: "Enter denoise values (0.0-1.0)",
+ validate: (value) => {
+ const rangePattern = /^\d+(\.\d+)?:\d+(\.\d+)?(:\d+(\.\d+)?)?$/;
+ const listPattern = /^[\d\.\s,]+$/;
+ return rangePattern.test(value.trim()) || listPattern.test(value.trim());
+ }
+ },
+
+ flux_guidance: {
+ example: "1.0, 2.0, 3.5, 5.0\n-- or --\n1:5:0.5",
+ placeholder: "Enter Flux guidance values",
+ validate: (value) => {
+ const rangePattern = /^\d+(\.\d+)?:\d+(\.\d+)?(:\d+(\.\d+)?)?$/;
+ const listPattern = /^[\d\.\s,]+$/;
+ return rangePattern.test(value.trim()) || listPattern.test(value.trim());
+ }
+ },
+
+ prompt: {
+ example: "a beautiful sunset over the ocean\na mystical forest at dawn\na futuristic city skyline\na cozy cottage in the mountains",
+ placeholder: "Enter prompts (one per line)",
+ multiline: true,
+ validate: (value) => value.trim().length > 0
+ }
+};
+
+// Create a custom widget for value selection
+function createValueSelectorWidget(node, widgetName, axisType) {
+ const widget = node.widgets.find(w => w.name === widgetName);
+ if (!widget) return;
+
+ const helper = parameterHelpers[axisType];
+ if (!helper) return;
+
+ // Store original widget
+ const originalWidget = widget;
+
+ // Create container for enhanced UI
+ const container = document.createElement("div");
+ container.className = "xyz-value-selector";
+ container.style.cssText = `
+ position: relative;
+ margin: 5px 0;
+ `;
+
+ if (helper.getOptions) {
+ // Create multi-select for file-based options
+ createMultiSelect(container, helper, originalWidget);
+ } else if (helper.example) {
+ // Show example for numeric/text inputs
+ createExampleHelper(container, helper, originalWidget);
+ }
+
+ // Replace widget element
+ if (widget.inputEl && widget.inputEl.parentNode) {
+ widget.inputEl.parentNode.insertBefore(container, widget.inputEl);
+ container.appendChild(widget.inputEl);
+ }
+}
+
+function createMultiSelect(container, helper, widget) {
+ // Create select button
+ const selectBtn = document.createElement("button");
+ selectBtn.textContent = "Select Values...";
+ selectBtn.style.cssText = `
+ margin: 5px 0;
+ padding: 5px 10px;
+ background: #4a90e2;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ `;
+
+ selectBtn.onclick = async () => {
+ const options = await helper.getOptions();
+ showMultiSelectDialog(options, widget, helper.placeholder);
+ };
+
+ container.appendChild(selectBtn);
+}
+
+function createExampleHelper(container, helper, widget) {
+ // Create example display
+ const exampleDiv = document.createElement("div");
+ exampleDiv.style.cssText = `
+ background: #2a2a2a;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px;
+ margin: 5px 0;
+ font-size: 11px;
+ color: #aaa;
+ display: none;
+ `;
+
+ const exampleText = document.createElement("pre");
+ exampleText.style.cssText = `
+ margin: 0;
+ font-family: monospace;
+ white-space: pre-wrap;
+ `;
+ exampleText.textContent = `Example:\n${helper.example}`;
+ exampleDiv.appendChild(exampleText);
+
+ // Show/hide example based on focus
+ widget.inputEl.addEventListener("focus", () => {
+ exampleDiv.style.display = "block";
+ });
+
+ widget.inputEl.addEventListener("blur", () => {
+ setTimeout(() => {
+ exampleDiv.style.display = "none";
+ }, 200);
+ });
+
+ // Update placeholder
+ if (helper.placeholder) {
+ widget.inputEl.placeholder = helper.placeholder;
+ }
+
+ // Add validation
+ if (helper.validate) {
+ widget.inputEl.addEventListener("input", () => {
+ const isValid = helper.validate(widget.inputEl.value);
+ widget.inputEl.style.borderColor = isValid ? "" : "#ff6b6b";
+ });
+ }
+
+ container.appendChild(exampleDiv);
+}
+
+function showMultiSelectDialog(options, widget, placeholder) {
+ // Create modal overlay
+ const overlay = document.createElement("div");
+ overlay.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `;
+
+ // Create dialog
+ const dialog = document.createElement("div");
+ dialog.style.cssText = `
+ background: #1e1e1e;
+ border: 2px solid #444;
+ border-radius: 8px;
+ padding: 20px;
+ max-width: 500px;
+ max-height: 70vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ `;
+
+ // Title
+ const title = document.createElement("h3");
+ title.textContent = placeholder || "Select Values";
+ title.style.cssText = "margin: 0 0 15px 0; color: #fff;";
+ dialog.appendChild(title);
+
+ // Search input
+ const searchInput = document.createElement("input");
+ searchInput.type = "text";
+ searchInput.placeholder = "Search...";
+ searchInput.style.cssText = `
+ width: 100%;
+ padding: 8px;
+ margin-bottom: 10px;
+ background: #2a2a2a;
+ border: 1px solid #444;
+ border-radius: 4px;
+ color: #fff;
+ `;
+ dialog.appendChild(searchInput);
+
+ // Options container
+ const optionsContainer = document.createElement("div");
+ optionsContainer.style.cssText = `
+ flex: 1;
+ overflow-y: auto;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 10px;
+ background: #2a2a2a;
+ `;
+
+ // Get current values
+ const currentValues = widget.value.split(',').map(v => v.trim()).filter(v => v);
+
+ // Create checkboxes
+ const checkboxes = [];
+ options.forEach(option => {
+ const label = document.createElement("label");
+ label.style.cssText = `
+ display: block;
+ padding: 5px;
+ cursor: pointer;
+ color: #ddd;
+ `;
+
+ const checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.value = option;
+ checkbox.checked = currentValues.includes(option);
+ checkbox.style.marginRight = "8px";
+
+ label.appendChild(checkbox);
+ label.appendChild(document.createTextNode(option));
+
+ checkboxes.push({ checkbox, label, option });
+ optionsContainer.appendChild(label);
+ });
+
+ dialog.appendChild(optionsContainer);
+
+ // Search functionality
+ searchInput.addEventListener("input", () => {
+ const search = searchInput.value.toLowerCase();
+ checkboxes.forEach(({ label, option }) => {
+ label.style.display = option.toLowerCase().includes(search) ? "block" : "none";
+ });
+ });
+
+ // Buttons
+ const buttonContainer = document.createElement("div");
+ buttonContainer.style.cssText = `
+ margin-top: 15px;
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ `;
+
+ const selectAllBtn = document.createElement("button");
+ selectAllBtn.textContent = "Select All";
+ selectAllBtn.style.cssText = `
+ padding: 8px 15px;
+ background: #666;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ `;
+ selectAllBtn.onclick = () => {
+ checkboxes.forEach(({ checkbox, label }) => {
+ if (label.style.display !== "none") {
+ checkbox.checked = true;
+ }
+ });
+ };
+
+ const clearBtn = document.createElement("button");
+ clearBtn.textContent = "Clear";
+ clearBtn.style.cssText = selectAllBtn.style.cssText;
+ clearBtn.onclick = () => {
+ checkboxes.forEach(({ checkbox }) => checkbox.checked = false);
+ };
+
+ const cancelBtn = document.createElement("button");
+ cancelBtn.textContent = "Cancel";
+ cancelBtn.style.cssText = selectAllBtn.style.cssText;
+ cancelBtn.onclick = () => document.body.removeChild(overlay);
+
+ const applyBtn = document.createElement("button");
+ applyBtn.textContent = "Apply";
+ applyBtn.style.cssText = `
+ padding: 8px 15px;
+ background: #4a90e2;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ `;
+ applyBtn.onclick = () => {
+ const selected = checkboxes
+ .filter(({ checkbox }) => checkbox.checked)
+ .map(({ checkbox }) => checkbox.value);
+ widget.value = selected.join(", ");
+ widget.callback?.(widget.value);
+ document.body.removeChild(overlay);
+ };
+
+ buttonContainer.appendChild(selectAllBtn);
+ buttonContainer.appendChild(clearBtn);
+ buttonContainer.appendChild(cancelBtn);
+ buttonContainer.appendChild(applyBtn);
+ dialog.appendChild(buttonContainer);
+
+ overlay.appendChild(dialog);
+ document.body.appendChild(overlay);
+
+ // Focus search
+ searchInput.focus();
+
+ // Close on escape
+ const handleEscape = (e) => {
+ if (e.key === "Escape") {
+ document.body.removeChild(overlay);
+ document.removeEventListener("keydown", handleEscape);
+ }
+ };
+ document.addEventListener("keydown", handleEscape);
+}
+
+// Register the extension
+app.registerExtension({
+ name: "ComfyAssets.XYZGridWidgets",
+
+ async nodeCreated(node) {
+ if (node.comfyClass === "XYZPlotController") {
+ // Wait for widgets to be created
+ setTimeout(() => {
+ setupEnhancedWidgets(node);
+ }, 100);
+ }
+ }
+});
+
+function setupEnhancedWidgets(node) {
+ const axisTypes = ['x_axis_type', 'y_axis_type', 'z_axis_type'];
+
+ axisTypes.forEach(axisType => {
+ const typeWidget = node.widgets.find(w => w.name === axisType);
+ if (!typeWidget) return;
+
+ const axisPrefix = axisType.split('_')[0]; // 'x', 'y', or 'z'
+ const valueWidgetName = `${axisPrefix}_values`;
+
+ // Monitor axis type changes
+ const originalCallback = typeWidget.callback;
+ typeWidget.callback = function(value) {
+ if (originalCallback) {
+ originalCallback.call(this, value);
+ }
+
+ // Clear any existing enhancements
+ const valueWidget = node.widgets.find(w => w.name === valueWidgetName);
+ if (valueWidget && valueWidget.inputEl) {
+ const container = valueWidget.inputEl.parentNode;
+ if (container && container.className === "xyz-value-selector") {
+ // Remove enhancements
+ while (container.firstChild !== valueWidget.inputEl) {
+ container.removeChild(container.firstChild);
+ }
+ while (container.lastChild !== valueWidget.inputEl) {
+ container.removeChild(container.lastChild);
+ }
+ }
+ }
+
+ // Add new enhancements based on type
+ if (value && value !== "none") {
+ setTimeout(() => {
+ createValueSelectorWidget(node, valueWidgetName, value);
+ }, 50);
+ }
+ };
+ });
+}
\ No newline at end of file