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..3616e10d
--- /dev/null
+++ b/public/kapa.js
@@ -0,0 +1,101 @@
+(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: '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',
+ 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'],
+ });
+
+ // react-remove-scroll stamps data-scroll-locked on body and injects a