diff --git a/neuron-settings.js b/neuron-settings.js index d740f51ed0..f261fbd056 100644 --- a/neuron-settings.js +++ b/neuron-settings.js @@ -586,7 +586,8 @@ module.exports = { css: [ require("path").resolve(__dirname, "neuron/theme/header-balance.css"), require("path").resolve(__dirname, "neuron/theme/template-browser.css"), - require("path").resolve(__dirname, "neuron/theme/featurebase-left.css") + require("path").resolve(__dirname, "neuron/theme/featurebase-left.css"), + require("path").resolve(__dirname, "neuron/theme/chat-widget-layout.css") ], scripts: [ diff --git a/neuron/theme/chat-widget-layout.css b/neuron/theme/chat-widget-layout.css new file mode 100644 index 0000000000..1f73d6500c --- /dev/null +++ b/neuron/theme/chat-widget-layout.css @@ -0,0 +1,106 @@ +/* Clean Sidebar Layout - Simple Approach */ + +/* Chat widget positioning */ +#neuron-chat-widget { + position: fixed !important; + top: 48px !important; + right: 0px !important; + width: 380px !important; + height: calc(100vh - 48px) !important; + z-index: 10000 !important; + background: #1D1D1D !important; + border: 1px solid #333 !important; +} + +/* Chat bubble positioning - above sidebar */ +#neuron-chat-bubble { + z-index: 10003 !important; /* Above sidebar (10001) and separator (10002) */ +} + +/* SCENARIO 1: Chat open - Move sidebar left by 380px */ +body.neuron-chat-open #red-ui-sidebar { + right: 380px !important; + z-index: 10001 !important; + /* Let Node-RED manage width naturally */ +} + +/* SCENARIO 2: Chat closed - Sidebar at normal position (right edge) */ +body.neuron-chat-closed #red-ui-sidebar { + right: 0px !important; + z-index: 10001 !important; + /* Let Node-RED manage width naturally */ +} + +/* Sidebar separator (resize bar) positioning */ +/* When chat open: separator = sidebar_width + 380px (chat width) */ +body.neuron-chat-open #red-ui-sidebar-separator { + z-index: 10002 !important; + /* Position will be calculated by JavaScript based on actual sidebar width */ +} + +/* When chat closed: separator = sidebar_width */ +body.neuron-chat-closed #red-ui-sidebar-separator { + z-index: 10002 !important; + /* Position will be calculated by JavaScript based on actual sidebar width */ +} + +/* Canvas (workspace) positioning */ +/* Canvas positioning when sidebar is open - handled by JavaScript */ +/* These rules are intentionally empty as positioning is calculated dynamically */ + +/* When sidebar is closed */ +body.red-ui-sidebar-closed.neuron-chat-open #red-ui-workspace { + right: 380px !important; /* Just chat width */ +} + +body.red-ui-sidebar-closed.neuron-chat-closed #red-ui-workspace { + right: 0px !important; /* Full width */ +} + +body.red-ui-sidebar-closed #red-ui-editor-stack { + right: 1px !important; +} + +/* Override Node-RED's sidebar separator positioning with maximum specificity */ +#red-ui-main-container.red-ui-sidebar-closed body.neuron-chat-open #red-ui-sidebar-separator, +body.neuron-chat-open #red-ui-main-container.red-ui-sidebar-closed #red-ui-sidebar-separator, +html body.neuron-chat-open .red-ui-sidebar-closed #red-ui-sidebar-separator, +html #red-ui-main-container.red-ui-sidebar-closed body.neuron-chat-open #red-ui-sidebar-separator { + right: 380px !important; /* Force separator to chat's left edge when sidebar closed */ +} + +/* Additional override with style attribute specificity simulation */ +body.neuron-chat-open #red-ui-sidebar-separator[style] { + right: 380px !important; +} + +#red-ui-main-container.red-ui-sidebar-closed body.neuron-chat-closed #red-ui-sidebar-separator, +body.neuron-chat-closed #red-ui-main-container.red-ui-sidebar-closed #red-ui-sidebar-separator, +html body.neuron-chat-closed .red-ui-sidebar-closed #red-ui-sidebar-separator { + right: 0px !important; /* Force separator to right edge when both closed */ +} + +/* Ensure UI elements appear above everything */ +.red-ui-menu-dropdown, +#template-browser-dialog, +.red-ui-dialog, +.ui-dialog, +.red-ui-popover, +.popover, +.red-ui-tourGuide, +.red-ui-notifications, +.red-ui-menu, +.dropdown-menu, +.context-menu, +.red-ui-editor-tray, +#red-ui-header .red-ui-menu-dropdown { + z-index: 11000 !important; +} + +/* Smooth transitions */ +#red-ui-workspace, +#red-ui-editor-stack, +#red-ui-sidebar, +#red-ui-sidebar-separator { + transition: right 0.3s ease !important; +} \ No newline at end of file diff --git a/neuron/theme/dedicated-chat-window.html b/neuron/theme/dedicated-chat-window.html index 244292dcb6..7049c23533 100644 --- a/neuron/theme/dedicated-chat-window.html +++ b/neuron/theme/dedicated-chat-window.html @@ -974,52 +974,78 @@ } } + // Use updated flow context if available, otherwise fall back to localStorage + let flowContextToUse = null; + let rawFlowJsonToUse = null; + + if (window.currentFlowContext) { + flowContextToUse = window.currentFlowContext; + rawFlowJsonToUse = window.currentFlowContext.rawFlowJson; + console.log('🔄 [FLOW SYNC] Using real-time flow context from main window'); + } else { + console.log('⚠️ [FLOW SYNC] No real-time context, checking localStorage...'); + // Fall back to localStorage + const chatData = getUserData('chat'); + if (chatData?.flowContext) { + flowContextToUse = chatData.flowContext; + rawFlowJsonToUse = chatData.rawFlowJson; + console.log('🔄 [FLOW SYNC] Using flow context from localStorage'); + } + } + const requestBody = { message: message, context: 'Neuron software context', - flowContext: null, // Will be populated if available - rawFlowJson: null // Will be populated if available + flowContext: flowContextToUse, + rawFlowJson: rawFlowJsonToUse }; - // Try to get flow context from localStorage if available - try { - const chatData = getUserData('chat'); - console.log('📱 [SERVER DEBUG] Retrieved chat data for flow context:', chatData); - - if (chatData && chatData.flowContext) { - requestBody.flowContext = chatData.flowContext; - console.log('📱 [SERVER DEBUG] Using flow context from localStorage:', chatData.flowContext.flowName); - } - - if (chatData && chatData.rawFlowJson) { - requestBody.rawFlowJson = chatData.rawFlowJson; - console.log('📱 [SERVER DEBUG] Using raw flow JSON from localStorage, nodes:', chatData.rawFlowJson.length); - } - - // If we don't have raw flow JSON, try to get it from the parent window - if (!requestBody.rawFlowJson && window.opener && !window.opener.closed) { - try { - console.log('📱 [SERVER DEBUG] Attempting to get raw flow JSON from parent window...'); - // Try to access the parent window's RED object - if (window.opener.RED && window.opener.RED.nodes && typeof window.opener.RED.nodes.createCompleteNodeSet === 'function') { - requestBody.rawFlowJson = window.opener.RED.nodes.createCompleteNodeSet(); - console.log('📱 [SERVER DEBUG] Successfully got raw flow JSON from parent window, nodes:', requestBody.rawFlowJson.length); - } - } catch (error) { - console.warn('📱 [SERVER DEBUG] Could not get raw flow JSON from parent window:', error); + // Log flow context usage with detailed debugging + console.log('🔍 [DEBUG] Checking flow context availability...'); + console.log('🔍 [DEBUG] window.currentFlowContext exists:', !!window.currentFlowContext); + console.log('🔍 [DEBUG] window.currentFlowContext value:', window.currentFlowContext); + console.log('🔍 [DEBUG] window.lastFlowUpdate:', window.lastFlowUpdate); + + if (window.currentFlowContext) { + console.log('🔄 [FLOW SYNC] Using updated flow context in dedicated window'); + console.log('🔄 [FLOW SYNC] Flow name:', window.currentFlowContext.flowName); + console.log('🔄 [FLOW SYNC] Node count:', window.currentFlowContext.nodeCount); + console.log('🔄 [FLOW SYNC] Last update:', new Date(window.lastFlowUpdate || 0).toLocaleTimeString()); + } else { + console.log('⚠️ [FLOW SYNC] No current flow context available in dedicated window'); + console.log('⚠️ [FLOW SYNC] Falling back to localStorage flow context'); + } + + // Log what we're actually sending (removed localStorage override) + console.log('📱 [SERVER DEBUG] Final flow context being sent:', requestBody.flowContext); + if (requestBody.flowContext) { + console.log('📱 [SERVER DEBUG] Flow name:', requestBody.flowContext.flowName); + console.log('📱 [SERVER DEBUG] Node count:', requestBody.flowContext.nodeCount); + console.log('📱 [SERVER DEBUG] Node types:', requestBody.flowContext.nodeTypes); + } else { + console.log('⚠️ [SERVER DEBUG] No flow context available for AI request'); + } + + // If we don't have raw flow JSON, try to get it from the parent window + if (!requestBody.rawFlowJson && window.opener && !window.opener.closed) { + try { + console.log('📱 [SERVER DEBUG] Attempting to get raw flow JSON from parent window...'); + // Try to access the parent window's RED object + if (window.opener.RED && window.opener.RED.nodes && typeof window.opener.RED.nodes.createCompleteNodeSet === 'function') { + requestBody.rawFlowJson = window.opener.RED.nodes.createCompleteNodeSet(); + console.log('📱 [SERVER DEBUG] Successfully got raw flow JSON from parent window, nodes:', requestBody.rawFlowJson.length); } + } catch (error) { + console.warn('📱 [SERVER DEBUG] Could not get raw flow JSON from parent window:', error); } - - console.log('📱 [SERVER DEBUG] Final request body flow context:', { - hasFlowContext: !!requestBody.flowContext, - hasRawFlowJson: !!requestBody.rawFlowJson, - flowContextKeys: requestBody.flowContext ? Object.keys(requestBody.flowContext) : [], - rawFlowJsonNodeCount: requestBody.rawFlowJson ? requestBody.rawFlowJson.length : 0 - }); - - } catch (error) { - console.warn('📱 [SERVER DEBUG] Could not get flow context from localStorage:', error); } + + console.log('📱 [SERVER DEBUG] Final request body flow context:', { + hasFlowContext: !!requestBody.flowContext, + hasRawFlowJson: !!requestBody.rawFlowJson, + flowContextKeys: requestBody.flowContext ? Object.keys(requestBody.flowContext) : [], + rawFlowJsonNodeCount: requestBody.rawFlowJson ? requestBody.rawFlowJson.length : 0 + }); // Get authentication token if available const authToken = localStorage.getItem('chat-token'); @@ -1159,6 +1185,15 @@ console.log('📱 [DEDICATED CHAT] Reloading chat data...'); loadChatData(); }, 100); + } else if (event.data && event.data.type === 'FLOW_STATE_UPDATE') { + console.log('🔄 [FLOW SYNC] Received flow state update from main window'); + console.log('🔄 [FLOW SYNC] New flow context:', event.data.flowContext); + + // Store the updated flow context for use in chat messages + window.currentFlowContext = event.data.flowContext; + window.lastFlowUpdate = event.data.timestamp; + + console.log('🔄 [FLOW SYNC] Flow context updated in dedicated window'); } }); diff --git a/neuron/theme/menu-customizer.js b/neuron/theme/menu-customizer.js index e01c5f9b6c..c817a33bea 100644 --- a/neuron/theme/menu-customizer.js +++ b/neuron/theme/menu-customizer.js @@ -18,13 +18,28 @@ newMenuItem.style.cssText = 'margin: 0; padding: 0;'; newMenuItem.innerHTML = ` - - - Neuron AI Node Builder + + + Neuron documentation `; - // Add custom event handlers to prevent interference + // Create the tutorials menu item + const tutorialsMenuItem = document.createElement('li'); + tutorialsMenuItem.id = 'menu-item-neuron-tutorials'; + tutorialsMenuItem.className = 'neuron-tutorials-item custom-menu-item'; + tutorialsMenuItem.style.cssText = 'margin: 0; padding: 0;'; + + tutorialsMenuItem.innerHTML = ` + + + Tutorials + + `; + + // Add custom event handlers for documentation menu item const link = newMenuItem.querySelector('a'); link.addEventListener('mouseenter', function(e) { e.stopPropagation(); @@ -38,23 +53,40 @@ this.style.color = 'inherit'; }); + // Add custom event handlers for tutorials menu item + const tutorialsLink = tutorialsMenuItem.querySelector('a'); + tutorialsLink.addEventListener('mouseenter', function(e) { + e.stopPropagation(); + this.style.backgroundColor = '#2a2a2a'; + this.style.color = '#ffffff'; + }); + + tutorialsLink.addEventListener('mouseleave', function(e) { + e.stopPropagation(); + this.style.backgroundColor = 'transparent'; + this.style.color = 'inherit'; + }); + // Create a separator first const separator = document.createElement('li'); separator.className = 'red-ui-menu-divider'; - // Insert separator and menu item after the version number + // Insert separator and menu items after the version number const versionItem = document.querySelector('#menu-item-node-red-version'); if (versionItem) { // Insert separator after the version item versionItem.parentNode.insertBefore(separator, versionItem.nextSibling); - // Then insert our menu item after the separator + // Then insert documentation menu item after the separator versionItem.parentNode.insertBefore(newMenuItem, separator.nextSibling); + // Then insert tutorials menu item after the documentation item + versionItem.parentNode.insertBefore(tutorialsMenuItem, newMenuItem.nextSibling); // Debug: Log the DOM structure console.log('🔍 DOM Structure after insertion:'); console.log('Version item:', versionItem); console.log('Separator:', separator); - console.log('Our menu item:', newMenuItem); + console.log('Documentation menu item:', newMenuItem); + console.log('Tutorials menu item:', tutorialsMenuItem); console.log('Version item next sibling:', versionItem.nextSibling); // Force the version item to maintain its styling with maximum specificity @@ -90,7 +122,7 @@ } } - console.log('✅ Custom menu item injected successfully!'); + console.log('✅ Custom menu items injected successfully! (Documentation + Tutorials)'); } else { console.log('⚠️ Keyboard shortcuts menu item not found, retrying...'); // Retry after a short delay @@ -110,9 +142,10 @@ const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { - // Check if our custom item exists - const existingItem = document.querySelector('#menu-item-neuron-docs'); - if (!existingItem) { + // Check if our custom items exist + const existingDocsItem = document.querySelector('#menu-item-neuron-docs'); + const existingTutorialsItem = document.querySelector('#menu-item-neuron-tutorials'); + if (!existingDocsItem || !existingTutorialsItem) { injectCustomMenuItem(); } } diff --git a/neuron/theme/neuron-chat-widget.js b/neuron/theme/neuron-chat-widget.js index db7300bb07..2cafc0bb14 100644 --- a/neuron/theme/neuron-chat-widget.js +++ b/neuron/theme/neuron-chat-widget.js @@ -310,6 +310,278 @@ }); } + // Function to adjust sidebar position for chat widget using CSS classes and dynamic positioning + function adjustSidebarForChatWidget(chatOpen) { + const body = document.body; + + // Simple state management - just set CSS classes + if (chatOpen) { + body.classList.add('neuron-chat-open'); + body.classList.remove('neuron-chat-closed'); + console.log('Chat opened'); + } else { + body.classList.add('neuron-chat-closed'); + body.classList.remove('neuron-chat-open'); + console.log('Chat closed'); + } + + // Update sidebar state detection + updateSidebarStateClass(); + + // Calculate and set positions based on actual sidebar width + updateLayoutPositions(chatOpen); + + console.log('Body classes:', body.className); + } + + // Simple function to calculate positions based on actual sidebar width + function updateLayoutPositions(chatOpen) { + const sidebar = document.getElementById('red-ui-sidebar'); + const separator = document.getElementById('red-ui-sidebar-separator'); + const workspace = document.getElementById('red-ui-workspace'); + const editorStack = document.getElementById('red-ui-editor-stack'); + const mainContainer = document.getElementById('red-ui-main-container'); + + // Skip if essential elements don't exist + if (!sidebar || !mainContainer) return; + + const sidebarClosed = mainContainer.classList.contains('red-ui-sidebar-closed'); + const chatWidgetWidth = 380; + const separatorWidth = 7; // Node-RED's default separator width + + if (sidebarClosed) { + // Sidebar is closed - position separator and workspace appropriately + if (chatOpen) { + // Chat open, sidebar closed: separator at chat's left edge + if (separator) { + separator.style.setProperty('right', chatWidgetWidth + 'px', 'important'); + console.log('DEBUG: Sidebar closed, chat open - separator at:', chatWidgetWidth + 'px', '(with !important)'); + } + if (workspace) { + const workspacePos = chatWidgetWidth + separatorWidth; + workspace.style.setProperty('right', workspacePos + 'px', 'important'); + console.log('DEBUG: Set workspace right to:', workspacePos + 'px', '(chat + separator width, with !important)'); + } + if (editorStack) { + const editorPos = chatWidgetWidth + separatorWidth + 1; + editorStack.style.setProperty('right', editorPos + 'px', 'important'); + console.log('DEBUG: Set editor stack right to:', editorPos + 'px', '(with !important)'); + } + } else { + // Both closed: separator at right edge, workspace accounts for separator width + if (separator) { + separator.style.setProperty('right', '0px', 'important'); + console.log('DEBUG: Both closed - separator at right edge (with !important)'); + } + if (workspace) { + workspace.style.setProperty('right', separatorWidth + 'px', 'important'); + console.log('DEBUG: Set workspace to account for separator width:', separatorWidth + 'px', '(with !important)'); + } + if (editorStack) { + const editorPos = separatorWidth + 1; + editorStack.style.setProperty('right', editorPos + 'px', 'important'); + console.log('DEBUG: Set editor stack right to:', editorPos + 'px', '(with !important)'); + } + } + } else { + // Sidebar is open - calculate based on actual width + const sidebarWidth = sidebar.offsetWidth || 315; + console.log('Calculating positions for sidebar width:', sidebarWidth); + + if (chatOpen) { + // Chat open: separator at sidebar_width + chat_width, workspace accounts for separator width + const separatorPos = sidebarWidth + chatWidgetWidth; + const workspacePos = separatorPos + separatorWidth; + + console.log('DEBUG: Chat open positioning - sidebarWidth:', sidebarWidth, 'chatWidth:', chatWidgetWidth, 'separatorWidth:', separatorWidth, 'separatorPos:', separatorPos, 'workspacePos:', workspacePos); + + if (separator) { + separator.style.setProperty('right', separatorPos + 'px', 'important'); + console.log('DEBUG: Set separator right to:', separatorPos + 'px', '(with !important)'); + } + if (workspace) { + workspace.style.setProperty('right', workspacePos + 'px', 'important'); + console.log('DEBUG: Set workspace right to:', workspacePos + 'px', '(accounting for separator width, with !important)'); + } + if (editorStack) { + editorStack.style.setProperty('right', (workspacePos + 1) + 'px', 'important'); + console.log('DEBUG: Set editor stack right to:', (workspacePos + 1) + 'px', '(with !important)'); + } + + console.log('Chat open - separator at:', separatorPos, 'workspace at:', workspacePos); + } else { + // Chat closed: separator at sidebar_width, workspace accounts for separator width + if (separator) separator.style.setProperty('right', sidebarWidth + 'px', 'important'); + if (workspace) { + const workspacePos = sidebarWidth + separatorWidth; + workspace.style.setProperty('right', workspacePos + 'px', 'important'); + console.log('DEBUG: Set workspace right to:', workspacePos + 'px', '(sidebar + separator width, with !important)'); + } + if (editorStack) { + const editorPos = sidebarWidth + separatorWidth + 1; + editorStack.style.setProperty('right', editorPos + 'px', 'important'); + console.log('DEBUG: Set editor stack right to:', editorPos + 'px', '(with !important)'); + } + + console.log('Chat closed - separator at:', sidebarWidth, 'workspace at:', sidebarWidth + separatorWidth); + } + } + } + + // REMOVED: Complex dynamic positioning function - using pure CSS approach instead + function updateDynamicPositioning_DISABLED(chatOpen) { + const sidebar = document.getElementById('red-ui-sidebar'); + const sidebarSeparator = document.getElementById('red-ui-sidebar-separator'); + const workspace = document.getElementById('red-ui-workspace'); + const editorStack = document.getElementById('red-ui-editor-stack'); + const mainContainer = document.getElementById('red-ui-main-container'); + + // Check if essential elements exist + if (!mainContainer) { + console.log('Cannot update dynamic positioning - main container not found'); + return; + } + + const chatWidgetWidth = 380; + const sidebarClosed = mainContainer.classList.contains('red-ui-sidebar-closed'); + + // Trigger Node-RED's sidebar resize event to ensure content fits properly + function triggerSidebarResize() { + if (typeof RED !== 'undefined' && RED.events) { + setTimeout(() => { + RED.events.emit("sidebar:resize"); + console.log('Triggered sidebar:resize event for content adjustment'); + + // Also trigger a window resize event to ensure all components recalculate + window.dispatchEvent(new Event('resize')); + console.log('Triggered window resize event for full layout recalculation'); + }, 100); + } + } + + if (chatOpen) { + if (sidebarClosed) { + // Sidebar closed, chat open: separator at chat's left edge + if (sidebarSeparator) sidebarSeparator.style.right = chatWidgetWidth + 'px'; + if (workspace) workspace.style.right = chatWidgetWidth + 'px'; + if (editorStack) editorStack.style.right = (chatWidgetWidth + 1) + 'px'; + } else { + // Both open: calculate based on actual sidebar width + const actualSidebarWidth = sidebar ? sidebar.offsetWidth : 315; + const totalWidth = chatWidgetWidth + actualSidebarWidth; + + if (sidebar) sidebar.style.right = chatWidgetWidth + 'px'; + if (sidebarSeparator) sidebarSeparator.style.right = totalWidth + 'px'; + if (workspace) workspace.style.right = totalWidth + 'px'; + if (editorStack) editorStack.style.right = (totalWidth + 1) + 'px'; + + console.log('Dynamic positioning - Sidebar width:', actualSidebarWidth, 'Total width:', totalWidth); + } + } else { + // Chat closed: reset to normal or handle sidebar-only state + if (sidebarClosed) { + if (sidebarSeparator) sidebarSeparator.style.right = '0px'; + if (workspace) workspace.style.right = '0px'; + if (editorStack) editorStack.style.right = '1px'; + } else { + const actualSidebarWidth = sidebar ? sidebar.offsetWidth : 315; + if (sidebar) sidebar.style.right = '0px'; + if (sidebarSeparator) sidebarSeparator.style.right = actualSidebarWidth + 'px'; + if (workspace) workspace.style.right = (actualSidebarWidth + 7) + 'px'; // Node-RED default margin + if (editorStack) editorStack.style.right = (actualSidebarWidth + 8) + 'px'; + } + } + + // Trigger sidebar content resize after positioning changes + triggerSidebarResize(); + } + + // Function to monitor and update sidebar state class + function updateSidebarStateClass() { + const body = document.body; + const mainContainer = document.getElementById('red-ui-main-container'); + + // Check if main container exists (Node-RED DOM might not be ready yet) + if (!mainContainer) { + console.log('Main container not found - Node-RED DOM not ready yet'); + return false; // Return false to indicate failure + } + + // Check if Node-RED has the sidebar-closed class and mirror it on body + if (mainContainer.classList.contains('red-ui-sidebar-closed')) { + body.classList.add('red-ui-sidebar-closed'); + console.log('Sidebar is closed - added red-ui-sidebar-closed to body'); + console.log('Main container classes:', mainContainer.className); + } else { + body.classList.remove('red-ui-sidebar-closed'); + console.log('Sidebar is open - removed red-ui-sidebar-closed from body'); + console.log('Main container classes:', mainContainer.className); + } + + return true; // Return true to indicate success + } + + // Global test function for debugging (accessible from browser console) + window.testNeuronLayout = function() { + console.log('=== NEURON LAYOUT DEBUG ==='); + const body = document.body; + const mainContainer = document.getElementById('red-ui-main-container'); + const workspace = document.getElementById('red-ui-workspace'); + const sidebar = document.getElementById('red-ui-sidebar'); + const chatWidget = document.getElementById('neuron-chat-widget'); + + console.log('Body classes:', body.className); + console.log('Main container classes:', mainContainer ? mainContainer.className : 'NOT FOUND'); + + if (workspace) { + const computedStyle = getComputedStyle(workspace); + console.log('Workspace - Right position:', computedStyle.right); + console.log('Workspace - Computed right:', workspace.getBoundingClientRect().right); + } + + if (sidebar) { + const computedStyle = getComputedStyle(sidebar); + console.log('Sidebar - Right position:', computedStyle.right); + console.log('Sidebar - Z-index:', computedStyle.zIndex); + console.log('Sidebar - Actual width:', sidebar.offsetWidth); + console.log('Sidebar - Computed right:', sidebar.getBoundingClientRect().right); + } + + if (chatWidget) { + const computedStyle = getComputedStyle(chatWidget); + console.log('Chat widget - Right position:', computedStyle.right); + console.log('Chat widget - Z-index:', computedStyle.zIndex); + console.log('Chat widget - Computed right:', chatWidget.getBoundingClientRect().right); + } + + console.log('=== END DEBUG ==='); + }; + + // Set up observer to watch for sidebar state changes + function setupSidebarStateObserver() { + const mainContainer = document.getElementById('red-ui-main-container'); + + if (mainContainer) { + // Initial state check + updateSidebarStateClass(); + + // Create observer to watch for class changes + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + updateSidebarStateClass(); + } + }); + }); + + // Start observing + observer.observe(mainContainer, { + attributes: true, + attributeFilter: ['class'] + }); + } + } + // Create floating chat widget function createChatWidget() { // Check if widget already exists @@ -425,7 +697,7 @@ widget.style.cssText = ` position: fixed; top: 48px; - right: 0; + right: 0px; width: 380px; height: calc(100vh - 48px); background: #1D1D1D; @@ -740,14 +1012,53 @@ document.body.appendChild(widget); + // Set initial state + adjustSidebarForChatWidget(true); + + // Set up observer to watch for sidebar state changes + setupSidebarStateObserver(); + + // Listen for Node-RED sidebar resize events + if (typeof RED !== 'undefined' && RED.events) { + RED.events.on("sidebar:resize", function() { + console.log('Sidebar resize event detected'); + // Update positions when sidebar is manually resized + const chatOpen = document.body.classList.contains('neuron-chat-open'); + updateLayoutPositions(chatOpen); + }); + } + + // Force initial state check with retries to ensure DOM is ready + let retryCount = 0; + const maxRetries = 10; + + function initializeWithRetry() { + const success = updateSidebarStateClass(); + if (success) { + console.log('DOM ready - initializing chat widget layout'); + adjustSidebarForChatWidget(true); + } else if (retryCount < maxRetries) { + retryCount++; + console.log(`DOM not ready, retry ${retryCount}/${maxRetries} in 500ms`); + setTimeout(initializeWithRetry, 500); + } else { + console.log('Max retries reached - initializing without sidebar state detection'); + // Initialize anyway but without sidebar state detection + document.body.classList.add('neuron-chat-open'); + document.body.classList.remove('neuron-chat-closed'); + } + } + + setTimeout(initializeWithRetry, 500); + // Add event listeners only if we have the necessary elements if (input && sendBtn) { setupChatEvents(widget); - } else { - // Even when not authenticated, we need to set up the minimize button - setupMinimizeButton(widget); } + // Always set up the minimize button and new window button (regardless of auth state) + setupMinimizeButton(widget); + // Style login container if it exists const loginContainer = widget.querySelector('.login-container'); if (loginContainer) { @@ -2140,6 +2451,9 @@ widget.style.background = 'white'; hideNotificationIndicator(); + // Move sidebar left to make room for chat widget + adjustSidebarForChatWidget(true); + // Show header text const headerText = widget.querySelector('.chat-header span'); if (headerText) headerText.style.display = 'block'; @@ -2162,11 +2476,29 @@ // Hide the main widget widget.style.display = 'none'; + // Reset sidebar position when chat is closed + adjustSidebarForChatWidget(false); + // Create new chat bubble element createChatBubble(); } }); } + + // New window button click handler - always attach regardless of auth state + const newWindowBtn = widget.querySelector('.chat-new-window'); + if (newWindowBtn && !newWindowBtn.hasAttribute('data-handler-attached')) { + console.log('✅ Found new window button in setupMinimizeButton, attaching click handler'); + newWindowBtn.addEventListener('click', () => { + console.log('🔄 [DEDICATED WINDOW] New window button clicked'); + openChatInNewWindow(); + }); + newWindowBtn.setAttribute('data-handler-attached', 'true'); + } else if (newWindowBtn) { + console.log('✅ New window button already has handler attached'); + } else { + console.log('❌ New window button not found in setupMinimizeButton'); + } } // Setup chat functionality @@ -2446,20 +2778,22 @@ if (chatBubble) { chatBubble.remove(); } + + // Move sidebar left to make room for chat widget + adjustSidebarForChatWidget(true); } else { // Hide the main widget widget.style.display = 'none'; + // Reset sidebar position when chat is closed + adjustSidebarForChatWidget(false); + // Create new chat bubble element createChatBubble(); } }); - // New window button click handler - const newWindowBtn = widget.querySelector('.chat-new-window'); - newWindowBtn.addEventListener('click', () => { - openChatInNewWindow(); - }); + // Moved to setupMinimizeButton function to ensure it's always attached // Focus input when clicking on widget widget.addEventListener('click', (e) => { @@ -2779,6 +3113,10 @@ if (widget) { widget.style.display = 'flex'; console.log('🔄 [CHAT SYNC] Widget made visible from closing notification'); + + // Reset the layout since chat widget is now visible again + adjustSidebarForChatWidget(true); + console.log('🔄 [CHAT SYNC] Layout restored from closing notification'); } // Clear messages and reload @@ -3008,7 +3346,7 @@ background: white; border-radius: 50%; box-shadow: 0 2px 8px rgba(0,0,0,0.2); - z-index: 10000; + z-index: 10003; cursor: pointer; display: flex; align-items: center; @@ -3049,6 +3387,8 @@ const mainWidget = document.getElementById('neuron-chat-widget'); if (mainWidget) { mainWidget.style.display = 'flex'; + // Move sidebar left to make room for chat widget + adjustSidebarForChatWidget(true); } // Remove the bubble @@ -3071,6 +3411,18 @@ // Open chat in new window async function openChatInNewWindow() { + console.log('🔄 [DEDICATED WINDOW] openChatInNewWindow function called'); + + // Force a fresh flow sync before opening dedicated window + try { + console.log('🔄 [DEDICATED WINDOW] Forcing fresh flow sync...'); + flowHasChanged = true; // Force sync + await syncFlowContext('dedicated-window-open'); + console.log('🔄 [DEDICATED WINDOW] Fresh flow sync completed'); + } catch (error) { + console.log('⚠️ [DEDICATED WINDOW] Flow sync failed, continuing with cached data:', error); + } + try { // Get existing chat data from localStorage to preserve message IDs and timestamps let chatData = getUserData('chat'); @@ -3173,11 +3525,16 @@ originalWidget.style.display = 'none'; } - // Hide chat bubble as well + // Remove chat bubble completely if (chatBubble) { - chatBubble.style.display = 'none'; + chatBubble.remove(); + console.log('🔄 [CHAT SYNC] Removed chat bubble'); } + // Reset the layout since chat widget is now hidden + adjustSidebarForChatWidget(false); + console.log('🔄 [CHAT SYNC] Layout reset - sidebar should return to normal position'); + // Verify data was saved and is accessible console.log('🔄 [CHAT SYNC] Verifying data persistence after window open...'); const savedData = getUserData('chat'); @@ -3194,17 +3551,67 @@ } }, 500); + // Set up flow state monitoring for the dedicated window + let flowMonitorInterval = null; + + function startFlowMonitoring() { + console.log('🔄 [FLOW MONITOR] Starting flow monitoring for dedicated window'); + flowMonitorInterval = setInterval(async () => { + try { + // Check if flow has changed by comparing node count + const currentFlowContext = getCurrentFlowContext(); + const currentNodeCount = currentFlowContext?.nodeCount || 0; + const lastNodeCount = window.lastMonitoredNodeCount || 0; + + if (currentNodeCount !== lastNodeCount) { + console.log('🔄 [FLOW MONITOR] Flow change detected, syncing to dedicated window'); + console.log('🔄 [FLOW MONITOR] Node count changed:', lastNodeCount, '→', currentNodeCount); + + // Sync the flow context + flowHasChanged = true; + await syncFlowContext('flow-monitor'); + + // Send updated flow context to dedicated window + if (newWindow && !newWindow.closed) { + newWindow.postMessage({ + type: 'FLOW_STATE_UPDATE', + flowContext: currentFlowContext, + timestamp: Date.now() + }, '*'); + console.log('🔄 [FLOW MONITOR] Sent flow update to dedicated window'); + console.log('🔄 [FLOW MONITOR] Flow context sent:', currentFlowContext); + } + + // Update the last node count + window.lastMonitoredNodeCount = currentNodeCount; + } else { + console.log('🔄 [FLOW MONITOR] No changes detected (nodeCount:', currentNodeCount, ')'); + } + } catch (error) { + console.log('⚠️ [FLOW MONITOR] Error during flow monitoring:', error); + } + }, 2000); // Check every 2 seconds + } + + // Start monitoring + startFlowMonitoring(); + // Listen for new window close event const checkClosed = setInterval(() => { if (newWindow.closed) { clearInterval(checkClosed); - // Show original widget and chat bubble again when new window closes + + // Stop flow monitoring when window closes + if (flowMonitorInterval) { + clearInterval(flowMonitorInterval); + console.log('🔄 [FLOW MONITOR] Stopped flow monitoring - window closed'); + } + // Show original widget when new window closes if (originalWidget) { originalWidget.style.display = 'flex'; + console.log('🔄 [CHAT SYNC] Restored original widget'); } - if (chatBubble) { - chatBubble.style.display = 'flex'; - } + // Don't restore old bubble - let the layout logic create a new one if needed // Clear existing widget messages and load updated chat history from localStorage setTimeout(() => { @@ -3214,6 +3621,10 @@ if (originalWidget) { originalWidget.style.display = 'flex'; console.log('🔄 [CHAT SYNC] Main widget made visible'); + + // Reset the layout since chat widget is now visible again + adjustSidebarForChatWidget(true); + console.log('🔄 [CHAT SYNC] Layout restored - sidebar should move left for chat widget'); } // Clear messages @@ -3405,6 +3816,10 @@ if (widget && widget.style.display === 'none') { console.log('🔄 [CHAT SYNC] Widget was hidden, making it visible...'); widget.style.display = 'flex'; + + // Reset the layout since chat widget is now visible again + adjustSidebarForChatWidget(true); + console.log('🔄 [CHAT SYNC] Layout restored after making widget visible'); } // Load chat data immediately (no additional timeout needed since we already debounced) diff --git a/neuron/theme/template-browser.css b/neuron/theme/template-browser.css index 298eb0f81b..b7f741c7cd 100644 --- a/neuron/theme/template-browser.css +++ b/neuron/theme/template-browser.css @@ -1,18 +1,29 @@ -/* Template Browser Dialog Styles */ +/* Template Browser Dialog Styles - Match Chat Widget Design */ #template-browser-dialog { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1D1D1D !important; color: white !important; + border: 1px solid #333 !important; + border-radius: 0 !important; + box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; + overflow: hidden !important; /* Prevent jQuery UI dialog scrollbar */ +} + +/* Ensure jQuery UI dialog wrapper doesn't have scrollbars */ +.ui-dialog-content { + overflow: hidden !important; + padding: 0 !important; } .template-browser-content { - padding: 20px; + padding: 12px; height: 100%; display: flex; flex-direction: column; background: #1D1D1D; color: white; + overflow: hidden; /* Prevent outer scroll */ } .template-search-bar { @@ -25,9 +36,9 @@ flex: 1; padding: 12px 16px; border: 1px solid #333; - border-radius: 6px; + border-radius: 0; font-size: 14px; - background: #2d3748; + background: #1D1D1D; color: white; outline: none; transition: border-color 0.2s; @@ -44,7 +55,7 @@ .template-search-bar button { padding: 8px 12px; border: 1px solid #007bff; - border-radius: 4px; + border-radius: 0; background: #007bff; color: white; cursor: pointer; @@ -111,10 +122,11 @@ flex: 1; overflow-y: auto; border: 1px solid #333; - border-radius: 8px; - background: #2d3748; + border-radius: 0; + background: #1D1D1D; scrollbar-width: thin; - scrollbar-color: #c1c1c1 #2d3748; + scrollbar-color: #c1c1c1 #1D1D1D; + padding-bottom: 12px; /* Add bottom spacing */ } .template-list::-webkit-scrollbar { @@ -122,7 +134,7 @@ } .template-list::-webkit-scrollbar-track { - background: #2d3748; + background: #f8f9fa; border-radius: 4px; } @@ -135,22 +147,31 @@ background: #a8a8a8; } +/* Add visual spacer at bottom of template list */ +.template-list::after { + content: ''; + display: block; + height: 12px; + background: transparent; +} + .template-item { border-bottom: 1px solid #333; - padding: 20px; + padding: 12px; transition: all 0.3s ease; - background: #2d3748; + background: #1D1D1D; margin: 0; + cursor: pointer; } .template-item:hover { - background: #3a4553; - transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(0, 123, 255, 0.2); + background: #2a2a2a; + border-color: #555; } .template-item:last-child { - border-bottom: none; + border-bottom: 1px solid #333; /* Keep bottom border for visual separation */ + margin-bottom: 12px; /* Add bottom margin for spacing */ } .template-header { @@ -304,7 +325,7 @@ .tab-panel { display: none; height: 100%; - overflow-y: auto; + overflow: hidden; /* Remove inner scroll */ } .tab-panel.active { @@ -313,11 +334,32 @@ .template-readme-content { height: 100%; - overflow-y: auto; + overflow-y: auto; /* Restore scrollbar for template info */ padding: 15px; - background: #2d3748; - border-radius: 6px; + background: #1D1D1D; + border-radius: 0; border: 1px solid #333; + scrollbar-width: thin; + scrollbar-color: #c1c1c1 #1D1D1D; +} + +/* Template readme scrollbar styling to match chat widget */ +.template-readme-content::-webkit-scrollbar { + width: 8px; +} + +.template-readme-content::-webkit-scrollbar-track { + background: #f8f9fa; + border-radius: 4px; +} + +.template-readme-content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +.template-readme-content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; } .template-readme-content pre { diff --git a/public/health-indicator.js b/public/health-indicator.js index 9524d4daec..07c4f0de8b 100644 --- a/public/health-indicator.js +++ b/public/health-indicator.js @@ -18,6 +18,7 @@ wrapper.style.verticalAlign = 'middle'; wrapper.style.position = 'relative'; wrapper.style.cursor = 'pointer'; + wrapper.style.transform = 'translateY(3px)'; /* Move down 3px for better alignment */ // Icon var icon = document.createElement('span');