diff --git a/www/apps/api-reference/app/globals.css b/www/apps/api-reference/app/globals.css
index 88cbdb7db188d..c06802320a6c6 100644
--- a/www/apps/api-reference/app/globals.css
+++ b/www/apps/api-reference/app/globals.css
@@ -50,4 +50,8 @@
body[data-modal="opened"] {
@apply !overflow-hidden;
}
+}
+
+.grecaptcha-badge {
+ visibility: hidden;
}
\ No newline at end of file
diff --git a/www/packages/docs-ui/package.json b/www/packages/docs-ui/package.json
index 50a06b94376d6..fd851834ae45d 100644
--- a/www/packages/docs-ui/package.json
+++ b/www/packages/docs-ui/package.json
@@ -68,7 +68,6 @@
"mermaid": "^10.9.0",
"npm-to-yarn": "^2.1.0",
"prism-react-renderer": "2.3.1",
- "react-google-recaptcha": "^3.1.0",
"react-instantsearch": "^7.0.3",
"react-markdown": "^8.0.7",
"react-medium-image-zoom": "^5.1.10",
diff --git a/www/packages/docs-ui/src/components/AiAssistant/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/index.tsx
index 9c9803fe4e52b..cde748ad1251c 100644
--- a/www/packages/docs-ui/src/components/AiAssistant/index.tsx
+++ b/www/packages/docs-ui/src/components/AiAssistant/index.tsx
@@ -393,6 +393,7 @@ export const AiAssistant = () => {
apply
>
}
+ clickable={true}
>
(
+ promise: Promise
,
+ timeout: number
+): Promise {
+ 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
+ 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 => {
+ 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 }
+}
diff --git a/www/packages/docs-ui/src/providers/AiAssistant/index.tsx b/www/packages/docs-ui/src/providers/AiAssistant/index.tsx
index 79ff129fd2739..aa230394b3d4a 100644
--- a/www/packages/docs-ui/src/providers/AiAssistant/index.tsx
+++ b/www/packages/docs-ui/src/providers/AiAssistant/index.tsx
@@ -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,18 +31,13 @@ export const AiAssistantProvider = ({
children,
}: AiAssistantProviderProps) => {
const { analytics } = useAnalytics()
- const recaptchaRef = React.createRef()
-
- 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
@@ -50,7 +45,7 @@ export const AiAssistantProvider = ({
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}
-
- 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"
- />
)
}
diff --git a/www/yarn.lock b/www/yarn.lock
index 57e5f5b67b993..09f7f405ea5ca 100644
--- a/www/yarn.lock
+++ b/www/yarn.lock
@@ -13595,7 +13595,6 @@ __metadata:
prism-react-renderer: 2.3.1
react: ^18.2.0
react-dom: ^18.2.0
- react-google-recaptcha: ^3.1.0
react-instantsearch: ^7.0.3
react-markdown: ^8.0.7
react-medium-image-zoom: ^5.1.10
@@ -16265,7 +16264,7 @@ __metadata:
languageName: node
linkType: hard
-"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0":
+"hoist-non-react-statics@npm:^3.1.0":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies:
@@ -21556,7 +21555,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.0.0, prop-types@npm:^15.5.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:^15.0.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -21743,18 +21742,6 @@ __metadata:
languageName: node
linkType: hard
-"react-async-script@npm:^1.2.0":
- version: 1.2.0
- resolution: "react-async-script@npm:1.2.0"
- dependencies:
- hoist-non-react-statics: ^3.3.0
- prop-types: ^15.5.0
- peerDependencies:
- react: ">=16.4.1"
- checksum: 89450912110c380abc08258ce17d2fb18d31d6b7179a74f6bc504c0761a4ca271edb671e402fa8e5ea4250b5c17fa953af80a9f1c4ebb26c9e81caee8476c903
- languageName: node
- linkType: hard
-
"react-currency-input-field@npm:^3.6.11":
version: 3.6.11
resolution: "react-currency-input-field@npm:3.6.11"
@@ -21860,18 +21847,6 @@ __metadata:
languageName: node
linkType: hard
-"react-google-recaptcha@npm:^3.1.0":
- version: 3.1.0
- resolution: "react-google-recaptcha@npm:3.1.0"
- dependencies:
- prop-types: ^15.5.0
- react-async-script: ^1.2.0
- peerDependencies:
- react: ">=16.4.1"
- checksum: 5ecaa6b88f238defd939012cb2671b4cbda59fe03f059158994b8c5215db482412e905eae6a67d23cef220d77cfe0430ab91ce7849d476515cda72f3a8a0a746
- languageName: node
- linkType: hard
-
"react-helmet-async@npm:*, react-helmet-async@npm:^1.3.0":
version: 1.3.0
resolution: "react-helmet-async@npm:1.3.0"