docs: add AI Assistant (#5249)

* added components

* added ai assistant button

* change styling

* improve AI assistant

* change to a drawer

* added command support into search

* add AiAssistant to all projects

* remove usage of Text component

* added error handling

* use recaptcha

* fix new configurations

* fix background color

* change suggested questions
This commit is contained in:
Shahed Nasser
2023-10-05 11:10:44 +03:00
committed by GitHub
parent b6bea74914
commit b3f75d8f21
45 changed files with 1864 additions and 437 deletions
@@ -0,0 +1,120 @@
"use client"
import React, { createContext, useContext } from "react"
import { useAnalytics } from "@/providers"
import { AiAssistant } from "@/components"
import ReCAPTCHA from "react-google-recaptcha"
export type AiAssistantFeedbackType = "upvote" | "downvote"
export type AiAssistantContextType = {
getAnswer: (question: string, thread_id?: string) => Promise<Response>
sendFeedback: (
questionId: string,
reaction: AiAssistantFeedbackType
) => Promise<Response>
}
const AiAssistantContext = createContext<AiAssistantContextType | null>(null)
export type AiAssistantProviderProps = {
children?: React.ReactNode
apiUrl: string
recaptchaSiteKey: string
websiteId: string
}
export const AiAssistantProvider = ({
apiUrl,
recaptchaSiteKey,
websiteId,
children,
}: AiAssistantProviderProps) => {
const { analytics } = useAnalytics()
const recaptchaRef = React.createRef<ReCAPTCHA>()
const getReCaptchaToken = async () => {
if (recaptchaRef?.current) {
const recaptchaToken = await recaptchaRef.current.executeAsync()
return recaptchaToken || ""
}
return ""
}
const sendRequest = async (
apiPath: string,
method = "GET",
headers?: HeadersInit,
body?: BodyInit
) => {
return await fetch(`${apiUrl}${apiPath}`, {
method,
headers: {
"X-RECAPTCHA-TOKEN": await getReCaptchaToken(),
"X-WEBSITE-ID": websiteId,
...headers,
},
body,
})
}
const getAnswer = async (question: string, threadId?: string) => {
const questionParam = encodeURI(question)
return await sendRequest(
threadId
? `/query/v1/thread/${threadId}/stream?query=${questionParam}`
: `/query/v1/stream?query=${questionParam}`
)
}
const sendFeedback = async (
questionId: string,
reaction: AiAssistantFeedbackType
) => {
return await sendRequest(
`/query/v1/question-answer/${questionId}/feedback`,
"POST",
{
"Content-Type": "application/json",
},
JSON.stringify({
question_id: questionId,
reaction,
user_identifier: analytics?.user().anonymousId() || "",
})
)
}
return (
<AiAssistantContext.Provider
value={{
getAnswer,
sendFeedback,
}}
>
{children}
<AiAssistant />
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
sitekey={recaptchaSiteKey}
onErrored={() =>
console.error(
"ReCAPTCHA token not yet configured. Please reach out to the kapa team at founders@kapa.ai to complete the setup."
)
}
className="grecaptcha-badge"
/>
</AiAssistantContext.Provider>
)
}
export const useAiAssistant = () => {
const context = useContext(AiAssistantContext)
if (!context) {
throw new Error("useAiAssistant must be used within a AiAssistantProvider")
}
return context
}
@@ -6,10 +6,21 @@ import React, {
useEffect,
useState,
useMemo,
useRef,
} from "react"
import { SearchModal, SearchModalProps } from "@/components"
import { BadgeProps, Modal, Search, SearchProps } from "@/components"
import { checkArraySameElms } from "../../utils"
import algoliasearch, { SearchClient } from "algoliasearch/lite"
import clsx from "clsx"
import { CSSTransition, SwitchTransition } from "react-transition-group"
export type SearchCommand = {
name: string
component: React.ReactNode
icon?: React.ReactNode
title: string
badge?: BadgeProps
}
export type SearchContextType = {
isOpen: boolean
@@ -17,6 +28,10 @@ export type SearchContextType = {
defaultFilters: string[]
setDefaultFilters: (value: string[]) => void
searchClient: SearchClient
commands: SearchCommand[]
command: SearchCommand | null
setCommand: React.Dispatch<React.SetStateAction<SearchCommand | null>>
modalRef: React.MutableRefObject<HTMLDialogElement | null>
}
const SearchContext = createContext<SearchContextType | null>(null)
@@ -32,7 +47,9 @@ export type SearchProviderProps = {
children: React.ReactNode
initialDefaultFilters?: string[]
algolia: AlgoliaProps
searchProps: Omit<SearchModalProps, "algolia">
searchProps: Omit<SearchProps, "algolia">
commands?: SearchCommand[]
modalClassName?: string
}
export const SearchProvider = ({
@@ -40,11 +57,16 @@ export const SearchProvider = ({
initialDefaultFilters = [],
searchProps,
algolia,
commands = [],
modalClassName,
}: SearchProviderProps) => {
const [isOpen, setIsOpen] = useState(false)
const [defaultFilters, setDefaultFilters] = useState<string[]>(
initialDefaultFilters
)
const [command, setCommand] = useState<SearchCommand | null>(null)
const modalRef = useRef<HTMLDialogElement | null>(null)
const searchClient: SearchClient = useMemo(() => {
const algoliaClient = algoliasearch(algolia.appId, algolia.apiKey)
@@ -89,10 +111,53 @@ export const SearchProvider = ({
defaultFilters,
setDefaultFilters,
searchClient,
commands,
command,
setCommand,
modalRef,
}}
>
{children}
<SearchModal {...searchProps} algolia={algolia} />
<Modal
contentClassName={clsx(
"!p-0 overflow-hidden relative h-full",
"rounded-none md:rounded-docs_lg flex flex-col justify-between"
)}
modalContainerClassName={clsx(
"!rounded-none md:!rounded-docs_lg",
"md:!h-[480px] h-screen",
"md:!w-[640px] w-screen",
"bg-medusa-bg-base"
)}
open={isOpen}
onClose={() => setIsOpen(false)}
passedRef={modalRef}
className={modalClassName}
>
<SwitchTransition>
<CSSTransition
classNames={{
enter:
command === null
? "animate-fadeInLeft animate-fast"
: "animate-fadeInRight animate-fast",
exit:
command === null
? "animate-fadeOutLeft animate-fast"
: "animate-fadeOutRight animate-fast",
}}
timeout={300}
key={command?.name || "search"}
>
<>
{command === null && (
<Search {...searchProps} algolia={algolia} />
)}
{command?.component}
</>
</CSSTransition>
</SwitchTransition>
</Modal>
</SearchContext.Provider>
)
}
@@ -1,3 +1,4 @@
export * from "./AiAssistant"
export * from "./Analytics"
export * from "./ColorMode"
export * from "./Mobile"