Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(v1): migrate kapa #10199

Merged
merged 1 commit into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading