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)
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');