From 1d2f6f4ee6c4f433db8a2dd8d70c7a556c345430 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 11:40:01 +0200 Subject: [PATCH 1/8] feat: add Kapa.ai Ask AI widget to docs header Adds the Kapa.ai AI assistant widget with a branded "Ask AI" button in the site header, clearly distinguished from the Pagefind doc search. - AskAIButton.astro: sparkle icon + "Ask AI" label, icon-only on mobile - kapa.js: dynamic widget bootstrap that reads data-theme before injecting kapa's script, so initial colours are always correct for the active mode - Dark mode live switching: MutationObserver flips data-mantine-color-scheme on kapa's open shadow root and injects ICP brand tokens as CSS variable overrides, keeping the modal in sync when the user toggles theme mid-session - CSP updated: script-src, style-src, img-src, connect-src, and frame-src extended for widget.kapa.ai, proxy.kapa.ai, metrics.kapa.ai, and hCaptcha --- astro.config.mjs | 4 ++ public/.ic-assets.json5 | 2 +- public/kapa.js | 79 ++++++++++++++++++++++++++++++++ src/components/AskAIButton.astro | 13 ++++++ src/components/Header.astro | 4 +- src/styles/custom.css | 58 +++++++++++++++++++++++ 6 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 public/kapa.js create mode 100644 src/components/AskAIButton.astro diff --git a/astro.config.mjs b/astro.config.mjs index 347d4f8f..fe81e05d 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -102,6 +102,10 @@ export default defineConfig({ tag: "script", attrs: { src: "/matomo.js", async: true }, }, + { + tag: "script", + attrs: { src: "/kapa.js", defer: true }, + }, { tag: "script", content: `document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('a[href^="http"]').forEach(a=>{a.setAttribute('target','_blank');a.setAttribute('rel','noopener noreferrer')});document.querySelectorAll('[data-copy]').forEach(b=>{b.addEventListener('click',()=>{navigator.clipboard.writeText(b.dataset.copy);const i=b.querySelector('svg');if(i){const orig=i.innerHTML;i.innerHTML='';i.style.stroke='#22c55e';i.style.opacity='1';setTimeout(()=>{i.innerHTML=orig;i.style.stroke='';i.style.opacity=''},1500)}})});const sb=document.getElementById('skills-give-btn');const sl=document.getElementById('skills-give-label');if(sb&&sl){const orig=sl.textContent;sb.addEventListener('click',()=>{navigator.clipboard.writeText('Fetch https://skills.internetcomputer.org/llms.txt and follow its instructions when building on ICP').catch(()=>{});sl.textContent='Now paste into your agent';setTimeout(()=>{sl.textContent=orig},3000)})}})`, diff --git a/public/.ic-assets.json5 b/public/.ic-assets.json5 index f5aa50e9..12d50c26 100644 --- a/public/.ic-assets.json5 +++ b/public/.ic-assets.json5 @@ -12,7 +12,7 @@ // plugins such as Form Analytics or Heatmaps that call eval() internally). // Better long-term fix: disable those plugins in the Matomo Cloud dashboard // so the bundled matomo.js no longer needs eval(), then remove 'unsafe-eval'. - "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.matomo.cloud; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://www.plantuml.com; font-src 'self' data:; connect-src 'self' https://icp0.io https://*.icp0.io https://internetcomputer.matomo.cloud; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests", + "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.matomo.cloud https://widget.kapa.ai https://hcaptcha.com https://*.hcaptcha.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; img-src 'self' data: https://www.plantuml.com https://widget.kapa.ai; font-src 'self' data:; connect-src 'self' https://icp0.io https://*.icp0.io https://internetcomputer.matomo.cloud https://proxy.kapa.ai https://metrics.kapa.ai https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app https://hcaptcha.com https://*.hcaptcha.com; frame-src https://hcaptcha.com https://*.hcaptcha.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "strict-origin-when-cross-origin", "Permissions-Policy": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" diff --git a/public/kapa.js b/public/kapa.js new file mode 100644 index 00000000..0d295761 --- /dev/null +++ b/public/kapa.js @@ -0,0 +1,79 @@ +(function () { + var dark = document.documentElement.dataset.theme === 'dark'; + + // Inject kapa widget script with initial theme colours. + // data-theme is set by ThemeProvider's is:inline script before this deferred script runs. + var s = document.createElement('script'); + s.src = 'https://widget.kapa.ai/kapa-widget.bundle.js'; + s.async = true; + Object.assign(s.dataset, { + websiteId: '73cafe70-9be1-494b-bd31-b849fc29799f', + projectName: 'Internet Computer', + projectColor: '#cc5a2b', + projectLogo: 'https://docs.internetcomputer.org/favicon.svg', + modalOverrideOpenClass: 'ask-ai-widget-trigger', + modalAskAiInputPlaceholder: 'Ask anything about building on ICP...', + modalExampleQuestions: 'How do I deploy a canister?,What are cycles?,How do I call an external API from a canister?', + modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.', + buttonHide: 'true', + botProtectionMechanism: 'hcaptcha', + userAnalyticsFingerprintEnabled: 'true', + modalZIndex: '1001', + }); + if (dark) { + Object.assign(s.dataset, { + modalBgColor: '#1b1812', + fontColor: '#f0ebe0', + modalBorderColor: '#2d2820', + }); + } + document.head.appendChild(s); + + // ICP brand tokens injected into kapa's Mantine shadow DOM. + // Kapa hardcodes data-mantine-color-scheme="light" on #kapa-widget-root regardless of + // the data-modal-bg-color attribute, so we flip the attribute and override Mantine's CSS + // variables directly inside the open shadow root. + var TOKENS = { + light: '#kapa-widget-root{--mantine-color-body:#fdfaf3;--mantine-color-default:#f8f5ef;--mantine-color-default-border:#e5ddcf;--mantine-color-text:#1a1714;--mantine-color-placeholder:#6b6660;}', + dark: '#kapa-widget-root{--mantine-color-body:#1b1812;--mantine-color-default:#221e18;--mantine-color-default-border:#2d2820;--mantine-color-text:#f0ebe0;--mantine-color-placeholder:#a29a8d;}', + }; + + function sync() { + var c = document.getElementById('kapa-widget-container'); + if (!c || !c.shadowRoot) return false; + var r = c.shadowRoot.querySelector('#kapa-widget-root'); + if (!r) return false; + var scheme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'; + r.setAttribute('data-mantine-color-scheme', scheme); + var st = c.shadowRoot.querySelector('#kapa-icp-tokens'); + if (!st) { + st = document.createElement('style'); + st.id = 'kapa-icp-tokens'; + c.shadowRoot.appendChild(st); + } + st.textContent = TOKENS[scheme]; + return true; + } + + // Watch for kapa-widget-container being added to the DOM (kapa loads async). + var bodyObserver = new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var added = mutations[i].addedNodes; + for (var j = 0; j < added.length; j++) { + if (added[j].id === 'kapa-widget-container') { + // Give kapa's React one tick to finish rendering the shadow root. + setTimeout(function () { if (sync()) bodyObserver.disconnect(); }, 0); + return; + } + } + } + }); + bodyObserver.observe(document.body, { childList: true }); + sync(); // In case kapa was already in the DOM (e.g. client-side navigation). + + // Re-sync whenever the user toggles the site theme. + new MutationObserver(sync).observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], + }); +})(); diff --git a/src/components/AskAIButton.astro b/src/components/AskAIButton.astro new file mode 100644 index 00000000..d4fefdf6 --- /dev/null +++ b/src/components/AskAIButton.astro @@ -0,0 +1,13 @@ +--- +--- + diff --git a/src/components/Header.astro b/src/components/Header.astro index 01733b33..aad83c60 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -6,6 +6,7 @@ import Search from 'virtual:starlight/components/Search'; import SiteTitle from 'virtual:starlight/components/SiteTitle'; import SocialIcons from 'virtual:starlight/components/SocialIcons'; import ThemeSelect from 'virtual:starlight/components/ThemeSelect'; +import AskAIButton from './AskAIButton.astro'; const shouldRenderSearch = config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro'; @@ -15,8 +16,9 @@ const shouldRenderSearch =
-
+
{shouldRenderSearch && } +
{/* Mobile-only theme toggle — desktop uses the one inside right-group */}
diff --git a/src/styles/custom.css b/src/styles/custom.css index f886470a..08f1c4b3 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -329,6 +329,64 @@ h1, h2, h3, h4, h5, h6 { filter: invert(1) hue-rotate(180deg); } +/* ── Ask AI button (Kapa.ai widget trigger) ─────────────────── */ + +.search-group { + gap: 0.5rem; +} + +.ask-ai-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + align-self: stretch; + background: transparent; + border: 1px solid var(--icp-rule); + border-radius: 6px; + padding: 0 0.75rem; + color: var(--icp-fg); + font-family: inherit; + font-size: 0.8125rem; + font-weight: 500; + line-height: 1; + white-space: nowrap; + flex-shrink: 0; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.ask-ai-btn:hover { + background: var(--icp-accent-dim); + border-color: var(--icp-accent); +} + +.ask-ai-icon { + color: var(--icp-accent); + flex-shrink: 0; + animation: ask-ai-sparkle 3.5s ease-in-out infinite; +} + +.ask-ai-btn:hover .ask-ai-icon { + animation-duration: 1s; +} + +@keyframes ask-ai-sparkle { + 0%, 60%, 100% { opacity: 1; } + 80% { opacity: 0.5; } +} + +@media (max-width: 49.999rem) { + .ask-ai-label { + display: none; + } + + .ask-ai-btn { + padding: 0 0.5rem; + align-self: center; + height: 2rem; + } +} + /* ── Agent signaling ──────────────────────────────────────── */ /* Visually hidden but present in DOM for HTML-to-markdown converters. From 08bc02c3cc224cdfeb61068ad68d043a5933252e Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 11:46:22 +0200 Subject: [PATCH 2/8] docs: update kapa example questions for cloud-developer audience --- public/kapa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/kapa.js b/public/kapa.js index 0d295761..44fe0c5c 100644 --- a/public/kapa.js +++ b/public/kapa.js @@ -13,7 +13,7 @@ projectLogo: 'https://docs.internetcomputer.org/favicon.svg', modalOverrideOpenClass: 'ask-ai-widget-trigger', modalAskAiInputPlaceholder: 'Ask anything about building on ICP...', - modalExampleQuestions: 'How do I deploy a canister?,What are cycles?,How do I call an external API from a canister?', + modalExampleQuestions: 'How do I pay for my application?,Where does my app store data?,How do I add user login to my app?', modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.', buttonHide: 'true', botProtectionMechanism: 'hcaptcha', From 454b4dc8f95fe5e8b6ed23768a9d0b8c38b390ad Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 11:52:59 +0200 Subject: [PATCH 3/8] docs: add 4th example question and reorder for developer onboarding flow --- public/kapa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/kapa.js b/public/kapa.js index 44fe0c5c..31c9f7fc 100644 --- a/public/kapa.js +++ b/public/kapa.js @@ -13,7 +13,7 @@ projectLogo: 'https://docs.internetcomputer.org/favicon.svg', modalOverrideOpenClass: 'ask-ai-widget-trigger', modalAskAiInputPlaceholder: 'Ask anything about building on ICP...', - modalExampleQuestions: 'How do I pay for my application?,Where does my app store data?,How do I add user login to my app?', + modalExampleQuestions: 'What makes ICP different from traditional clouds?,How do I pay for my application?,Where does my app store data?,How do I add user login to my app?', modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.', buttonHide: 'true', botProtectionMechanism: 'hcaptcha', From e6f212a24d65c06ae4b4cbdca3bbfc74dbd7ac4f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 11:55:52 +0200 Subject: [PATCH 4/8] docs: refine example questions order and phrasing --- public/kapa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/kapa.js b/public/kapa.js index 31c9f7fc..d893aa20 100644 --- a/public/kapa.js +++ b/public/kapa.js @@ -13,7 +13,7 @@ projectLogo: 'https://docs.internetcomputer.org/favicon.svg', modalOverrideOpenClass: 'ask-ai-widget-trigger', modalAskAiInputPlaceholder: 'Ask anything about building on ICP...', - modalExampleQuestions: 'What makes ICP different from traditional clouds?,How do I pay for my application?,Where does my app store data?,How do I add user login to my app?', + modalExampleQuestions: 'What makes ICP different from traditional clouds?,How does my app store data?,How do I pay for my application?,How do I add user login to my app?', modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.', buttonHide: 'true', botProtectionMechanism: 'hcaptcha', From 6977f67a3dd55715ad0d5eff62327c9f0a998335 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 11:58:13 +0200 Subject: [PATCH 5/8] docs: add security question to kapa example questions --- public/kapa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/kapa.js b/public/kapa.js index d893aa20..f961b357 100644 --- a/public/kapa.js +++ b/public/kapa.js @@ -13,7 +13,7 @@ projectLogo: 'https://docs.internetcomputer.org/favicon.svg', modalOverrideOpenClass: 'ask-ai-widget-trigger', modalAskAiInputPlaceholder: 'Ask anything about building on ICP...', - modalExampleQuestions: 'What makes ICP different from traditional clouds?,How does my app store data?,How do I pay for my application?,How do I add user login to my app?', + modalExampleQuestions: 'What makes ICP different from traditional clouds?,How does my app store data?,How do I pay for my application?,How do I add user login to my app?,Is my app\'s data private on ICP?', modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.', buttonHide: 'true', botProtectionMechanism: 'hcaptcha', From 51319f1e9756f92970b5c8d8e0b715fb6cae3731 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 12:13:41 +0200 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20finalise=20kapa=20widget=20?= =?UTF-8?q?=E2=80=94=20example=20questions=20and=20scrollbar=20stability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add coding agents question, reorder to two full-width intros - Fix header layout shift on modal open with scrollbar-gutter: stable --- public/kapa.js | 2 +- src/styles/custom.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/kapa.js b/public/kapa.js index f961b357..9e674280 100644 --- a/public/kapa.js +++ b/public/kapa.js @@ -13,7 +13,7 @@ projectLogo: 'https://docs.internetcomputer.org/favicon.svg', modalOverrideOpenClass: 'ask-ai-widget-trigger', modalAskAiInputPlaceholder: 'Ask anything about building on ICP...', - modalExampleQuestions: 'What makes ICP different from traditional clouds?,How does my app store data?,How do I pay for my application?,How do I add user login to my app?,Is my app\'s data private on ICP?', + modalExampleQuestions: 'What makes ICP different from traditional clouds?,How can I build and deploy ICP apps with coding agents?,How does my app store data?,How do I pay for my application?,How do I add user login to my app?,Is my app\'s data private on ICP?', modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.', buttonHide: 'true', botProtectionMechanism: 'hcaptcha', diff --git a/src/styles/custom.css b/src/styles/custom.css index 08f1c4b3..17611f4e 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -38,6 +38,10 @@ --sl-content-width: 55rem; } +html { + scrollbar-gutter: stable; +} + /* Newsreader for all headings */ h1, h2, h3, h4, h5, h6 { font-family: 'Newsreader', 'Source Serif 4', 'EB Garamond', ui-serif, Georgia, serif; From 8dfca22d86bb17fe185d6ff456dab4f3882595df Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Apr 2026 14:10:51 +0200 Subject: [PATCH 7/8] fix: prevent layout shift when kapa modal opens on scrollable pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react-remove-scroll stamps data-scroll-locked on body and injects `body[data-scroll-locked] { margin-right: 17px !important }` to compensate for the scrollbar disappearing on scroll-lock. With scrollbar-gutter: stable already reserving that space, the margin double-compensates and shifts content 17px left. Primary fix: @layer kapa-fix in custom.css — a layered !important beats an unlayered !important per CSS Cascade 5 regardless of specificity or source order. Backup: MutationObserver in kapa.js patches the injected node's text content directly. --- public/kapa.js | 22 ++++++++++++++++++++++ src/styles/custom.css | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/public/kapa.js b/public/kapa.js index 9e674280..3616e10d 100644 --- a/public/kapa.js +++ b/public/kapa.js @@ -76,4 +76,26 @@ attributes: true, attributeFilter: ['data-theme'], }); + + // react-remove-scroll stamps data-scroll-locked on body and injects a