-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c05e548
commit dbc7dc6
Showing
6 changed files
with
164 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,4 +50,8 @@ | |
body[data-modal="opened"] { | ||
@apply !overflow-hidden; | ||
} | ||
} | ||
|
||
.grecaptcha-badge { | ||
visibility: hidden; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -393,6 +393,7 @@ export const AiAssistant = () => { | |
apply | ||
</> | ||
} | ||
clickable={true} | ||
> | ||
<div | ||
className={clsx( | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 "../.." | ||
|
||
/** | ||
* 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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
import React, { createContext, useContext } from "react" | ||
import { useAnalytics } from "@/providers" | ||
import { AiAssistant } from "@/components" | ||
import ReCAPTCHA from "react-google-recaptcha" | ||
import { RecaptchaAction, useRecaptcha } from "../../hooks/use-recaptcha" | ||
|
||
export type AiAssistantFeedbackType = "upvote" | "downvote" | ||
|
||
|
@@ -31,26 +31,21 @@ 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 | ||
) => { | ||
return await fetch(`${apiUrl}${apiPath}`, { | ||
method, | ||
headers: { | ||
"X-RECAPTCHA-TOKEN": await getReCaptchaToken(), | ||
"X-RECAPTCHA-TOKEN": await getReCaptchaToken(action), | ||
"X-WEBSITE-ID": websiteId, | ||
...headers, | ||
}, | ||
|
@@ -63,7 +58,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 | ||
) | ||
} | ||
|
||
|
@@ -73,6 +69,7 @@ export const AiAssistantProvider = ({ | |
) => { | ||
return await sendRequest( | ||
`/query/v1/question-answer/${questionId}/feedback`, | ||
RecaptchaAction.FeedbackSubmit, | ||
"POST", | ||
{ | ||
"Content-Type": "application/json", | ||
|
@@ -94,17 +91,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 [email protected] to complete the setup." | ||
) | ||
} | ||
className="grecaptcha-badge" | ||
/> | ||
</AiAssistantContext.Provider> | ||
) | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters