diff --git a/assistant.js b/assistant.js index b843607b..e3563357 100644 --- a/assistant.js +++ b/assistant.js @@ -29,18 +29,6 @@ ` - const toggleCloseButton = document.createElement('button') - toggleCloseButton.id = 'bot-toggle-close' - toggleCloseButton.classList.add('bot-toggle-close') - toggleCloseButton.setAttribute('aria-label', 'Close bot') - toggleCloseButton.innerHTML = ` - - ` - - const resizeHandle = document.createElement('div') - resizeHandle.classList.add('bot-resize-handle') - resizeHandle.setAttribute('aria-label', 'Resize panel') - const mobileDismiss = document.createElement('div') mobileDismiss.classList.add('bot-mobile-dismiss') mobileDismiss.setAttribute('aria-label', 'Swipe down to close') @@ -58,19 +46,8 @@ botContainer.id = 'docs-bot' botContainer.classList.add('bot-iframe-container') - // The default docs bot covers everything under botpress.com/docs. - // The ADK section gets its own bot (agent-0) with knowledge scoped to - // ADK pages + the ADK skill references. The iframe URL is swapped on - // route changes — see checkPathChange below. - const DEFAULT_BOT_URL = 'https://botpress.github.io/docs-bot/' - const ADK_BOT_URL = 'https://botpress.github.io/docs-bot/adk-bot-frontend/' - - function isAdkRoute() { - // Match /docs/adk/ on the live site (pathname includes /docs/ prefix). - // The bare /adk and /adk/ teaser routes keep using the default docs bot. - return /\/adk\/.+/.test(window.location.pathname) - } - + // A single docs assistant (marg) covers everything under botpress.com/docs. + const MARG_BOT_URL = 'https://botpress.github.io/docs-bot/marg-frontend/' const iframe = document.createElement('iframe') iframe.title = 'Botpress' @@ -80,80 +57,46 @@ iframe.style.top = '0' iframe.style.left = '0' iframe.allow = 'clipboard-write' - iframe.src = DEFAULT_BOT_URL - - const adkIframe = document.createElement('iframe') - adkIframe.title = 'Botpress ADK' - adkIframe.style.width = '100%' - adkIframe.style.height = '100%' - adkIframe.style.position = 'absolute' - adkIframe.style.top = '0' - adkIframe.style.left = '0' - adkIframe.allow = 'clipboard-write' - adkIframe.src = ADK_BOT_URL + iframe.src = MARG_BOT_URL // "ready" means the React app inside the iframe has mounted and rendered its // first frame. We detect this via the `requestTheme` postMessage the frontend - // sends from a useEffect on mount — that fires after the first paint, not just - // after the HTML document loads. The `load` event fires too early (HTML loaded - // but React not yet mounted), so using it directly causes a blank-white flash. - let defaultReady = false - let adkReady = false - - function markReady(isAdk) { - if (isAdk) adkReady = true - else defaultReady = true + // sends from a useEffect on mount — the `load` event fires too early (HTML + // loaded but React not yet mounted), so using it directly causes a flash. + let ready = false + + function markReady() { + ready = true showActiveIframe() } - // Fallback: if the iframe never sends requestTheme (e.g. default docs bot - // doesn't have the hook), mark ready 600ms after the HTML load event so the - // panel isn't stuck hidden forever. + // Fallback: if the iframe never sends requestTheme, mark ready 600ms after + // the HTML load event so the panel isn't stuck hidden forever. iframe.addEventListener('load', () => { - setTimeout(() => { if (!defaultReady) markReady(false) }, 600) - }) - adkIframe.addEventListener('load', () => { - setTimeout(() => { if (!adkReady) markReady(true) }, 600) + setTimeout(() => { if (!ready) markReady() }, 600) }) function showActiveIframe() { - const adk = isAdkRoute() - const target = adk ? adkIframe : iframe - const other = adk ? iframe : adkIframe - const ready = adk ? adkReady : defaultReady - if (ready) { - // Bring the active bot to the front (z-index swap, no repaint = no flash). - target.style.zIndex = '2' - target.style.pointerEvents = 'auto' - other.style.zIndex = '0' - other.style.pointerEvents = 'none' + iframe.style.zIndex = '2' + iframe.style.pointerEvents = 'auto' } - // If target isn't ready yet, leave both layers as-is — 'other' keeps its - // current z-index so the panel stays populated. markReady() fires again - // once the target's React app has mounted. } + // Kept as a function so downstream message senders need no changes. function getActiveIframe() { - return isAdkRoute() ? adkIframe : iframe + return iframe } - // Default starts above ADK so non-ADK pages show the correct bot before - // either React app sends its first readiness signal (requestTheme). iframe.style.zIndex = '1' iframe.style.pointerEvents = 'none' - adkIframe.style.zIndex = '0' - adkIframe.style.pointerEvents = 'none' botContainer.style.position = 'relative' botContainer.appendChild(iframe) - botContainer.appendChild(adkIframe) showActiveIframe() panel.appendChild(mobileDismiss) - resizeHandle.appendChild(toggleCloseButton) panel.appendChild(botContainer) - panel.appendChild(resizeHandle) document.body.appendChild(overlay) document.body.appendChild(panel) @@ -248,62 +191,26 @@ updateOverlay() } - let isResizing = false - let startX = 0 - let startWidth = 0 - let hasMoved = false - const clickThreshold = 5 - - resizeHandle.addEventListener('mousedown', (e) => { - if (e.target.closest('#bot-toggle-close')) { - return - } - isResizing = true - hasMoved = false - startX = e.clientX - startWidth = parseInt(window.getComputedStyle(panel).width, 10) - panel.classList.add('resizing') - document.body.style.cursor = 'col-resize' - document.body.style.userSelect = 'none' - e.preventDefault() - e.stopPropagation() - }) - - document.addEventListener('mousemove', (e) => { - if (!isResizing) return - - const moveDistance = Math.abs(e.clientX - startX) - if (moveDistance > clickThreshold) { - hasMoved = true - } - - if (hasMoved) { - const diff = startX - e.clientX - const maxWidth = window.innerWidth * 1 - const newWidth = Math.max(368, Math.min(maxWidth, startWidth + diff)) - panel.style.width = newWidth + 'px' - } - - e.preventDefault() - e.stopPropagation() - }) - - document.addEventListener('mouseup', (e) => { - if (isResizing) { - isResizing = false - panel.classList.remove('resizing') - document.body.style.cursor = '' - document.body.style.userSelect = '' - - if (!hasMoved) { - togglePanel() - } - - hasMoved = false - e.preventDefault() - e.stopPropagation() + // Maximize/restore the panel width (driven by the iframe's expand button). + // Remembers the prior inline width so "restore" returns to the CSS default + // or a drag-resized width. Persisted so it survives in-docs navigation. + let widePrevWidth = '' + let isWide = false + function setWide(wide) { + if (wide === isWide) return + if (wide) { + widePrevWidth = panel.style.width + const target = Math.round(Math.min(window.innerWidth * 0.6, 820)) + panel.style.width = target + 'px' + } else { + panel.style.width = widePrevWidth } - }) + isWide = wide + try { sessionStorage.setItem('bot-panel-wide', wide ? '1' : '0') } catch (e) {} + } + function toggleWidth() { + setWide(!isWide) + } let touchStartY = 0 let touchCurrentY = 0 @@ -363,10 +270,6 @@ panel.addEventListener('touchcancel', handleTouchEnd, { passive: false }) toggleButton.addEventListener('click', togglePanel) - toggleCloseButton.addEventListener('click', (e) => { - e.stopPropagation() - closePanel() - }) mobileDismiss.addEventListener('click', closePanel) overlay.addEventListener('click', (e) => { if (isMobile() && panel.classList.contains('bot-panel-expanded')) { @@ -413,6 +316,10 @@ closePanel() } + if (event.data.type === 'toggleWidth') { + toggleWidth() + } + if (event.data.type === 'requestCurrentPage') { sendPanelOpenedMessage() } @@ -452,11 +359,7 @@ const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light' // The iframe's React app just mounted — mark it as visually ready so we // can show it without a blank-white flash. - if (event.source === adkIframe.contentWindow) { - if (!adkReady) markReady(true) - } else if (event.source === iframe.contentWindow) { - if (!defaultReady) markReady(false) - } + if (event.source === iframe.contentWindow && !ready) markReady() // Respond with the current theme to whichever iframe asked. if (event.source && typeof event.source.postMessage === 'function') { try { event.source.postMessage({ type: 'themeChanged', theme }, '*') } catch (_e) {} @@ -464,13 +367,10 @@ } }) - // Watch for docs theme toggles and forward to both iframes so they stay in - // sync regardless of which one is currently visible. + // Watch for docs theme toggles and forward to the iframe so it stays in sync. const themeObserver = new MutationObserver(() => { const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light' - ;[iframe, adkIframe].forEach(f => { - if (f && f.contentWindow) f.contentWindow.postMessage({ type: 'themeChanged', theme }, '*') - }) + if (iframe.contentWindow) iframe.contentWindow.postMessage({ type: 'themeChanged', theme }, '*') }) themeObserver.observe(document.documentElement, { attributes: true, @@ -541,23 +441,19 @@ if (window.location.pathname !== lastPath) { lastPath = window.location.pathname - showActiveIframe() - - const isExpanded = panel.classList.contains('bot-panel-expanded') - if (isExpanded) { - const activeIframe = isAdkRoute() ? adkIframe : iframe - if (activeIframe && activeIframe.contentWindow) { - activeIframe.contentWindow.postMessage( - { - type: 'pageChanged', - data: { - path: window.location.pathname, - title: document.title.replace(' - Botpress', ''), - }, + // Always tell the bot the page changed so it can refresh its page + // context, even when the panel is collapsed (it routes on the page URL). + if (iframe.contentWindow) { + iframe.contentWindow.postMessage( + { + type: 'pageChanged', + data: { + path: window.location.pathname, + title: document.title.replace(' - Botpress', ''), }, - '*' - ) - } + }, + '*' + ) } } } @@ -580,6 +476,14 @@ } function initInputBubble() { + // The iframe is created in a separate IIFE, so resolve it from the DOM here + // (this closure can't see initBotPanel's getActiveIframe). Without this the + // bottom "Ask a question" bar threw a ReferenceError and silently did nothing. + function getActiveIframe() { + const container = document.getElementById('docs-bot') + return container ? container.querySelector('iframe') : null + } + const inputBubble = document.createElement('div') inputBubble.id = 'ask-ai-input-bubble' inputBubble.classList.add('ask-ai-input-bubble') @@ -834,6 +738,13 @@ function initAskAIOverride() { const overriddenButtons = new WeakSet() + // Resolve the iframe from the DOM (defined in a separate IIFE) so this + // closure can message it. + function getActiveIframe() { + const container = document.getElementById('docs-bot') + return container ? container.querySelector('iframe') : null + } + function findAndOverrideButton() { const button = document.getElementById('page-context-menu-button') diff --git a/styles.css b/styles.css index 7a2b4992..f74d6cbc 100644 --- a/styles.css +++ b/styles.css @@ -182,52 +182,6 @@ img { height: 16px; } -#bot-toggle-close { - width: 35px; - height: 35px; - flex-shrink: 0; - color: light-dark(#1a1a1a, #fcfcfc); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - z-index: 32; - pointer-events: none; - background-color: light-dark(#fcfcfc, #1a1a1a); - border: 1px solid light-dark(rgb(226, 222, 230), rgb(255 255 255/0.07)); - border-radius: 0.75em; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - opacity: 1; - transition: opacity 0.2s ease-in-out; -} - -.bot-resize-handle:hover { - cursor: col-resize; - pointer-events: auto; -} - -.bot-panel-collapsed #bot-toggle-close { - opacity: 0; - pointer-events: none; -} - -.bot-panel-expanded .bot-resize-handle:hover #bot-toggle-close { - pointer-events: auto; -} - -#bot-toggle-close:hover { - background-color: light-dark(#f0f0f0, #2a2a2a); -} - -#bot-toggle-close svg { - width: 16px; - height: 16px; -} - .bot-panel { position: fixed; top: 3.55rem; @@ -240,7 +194,7 @@ img { display: flex; flex-direction: row; transform: translateX(100%); - resize: horizontal; + resize: none; overflow: visible; transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1); } @@ -253,22 +207,6 @@ img { transform: translateX(100%); } -.bot-resize-handle { - position: absolute; - left: -20px; - top: 0; - bottom: 0; - width: 40px; - height: 100%; - cursor: col-resize; - background-color: transparent; - z-index: 30; - pointer-events: auto; - display: flex; - align-items: center; - justify-content: center; -} - .bot-iframe-container { flex: 1; height: 100%; @@ -279,14 +217,6 @@ img { border-left: 1px solid light-dark(rgb(226, 222, 230), rgb(255 255 255/0.07)); } -.bot-panel.resizing { - transition: none; -} - -.bot-panel.resizing .bot-iframe-container { - pointer-events: none; -} - .bot-iframe { width: 100%; height: 100%; @@ -328,15 +258,6 @@ img { display: none !important; } - .bot-resize-handle { - display: none !important; - pointer-events: none !important; - } - - #bot-toggle-close { - display: none !important; - } - /* Show mobile dismiss arrow */ .bot-mobile-dismiss { display: flex;