Files
gsusgg 25ba6bfbe1 Update (#1)
* feat: Improve prompt handling and UI layout and other fixes

* feat: Implement image zoom/pan and improve modal display

* feat: Enhance UI persistence and randomized input display

- Display randomized numbers in input fields instead of disabling them.
- Persist generated preview images across page navigations.
- Ensure randomized form data is saved and loaded from local storage.
- Update .gitignore to include Gemini documentation.
- Add package-lock.json for dependency consistency.

* fix: Improve gallery folder navigation and "Folder up" functionality

- Add "Folder up" button to gallery for easier navigation.
- Implement path normalization to correctly handle mixed path separators (forward and backward slashes).
- Fix issue where "Folder up" button would incorrectly navigate to root from nested folders.

* feat: Enhance batch image display and modal styling\n\n- Backend: Modify CozyGenOutput to send individual WebSocket messages for each image in a batch, enabling proper display of all generated images.\n- Frontend: Implement a responsive grid layout for batch image previews on the generate page ("mini gallery").\n- Frontend: Reduce padding of the image preview modal for a more compact look.\n- Frontend: Adjust gap-y styling in DynamicForm.jsx.\n- Workflow: Update TestingTemplate.json with new dynamic input configurations.

* feat: Add CozyGen_example.json workflow

* fixing dev

* feat: Remove bypass functionality and improve dynamic input handling

* docs: Update TechDoc.md with recent error resolutions

* feat: Implement frontend bypass control for CozyGenDynamicInput

This commit introduces the functionality to bypass nodes in the ComfyUI workflow
via the frontend UI. When the "bypass" toggle is enabled for a CozyGenDynamicInput,
the connected node (e.g., LoraLoader) is removed from the workflow, and its
upstream and downstream connections are rewired.

- Added logic in `js/src/pages/MainPage.jsx` to process bypassed dynamic inputs.
- Implemented specific rewiring for `LoraLoader` nodes (model and clip outputs).
- Included generic rewiring for other nodes (limited to single input/output scenarios).
- Updated frontend build artifacts.

* feat: Revert image preview to single image display

* Update README

* feat: Implement CozyGen Image Input node and related frontend/backend changes

* catchup

* feat: Implement image upload and refactor CozyGenImageInput

* feat: Implement image upload and refactor CozyGenImageInput

- Refactored CozyGenImageInput node to accept Base64 image data.
- Implemented frontend logic in MainPage.jsx for image upload and injecting Base64 into workflow.
- Updated ImageInput.jsx to handle Base64 images for CozyGenImageInput nodes.
- Resolved merge conflicts in .gitignore.

* feat: Improve frontend control grouping and image upload visibility

This commit addresses several issues on the CozyGen generate page:

- Resolves `NameError: name 'io' is not defined` in `nodes.py` by explicitly importing `BytesIO` within the `load_image` function. This ensures proper module access within ComfyUI's execution context.
- Refactors `js/src/pages/MainPage.jsx` to correctly group dynamic input controls. Instead of rendering a separate "controls" box for each input, all `CozyGenDynamicInput` nodes are now passed to a single `DynamicForm` component, resulting in a unified and organized control panel.
- Implements conditional rendering for the "Upload Image for Workflow" section in `js/src/pages/MainPage.jsx`. This section now only appears when the loaded workflow contains a `CozyGenImageInput` node, improving UI clarity and relevance.

* feat: Implement multi-image input and optimize image handling

This commit introduces robust support for multiple image inputs and optimizes image handling to prevent browser storage issues:

- Refactors `js/src/components/ImageInput.jsx` to directly handle image uploads to the backend. When an image is selected, it is now immediately uploaded, and only its filename (instead of the large Base64 string) is stored in the frontend's `formData`. This resolves the "Quota Exceeded" DOMException previously encountered.
- Updates `js/src/pages/MainPage.jsx` by removing the single `uploadedImageBase64` state and its associated logic. The `handleGenerate` function is modified to correctly retrieve and inject image filenames from `formData` into the workflow JSON for all `CozyGenImageInput` nodes.
- Adds a `param_name` widget to the `CozyGenImageInput` node in `nodes.py`, allowing for customizable titles for image input fields in the frontend. The `load_image` function in `nodes.py` has been updated to accept this new parameter.
- Includes a new workflow file.
- Updates frontend build artifacts.

* feat: Implement smart image resize and improve gallery UI

This commit introduces a smart image resize feature and enhances the gallery modal UI:

- Implements an automatic smart resize feature in `js/src/components/ImageInput.jsx`. When an image is uploaded, it is now automatically resized to fit within a 2048-pixel sum for its dimensions (width + height), while preserving its aspect ratio. This is controlled by a 'Smart Resize' toggle.
- Simplifies the gallery image modal in `js/src/pages/Gallery.jsx` by replacing the full workflow JSON display with a 'Copy Workflow to Clipboard' button. This provides a cleaner user experience while still allowing users to access the workflow data.
- Updates `js/src/pages/MainPage.jsx` and `nodes.py` to support the new image handling and resize logic, ensuring that `param_name` and `image_filename` are correctly passed to the backend.
- Updates frontend build artifacts.

* Fix: Image preview and redundant input box on generate page\n\n- Corrected image preview URL construction in ImageInput.jsx.\n- Removed redundant 'Image Input' title block from MainPage.jsx.\n- Updated TechDoc.md to reflect these changes.

* Feat: Compact Image Input control box\n\n- Redesigned the ImageInput component for a more compact layout.\n- Browse and resize options are now on the left, with a smaller inline thumbnail on the right.\n- Updated TechDoc.md to reflect this UI/UX improvement.

* Full commit for gallery and img2img changes

* Standardized image modal view

* Fixed modal width issue

* Actually fixed modal, for real this time.hopefully.

* Added pagination to gallery. Added image modal navigation.

* Cleanup for main
2025-09-11 23:25:29 -05:00

468 lines
20 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const controlsContainer = document.getElementById('controls');
const generateButton = document.getElementById('generate-button');
const previewContainer = document.getElementById('preview');
const workflowDropdown = document.getElementById('workflow-dropdown');
const viewGalleryButton = document.getElementById('view-gallery-button'); // New
// Modal elements
const imageModal = document.getElementById('image-modal');
const modalImage = document.getElementById('modal-image');
const closeButton = document.getElementsByClassName('close-button')[0];
// Event listener for image preview click
previewContainer.addEventListener('click', () => {
const currentImage = previewContainer.querySelector('img');
if (currentImage) {
modalImage.src = currentImage.src;
imageModal.style.display = 'flex';
}
});
// Event listener for close button
closeButton.addEventListener('click', () => {
imageModal.style.display = 'none';
});
// Event listener for clicking outside the image to close the modal
imageModal.addEventListener('click', (event) => {
if (event.target === imageModal) {
imageModal.style.display = 'none';
}
});
let workflow = null;
const client_id = Math.random().toString(36).substring(2);
let socket = null;
function setupWebSocket() {
socket = new WebSocket(`ws://${window.location.host}/ws`);
socket.onopen = () => {
console.log('WebSocket connection established.');
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("CozyGen: Raw WebSocket message received:", data); // Keep general log for now
if (data.type === 'cozygen_image_ready') { // Listen for our custom message
console.log("CozyGen: Custom image ready received:", data.data);
const imageUrl = data.data.image_url;
const img = document.createElement('img');
img.src = imageUrl;
previewContainer.innerHTML = '';
previewContainer.appendChild(img);
}
};
socket.onclose = () => {
console.log('WebSocket connection closed. Reconnecting...');
setTimeout(setupWebSocket, 1000);
};
}
// New function to populate the dropdown
async function populateWorkflowDropdown() {
try {
const response = await fetch('/cozygen/workflows');
if (!response.ok) {
throw new Error('Failed to load workflow list.');
}
const data = await response.json();
const workflows = data.workflows;
workflowDropdown.innerHTML = '';
if (workflows.length === 0) {
// Handle case with no workflows
const option = document.createElement('option');
option.textContent = 'No workflows found in /workflows folder';
option.disabled = true;
workflowDropdown.appendChild(option);
return null; // No default workflow
}
// Add all workflow options
workflows.forEach(wf => {
const option = document.createElement('option');
option.value = wf;
option.textContent = wf;
workflowDropdown.appendChild(option);
});
// Set initial selection to the first workflow
const defaultWorkflow = workflows[0];
workflowDropdown.value = defaultWorkflow;
return defaultWorkflow; // Return the name of the default workflow
} catch (error) {
console.error('Error populating workflow dropdown:', error);
workflowDropdown.innerHTML = '';
const option = document.createElement('option');
option.textContent = 'Error loading workflows';
option.disabled = true;
workflowDropdown.appendChild(option);
return null;
}
}
// Modified loadWorkflow to accept a filename
async function loadWorkflow(workflowFileName) {
if (!workflowFileName) {
controlsContainer.innerHTML = `<p>Please select a workflow.</p>`;
return;
}
try {
// ALWAYS fetch from the dedicated workflows endpoint for consistency.
const fetchUrl = `/cozygen/workflows/${workflowFileName}`;
const response = await fetch(fetchUrl);
if (!response.ok) {
throw new Error(`Failed to load workflow: ${workflowFileName}`);
}
workflow = await response.json();
renderControls();
} catch (error) {
console.error(error);
controlsContainer.innerHTML = `<p>Error loading workflow: ${workflowFileName}. Make sure it's a valid workflow JSON.</p>`;
}
}
function createParameterControl(id, node) {
const controlWrapper = document.createElement('div');
controlWrapper.className = 'control-wrapper';
const { title, type, default: defaultValue, min, max, choices: choicesString } = node.inputs;
const label = document.createElement('label');
label.textContent = title;
let input;
if (type === 'INT' || type === 'FLOAT') {
input = document.createElement('input');
input.type = 'number';
input.min = min;
input.max = max;
input.value = defaultValue;
if (type === 'FLOAT') input.step = '0.01';
} else if (type === 'DROPDOWN') {
input = document.createElement('select');
if (choicesString) {
choicesString.split(',').map(c => c.trim()).forEach(choice => {
const option = document.createElement('option');
option.value = choice;
option.textContent = choice;
input.appendChild(option);
});
}
input.value = defaultValue;
} else { // STRING
input = document.createElement('input');
input.type = 'text';
input.value = defaultValue;
}
input.dataset.nodeId = id;
controlWrapper.appendChild(label);
controlWrapper.appendChild(input);
return controlWrapper;
}
function createDynamicInputControl(id, node) {
const controlWrapper = document.createElement('div');
controlWrapper.className = 'control-wrapper';
const { param_name, param_type, default_value, min_value, max_value, choices, multiline, Multiline, add_randomize_toggle } = node.inputs;
const label = document.createElement('label');
label.textContent = param_name;
let input;
if (param_type === 'INT' || param_type === 'FLOAT') {
input = document.createElement('input');
input.type = 'number';
input.min = min_value;
input.max = max_value;
input.value = default_value;
if (param_type === 'FLOAT') input.step = '0.01';
} else if (param_type === 'DROPDOWN') {
input = document.createElement('select');
if (choices) {
choices.split(',').map(c => c.trim()).forEach(choice => {
const option = document.createElement('option');
option.value = choice;
option.textContent = choice;
input.appendChild(option);
});
}
input.value = default_value;
} else if (param_type === 'BOOLEAN') {
input = document.createElement('input');
input.type = 'checkbox';
input.checked = (default_value.toLowerCase() === 'true');
} else { // STRING
if (multiline || Multiline) {
input = document.createElement('textarea');
input.rows = 3;
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = (input.scrollHeight) + 'px';
});
} else {
input = document.createElement('input');
input.type = 'text';
}
input.value = default_value;
}
if (label && input) {
input.dataset.nodeId = id;
input.dataset.inputName = 'default_value';
controlWrapper.appendChild(label);
controlWrapper.appendChild(input);
}
if ((param_type === 'INT' || param_type === 'FLOAT') && add_randomize_toggle) {
const randomizeContainer = document.createElement('div');
randomizeContainer.className = 'randomize-container'; // Use a more generic class name
const randomizeCheckbox = document.createElement('input');
randomizeCheckbox.type = 'checkbox';
randomizeCheckbox.id = `randomize-control-${id}`; // More generic ID
randomizeCheckbox.dataset.nodeId = id;
randomizeCheckbox.checked = false;
const randomizeLabel = document.createElement('label');
randomizeLabel.htmlFor = `randomize-control-${id}`;
randomizeLabel.textContent = 'Randomize'; // Simpler label
randomizeContainer.appendChild(randomizeCheckbox);
randomizeContainer.appendChild(randomizeLabel);
controlWrapper.appendChild(randomizeContainer);
}
return controlWrapper;
}
function renderControls() {
if (!workflow) return;
controlsContainer.innerHTML = '';
// Get all nodes that should be rendered as controls
const parameterNodes = Object.entries(workflow).filter(([, node]) => node.class_type === 'CozyGenParameter');
const dynamicInputNodes = Object.entries(workflow).filter(([, node]) => node.class_type === 'CozyGenDynamicInput');
// Combine them into a single list
const allControlNodes = [...parameterNodes, ...dynamicInputNodes];
// Sort the combined list by priority. Both node types are expected to have a 'priority' input.
allControlNodes.sort(([, a], [, b]) => {
const priorityA = a.inputs.priority || 0;
const priorityB = b.inputs.priority || 0;
return priorityA - priorityB;
});
// Process the sorted list to render controls, grouping where necessary
let i = 0;
while (i < allControlNodes.length) {
const [id, node] = allControlNodes[i];
const classType = node.class_type;
const type = (classType === 'CozyGenParameter') ? node.inputs.type : node.inputs.param_type;
let group = [];
if (type === 'INT' || type === 'FLOAT') {
// Check for a consecutive group of number inputs
for (let j = i; j < allControlNodes.length; j++) {
const [, groupNode] = allControlNodes[j];
const groupClassType = groupNode.class_type;
const groupType = (groupClassType === 'CozyGenParameter') ? groupNode.inputs.type : groupNode.inputs.param_type;
if (['INT', 'FLOAT'].includes(groupType)) {
group.push(allControlNodes[j]);
} else {
break;
}
}
}
if (group.length > 1) {
// Render a group of controls side-by-side
const groupContainer = document.createElement('div');
groupContainer.className = 'control-group';
group.forEach(([groupId, groupNode]) => {
const createFn = (groupNode.class_type === 'CozyGenParameter') ? createParameterControl : createDynamicInputControl;
groupContainer.appendChild(createFn(groupId, groupNode));
});
controlsContainer.appendChild(groupContainer);
i += group.length; // Move index past the group
} else {
// Render a single, full-width control
const createFn = (classType === 'CozyGenParameter') ? createParameterControl : createDynamicInputControl;
const controlElement = createFn(id, node);
controlElement.classList.add('full-width');
controlsContainer.appendChild(controlElement);
i++; // Move to the next item
}
}
}
async function queuePrompt() {
if (!workflow) return;
// Create a deep copy of the workflow to avoid modifying the original
const workflowToSend = JSON.parse(JSON.stringify(workflow));
// Collect current parameter values from UI
const parameterValues = {};
// Select all relevant input elements
const inputs = controlsContainer.querySelectorAll('input[type="text"], input[type="number"], input[type="checkbox"], select, textarea');
inputs.forEach(input => {
const nodeId = input.dataset.nodeId;
const inputName = input.dataset.inputName || 'default'; // For CozyGenParameter, it's 'default', for CozyGenDynamicInput, it's 'default_value'
const node = workflow[nodeId];
let nodeType;
if (node.class_type === 'CozyGenDynamicInput') {
nodeType = node.inputs.param_type;
} else {
nodeType = node.inputs.type;
}
let value;
if (input.type === 'checkbox') {
value = input.checked;
} else {
value = input.value;
}
// Special handling for randomization
if (node.class_type === 'CozyGenDynamicInput' && (node.inputs.param_type === 'INT' || node.inputs.param_type === 'FLOAT')) {
const randomizeCheckbox = document.getElementById(`randomize-control-${nodeId}`);
if (randomizeCheckbox && randomizeCheckbox.checked) {
value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
// Update the input field with the new random value
const numberInput = document.querySelector(`input[data-node-id="${nodeId}"][data-input-name="default_value"]`);
if(numberInput) numberInput.value = value;
}
}
// Convert value to correct type based on node definition
if (nodeType === 'INT') {
value = parseInt(value);
}
else if (nodeType === 'FLOAT') {
value = parseFloat(value);
}
else if (nodeType === 'BOOLEAN') {
value = (value === true || value === "true");
}
// DROPDOWN and STRING values are already strings
// Store the value, keyed by nodeId and inputName
if (!parameterValues[nodeId]) {
parameterValues[nodeId] = {};
}
parameterValues[nodeId][inputName] = value;
});
// Iterate through the workflow to inject parameter values and remove CozyGenParameter nodes
const nodesToDelete = [];
for (const nodeId in workflowToSend) {
const node = workflowToSend[nodeId];
// If it's a CozyGenParameter node, mark it for a deletion
if (node.class_type === 'CozyGenParameter') {
nodesToDelete.push(nodeId);
continue; // Skip processing inputs for CozyGenParameter nodes
}
// If it's a CozyGenDynamicInput node, update its default_value input
if (node.class_type === 'CozyGenDynamicInput') {
if (parameterValues[nodeId] && parameterValues[nodeId].default_value !== undefined) {
node.inputs.default_value = parameterValues[nodeId].default_value;
}
// No need to delete CozyGenDynamicInput nodes, as they remain in the workflow
continue; // Skip processing inputs for CozyGenDynamicInput nodes here
}
// Iterate through inputs of other nodes to replace CozyGenParameter references
if (node.inputs) {
for (const inputName in node.inputs) {
const inputValue = node.inputs[inputName];
// Check if the input value is a reference to a CozyGenParameter or CozyGenDynamicInput node
if (Array.isArray(inputValue) && inputValue.length === 2) {
const referencedNodeId = inputValue[0];
const referencedNode = workflowToSend[referencedNodeId];
if (referencedNode && referencedNode.class_type === 'CozyGenParameter' && parameterValues.hasOwnProperty(referencedNodeId)) {
// Replace the reference with the actual value from CozyGenParameter
node.inputs[inputName] = parameterValues[referencedNodeId].default;
} else if (referencedNode && referencedNode.class_type === 'CozyGenDynamicInput' && parameterValues.hasOwnProperty(referencedNodeId)) {
// Replace the reference with the actual value from CozyGenDynamicInput
node.inputs[inputName] = parameterValues[referencedNodeId].default_value;
}
}
}
}
}
// Delete CozyGenParameter nodes from the workflowToSend
nodesToDelete.forEach(id => {
delete workflowToSend[id];
});
// Fix for CozyGenOutput node: rename 'image' input to 'images'
for (const nodeId in workflowToSend) {
const node = workflowToSend[nodeId];
if (node.class_type === 'CozyGenOutput' && node.inputs && node.inputs.image) {
node.inputs.images = node.inputs.image;
delete node.inputs.image;
}
}
const prompt = {
prompt: workflowToSend,
client_id: client_id
};
console.log('Prompt object being sent:', prompt); // Log the prompt object
try {
const response = await fetch('/prompt', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(prompt)
});
if (!response.ok) {
throw new Error('Failed to queue prompt');
}
const data = await response.json();
console.log('Prompt queued:', data);
} catch (error) {
console.error(error);
}
}
generateButton.addEventListener('click', queuePrompt);
workflowDropdown.addEventListener('change', (event) => {
loadWorkflow(event.target.value);
});
viewGalleryButton.addEventListener('click', () => {
window.location.href = 'gallery.html';
});
async function initializeApp() {
const defaultWorkflow = await populateWorkflowDropdown();
if (defaultWorkflow) {
await loadWorkflow(defaultWorkflow);
}
setupWebSocket();
}
initializeApp();
});