Skip to content

Commit

Permalink
Docs: Combine search results (#32287)
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 98085f7b5e99eb7c6058577f0739b192f1802afd
  • Loading branch information
mikewheaton authored and Convex, Inc. committed Dec 17, 2024
1 parent 465fd6c commit 80c16aa
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 101 deletions.
2 changes: 1 addition & 1 deletion docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ const config = {
{
src: "https://widget.kapa.ai/kapa-widget.bundle.js",
"data-button-hide": "true",
"data-modal-override-open-id": "kapa-ai-chat-button",
"data-modal-override-open-class": "js-launch-kapa-ai",
"data-website-id": "a20c0988-f33e-452b-9174-5045a58b965d",
"data-project-name": "Convex",
"data-project-color": "#141414",
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@docusaurus/theme-common": "2.3.1",
"heroicons": "^2.0.16",
"react-dropdown": "~1.11.0",
"react-instantsearch-hooks-web": "~6.47.3",
"algoliasearch": "~4.20.0",
"react-markdown": "~8.0.7",
"@types/react": "^18.0.0",
Expand Down
8 changes: 1 addition & 7 deletions src/components/ConvexAiChat/ConvexAiChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,5 @@ import React from "react";
import "convex-ai-chat/react/styles-docs.css";

export function ConvexAiChat() {
return (
<>
<button className="convex-ai-chat" id="kapa-ai-chat-button">
Ask AI
</button>
</>
);
return <button className="convex-ai-chat js-launch-kapa-ai">Ask AI</button>;
}
23 changes: 10 additions & 13 deletions src/components/ConvexSearch/ConvexSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import algoliasearch from "algoliasearch/lite";
import BrowserOnly from "@docusaurus/BrowserOnly";
import React, { useCallback, useEffect, useState } from "react";
import { InstantSearch } from "react-instantsearch-hooks-web";
import Dialog from "./Dialog";
import SearchButton from "./SearchButton";
import "./styles.css";

const searchClient = algoliasearch(
"1KIE511890",
"d5802c3142d1d81cebdac1ccbb02ea9f",
);

const ConvexSearch = () => {
const [dialogOpen, setDialogOpen] = useState(false);

Expand All @@ -34,12 +28,15 @@ const ConvexSearch = () => {
}, []);

return (
<InstantSearch searchClient={searchClient} indexName="docs">
<div className="cs-root cs-root--custom">
<SearchButton onClick={() => setDialogOpen(true)} />
<Dialog open={dialogOpen} onClose={handleCloseDialog} />
</div>
</InstantSearch>
// Render this only on the client, as it uses `document` and `window`.
<BrowserOnly>
{() => (
<div className="cs-root cs-root--custom">
<SearchButton onClick={() => setDialogOpen(true)} />
<Dialog open={dialogOpen} onClose={handleCloseDialog} />
</div>
)}
</BrowserOnly>
);
};

Expand Down
41 changes: 36 additions & 5 deletions src/components/ConvexSearch/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSearchBox } from "react-instantsearch-hooks-web";
import KeyboardLegend from "./KeyboardLegend";
import Results from "./Results";
import SearchBox from "./SearchBox";
Expand All @@ -11,10 +10,21 @@ type Props = {
};

const Dialog = ({ open, onClose }: Props) => {
const { refine } = useSearchBox();
const [query, setQuery] = useState("");
const [debouncedQuery, setDebouncedQuery] = useState("");
const [container] = useState(() => document.createElement("div"));

// Debounce the query to reduce search requests.
useEffect(() => {
const timeoutId = setTimeout(() => {
setDebouncedQuery(query);
}, 250);

return () => {
clearTimeout(timeoutId);
};
}, [query]);

// Append the container to the body, outside of the current component tree.
useEffect(() => {
document.body.appendChild(container);
Expand All @@ -31,12 +41,10 @@ const Dialog = ({ open, onClose }: Props) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setQuery(value);
refine(value);
};

const handleClear = () => {
setQuery("");
refine("");
};

useEffect(() => {
Expand Down Expand Up @@ -73,7 +81,30 @@ const Dialog = ({ open, onClose }: Props) => {
</div>
{query !== "" && (
<>
<Results />
<button
className="cs-dialog-aiButton js-launch-kapa-ai"
onClick={onClose}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
style={{ color: "rgb(20, 20, 20)" }}
>
<path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z"></path>
</svg>
<div className="cs-dialog-aiButton-text">
<strong>Ask AI</strong>
<span>Get an instant AI answer</span>
</div>
</button>
<Results query={debouncedQuery} />
<KeyboardLegend />
</>
)}
Expand Down
5 changes: 2 additions & 3 deletions src/components/ConvexSearch/Hit.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from "react";
import { Highlight } from "react-instantsearch-hooks-web";
import Markdown from "./Markdown";
import {
DiscordHit,
Expand Down Expand Up @@ -33,7 +32,7 @@ const renderDocsContent = (hit: DocsHit) => {
{hit.title}
</span>
<div className="cs-hit-content">
<Highlight hit={hit} attribute="contents" />
<Markdown text={hit.contents} />
</div>
</a>
);
Expand All @@ -56,7 +55,7 @@ const renderStackContent = (hit: StackHit) => {
{hit.title}
</span>
<div className="cs-hit-content">
<Highlight hit={hit} attribute="content" />
<Markdown text={hit.content} />
</div>
</a>
);
Expand Down
12 changes: 8 additions & 4 deletions src/components/ConvexSearch/HitList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useEffect, useRef, useState } from "react";
import { useHits } from "react-instantsearch-hooks-web";
import Hit from "./Hit";
import { SearchHit } from "./types";

export default function HitList() {
const { hits } = useHits<SearchHit>();
interface HitListProps {
hits: SearchHit[];
}

const HitList = React.memo(({ hits }: HitListProps) => {
const [selectedHit, setSelectedHit] = useState(0);
const [usingKeyboard, setUsingKeyboard] = useState(false);
const listRef = useRef<HTMLUListElement>(null);
Expand Down Expand Up @@ -77,4 +79,6 @@ export default function HitList() {
{hits.length === 0 && <li>No results found.</li>}
</ul>
);
}
});

export default HitList;
85 changes: 46 additions & 39 deletions src/components/ConvexSearch/Results.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,56 @@
import React, { useState } from "react";
import { Index } from "react-instantsearch-hooks-web";
import algoliasearch from "algoliasearch/lite";
import React, { useEffect, useState } from "react";
import { SearchHit } from "./types";
import HitList from "./HitList";

const indexes = [
{
name: "docs",
title: "Docs",
link: "https://docs.convex.dev",
},
{
name: "stack",
title: "Stack",
link: "https://stack.convex.dev",
},
{
name: "discord",
title: "Discord",
link: "https://discord.com/invite/nk6C2qTeCq",
},
];
type SearchResults = {
hits: SearchHit[];
}[];

export default function Results() {
const [selectedIndex, setSelectedIndex] = useState(0);
const searchClient = algoliasearch(
"1KIE511890",
"d5802c3142d1d81cebdac1ccbb02ea9f",
);

const indexes = ["docs", "stack", "discord"];

interface ResultsProps {
query: string;
}

export default function Results({ query }: ResultsProps) {
const [searchResults, setSearchResults] = useState<SearchHit[]>([]);

useEffect(() => {
if (query) {
searchClient
.search<SearchHit>(
indexes.map((indexName) => ({
indexName,
query,
params: {
hitsPerPage: 10,
},
})),
)
.then((response) => {
const [docsResults, stackResults, discordResults] =
response.results as SearchResults;
setSearchResults([
...docsResults.hits,
...stackResults.hits,
...discordResults.hits,
]);
});
} else {
setSearchResults([]); // Clear results if query is empty.
}
}, [query]);

return (
<div className="cs-results">
<div className="cs-results-header">
{indexes.map(({ name, title }, index) => (
<button
className={`cs-results-header-button ${
index === selectedIndex
? "cs-results-header-button--selected"
: ""
}`}
key={name}
onClick={() => setSelectedIndex(index)}
>
{title}
</button>
))}
</div>
<div className="cs-results-container">
<Index indexName={indexes[selectedIndex].name}>
<HitList />
</Index>
<HitList hits={searchResults} />
</div>
</div>
);
Expand Down
61 changes: 37 additions & 24 deletions src/components/ConvexSearch/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
--cs-key-shadow: inset 0 -2px 0 0 #c2c2c2, inset 0 0 1px 1px #fff,
0 1px 2px 1px rgba(30, 35, 90, 0.4);
--cs-markdown-code-bg: #141414;
--cs-results-header-button-text: #141414;
--cs-results-header-button-underline: #3f3f3f;
--cs-search-bg: #ffffff;
--cs-search-border: #2122b5;
--cs-search-placeholder: #797979;
Expand Down Expand Up @@ -115,6 +113,40 @@
flex-grow: 1;
}

.cs-dialog-aiButton {
background: var(--cs-hit-bg);
color: var(--cs-hit-content-text);
border: none;
padding: 1rem;
display: flex;
align-items: center;
gap: 1rem;
text-align: left;
border: 2px solid var(--cs-hit-border);
border-radius: 0.25rem;
}

.cs-dialog-aiButton:hover {
cursor: pointer;
border-color: var(--cs-hit-border-active);
}

.cs-dialog-aiButton-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
}

.cs-dialog-aiButton-text strong {
font-size: 1.25rem;
font-weight: 500;
}

.cs-dialog-aiButton-text span {
font-size: 0.875rem;
opacity: 0.8;
}

.cs-dialog-closeButton {
background: none;
border: none;
Expand Down Expand Up @@ -225,32 +257,13 @@
flex-grow: 1;
}

.cs-results-header {
display: flex;
flex-direction: row;
gap: 0.75rem;
}

.cs-results-header-button {
border: none;
background: none;
font-family: Inter, sans-serif;
font-size: 1.125rem;
cursor: pointer;
padding: 0.25rem 0;
border-bottom: 2px solid transparent;
transition: border-color 0.2s ease-in-out;
color: var(--cs-results-header-button-text);
}

.cs-results-header-button.cs-results-header-button--selected {
border-color: var(--cs-results-header-button-underline);
}

.cs-results-container {
color-scheme: light;
overflow-y: auto;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.cs-hitList {
Expand Down
4 changes: 2 additions & 2 deletions src/css/ai-chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
display: flex;
flex-direction: row;
font-size: 0.875rem;
gap: 0.25rem;
padding: 0.75rem;
height: 2.75rem;
padding: 0 0.75rem;
transition: all 0.2s ease-in-out;
font-family: Inter, sans-serif;
margin-left: 0.5rem;
Expand Down
2 changes: 0 additions & 2 deletions src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -1149,8 +1149,6 @@ html[data-theme="dark"] .cs-root.cs-root--custom {
--cs-key-shadow: inset 0 -2px 0 0 #6e6e6e, inset 0 0 1px 1px #3f3f3f,
0 1px 2px 1px rgba(30, 35, 90, 0.4);
--cs-markdown-code-bg: #000000;
--cs-results-header-button-text: #f6f6f6;
--cs-results-header-button-underline: #e5e5e5;
--cs-search-bg: #000000;
--cs-search-border: #f3c01c;
--cs-search-placeholder: #c2c2c2;
Expand Down

0 comments on commit 80c16aa

Please sign in to comment.