This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git


The following commit(s) were added to refs/heads/master by this push:
     new be33b3e20ed [Feature](website) Add community roadmap page and improve 
homepage loading (#4038)
be33b3e20ed is described below

commit be33b3e20ed114e295a137c946eacb9b3030b132
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Aug 5 20:22:46 2026 +0800

    [Feature](website) Add community roadmap page and improve homepage loading 
(#4038)
    
    ## Summary
    
    - add a community roadmap landing page and route
    - give both Ask Me entry points immediate loading, slow-loading, and
    failure feedback
    - queue early Ask Me clicks and automatically open the Kapa dialog once
    the widget is ready
    - lazy-load and asynchronously decode below-the-fold homepage images
    
    ## Validation
    
    - `yarn start --no-open --port 3000` — English development site compiled
    successfully
    - `node --check static/js/custom-script.js`
    - `git diff --check`
    - simulated a delayed Kapa response: loading feedback appeared
    immediately, changed after 8 seconds, and the queued click opened the
    dialog after readiness
    - simulated a failed Kapa response: both buttons showed `AI unavailable`
    - verified Deployment, Ecosystem, and Stats lazy-loading attributes in
    the browser
    
    ## Versions
    
    - Not applicable: website UI changes only
    
    ## Languages
    
    - [x] English
    - [ ] Chinese
    - [ ] Japanese candidate translation needed
    
    ## Docs Checklist
    
    - [x] Checked by AI
    - [x] Test Cases Built
    - [x] Updated required version and language counterparts, or explained
    why not
    - [x] If only one language changed, confirmed whether source/translation
    counterparts need sync
    
    The full repository `yarn typecheck` remains blocked by pre-existing
    unrelated type errors; none of the touched files appeared in the error
    list.
    
    ---------
    
    Co-authored-by: morningman <[email protected]>
---
 docusaurus.config.js                               |   2 +-
 .../community-roadmap/CommunityRoadmap.scss        | 315 +++++++++++++++++++++
 .../community-roadmap/CommunityRoadmap.tsx         | 115 ++++++++
 src/components/home-next/NavbarNext.tsx            |  13 +-
 .../home-next/sections/DeploymentSection.tsx       |  16 +-
 .../home-next/sections/EcosystemSection.tsx        |   6 +
 src/components/home-next/sections/HeroSection.tsx  |  14 +-
 src/components/home-next/sections/StatsSection.tsx |   1 +
 src/pages/community/roadmap/index.tsx              |   6 +
 src/scss/custom.scss                               |  25 ++
 static/js/custom-script.js                         | 150 ++++++++++
 11 files changed, 652 insertions(+), 11 deletions(-)

diff --git a/docusaurus.config.js b/docusaurus.config.js
index b20f553884a..11df557dc77 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -594,7 +594,7 @@ const config = {
                             },
                             {
                                 label: 'Roadmap',
-                                href: 
'https://github.com/apache/doris/issues/60036',
+                                href: '/community/roadmap',
                             },
                             {
                                 label: 'Improvement proposal',
diff --git a/src/components/community-roadmap/CommunityRoadmap.scss 
b/src/components/community-roadmap/CommunityRoadmap.scss
new file mode 100644
index 00000000000..a60afc4eb3a
--- /dev/null
+++ b/src/components/community-roadmap/CommunityRoadmap.scss
@@ -0,0 +1,315 @@
+@use '../shared/typography' as type;
+
+.community-roadmap {
+    position: relative;
+    isolation: isolate;
+    min-height: calc(100vh - 64px);
+    overflow: hidden;
+    background:
+        radial-gradient(
+            ellipse at 76% 10%,
+            rgb(var(--brand-primary-glow-rgb) / 20%),
+            transparent 52%
+        ),
+        radial-gradient(ellipse at 2% 94%, rgb(var(--brand-accent-rgb) / 8%), 
transparent 54%),
+        linear-gradient(
+            180deg,
+            var(--brand-primary-dark),
+            color-mix(in srgb, var(--brand-primary-dark) 80%, 
var(--brand-primary-darker))
+        );
+    color: var(--brand-cream-light);
+    font-family: var(--font-family-base);
+
+    * {
+        box-sizing: border-box;
+    }
+}
+
+.community-roadmap__background {
+    position: absolute;
+    inset: 0;
+    z-index: -1;
+    background-image: radial-gradient(
+        color-mix(in srgb, var(--brand-cream-light) 10%, transparent) 1.2px,
+        transparent 1.2px
+    );
+    background-size: 28px 28px;
+    mask-image: linear-gradient(180deg, black, black 72%, transparent);
+    pointer-events: none;
+}
+
+.community-roadmap__inner {
+    width: min(calc(100% - 112px), 1328px);
+    margin: 0 auto;
+    padding: 62px 0 50px;
+}
+
+.community-roadmap__hero {
+    display: grid;
+    grid-template-columns: minmax(0, 1.48fr) minmax(300px, 0.52fr);
+    align-items: end;
+    gap: 80px;
+    margin-bottom: 58px;
+}
+
+.community-roadmap__hero h1 {
+    max-width: 900px;
+    margin: 0 !important;
+    color: var(--brand-cream-light) !important;
+    font-family: var(--font-mono) !important;
+    font-size: clamp(46px, 5.15vw, 76px) !important;
+    font-weight: 700;
+    letter-spacing: -0.055em;
+    line-height: 0.98;
+    text-wrap: balance;
+
+    span {
+        color: var(--brand-accent);
+    }
+}
+
+.community-roadmap__intro {
+    margin: 0;
+    padding: 0 0 7px 24px;
+    border-left: 2px solid var(--brand-primary-glow);
+    color: color-mix(in srgb, var(--brand-cream-light) 78%, transparent);
+    font-size: 17px;
+    line-height: 1.65;
+    text-wrap: pretty;
+
+    strong {
+        color: var(--brand-cream-light);
+        font-weight: 650;
+    }
+}
+
+.community-roadmap__grid {
+    display: grid;
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+    gap: 14px;
+}
+
+.community-roadmap__card {
+    position: relative;
+    display: flex;
+    min-height: 312px;
+    flex-direction: column;
+    overflow: hidden;
+    padding: 28px 30px 26px;
+    border: 1px solid color-mix(in srgb, var(--brand-border-soft) 72%, 
transparent);
+    border-radius: 3px;
+    background: var(--brand-paper);
+    box-shadow: 0 26px 54px -38px rgb(var(--brand-shadow-rgb) / 80%);
+    color: var(--brand-ink);
+    text-decoration: none !important;
+    transition:
+        transform 180ms ease,
+        border-color 180ms ease,
+        background-color 180ms ease,
+        box-shadow 180ms ease;
+
+    &:hover {
+        z-index: 1;
+        border-color: var(--brand-primary-glow);
+        background: var(--brand-surface-soft);
+        box-shadow: 0 34px 64px -38px rgb(var(--brand-shadow-rgb) / 95%);
+        color: var(--brand-ink);
+        text-decoration: none !important;
+        transform: translateY(-7px);
+    }
+
+    &:focus-visible {
+        z-index: 2;
+        outline: 3px solid var(--brand-accent);
+        outline-offset: 4px;
+    }
+}
+
+.community-roadmap__card-number {
+    position: absolute;
+    right: 24px;
+    bottom: -20px;
+    color: color-mix(in srgb, var(--brand-primary) 10%, transparent);
+    font: 700 110px/0.8 var(--font-mono);
+    letter-spacing: -0.09em;
+    pointer-events: none;
+}
+
+.community-roadmap__card-top {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 18px;
+
+    p {
+        margin: 0;
+        color: var(--brand-primary-deep);
+        @include type.mono-text(12px, 700, 1.25, 0.095em, uppercase);
+    }
+}
+
+.community-roadmap__card-arrow {
+    display: grid;
+    width: 38px;
+    height: 38px;
+    flex: 0 0 38px;
+    place-items: center;
+    border: 1px solid color-mix(in srgb, var(--brand-ink) 22%, transparent);
+    border-radius: 50%;
+    font: 500 20px/1 var(--font-mono);
+    transition:
+        transform 180ms ease,
+        color 180ms ease,
+        background-color 180ms ease;
+}
+
+.community-roadmap__card:hover .community-roadmap__card-arrow {
+    background: var(--brand-primary-deep);
+    color: var(--brand-paper);
+    transform: translate(2px, -2px);
+}
+
+.community-roadmap__card h3 {
+    position: relative;
+    z-index: 1;
+    margin: 42px 0 15px;
+    color: var(--brand-ink);
+    font-family: var(--font-mono);
+    font-size: clamp(26px, 2.4vw, 36px);
+    font-weight: 700;
+    letter-spacing: -0.045em;
+    line-height: 1.05;
+
+    span {
+        display: block;
+    }
+}
+
+.community-roadmap__card-description {
+    position: relative;
+    z-index: 1;
+    max-width: 34ch;
+    margin: 0;
+    color: color-mix(in srgb, var(--brand-ink-soft) 84%, transparent);
+    font-size: 15px;
+    line-height: 1.58;
+    text-wrap: pretty;
+}
+
+.community-roadmap__card-meta {
+    position: relative;
+    z-index: 1;
+    margin: auto 0 0;
+    padding-top: 26px;
+    color: color-mix(in srgb, var(--brand-ink-soft) 66%, transparent);
+    @include type.mono-text(12px, 650, 1.2);
+}
+
+.community-roadmap__footer-action {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 34px;
+    padding-top: 24px;
+    border-top: 1px solid color-mix(in srgb, var(--brand-cream-light) 14%, 
transparent);
+
+    a {
+        color: var(--brand-cream-light);
+        font: 700 13px/1.4 var(--font-mono);
+        text-decoration-color: var(--brand-primary-glow);
+        text-underline-offset: 5px;
+
+        &:hover {
+            color: var(--brand-accent);
+        }
+
+        &:focus-visible {
+            outline: 3px solid var(--brand-accent);
+            outline-offset: 4px;
+        }
+    }
+}
+
+@media (max-width: 960px) {
+    .community-roadmap__inner {
+        width: min(calc(100% - 48px), 820px);
+    }
+
+    .community-roadmap__hero {
+        grid-template-columns: 1fr;
+        gap: 34px;
+    }
+
+    .community-roadmap__intro {
+        max-width: 680px;
+    }
+
+    .community-roadmap__grid {
+        grid-template-columns: 1fr;
+        gap: 12px;
+    }
+
+    .community-roadmap__card {
+        min-height: 230px;
+        padding-right: 25%;
+    }
+
+    .community-roadmap__card h3 {
+        margin-top: 28px;
+    }
+}
+
+@media (max-width: 620px) {
+    .community-roadmap__inner {
+        width: min(calc(100% - 32px), 520px);
+        padding: 42px 0 36px;
+    }
+
+    .community-roadmap__hero {
+        gap: 26px;
+        margin-bottom: 42px;
+    }
+
+    .community-roadmap__hero h1 {
+        font-size: clamp(40px, 13vw, 56px) !important;
+        line-height: 1.01;
+    }
+
+    .community-roadmap__intro {
+        padding-left: 18px;
+        font-size: 16px;
+    }
+
+    .community-roadmap__card {
+        min-height: 292px;
+        padding: 24px 24px 22px;
+    }
+
+    .community-roadmap__card-number {
+        right: 18px;
+        font-size: 96px;
+    }
+
+    .community-roadmap__card h3 {
+        margin-top: 48px;
+        font-size: 30px;
+    }
+
+    .community-roadmap__card-description {
+        max-width: 29ch;
+    }
+
+    .community-roadmap__footer-action {
+        margin-top: 28px;
+        padding-top: 20px;
+    }
+
+}
+
+@media (prefers-reduced-motion: reduce) {
+    .community-roadmap__card,
+    .community-roadmap__card-arrow {
+        transition: none;
+    }
+}
diff --git a/src/components/community-roadmap/CommunityRoadmap.tsx 
b/src/components/community-roadmap/CommunityRoadmap.tsx
new file mode 100644
index 00000000000..6ae007503f5
--- /dev/null
+++ b/src/components/community-roadmap/CommunityRoadmap.tsx
@@ -0,0 +1,115 @@
+import React, { JSX } from 'react';
+import Link from '@docusaurus/Link';
+import { LayoutNext } from '../home-next/LayoutNext';
+import './CommunityRoadmap.scss';
+
+const ROADMAP_URL = 'https://github.com/apache/doris/issues/60036';
+const TRACKING_ISSUES_URL =
+    
'https://github.com/apache/doris/issues?q=state%3Aopen%20label%3A%22tracking%20issue%22';
+const GOOD_FIRST_ISSUE_URL = 'https://github.com/apache/doris/issues/17176';
+
+interface DevelopmentDestination {
+    phase: string;
+    title: string;
+    titleDetail?: string;
+    description: string;
+    meta: string;
+    href: string;
+    ariaLabel: string;
+}
+
+const DESTINATIONS: DevelopmentDestination[] = [
+    {
+        phase: 'Understand the direction',
+        title: 'Roadmap',
+        titleDetail: '2026',
+        description: 'Start with the high-level direction and priorities 
shaping Apache Doris.',
+        meta: 'Issue #60036',
+        href: ROADMAP_URL,
+        ariaLabel: 'Read the Apache Doris Roadmap 2026 on GitHub (opens in a 
new tab)',
+    },
+    {
+        phase: 'Follow active work',
+        title: 'Tracking Issues',
+        description: 'Explore topic-level coordination across features, 
improvements, and engineering work.',
+        meta: 'Live label search',
+        href: TRACKING_ISSUES_URL,
+        ariaLabel: 'Browse open Apache Doris tracking issues on GitHub (opens 
in a new tab)',
+    },
+    {
+        phase: 'Find a place to contribute',
+        title: 'Good First Issue',
+        description: 'Begin with a curated path toward approachable work for 
new contributors.',
+        meta: 'Issue #17176',
+        href: GOOD_FIRST_ISSUE_URL,
+        ariaLabel: 'View Apache Doris Good First Issue opportunities on GitHub 
(opens in a new tab)',
+    },
+];
+
+function ExternalArrow(): JSX.Element {
+    return (
+        <span className="community-roadmap__card-arrow" aria-hidden="true">
+            ↗
+        </span>
+    );
+}
+
+export default function CommunityRoadmap(): JSX.Element {
+    return (
+        <LayoutNext
+            title="Community Development"
+            description="Explore the Apache Doris roadmap, active tracking 
issues, and approachable contribution opportunities."
+        >
+            <div className="community-roadmap">
+                <div className="community-roadmap__background" 
aria-hidden="true" />
+
+                <div className="community-roadmap__inner">
+                    <header className="community-roadmap__hero">
+                        <div className="community-roadmap__hero-title">
+                            <h1>
+                                See what we’re <span>building next.</span>
+                            </h1>
+                        </div>
+                        <p className="community-roadmap__intro">
+                            <strong>Apache Doris plans and builds in the 
open.</strong> Use this guide to understand the
+                            direction, follow active engineering work, and 
find a place to contribute.
+                        </p>
+                    </header>
+
+                    <section className="community-roadmap__destinations" 
aria-label="Community development links">
+                        <div className="community-roadmap__grid">
+                            {DESTINATIONS.map((destination, index) => (
+                                <a
+                                    className="community-roadmap__card"
+                                    href={destination.href}
+                                    target="_blank"
+                                    rel="noopener noreferrer"
+                                    aria-label={destination.ariaLabel}
+                                    key={destination.title}
+                                >
+                                    <span 
className="community-roadmap__card-number" aria-hidden="true">
+                                        {String(index + 1).padStart(2, '0')}
+                                    </span>
+                                    <div 
className="community-roadmap__card-top">
+                                        <p>{destination.phase}</p>
+                                        <ExternalArrow />
+                                    </div>
+                                    <h3>
+                                        <span>{destination.title}</span>
+                                        {destination.titleDetail && 
<span>{destination.titleDetail}</span>}
+                                    </h3>
+                                    <p 
className="community-roadmap__card-description">{destination.description}</p>
+                                    <p 
className="community-roadmap__card-meta">Opens on GitHub · 
{destination.meta}</p>
+                                </a>
+                            ))}
+                        </div>
+                    </section>
+
+                    <div className="community-roadmap__footer-action">
+                        <Link 
to="/community/how-to-contribute/contribute-to-doris">Read the contribution 
guide →</Link>
+                    </div>
+                </div>
+            </div>
+        </LayoutNext>
+    );
+}
diff --git a/src/components/home-next/NavbarNext.tsx 
b/src/components/home-next/NavbarNext.tsx
index 62656f2466c..f54707bccdc 100644
--- a/src/components/home-next/NavbarNext.tsx
+++ b/src/components/home-next/NavbarNext.tsx
@@ -28,6 +28,7 @@ function buildNavItems(
     v3xDocsHref: string,
     v21DocsHref: string,
     releasesHref: string,
+    roadmapHref: string,
     joinCommunityHref: string,
     communityReportHref: string,
 ): NavItem[] {
@@ -72,7 +73,7 @@ function buildNavItems(
             label: 'Community',
             items: [
                 { label: 'Community Report', href: communityReportHref },
-                { label: 'Roadmap', href: 
'https://github.com/apache/doris/issues/60036', external: true },
+                { label: 'Roadmap', href: roadmapHref },
                 { label: 'Build with Us', href: joinCommunityHref },
                 { label: 'Join GitHub Discussions', href: 
'https://github.com/apache/doris/discussions', external: true },
             ],
@@ -128,6 +129,7 @@ export function NavbarNext(): JSX.Element {
     const v3xDocsHref = 
`${localePrefix}/docs/3.x/gettingStarted/what-is-apache-doris`;
     const v21DocsHref = 
`${localePrefix}/docs/2.1/gettingStarted/what-is-apache-doris`;
     const releasesHref = `${localePrefix}/releases/all-release`;
+    const roadmapHref = `${localePrefix}/community/roadmap`;
     const joinCommunityHref = `${localePrefix}/community/join-community`;
     const communityReportHref = `${localePrefix}/community-report`;
     const navItems = buildNavItems(
@@ -136,6 +138,7 @@ export function NavbarNext(): JSX.Element {
         v3xDocsHref,
         v21DocsHref,
         releasesHref,
+        roadmapHref,
         joinCommunityHref,
         communityReportHref,
     );
@@ -202,9 +205,15 @@ export function NavbarNext(): JSX.Element {
                         id="navbar-ask-ai-btn"
                         className="navbar-next__ask-ai"
                         aria-label="Ask Me"
+                        aria-busy="false"
+                        aria-disabled="false"
+                        data-kapa-trigger
+                        suppressHydrationWarning
                     >
                         <StarGreenIcon />
-                        <span>Ask Me</span>
+                        <span data-kapa-label aria-live="polite" 
suppressHydrationWarning>
+                            Ask Me
+                        </span>
                     </button>
                     <a
                         href={`https://github.com/${GITHUB_REPO}`}
diff --git a/src/components/home-next/sections/DeploymentSection.tsx 
b/src/components/home-next/sections/DeploymentSection.tsx
index 934d92d37ce..1c35c8ba5ed 100644
--- a/src/components/home-next/sections/DeploymentSection.tsx
+++ b/src/components/home-next/sections/DeploymentSection.tsx
@@ -14,6 +14,8 @@ interface DeploymentCardItem {
     image: {
         src: string;
         alt: string;
+        width: number;
+        height: number;
     };
     cta: {
         label: string;
@@ -33,6 +35,8 @@ const DEPLOYMENT_CARDS: DeploymentCardItem[] = [
         image: {
             src: '/images/next/home-page/cs-coupled.jpg',
             alt: 'Apache Doris compute-storage coupled mode architecture 
diagram',
+            width: 1774,
+            height: 887,
         },
         cta: {
             label: 'Deploy Now!',
@@ -50,6 +54,8 @@ const DEPLOYMENT_CARDS: DeploymentCardItem[] = [
         image: {
             src: '/images/next/home-page/cs-decoupled.jpg',
             alt: 'Apache Doris compute-storage decoupled mode architecture 
diagram',
+            width: 1672,
+            height: 941,
         },
         cta: {
             label: 'Deploy Now!',
@@ -75,7 +81,15 @@ function DeploymentCard({ card }: { card: DeploymentCardItem 
}): JSX.Element {
                 </a>
             </div>
             <div className="deployment-next__visual">
-                <img className="deployment-next__visual-image" 
src={card.image.src} alt={card.image.alt} />
+                <img
+                    className="deployment-next__visual-image"
+                    src={card.image.src}
+                    alt={card.image.alt}
+                    width={card.image.width}
+                    height={card.image.height}
+                    loading="lazy"
+                    decoding="async"
+                />
             </div>
         </article>
     );
diff --git a/src/components/home-next/sections/EcosystemSection.tsx 
b/src/components/home-next/sections/EcosystemSection.tsx
index 9e06ab2805b..a5a09a976e8 100644
--- a/src/components/home-next/sections/EcosystemSection.tsx
+++ b/src/components/home-next/sections/EcosystemSection.tsx
@@ -208,6 +208,8 @@ function EcosystemBlock({ data, side, refMap }: 
EcosystemBlockProps): JSX.Elemen
                                     src={logo.logoSrc}
                                     alt=""
                                     aria-hidden="true"
+                                    loading="lazy"
+                                    decoding="async"
                                     draggable={false}
                                 />
                             </span>
@@ -526,6 +528,8 @@ function CompactEcosystem(): JSX.Element {
                                 className="ecosystem-next__doris-logo"
                                 src="/images/logo-doris.svg"
                                 alt="Apache Doris"
+                                loading="lazy"
+                                decoding="async"
                                 draggable={false}
                             />
                         </div>
@@ -588,6 +592,8 @@ export function EcosystemSection(): JSX.Element {
                                                     
className="ecosystem-next__doris-logo"
                                                     
src="/images/logo-doris.svg"
                                                     alt="Apache Doris"
+                                                    loading="lazy"
+                                                    decoding="async"
                                                     draggable={false}
                                                 />
                                             </div>
diff --git a/src/components/home-next/sections/HeroSection.tsx 
b/src/components/home-next/sections/HeroSection.tsx
index 23b63e8ebe9..4d80729128b 100644
--- a/src/components/home-next/sections/HeroSection.tsx
+++ b/src/components/home-next/sections/HeroSection.tsx
@@ -896,16 +896,16 @@ export function HeroSection(): JSX.Element {
                             <button
                                 type="button"
                                 className="hero-next__btn 
hero-next__btn--primary"
-                                onClick={() => {
-                                    // Reuse the Kapa modal trigger bound to 
the
-                                    // navbar Ask Me button via 
docusaurus.config.js
-                                    // (data-modal-override-open-selector).
-                                    
document.getElementById('navbar-ask-ai-btn')?.click();
-                                }}
                                 aria-label="Ask Me"
+                                aria-busy="false"
+                                aria-disabled="false"
+                                data-kapa-trigger
+                                suppressHydrationWarning
                             >
                                 <StarGreenIcon />
-                                Ask Me
+                                <span data-kapa-label aria-live="polite" 
suppressHydrationWarning>
+                                    Ask Me
+                                </span>
                             </button>
                             <a
                                 className="hero-next__btn 
hero-next__btn--ghost"
diff --git a/src/components/home-next/sections/StatsSection.tsx 
b/src/components/home-next/sections/StatsSection.tsx
index f8fbf68b97b..8f4bcde3287 100644
--- a/src/components/home-next/sections/StatsSection.tsx
+++ b/src/components/home-next/sections/StatsSection.tsx
@@ -356,6 +356,7 @@ function LogoTile({ logo, duplicate = false, expanded = 
false, onEnter, onLeave
                     alt={duplicate ? '' : logo.name}
                     title={logo.name}
                     loading="lazy"
+                    decoding="async"
                     draggable={false}
                 />
             </div>
diff --git a/src/pages/community/roadmap/index.tsx 
b/src/pages/community/roadmap/index.tsx
new file mode 100644
index 00000000000..98598b095a6
--- /dev/null
+++ b/src/pages/community/roadmap/index.tsx
@@ -0,0 +1,6 @@
+import React, { JSX } from 'react';
+import CommunityRoadmap from 
'@site/src/components/community-roadmap/CommunityRoadmap';
+
+export default function CommunityRoadmapPage(): JSX.Element {
+    return <CommunityRoadmap />;
+}
diff --git a/src/scss/custom.scss b/src/scss/custom.scss
index fa0ff88e363..c4996156343 100644
--- a/src/scss/custom.scss
+++ b/src/scss/custom.scss
@@ -165,6 +165,31 @@ body {
     z-index: 9999 !important;
 }
 
+[data-kapa-trigger][aria-busy='true'] {
+    cursor: progress;
+}
+
+[data-kapa-trigger][aria-busy='true'] svg {
+    animation: doris-kapa-loading 1s linear infinite;
+}
+
+[data-kapa-trigger][aria-disabled='true'] {
+    cursor: not-allowed;
+    opacity: 0.72;
+}
+
+@keyframes doris-kapa-loading {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
+@media (prefers-reduced-motion: reduce) {
+    [data-kapa-trigger][aria-busy='true'] svg {
+        animation: none;
+    }
+}
+
 // Centering the Kapa modal itself is done by injecting CSS into its shadow
 // root from static/js/custom-script.js, since light-DOM rules can't pierce it.
 
diff --git a/static/js/custom-script.js b/static/js/custom-script.js
index 410c23b1829..9de27bc3f4b 100644
--- a/static/js/custom-script.js
+++ b/static/js/custom-script.js
@@ -11,6 +11,156 @@
     y.parentNode.insertBefore(t, y);
 })(window, document, 'clarity', 'script', 'kfyqejiz0g');
 
+// Kapa's script is loaded asynchronously and can take a long time on a cold
+// cache. Keep Ask Me clicks made before the widget is ready, expose visible
+// loading/error feedback, and replay the latest intent once Kapa has mounted.
+// This listener runs before React hydrates, so clicks made immediately after
+// the server-rendered buttons appear are covered as well.
+(function manageKapaTrigger() {
+    var KAPA_SCRIPT_SELECTOR = 
'script[src="https://widget.kapa.ai/kapa-widget.bundle.js";]';
+    var TRIGGER_SELECTOR = '[data-kapa-trigger]';
+    var NAVBAR_TRIGGER_ID = 'navbar-ask-ai-btn';
+    var state = 'loading';
+    var pendingOpen = false;
+    var slow = false;
+    var replaying = false;
+    var slowTimer;
+    var pollTimer;
+
+    function getReadyHost() {
+        var host = document.getElementById('kapa-widget-container');
+        return host && host.shadowRoot ? host : null;
+    }
+
+    function syncTriggers() {
+        document.querySelectorAll(TRIGGER_SELECTOR).forEach(function (trigger) 
{
+            var label = trigger.querySelector('[data-kapa-label]');
+            if (label && !label.getAttribute('data-kapa-default-label')) {
+                label.setAttribute('data-kapa-default-label', 
label.textContent || 'Ask Me');
+            }
+
+            var waiting = pendingOpen && state === 'loading';
+            trigger.setAttribute('aria-busy', waiting ? 'true' : 'false');
+            trigger.setAttribute('aria-disabled', state === 'error' ? 'true' : 
'false');
+
+            if (!label) return;
+            var nextLabel;
+            if (waiting) {
+                nextLabel = slow ? 'Still loading…' : 'Loading AI…';
+            } else if (state === 'error') {
+                nextLabel = 'AI unavailable';
+            } else {
+                nextLabel = label.getAttribute('data-kapa-default-label') || 
'Ask Me';
+            }
+            if (label.textContent !== nextLabel) label.textContent = nextLabel;
+        });
+    }
+
+    function replayOpen() {
+        if (!pendingOpen || state !== 'ready') return;
+        var navbarTrigger = document.getElementById(NAVBAR_TRIGGER_ID);
+        if (!navbarTrigger) return;
+
+        pendingOpen = false;
+        slow = false;
+        window.clearTimeout(slowTimer);
+        syncTriggers();
+
+        replaying = true;
+        navbarTrigger.click();
+        replaying = false;
+    }
+
+    function markReady() {
+        if (state === 'ready') return;
+        state = 'ready';
+        window.clearInterval(pollTimer);
+        syncTriggers();
+        window.setTimeout(replayOpen, 0);
+    }
+
+    function markError() {
+        if (state === 'ready') return;
+        state = 'error';
+        pendingOpen = false;
+        slow = false;
+        window.clearTimeout(slowTimer);
+        window.clearInterval(pollTimer);
+        syncTriggers();
+    }
+
+    function checkReady() {
+        if (getReadyHost()) {
+            markReady();
+            return true;
+        }
+        return false;
+    }
+
+    function attachScriptListeners(script) {
+        if (!script || script.__dorisKapaListenersAttached) return;
+        script.__dorisKapaListenersAttached = true;
+        script.addEventListener('load', checkReady);
+        script.addEventListener('error', markError);
+    }
+
+    function requestOpen() {
+        if (state === 'error') {
+            syncTriggers();
+            return;
+        }
+        pendingOpen = true;
+        slow = false;
+        window.clearTimeout(slowTimer);
+        slowTimer = window.setTimeout(function () {
+            if (!pendingOpen || state !== 'loading') return;
+            slow = true;
+            syncTriggers();
+        }, 8000);
+        syncTriggers();
+        if (checkReady()) replayOpen();
+    }
+
+    document.addEventListener(
+        'click',
+        function (event) {
+            if (replaying) return;
+            var target = event.target;
+            var trigger = target && target.closest ? 
target.closest(TRIGGER_SELECTOR) : null;
+            if (!trigger) return;
+
+            // Once ready, Kapa's own listener handles the navbar button. The
+            // hero button still needs to proxy its click to that bound button.
+            if (state === 'ready' && trigger.id === NAVBAR_TRIGGER_ID) return;
+
+            event.preventDefault();
+            event.stopImmediatePropagation();
+            requestOpen();
+        },
+        true,
+    );
+
+    var observer = new MutationObserver(function (mutations) {
+        mutations.forEach(function (mutation) {
+            Array.prototype.forEach.call(mutation.addedNodes, function (node) {
+                if (node.nodeType !== 1) return;
+                if (node.matches && node.matches(KAPA_SCRIPT_SELECTOR)) {
+                    attachScriptListeners(node);
+                } else if (node.querySelector) {
+                    
attachScriptListeners(node.querySelector(KAPA_SCRIPT_SELECTOR));
+                }
+            });
+        });
+        syncTriggers();
+        checkReady();
+    });
+    observer.observe(document.documentElement, { childList: true, subtree: 
true });
+
+    attachScriptListeners(document.querySelector(KAPA_SCRIPT_SELECTOR));
+    pollTimer = window.setInterval(checkReady, 250);
+    syncTriggers();
+})();
+
 // Position the Kapa Ask Me modal below the sticky NavbarNext.
 // Kapa renders inside a Shadow DOM on `#kapa-widget-container`, so light-DOM
 // CSS can't reach it. We inject a <style> into the shadow root and re-inject


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to