Skip to content

Commit

Permalink
docs(v1): migrate kapa (#10199)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahednasser authored Nov 21, 2024
1 parent c05e548 commit dbc7dc6
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 51 deletions.
4 changes: 4 additions & 0 deletions www/apps/api-reference/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@
body[data-modal="opened"] {
@apply !overflow-hidden;
}
}

.grecaptcha-badge {
visibility: hidden;
}
1 change: 0 additions & 1 deletion www/packages/docs-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions www/packages/docs-ui/src/components/AiAssistant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ export const AiAssistant = () => {
apply
</>
}
clickable={true}
>
<div
className={clsx(
Expand Down
148 changes: 148 additions & 0 deletions www/packages/docs-ui/src/hooks/use-recaptcha/index.tsx
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 }
}
32 changes: 9 additions & 23 deletions www/packages/docs-ui/src/providers/AiAssistant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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,
},
Expand All @@ -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
)
}

Expand All @@ -73,6 +69,7 @@ export const AiAssistantProvider = ({
) => {
return await sendRequest(
`/query/v1/question-answer/${questionId}/feedback`,
RecaptchaAction.FeedbackSubmit,
"POST",
{
"Content-Type": "application/json",
Expand All @@ -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>
)
}
Expand Down
29 changes: 2 additions & 27 deletions www/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit dbc7dc6

Please sign in to comment.