Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ npm start
npm run build
```

## Search

Search is powered by the offline [`@easyops-cn/docusaurus-search-local`](https://github.com/easyops-cn/docusaurus-search-local)
plugin, which builds the index from Markdown at build time. The index is **only
generated during `npm run build`, not during `npm start`** — so to test search
locally, run a production build and serve it:

```bash
npm run build
npm run serve
```
## Deployment

App is deployed via Vercel integration
23 changes: 22 additions & 1 deletion website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,24 @@ const config = {
mermaid: true,
},

themes: ['@docusaurus/theme-mermaid'],
themes: [
'@docusaurus/theme-mermaid',
[
// Offline/local search: builds the search index from Markdown at build
// time. No external service or credentials required.
require.resolve('@easyops-cn/docusaurus-search-local'),
/** @type {import('@easyops-cn/docusaurus-search-local').PluginOptions} */
({
hashed: true,
indexDocs: true,
indexBlog: false,
// Docs are served at the site root (see routeBasePath below).
docsRouteBasePath: '/',
highlightSearchTermsOnTargetPage: true,
explicitSearchResultPath: true,
}),
],
],

presets: [
[
Expand Down Expand Up @@ -55,6 +72,10 @@ const config = {
src: 'img/logo.png',
},
items: [
{
type: 'search',
position: 'right',
},
{
href: 'https://kite.ai',
label: 'Kite',
Expand Down
4,106 changes: 1,928 additions & 2,178 deletions website/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@docusaurus/core": "^3.9.2",
"@docusaurus/preset-classic": "^3.9.2",
"@docusaurus/theme-mermaid": "^3.9.2",
"@easyops-cn/docusaurus-search-local": "^0.55.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.1.1",
"prism-react-renderer": "^2.4.1",
Expand All @@ -27,6 +28,10 @@
"@docusaurus/module-type-aliases": "^3.9.2",
"@docusaurus/types": "^3.9.2"
},
"overrides": {
"serialize-javascript": "^7.0.5",
"uuid": "^11.1.1"
},
"browserslist": {
"production": [
">0.5%",
Expand Down
116 changes: 116 additions & 0 deletions website/src/components/ask-ai/AskAIComingSoon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import './css/AskAI.css';

// Inlined so `fill="currentColor"` inherits the surrounding text color. An
// external SVG referenced via <img> is an isolated document and would NOT
// inherit `color`, rendering near-invisible in Kite's dark default theme.
const RobotIcon = ({ className }) => (
<svg
className={className}
width="16"
height="15"
viewBox="0 0 16 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M9 1.09756C9 1.42262 8.87127 1.71468 8.66667 1.91565V3.29268H12C13.1046 3.29268 14 4.27548 14 5.4878V12.8049C14 14.0172 13.1046 15 12 15H4C2.89543 15 2 14.0172 2 12.8049V5.4878C2 4.27548 2.89543 3.29268 4 3.29268H7.33333V1.91565C7.12873 1.71468 7 1.42262 7 1.09756C7 0.491393 7.44773 0 8 0C8.55227 0 9 0.491393 9 1.09756ZM4 4.7561C3.63181 4.7561 3.33333 5.0837 3.33333 5.4878V12.8049C3.33333 13.209 3.63181 13.5366 4 13.5366H12C12.3682 13.5366 12.6667 13.209 12.6667 12.8049V5.4878C12.6667 5.0837 12.3682 4.7561 12 4.7561H8.66667H7.33333H4ZM1.33333 6.95122H0V11.3415H1.33333V6.95122ZM14.6667 6.95122H16V11.3415H14.6667V6.95122ZM6 10.2439C6.55229 10.2439 7 9.75249 7 9.14634C7 8.5402 6.55229 8.04878 6 8.04878C5.44771 8.04878 5 8.5402 5 9.14634C5 9.75249 5.44771 10.2439 6 10.2439ZM10 10.2439C10.5523 10.2439 11 9.75249 11 9.14634C11 8.5402 10.5523 8.04878 10 8.04878C9.44773 8.04878 9 8.5402 9 9.14634C9 9.75249 9.44773 10.2439 10 10.2439Z"
fill="currentColor"
/>
</svg>
);

// "Ask AI" entry point. The assistant is not built yet, so clicking opens a
// lightweight "coming soon" dialog rather than performing a query.
const AskAIComingSoon = () => {
const [open, setOpen] = useState(false);
const dialogRef = useRef(null);
const triggerRef = useRef(null);
const closeBtnRef = useRef(null);
const wasOpen = useRef(false);

const close = useCallback(() => setOpen(false), []);

// Close on Escape or click outside; only listen while the dialog is open.
useEffect(() => {
if (!ExecutionEnvironment.canUseDOM || !open) return undefined;

const handleKeyDown = (e) => {
if (e.key === 'Escape') close();
};
const handleClickOutside = (e) => {
if (dialogRef.current && !dialogRef.current.contains(e.target)) {
close();
}
};

document.addEventListener('keydown', handleKeyDown);
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [open, close]);

// Move focus into the dialog on open and restore it to the trigger on close,
// so the aria-modal contract holds. (A full focus trap is deferred to the
// real Ask AI modal.)
useEffect(() => {
if (!ExecutionEnvironment.canUseDOM) return;
if (open) {
closeBtnRef.current?.focus();
} else if (wasOpen.current) {
triggerRef.current?.focus();
}
wasOpen.current = open;
}, [open]);

return (
<>
<button
ref={triggerRef}
type="button"
className="kite-ask-ai-option"
onClick={() => setOpen(true)}
aria-haspopup="dialog"
aria-expanded={open}
>
<RobotIcon className="kite-ask-ai-icon" />
<span>Ask AI</span>
<span className="kite-ask-ai-badge">Soon</span>
</button>

<div className={`kite-ask-ai-modal ${open ? 'show' : ''}`} role="presentation">
<div
className="kite-ask-ai-modal-content"
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="kite-ask-ai-title"
>
<RobotIcon className="kite-ask-ai-modal-icon" />
<h2 id="kite-ask-ai-title" className="kite-ask-ai-modal-title">
Ask AI is coming soon
</h2>
<p className="kite-ask-ai-modal-text">
We&apos;re building an AI assistant that answers questions straight
from the Kite documentation. In the meantime, use Search to find
what you need.
</p>
<button
ref={closeBtnRef}
type="button"
className="kite-ask-ai-modal-close"
onClick={close}
>
Got it
</button>
</div>
</div>
</>
);
};

export default AskAIComingSoon;
102 changes: 102 additions & 0 deletions website/src/components/ask-ai/css/AskAI.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* "Ask AI" chip inside the segmented search control. */
.kite-ask-ai-option {
display: inline-flex;
align-items: center;
gap: 6px;
height: 100%;
padding: 0 12px;
border: 0;
background-color: transparent;
color: var(--ifm-navbar-link-color);
font-size: 14px;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
}

.kite-ask-ai-option:hover {
background-color: var(--ifm-color-emphasis-200);
}

.kite-ask-ai-icon {
width: 16px;
height: 15px;
display: block;
}

.kite-ask-ai-badge {
font-size: 10px;
font-weight: 700;
line-height: 1;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 3px 6px;
border-radius: 999px;
/* Muted neutral so it reads as "not yet available" (not a feature
highlight) and clears WCAG AA contrast in both color modes. */
background-color: var(--ifm-color-emphasis-200);
color: var(--ifm-color-emphasis-700);
}

/* Coming-soon dialog. */
.kite-ask-ai-modal {
position: fixed;
inset: 0;
display: none;
justify-content: center;
align-items: flex-start;
background-color: rgba(0, 0, 0, 0.45);
z-index: 9999;
}

.kite-ask-ai-modal.show {
display: flex;
}

.kite-ask-ai-modal-content {
margin-top: 12vh;
width: 90%;
max-width: 440px;
padding: 32px 28px;
text-align: center;
background-color: var(--ifm-background-surface-color);
color: var(--ifm-font-color-base);
border-radius: 16px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
}

.kite-ask-ai-modal-icon {
display: inline-block;
width: 32px;
height: 30px;
color: var(--ifm-color-primary);
margin-bottom: 12px;
}

.kite-ask-ai-modal-title {
font-size: 20px;
font-weight: 700;
margin: 0 0 8px;
}

.kite-ask-ai-modal-text {
font-size: 15px;
line-height: 1.5;
margin: 0 0 20px;
color: var(--ifm-color-emphasis-700);
}

.kite-ask-ai-modal-close {
border: 0;
border-radius: 8px;
padding: 9px 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
background-color: var(--ifm-color-primary);
color: #fff;
}

.kite-ask-ai-modal-close:hover {
background-color: var(--ifm-color-primary-dark);
}
20 changes: 20 additions & 0 deletions website/src/components/custom-search/CustomSearchBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import OriginalSearchBar from '@theme-original/SearchBar';
import AskAIComingSoon from '@site/src/components/ask-ai/AskAIComingSoon';
import '@site/src/components/custom-search/css/CustomSearch.css';

// Segmented search control: an "Ask AI" option (coming soon) sitting beside
// the working local/offline search input. The Search half is the original
// theme SearchBar provided by @easyops-cn/docusaurus-search-local.
const CustomSearchBar = () => {
return (
<div className="kite-segmented-search">
<AskAIComingSoon />
<div className="kite-search-slot">
<OriginalSearchBar />
</div>
</div>
);
};

export default CustomSearchBar;
49 changes: 49 additions & 0 deletions website/src/components/custom-search/css/CustomSearch.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* Segmented "Ask AI | Search" control in the navbar. Uses Infima theme
variables so it adapts to both light and dark color modes. */
.kite-segmented-search {
display: flex;
align-items: stretch;
height: 36px;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 8px;
overflow: hidden;
background-color: var(--ifm-navbar-search-input-background-color);
}

/* The Search half wraps the original theme search bar. Flatten the inner
input so it blends into the segmented control instead of drawing its own
pill/border. */
.kite-search-slot {
display: flex;
align-items: center;
}

.kite-search-slot .navbar__search {
display: flex;
align-items: center;
}

.kite-search-slot .navbar__search-input {
height: 34px;
margin: 0;
border: 0;
border-radius: 0;
background-color: transparent;
}

/* Keep the two halves visually divided. */
.kite-segmented-search .kite-ask-ai-option {
border-right: 1px solid var(--ifm-color-emphasis-300);
}

@media (max-width: 996px) {
/* On narrow screens the navbar collapses; let the control shrink with it. */
.kite-segmented-search {
width: 100%;
}
.kite-search-slot,
.kite-search-slot .navbar__search,
.kite-search-slot .navbar__search-input {
width: 100%;
}
}
8 changes: 8 additions & 0 deletions website/src/theme/SearchBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import CustomSearchBar from '@site/src/components/custom-search/CustomSearchBar';

// Swizzle wrapper for the theme search bar. Renders Kite's segmented
// "Ask AI | Search" control instead of the default search input.
export default function SearchBarWrapper() {
return <CustomSearchBar />;
}
Loading