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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user