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;