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