docs: migrate Kapa (#10198)

* docs: migrate kapa

* remove unused dependency

* fix isbrowser
This commit is contained in:
Shahed Nasser
2024-11-21 15:05:50 +02:00
committed by GitHub
parent f541e21c62
commit 35afa04a93
10 changed files with 180 additions and 61 deletions
@@ -327,6 +327,7 @@ export const AiAssistant = () => {
apply
</>
}
clickable={true}
>
<Badge variant="neutral">AI Assistant</Badge>
</Tooltip>
@@ -0,0 +1,148 @@
"use client"
// NOTE: This was shared by Kapa team with minor modifications.
import { useEffect, useState, useCallback } from "react"
import { useIsBrowser } from "../../providers"
/**
* Helper to execute a Promise with a timeout
*/
export async function executeWithTimeout<T>(
promise: Promise<T>,
timeout: number
): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error("Promise timed out."))
}, timeout)
promise
.then((result) => {
clearTimeout(timer)
resolve(result)
})
.catch((error) => {
clearTimeout(timer)
reject(error)
})
})
}
declare global {
interface Window {
grecaptcha: {
enterprise: {
execute: (id: string, action: { action: string }) => Promise<string>
ready: (callback: () => void) => void
}
}
}
}
const RECAPTCHA_SCRIPT_ID = "kapa-recaptcha-script"
/**
* Recaptcha action types to classify recaptcha assessments.
* IMPORTANT: Make sure these match the ones on the widget-proxy
*/
export enum RecaptchaAction {
AskAi = "ask_ai", // for /chat (/query) routes
FeedbackSubmit = "feedback_submit", // for /feedback routes
Search = "search", // for /search routes
}
type UseRecaptchaProps = {
siteKey: string
}
/**
* This hook loads the reCAPTCHA SDK and exposes the "grecaptcha.execute" function
* which returns a recpatcha token. The token must then be validated on the backend.
* We use a reCAPTCHA Enterprise Score-based key, which is returning a score when
* calling the reCAPTCHA Enterprise API with the returned token from the `execute`
* call. The score indicates the probability of the request being made by a human.
* @param siteKey the reCAPTCHA (enterprise) site key
* @param loadScript boolean flag to load the reCAPTCHA script
*/
export const useRecaptcha = ({ siteKey }: UseRecaptchaProps) => {
const [isScriptLoaded, setIsScriptLoaded] = useState(false)
// The recaptcha execute function is not immediately
// ready so we need to wait until we can call it.
const [isExecuteReady, setIsExecuteReady] = useState(false)
const { isBrowser } = useIsBrowser()
useEffect(() => {
if (!isBrowser) {
return
}
if (document.getElementById(RECAPTCHA_SCRIPT_ID)) {
setIsScriptLoaded(true)
return
}
const script = document.createElement("script")
script.id = RECAPTCHA_SCRIPT_ID
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${siteKey}`
script.async = true
script.defer = true
const handleLoad = () => {
setIsScriptLoaded(true)
}
const handleError = (event: Event) => {
console.error("Failed to load reCAPTCHA Enterprise script", event)
}
script.addEventListener("load", handleLoad)
script.addEventListener("error", handleError)
document.head.appendChild(script)
return () => {
if (script) {
script.removeEventListener("load", handleLoad)
script.removeEventListener("error", handleError)
document.head.removeChild(script)
}
}
}, [siteKey, isBrowser])
useEffect(() => {
if (isScriptLoaded && window.grecaptcha) {
try {
window.grecaptcha.enterprise.ready(() => {
setIsExecuteReady(true)
})
} catch (error) {
console.error("Error during reCAPTCHA ready initialization:", error)
}
}
}, [isScriptLoaded])
const execute = useCallback(
async (actionName: RecaptchaAction): Promise<string> => {
if (!isExecuteReady) {
console.error("reCAPTCHA is not ready")
return ""
}
try {
const token = await executeWithTimeout(
window.grecaptcha.enterprise.execute(siteKey, {
action: actionName,
}),
4000
)
return token
} catch (error) {
console.error("Error obtaining reCAPTCHA token:", error)
return ""
}
},
[isExecuteReady, siteKey]
)
return { execute }
}
@@ -3,8 +3,7 @@
import React, { createContext, useContext } from "react"
import { useAnalytics } from "@/providers"
import { AiAssistant } from "@/components"
// @ts-expect-error can't install the types package because it doesn't support React v19
import ReCAPTCHA from "react-google-recaptcha"
import { RecaptchaAction, useRecaptcha } from "../../hooks/use-recaptcha"
export type AiAssistantFeedbackType = "upvote" | "downvote"
@@ -35,18 +34,13 @@ export const AiAssistantProvider = ({
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 { execute: getReCaptchaToken } = useRecaptcha({
siteKey: recaptchaSiteKey,
})
const sendRequest = async (
apiPath: string,
action: RecaptchaAction,
method = "GET",
headers?: HeadersInit,
body?: BodyInit
@@ -54,7 +48,7 @@ export const AiAssistantProvider = ({
return await fetch(`${apiUrl}${apiPath}`, {
method,
headers: {
"X-RECAPTCHA-TOKEN": await getReCaptchaToken(),
"X-RECAPTCHA-ENTERPRISE-TOKEN": await getReCaptchaToken(action),
"X-WEBSITE-ID": websiteId,
...headers,
},
@@ -67,7 +61,8 @@ export const AiAssistantProvider = ({
return await sendRequest(
threadId
? `/query/v1/thread/${threadId}/stream?query=${questionParam}`
: `/query/v1/stream?query=${questionParam}`
: `/query/v1/stream?query=${questionParam}`,
RecaptchaAction.AskAi
)
}
@@ -77,6 +72,7 @@ export const AiAssistantProvider = ({
) => {
return await sendRequest(
`/query/v1/question-answer/${questionId}/feedback`,
RecaptchaAction.FeedbackSubmit,
"POST",
{
"Content-Type": "application/json",
@@ -99,17 +95,6 @@ export const AiAssistantProvider = ({
>
{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>
)
}