/** * ComfyUI Mobile Interface - Mobile UI Components * Handles mobile-specific UI interactions and components */ class MobileInterface { constructor(apiClient) { this.apiClient = apiClient; this.linearization = new GraphLinearization(); this.currentWorkflow = null; this.linearizedNodes = []; this.selectedNode = null; this.connectionState = { active: false, sourceNode: null, sourceSocket: null }; this.initialize(); } /** * Initialize mobile interface */ initialize() { this.setupEventListeners(); this.setupAPIEventListeners(); this.loadInitialWorkflow(); } /** * Setup UI event listeners */ setupEventListeners() { // Header actions document.getElementById('toggleView').addEventListener('click', () => { this.toggleToDesktopView(); }); document.getElementById('queuePrompt').addEventListener('click', () => { this.queueCurrentWorkflow(); }); // Bottom action bar document.getElementById('loadWorkflow').addEventListener('click', () => { this.showLoadWorkflowDialog(); }); document.getElementById('saveWorkflow').addEventListener('click', () => { this.showSaveWorkflowDialog(); }); document.getElementById('clearWorkflow').addEventListener('click', () => { this.clearWorkflow(); }); document.getElementById('addNode').addEventListener('click', () => { this.showAddNodeDialog(); }); // Modal controls document.getElementById('closeModal').addEventListener('click', () => { this.closeModal(); }); document.getElementById('cancelEdit').addEventListener('click', () => { this.closeModal(); }); document.getElementById('saveEdit').addEventListener('click', () => { this.saveNodeEdit(); }); // Bottom sheet controls document.getElementById('closeBottomSheet').addEventListener('click', () => { this.closeBottomSheet(); }); // Connection overlay document.getElementById('cancelConnection').addEventListener('click', () => { this.cancelConnection(); }); // Global gestures this.setupGlobalGestures(); } /** * Setup API event listeners */ setupAPIEventListeners() { this.apiClient.on('connected', () => { this.updateConnectionStatus('Connected'); this.enableQueueButton(); }); this.apiClient.on('disconnected', () => { this.updateConnectionStatus('Disconnected'); this.disableQueueButton(); }); this.apiClient.on('connection_error', (error) => { this.updateConnectionStatus('Connection Error'); this.disableQueueButton(); Utils.showToast('Connection error: ' + error.message, 'error'); }); this.apiClient.on('status_update', (data) => { this.updateQueueStatus(data); }); this.apiClient.on('progress_update', (data) => { this.updateProgress(data); }); this.apiClient.on('node_executing', (data) => { this.updateNodeStatus(data.node, 'executing'); }); this.apiClient.on('node_executed', (data) => { this.updateNodeStatus(data.node, 'executed'); }); this.apiClient.on('execution_start', (data) => { this.onExecutionStart(data); }); this.apiClient.on('execution_success', (data) => { this.onExecutionSuccess(data); }); this.apiClient.on('execution_error', (data) => { this.onExecutionError(data); }); } /** * Setup global gestures */ setupGlobalGestures() { const workflowContainer = document.querySelector('.workflow-container'); // Pull to refresh let startY = 0; let currentY = 0; let pulling = false; workflowContainer.addEventListener('touchstart', (e) => { if (workflowContainer.scrollTop === 0) { startY = e.touches[0].clientY; pulling = true; } }); workflowContainer.addEventListener('touchmove', (e) => { if (pulling) { currentY = e.touches[0].clientY; const pullDistance = currentY - startY; if (pullDistance > 50) { // Visual feedback for pull to refresh workflowContainer.style.transform = `translateY(${Math.min(pullDistance - 50, 30)}px)`; } } }); workflowContainer.addEventListener('touchend', () => { if (pulling) { const pullDistance = currentY - startY; if (pullDistance > 80) { this.refreshWorkflow(); } workflowContainer.style.transform = ''; pulling = false; } }); } /** * Load initial workflow */ async loadInitialWorkflow() { try { // Try to load a default workflow or show empty state const workflows = await this.apiClient.getWorkflows(); if (workflows.length > 0) { const workflow = await this.apiClient.loadWorkflow(workflows[0]); this.setWorkflow(workflow); } else { this.showEmptyState(); } } catch (error) { console.error('Failed to load initial workflow:', error); this.showEmptyState(); } } /** * Set current workflow * @param {Object} workflow - Workflow object */ setWorkflow(workflow) { this.currentWorkflow = workflow; this.linearizedNodes = this.linearization.linearizeWorkflow(workflow); this.renderWorkflowPipeline(); this.updateQueueButton(); } /** * Render workflow pipeline */ renderWorkflowPipeline() { const pipelineContainer = document.getElementById('workflowPipeline'); pipelineContainer.innerHTML = ''; if (this.linearizedNodes.length === 0) { this.showEmptyState(); return; } this.linearizedNodes.forEach((node, index) => { const nodeCard = this.createNodeCard(node, index); pipelineContainer.appendChild(nodeCard); }); } /** * Create node card element * @param {Object} node - Node data * @param {number} index - Node index * @returns {HTMLElement} Node card element */ createNodeCard(node, index) { const card = document.createElement('div'); card.className = 'node-card'; card.dataset.nodeId = node.id; card.dataset.nodeIndex = index; card.innerHTML = `
No editable parameters
'; } /** * Render widget input * @param {Object} widget - Widget data * @returns {string} HTML string */ renderWidgetInput(widget) { switch (widget.type) { case 'number': return ``; case 'slider': return ``; case 'text': return ``; case 'textarea': return ``; case 'select': const options = widget.options.values || []; return ``; default: return ``; } } /** * Save node edit */ saveNodeEdit() { if (!this.selectedNode) return; const form = document.getElementById('modalBody'); const inputs = form.querySelectorAll('[data-widget-name]'); inputs.forEach(input => { const widgetName = input.dataset.widgetName; const widget = this.selectedNode.widgets.find(w => w.name === widgetName); if (widget) { widget.value = input.value; // Update workflow data if (this.currentWorkflow.nodes[this.selectedNode.id].widgets_values) { this.currentWorkflow.nodes[this.selectedNode.id].widgets_values[widget.index] = input.value; } } }); // Re-render the node card this.renderWorkflowPipeline(); this.closeModal(); Utils.showToast('Node updated', 'success'); } /** * Close modal */ closeModal() { document.getElementById('nodeDetailModal').classList.remove('active'); this.selectedNode = null; } /** * Close bottom sheet */ closeBottomSheet() { document.getElementById('bottomSheet').classList.remove('active'); } /** * Scroll to node * @param {string} nodeId - Node ID */ scrollToNode(nodeId) { const nodeCard = document.querySelector(`[data-node-id="${nodeId}"]`); if (nodeCard) { Utils.scrollToElement(nodeCard); nodeCard.classList.add('fade-in'); setTimeout(() => nodeCard.classList.remove('fade-in'), 1000); } } /** * Queue current workflow */ async queueCurrentWorkflow() { if (!this.currentWorkflow) { Utils.showToast('No workflow to queue', 'error'); return; } try { const response = await this.apiClient.queuePrompt(this.currentWorkflow); Utils.showToast('Workflow queued successfully', 'success'); this.updateQueueButton(); } catch (error) { Utils.showToast('Failed to queue workflow: ' + error.message, 'error'); } } /** * Update connection status * @param {string} status - Connection status */ updateConnectionStatus(status) { document.getElementById('connectionStatus').textContent = status; } /** * Update queue status * @param {Object} data - Queue data */ updateQueueStatus(data) { const queueSize = data.exec_info?.queue_remaining || 0; document.getElementById('queueSize').textContent = queueSize; } /** * Update queue button state */ updateQueueButton() { const button = document.getElementById('queuePrompt'); const hasWorkflow = this.currentWorkflow && this.linearizedNodes.length > 0; const isConnected = this.apiClient.isWebSocketConnected(); button.disabled = !hasWorkflow || !isConnected; } /** * Enable queue button */ enableQueueButton() { this.updateQueueButton(); } /** * Disable queue button */ disableQueueButton() { document.getElementById('queuePrompt').disabled = true; } /** * Show empty state */ showEmptyState() { document.getElementById('workflowPipeline').innerHTML = ` `; } /** * Toggle to desktop view */ toggleToDesktopView() { window.location.href = '/'; } /** * Show load workflow dialog */ showLoadWorkflowDialog() { Utils.showToast('Load workflow feature coming soon', 'info'); } /** * Show save workflow dialog */ showSaveWorkflowDialog() { Utils.showToast('Save workflow feature coming soon', 'info'); } /** * Show add node dialog */ showAddNodeDialog() { Utils.showToast('Add node feature coming soon', 'info'); } /** * Clear workflow */ clearWorkflow() { if (confirm('Are you sure you want to clear the workflow?')) { this.currentWorkflow = null; this.linearizedNodes = []; this.showEmptyState(); this.updateQueueButton(); Utils.showToast('Workflow cleared', 'success'); } } /** * Refresh workflow */ refreshWorkflow() { if (this.currentWorkflow) { this.linearizedNodes = this.linearization.linearizeWorkflow(this.currentWorkflow); this.renderWorkflowPipeline(); Utils.showToast('Workflow refreshed', 'success'); } } /** * Update node status * @param {string} nodeId - Node ID * @param {string} status - New status */ updateNodeStatus(nodeId, status) { const nodeCard = document.querySelector(`[data-node-id="${nodeId}"]`); if (nodeCard) { const statusElement = nodeCard.querySelector('.node-status'); statusElement.textContent = status; statusElement.className = `node-status ${status}`; } } /** * Update progress * @param {Object} data - Progress data */ updateProgress(data) { // Update progress UI if needed console.log('Progress update:', data); } /** * Handle execution start * @param {Object} data - Execution data */ onExecutionStart(data) { Utils.showToast('Execution started', 'info'); } /** * Handle execution success * @param {Object} data - Execution data */ onExecutionSuccess(data) { Utils.showToast('Execution completed successfully', 'success'); // Reset all node statuses this.linearizedNodes.forEach(node => { this.updateNodeStatus(node.id, 'idle'); }); } /** * Handle execution error * @param {Object} data - Error data */ onExecutionError(data) { Utils.showToast('Execution failed: ' + data.exception_message, 'error'); // Reset all node statuses this.linearizedNodes.forEach(node => { this.updateNodeStatus(node.id, 'idle'); }); } // Node manipulation methods (placeholders) bypassNode(node) { Utils.showToast('Bypass node feature coming soon', 'info'); } muteNode(node) { Utils.showToast('Mute node feature coming soon', 'info'); } cloneNode(node) { Utils.showToast('Clone node feature coming soon', 'info'); } deleteNode(node) { if (confirm(`Are you sure you want to delete ${node.title}?`)) { Utils.showToast('Delete node feature coming soon', 'info'); } } } // Export for use in other files window.MobileInterface = MobileInterface;