From ffc77b858fb00e96212a7f404e917699dcf1ff46 Mon Sep 17 00:00:00 2001
From: dunnerz <74564541+dunnerz@users.noreply.github.com>
Date: Tue, 9 Sep 2025 18:41:12 +0100
Subject: [PATCH 1/2] Updated chat layout, fixed flow sync with dedicated
window, and added menu items
Have updated the layout of chat window, so it resizes the flow area and sidebar when chat is open. Fixed an issue with the dedicated popout chat window not synchronising flows properly. Also added Neuron Docs and Tutorials in the main menu
---
neuron-settings.js | 3 +-
neuron/theme/chat-widget-layout.css | 106 ++++++
neuron/theme/dedicated-chat-window.html | 111 ++++--
neuron/theme/menu-customizer.js | 55 ++-
neuron/theme/neuron-chat-widget.js | 447 +++++++++++++++++++++++-
5 files changed, 656 insertions(+), 66 deletions(-)
create mode 100644 neuron/theme/chat-widget-layout.css
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 = `
-
-
-
+
+
+
`;
- // 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 = `
+
+
+
+
+ `;
+
+ // 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)
From 835396e3a28a142a25af9dda62dad7353d6fe5a1 Mon Sep 17 00:00:00 2001
From: dunnerz <74564541+dunnerz@users.noreply.github.com>
Date: Thu, 11 Sep 2025 00:06:07 +0100
Subject: [PATCH 2/2] A few small fixes
Status button- fixed vertical alignment, Template menu- removed 2nd scrollbar, fix in dedicated window
---
neuron/theme/template-browser.css | 80 +++++++++++++++++++++++--------
public/health-indicator.js | 1 +
2 files changed, 62 insertions(+), 19 deletions(-)
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');