diff --git a/adk/assets/cursor.png b/adk/assets/cursor.png new file mode 100644 index 00000000..1cbbb26c Binary files /dev/null and b/adk/assets/cursor.png differ diff --git a/adk/assets/intro-new-dark.png b/adk/assets/intro-new-dark.png index 6ec88ff7..54869303 100644 Binary files a/adk/assets/intro-new-dark.png and b/adk/assets/intro-new-dark.png differ diff --git a/adk/assets/intro-new.png b/adk/assets/intro-new.png index fbb26c92..d56fee96 100644 Binary files a/adk/assets/intro-new.png and b/adk/assets/intro-new.png differ diff --git a/adk/quickstart-ai-assistant.mdx b/adk/quickstart-ai-assistant.mdx new file mode 100644 index 00000000..87de7893 --- /dev/null +++ b/adk/quickstart-ai-assistant.mdx @@ -0,0 +1,76 @@ +--- +title: Quickstart with Cursor +description: Use Cursor to build your first ADK agent. +--- + +Install the ADK CLI, give Cursor ADK knowledge with skills, then let it build your agent from a simple prompt. + + + Cursor building an ADK agent + + +## Install the CLI + + + ```bash macOS & Linux + curl -fsSL https://github.com/botpress/adk/releases/latest/download/install.sh | bash + ``` + + ```powershell Windows (PowerShell) + powershell -c "irm https://github.com/botpress/adk/releases/latest/download/install.ps1 | iex" + ``` + + +## Install ADK skills + +Skills give Cursor deep knowledge of ADK conventions, primitives, and CLI commands: + +```bash +bunx skills add botpress/skills -s '*' -a cursor -y +``` + +## Build your agent + +Open your project in Cursor, use the chat panel (Cmd + L), and give it a prompt: + +``` +Initialize a simple ADK hello world bot for me. +``` + +Cursor will scaffold the project and write the agent code. Once it's done, run these commands from the project folder: + +```bash +adk link # connects your agent to Botpress (opens a browser to log in) +adk dev # starts the dev console +``` + +Open the dev console and start chatting with your agent: + + + ADK dev console chat + ADK dev console chat + + +## Add more features + +Keep prompting Cursor to extend your bot. For example: + +``` +Add a tool that looks up the weather for a given city. +``` + +Chat with your bot in the dev console to try out your changes. + +## Deploy your agent + +When you're ready to share your bot, deploy it to Botpress Cloud: + +```bash +adk deploy +``` + +--- + + + Learn about project structure, models, state, and dependencies. + diff --git a/assistant.js b/assistant.js index fd555ad8..b843607b 100644 --- a/assistant.js +++ b/assistant.js @@ -71,18 +71,84 @@ return /\/adk\/.+/.test(window.location.pathname) } - function botUrlForCurrentRoute() { - return isAdkRoute() ? ADK_BOT_URL : DEFAULT_BOT_URL - } const iframe = document.createElement('iframe') iframe.title = 'Botpress' iframe.style.width = '100%' iframe.style.height = '100%' - iframe.src = botUrlForCurrentRoute() + iframe.style.position = 'absolute' + 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 + + // "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 + 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. + iframe.addEventListener('load', () => { + setTimeout(() => { if (!defaultReady) markReady(false) }, 600) + }) + adkIframe.addEventListener('load', () => { + setTimeout(() => { if (!adkReady) markReady(true) }, 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' + } + // 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. + } + + function getActiveIframe() { + return isAdkRoute() ? adkIframe : 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) @@ -116,7 +182,7 @@ } function focusComposerInput() { - const iframe = document.querySelector('iframe[title="Botpress"]') + const iframe = getActiveIframe() if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage( { @@ -128,7 +194,7 @@ } function sendPanelOpenedMessage() { - const iframe = document.querySelector('iframe[title="Botpress"]') + const iframe = getActiveIframe() if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage( { @@ -384,20 +450,27 @@ // light/dark. Mintlify sets `class="dark"` on in dark mode. if (event.data.type === 'requestTheme') { const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light' - const iframe = document.querySelector('iframe[title="Botpress"]') - if (iframe && iframe.contentWindow) { - iframe.contentWindow.postMessage({ type: 'themeChanged', theme }, '*') + // 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) + } + // 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) {} } } }) - // Watch for docs theme toggles and forward to the iframe. + // Watch for docs theme toggles and forward to both iframes so they stay in + // sync regardless of which one is currently visible. const themeObserver = new MutationObserver(() => { const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light' - const iframe = document.querySelector('iframe[title="Botpress"]') - if (iframe && iframe.contentWindow) { - iframe.contentWindow.postMessage({ type: 'themeChanged', theme }, '*') - } + ;[iframe, adkIframe].forEach(f => { + if (f && f.contentWindow) f.contentWindow.postMessage({ type: 'themeChanged', theme }, '*') + }) }) themeObserver.observe(document.documentElement, { attributes: true, @@ -421,6 +494,26 @@ window.addEventListener('hashchange', handleHashChange) updateOverlay() + // Card/button link clicks trigger Mintlify's page transition animation which + // causes the iframe to flash. Sidebar links don't trigger this. Hide the + // panel for the duration of the transition to prevent the flash. + let hideTimer = null + document.addEventListener('click', (e) => { + const link = e.target.closest('a') + if (!link) return + const href = link.getAttribute('href') + if (!href || href.startsWith('#') || href.startsWith('http') || href.startsWith('mailto')) return + if (panel.classList.contains('bot-panel-expanded')) return + if (hideTimer) clearTimeout(hideTimer) + panel.style.visibility = 'hidden' + toggleButton.style.visibility = 'hidden' + hideTimer = setTimeout(() => { + panel.style.visibility = '' + toggleButton.style.visibility = '' + hideTimer = null + }, 400) + }, true) + let resizeTimeout window.addEventListener('resize', () => { clearTimeout(resizeTimeout) @@ -448,20 +541,13 @@ if (window.location.pathname !== lastPath) { lastPath = window.location.pathname - // Swap the bot iframe when we cross the ADK boundary. Stripping the - // src triggers a full reload, so the agent's conversation resets — - // intentional, since we're switching to a different agent entirely. - const targetBotUrl = botUrlForCurrentRoute() - const iframeEl = document.querySelector('iframe[title="Botpress"]') - if (iframeEl && iframeEl.src !== targetBotUrl) { - iframeEl.src = targetBotUrl - } + showActiveIframe() const isExpanded = panel.classList.contains('bot-panel-expanded') if (isExpanded) { - const iframe = document.querySelector('iframe[title="Botpress"]') - if (iframe && iframe.contentWindow) { - iframe.contentWindow.postMessage( + const activeIframe = isAdkRoute() ? adkIframe : iframe + if (activeIframe && activeIframe.contentWindow) { + activeIframe.contentWindow.postMessage( { type: 'pageChanged', data: { @@ -561,7 +647,7 @@ if (toggleButton) { toggleButton.classList.add('bot-toggle-expanded') } - const iframe = document.querySelector('iframe[title="Botpress"]') + const iframe = getActiveIframe() if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage( { @@ -699,7 +785,7 @@ } } - const iframe = document.querySelector('iframe[title="Botpress"]') + const iframe = getActiveIframe() if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage( { @@ -769,7 +855,7 @@ } } - const iframe = document.querySelector('iframe[title="Botpress"]') + const iframe = getActiveIframe() if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage( { diff --git a/docs.json b/docs.json index e61161ce..f45c0493 100644 --- a/docs.json +++ b/docs.json @@ -451,7 +451,8 @@ "group": "Get started", "pages": [ "/adk/introduction", - "/adk/quickstart" + "/adk/quickstart", + "/adk/quickstart-ai-assistant" ] }, {