diff --git a/www/apps/api-reference/.env.sample b/www/apps/api-reference/.env.sample index 14ed595905..c8b8371632 100644 --- a/www/apps/api-reference/.env.sample +++ b/www/apps/api-reference/.env.sample @@ -15,7 +15,5 @@ NEXT_PUBLIC_DOCS_V1_URL= NEXT_PUBLIC_API_V1_URL= ALGOLIA_WRITE_API_KEY= ANALYZE_BUNDLE= -NEXT_PUBLIC_AI_ASSISTANT_URL= -NEXT_PUBLIC_AI_WEBSITE_ID= -NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY= +NEXT_PUBLIC_INTEGRATION_ID= NEXT_PUBLIC_GA_ID= \ No newline at end of file diff --git a/www/apps/api-reference/providers/index.tsx b/www/apps/api-reference/providers/index.tsx index 5be4f14097..1b9d7a4265 100644 --- a/www/apps/api-reference/providers/index.tsx +++ b/www/apps/api-reference/providers/index.tsx @@ -24,12 +24,8 @@ const Providers = ({ children }: ProvidersProps) => { diff --git a/www/apps/book/.env.sample b/www/apps/book/.env.sample index c7055dabd1..f9ea64ec27 100644 --- a/www/apps/book/.env.sample +++ b/www/apps/book/.env.sample @@ -13,10 +13,8 @@ NEXT_PUBLIC_API_URL= NEXT_PUBLIC_DOCS_V1_URL= ALGOLIA_WRITE_API_KEY= ANALYZE_BUNDLE= -NEXT_PUBLIC_AI_ASSISTANT_URL= -NEXT_PUBLIC_AI_WEBSITE_ID= -NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY= CLOUDINARY_CLOUD_NAME= NEXT_PUBLIC_BASE_PATH= NEXT_PUBLIC_GA_ID= -NEXT_PUBLIC_PROD_BASE_URL= \ No newline at end of file +NEXT_PUBLIC_PROD_BASE_URL= +NEXT_PUBLIC_INTEGRATION_ID= \ No newline at end of file diff --git a/www/apps/book/providers/index.tsx b/www/apps/book/providers/index.tsx index 11d92908ff..b4d2d5b843 100644 --- a/www/apps/book/providers/index.tsx +++ b/www/apps/book/providers/index.tsx @@ -34,16 +34,8 @@ const Providers = ({ children, aiAssistantProps = {} }: ProvidersProps) => { { { { {children} diff --git a/www/apps/user-guide/.env.example b/www/apps/user-guide/.env.example index f1126546ca..712ae20c81 100644 --- a/www/apps/user-guide/.env.example +++ b/www/apps/user-guide/.env.example @@ -13,8 +13,6 @@ NEXT_PUBLIC_API_URL= NEXT_PUBLIC_CLOUD_URL= ALGOLIA_WRITE_API_KEY= ANALYZE_BUNDLE= -NEXT_PUBLIC_AI_ASSISTANT_URL= -NEXT_PUBLIC_AI_WEBSITE_ID= -NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY= CLOUDINARY_CLOUD_NAME= -NEXT_PUBLIC_GA_ID= \ No newline at end of file +NEXT_PUBLIC_GA_ID= +NEXT_PUBLIC_INTEGRATION_ID= \ No newline at end of file diff --git a/www/apps/user-guide/providers/index.tsx b/www/apps/user-guide/providers/index.tsx index 6ec76414bc..5304a839f8 100644 --- a/www/apps/user-guide/providers/index.tsx +++ b/www/apps/user-guide/providers/index.tsx @@ -37,17 +37,8 @@ const Providers = ({ children }: ProvidersProps) => { { - const { - inputRef, - question, - setQuestion, - handleSubmit: submitQuestion, - loading, - getThreadItems, - } = useAiAssistantChat() - const { chatOpened } = useAiAssistant() +type AiAssistantChatWindowInputProps = { + chatWindowRef: React.RefObject +} + +export const AiAssistantChatWindowInput = ({ + chatWindowRef, +}: AiAssistantChatWindowInputProps) => { + const { chatOpened, inputRef, loading } = useAiAssistant() + const { submitQuery, conversation } = useChat() + const [question, setQuestion] = React.useState("") const formRef = useRef(null) const onSubmit = (e?: React.FormEvent) => { e?.preventDefault() - submitQuestion() + submitQuery(question) + setQuestion("") } const handleKeyboardDown = (e: React.KeyboardEvent) => { if (e.key === "ArrowUp" && !question) { - const lastQuestion = getThreadItems() - .reverse() - .find((item) => item.type === "question") + const lastQuestion = conversation.getLatest()?.question if (lastQuestion) { - setQuestion(lastQuestion.content) + setQuestion(lastQuestion) } return } @@ -87,6 +87,16 @@ export const AiAssistantChatWindowInput = () => { } }, [chatOpened, inputRef.current]) + useAiAssistantChatNavigation({ + getChatWindowElm: () => chatWindowRef.current as HTMLElement | null, + getInputElm: () => inputRef.current as HTMLTextAreaElement | null, + focusInput: () => + inputRef.current?.focus({ + preventScroll: true, + }), + question, + }) + return (
{ "appearance-none text-base md:text-small placeholder:text-medusa-fg-muted", "text-medusa-fg-base max-h-[210px] overflow-auto resize-none bg-transparent", "focus:outline-none focus:ring-0 disabled:cursor-not-allowed max-h-[210px]", - "disabled:!bg-transparent disabled:text-medusa-fg-disabled" + "disabled:!bg-transparent disabled:text-medusa-fg-disabled disabled:placeholder:text-medusa-fg-disabled" )} value={question} onChange={(e) => setQuestion(e.target.value)} diff --git a/www/packages/docs-ui/src/components/AiAssistant/ChatWindow/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/ChatWindow/index.tsx index 213adbcade..53600823fe 100644 --- a/www/packages/docs-ui/src/components/AiAssistant/ChatWindow/index.tsx +++ b/www/packages/docs-ui/src/components/AiAssistant/ChatWindow/index.tsx @@ -1,31 +1,37 @@ "use client" import clsx from "clsx" -import React, { useCallback, useEffect, useRef, useState } from "react" +import React, { + Fragment, + useCallback, + useEffect, + useRef, + useState, +} from "react" import { useAiAssistant, useIsBrowser } from "../../../providers" import { AiAssistantChatWindowHeader } from "./Header" -import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat" import { AiAssistantSuggestions } from "../Suggestions" import { AiAssistantThreadItem } from "../ThreadItem" import { AiAssistantChatWindowInput } from "./Input" -import { useAiAssistantChatNavigation, useKeyboardShortcut } from "../../.." +import { useKeyboardShortcut } from "../../.." import { AiAssistantChatWindowFooter } from "./Footer" +import { useChat } from "@kapaai/react-sdk" const DEFAULT_HEIGHT = "calc(100% - 8px)" export const AiAssistantChatWindow = () => { - const { chatOpened, setChatOpened, chatType: type } = useAiAssistant() + const { + chatOpened, + setChatOpened, + chatType: type, + inputRef, + contentRef, + loading, + } = useAiAssistant() + const { conversation, error } = useChat() const [height, setHeight] = useState(DEFAULT_HEIGHT) const [showFade, setShowFade] = useState(false) const { isBrowser } = useIsBrowser() - const { - inputRef, - thread, - getThreadItems: getChatThreadItems, - answer, - loading, - contentRef, - } = useAiAssistantChat() const chatWindowRef = useRef(null) useEffect(() => { @@ -39,21 +45,27 @@ export const AiAssistantChatWindow = () => { }, [chatOpened]) const getThreadItems = useCallback(() => { - const sortedThread = getChatThreadItems() - - return sortedThread.map((item, index) => ( - + return conversation.map((item, index) => ( + + + + )) - }, [getChatThreadItems]) - - useAiAssistantChatNavigation({ - getChatWindowElm: () => chatWindowRef.current as HTMLElement | null, - getInputElm: () => inputRef.current as HTMLTextAreaElement | null, - focusInput: () => - inputRef.current?.focus({ - preventScroll: true, - }), - }) + }, [conversation]) useKeyboardShortcut({ metakey: false, @@ -75,7 +87,8 @@ export const AiAssistantChatWindow = () => { } setShowFade( !loading && - parentElm.offsetHeight + parentElm.scrollTop < parentElm.scrollHeight + parentElm.offsetHeight + parentElm.scrollTop < + parentElm.scrollHeight - 1 ) } @@ -168,14 +181,13 @@ export const AiAssistantChatWindow = () => { )} >
- {!thread.length && } + {!conversation.length && } {getThreadItems()} - {(answer.length || loading) && ( + {error?.length && ( )} @@ -189,7 +201,7 @@ export const AiAssistantChatWindow = () => { )} >
- +
diff --git a/www/packages/docs-ui/src/components/AiAssistant/SearchWindow/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/SearchWindow/index.tsx index 7cd78e290c..80d76771a9 100644 --- a/www/packages/docs-ui/src/components/AiAssistant/SearchWindow/index.tsx +++ b/www/packages/docs-ui/src/components/AiAssistant/SearchWindow/index.tsx @@ -1,41 +1,52 @@ "use client" -import React, { useCallback } from "react" +import React, { Fragment, useCallback, useState } from "react" import { Badge, Button, InputText, Kbd, Tooltip, Link } from "@/components" -import { useSearch } from "@/providers" +import { useAiAssistant, useSearch } from "@/providers" import { ArrowUturnLeft } from "@medusajs/icons" import clsx from "clsx" import { AiAssistantThreadItem } from "../ThreadItem" import { AiAssistantSuggestions } from "../Suggestions" -import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat" import { useSearchNavigation } from "../../.." +import { useChat } from "@kapaai/react-sdk" export const AiAssistantSearchWindow = () => { - const { - handleSubmit, - getThreadItems: getChatThreadItems, - question, - setQuestion, - inputRef, - contentRef, - loading, - thread, - answer, - } = useAiAssistantChat() + const { conversation, submitQuery, error } = useChat() + const { inputRef, contentRef, loading } = useAiAssistant() + const [question, setQuestion] = useState("") const { setCommand } = useSearch() const getThreadItems = useCallback(() => { - const sortedThread = getChatThreadItems() - - return sortedThread.map((item, index) => ( - + return conversation.map((item, index) => ( + + + + )) - }, [getChatThreadItems]) + }, [conversation]) useSearchNavigation({ getInputElm: () => inputRef.current as HTMLInputElement | null, focusInput: () => inputRef.current?.focus(), - handleSubmit, + handleSubmit: () => { + if (question.length > 0) { + submitQuery(question) + } + }, }) return ( @@ -101,14 +112,15 @@ export const AiAssistantSearchWindow = () => {
- {!thread.length && } + {!conversation.length && ( + + )} {getThreadItems()} - {(answer.length || loading) && ( + {error?.length && ( )} @@ -123,7 +135,7 @@ export const AiAssistantSearchWindow = () => { >
- {thread.length === 0 && ( + {conversation.length === 0 && ( <> { )} - {thread.length > 0 && ( + {conversation.length > 0 && ( diff --git a/www/packages/docs-ui/src/components/AiAssistant/Suggestions/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/Suggestions/index.tsx index 4bed271fea..fec4a41953 100644 --- a/www/packages/docs-ui/src/components/AiAssistant/Suggestions/index.tsx +++ b/www/packages/docs-ui/src/components/AiAssistant/Suggestions/index.tsx @@ -1,56 +1,36 @@ "use client" -import React, { useMemo } from "react" +import React from "react" import { SearchSuggestionType } from "../../Search/Suggestions" -import { useAiAssistant } from "../../../providers" import { SearchHitGroupName } from "../../Search/Hits/GroupName" import { SearchSuggestionItem } from "../../Search/Suggestions/Item" -import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat" +import { useChat } from "@kapaai/react-sdk" type AiAssistantSuggestionsProps = React.AllHTMLAttributes export const AiAssistantSuggestions = (props: AiAssistantSuggestionsProps) => { - const { version } = useAiAssistant() - const { setQuestion, handleSubmit } = useAiAssistantChat() - const suggestions: SearchSuggestionType[] = useMemo(() => { - return version === "v2" - ? [ - { - title: "FAQ", - items: [ - "What is Medusa?", - "How can I create a module?", - "How can I create a data model?", - "How do I create a workflow?", - "How can I extend a data model in the Product Module?", - ], - }, - { - title: "Recipes", - items: [ - "How do I build a marketplace with Medusa?", - "How do I build digital products with Medusa?", - "How do I build subscription-based purchases with Medusa?", - "What other recipes are available in the Medusa documentation?", - ], - }, - ] - : [ - { - title: "FAQ", - items: [ - "What is Medusa?", - "How can I create an ecommerce store with Medusa?", - "How can I build a marketplace with Medusa?", - "How can I build subscription-based purchases with Medusa?", - "How can I build digital products with Medusa?", - "What can I build with Medusa?", - "What is Medusa Admin?", - "How do I configure the database in Medusa?", - ], - }, - ] - }, [version]) + const { submitQuery } = useChat() + const suggestions: SearchSuggestionType[] = [ + { + title: "FAQ", + items: [ + "What is Medusa?", + "How can I create a module?", + "How can I create a data model?", + "How do I create a workflow?", + "How can I extend a data model in the Product Module?", + ], + }, + { + title: "Recipes", + items: [ + "How do I build a marketplace with Medusa?", + "How do I build digital products with Medusa?", + "How do I build subscription-based purchases with Medusa?", + "What other recipes are available in the Medusa documentation?", + ], + }, + ] return (
@@ -60,8 +40,7 @@ export const AiAssistantSuggestions = (props: AiAssistantSuggestionsProps) => { {suggestion.items.map((item, itemIndex) => ( { - setQuestion(item) - handleSubmit(item) + submitQuery(item) }} key={itemIndex} tabIndex={itemIndex} diff --git a/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/Actions/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/Actions/index.tsx index 799aa561f0..3096490058 100644 --- a/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/Actions/index.tsx +++ b/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/Actions/index.tsx @@ -2,33 +2,29 @@ import React, { useState } from "react" import clsx from "clsx" import { Badge, Button, Link, type ButtonProps } from "@/components" import { ThumbDown, ThumbUp } from "@medusajs/icons" -import { AiAssistantFeedbackType, useAiAssistant } from "@/providers" -import { AiAssistantThread } from "../../../../providers/AiAssistant/Chat" +import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../../providers" +import { Reaction, useChat } from "@kapaai/react-sdk" export type AiAssistantThreadItemActionsProps = { - item: AiAssistantThread + item: AiAssistantThreadItemType } export const AiAssistantThreadItemActions = ({ item, }: AiAssistantThreadItemActionsProps) => { - const [feedback, setFeedback] = useState(null) - const { sendFeedback } = useAiAssistant() + const [feedback, setFeedback] = useState(null) + const { addFeedback } = useChat() const handleFeedback = async ( - reaction: AiAssistantFeedbackType, - question_id?: string + reaction: Reaction, + question_id?: string | null ) => { try { if (!question_id || feedback) { return } setFeedback(reaction) - const response = await sendFeedback(question_id, reaction) - - if (response.status !== 200) { - console.error("Error sending feedback:", response.status) - } + addFeedback(question_id, reaction) } catch (error) { console.error("Error sending feedback:", error) } diff --git a/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/index.tsx index 95ef9cc8ac..40680fcac8 100644 --- a/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/index.tsx +++ b/www/packages/docs-ui/src/components/AiAssistant/ThreadItem/index.tsx @@ -1,5 +1,5 @@ import clsx from "clsx" -import React from "react" +import React, { useMemo } from "react" import { AiAssistantIcon, CodeMdx, @@ -9,13 +9,22 @@ import { MDXComponents, } from "@/components" import { AiAssistantThreadItemActions } from "./Actions" -import { AiAssistantThread } from "../../../providers/AiAssistant/Chat" +import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../providers" +import { useChat } from "@kapaai/react-sdk" export type AiAssistantThreadItemProps = { - item: AiAssistantThread + item: AiAssistantThreadItemType } export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => { + const { error } = useChat() + const showLoading = useMemo(() => { + if (error?.length) { + return false + } + + return !item.question_id && item.content.length === 0 + }, [item, error]) return (
{ )} {item.type === "answer" && (
- {!item.question_id && item.content.length === 0 && } + {showLoading && } { - return + return ( + + ) }, }} + disallowedElements={["h1", "h2", "h3", "h4", "h5", "h6"]} > {item.content} diff --git a/www/packages/docs-ui/src/components/AiAssistant/TriggerButton/index.tsx b/www/packages/docs-ui/src/components/AiAssistant/TriggerButton/index.tsx index 3eb23014e2..5f12529346 100644 --- a/www/packages/docs-ui/src/components/AiAssistant/TriggerButton/index.tsx +++ b/www/packages/docs-ui/src/components/AiAssistant/TriggerButton/index.tsx @@ -9,7 +9,6 @@ import { useAiAssistant, useSearch, useSiteConfig } from "../../../providers" import { useKeyboardShortcut } from "../../../hooks" import Image from "next/image" -// const AI_ASSISTANT_ICON = "/images/ai-assistent-luminosity.png" const AI_ASSISTANT_ICON_ACTIVE = "/images/ai-assistent.png" export const AiAssistantTriggerButton = () => { diff --git a/www/packages/docs-ui/src/components/CodeBlock/Actions/AskAi/index.tsx b/www/packages/docs-ui/src/components/CodeBlock/Actions/AskAi/index.tsx index a796d74610..38576a587f 100644 --- a/www/packages/docs-ui/src/components/CodeBlock/Actions/AskAi/index.tsx +++ b/www/packages/docs-ui/src/components/CodeBlock/Actions/AskAi/index.tsx @@ -2,10 +2,10 @@ import React from "react" import { useAiAssistant, useSiteConfig } from "../../../../providers" -import { useAiAssistantChat } from "../../../../providers/AiAssistant/Chat" import clsx from "clsx" import { Tooltip } from "../../../Tooltip" import Image from "next/image" +import { useChat } from "@kapaai/react-sdk" export type CodeBlockCopyActionProps = { source: string @@ -16,15 +16,15 @@ export const CodeBlockAskAiAction = ({ source, inHeader, }: CodeBlockCopyActionProps) => { - const { setChatOpened } = useAiAssistant() - const { setQuestion, loading } = useAiAssistantChat() + const { setChatOpened, loading } = useAiAssistant() + const { submitQuery } = useChat() const { config } = useSiteConfig() const handleClick = () => { if (loading) { return } - setQuestion(`\`\`\`tsx\n${source.trim()}\n\`\`\`\n\nExplain the code above`) + submitQuery(`\`\`\`tsx\n${source.trim()}\n\`\`\`\n\nExplain the code above`) setChatOpened(true) } diff --git a/www/packages/docs-ui/src/components/ContentMenu/Actions/index.tsx b/www/packages/docs-ui/src/components/ContentMenu/Actions/index.tsx index 63ec3953a1..490e644137 100644 --- a/www/packages/docs-ui/src/components/ContentMenu/Actions/index.tsx +++ b/www/packages/docs-ui/src/components/ContentMenu/Actions/index.tsx @@ -1,12 +1,12 @@ "use client" import Link from "next/link" -import React from "react" +import React, { useMemo } from "react" import { MarkdownIcon } from "../../Icons/Markdown" import { useAiAssistant, useSiteConfig } from "../../../providers" import { usePathname } from "next/navigation" import { BroomSparkle } from "@medusajs/icons" -import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat" +import { useChat } from "@kapaai/react-sdk" export const ContentMenuActions = () => { const { @@ -14,14 +14,18 @@ export const ContentMenuActions = () => { } = useSiteConfig() const pathname = usePathname() const { setChatOpened } = useAiAssistant() - const { setQuestion, loading } = useAiAssistantChat() + const { isGeneratingAnswer, isPreparingAnswer, submitQuery } = useChat() + const loading = useMemo( + () => isGeneratingAnswer || isPreparingAnswer, + [isGeneratingAnswer, isPreparingAnswer] + ) const pageUrl = `${baseUrl}${basePath}${pathname}` const handleAiAssistantClick = () => { if (loading) { return } - setQuestion(`Explain the page ${pageUrl}`) + submitQuery(`Explain the page ${pageUrl}`) setChatOpened(true) } diff --git a/www/packages/docs-ui/src/hooks/use-ai-assistant-chat-navigation/index.ts b/www/packages/docs-ui/src/hooks/use-ai-assistant-chat-navigation/index.ts index 6dda96c8f6..bbff32abb6 100644 --- a/www/packages/docs-ui/src/hooks/use-ai-assistant-chat-navigation/index.ts +++ b/www/packages/docs-ui/src/hooks/use-ai-assistant-chat-navigation/index.ts @@ -7,13 +7,13 @@ import { useKeyboardShortcut, type useKeyboardShortcutOptions, } from "../use-keyboard-shortcut" -import { useAiAssistantChat } from "../../providers/AiAssistant/Chat" export type UseAiAssistantChatNavigationProps = { getChatWindowElm: () => HTMLElement | null getInputElm: () => HTMLTextAreaElement | null focusInput: () => void keyboardProps?: Partial + question: string } export const useAiAssistantChatNavigation = ({ @@ -21,10 +21,10 @@ export const useAiAssistantChatNavigation = ({ focusInput, keyboardProps, getChatWindowElm, + question, }: UseAiAssistantChatNavigationProps) => { const shortcutKeys = useMemo(() => ["ArrowUp", "ArrowDown", "Enter"], []) const { chatOpened } = useAiAssistant() - const { question } = useAiAssistantChat() const handleKeyAction = (e: KeyboardEvent) => { const chatElm = getChatWindowElm() diff --git a/www/packages/docs-ui/src/hooks/use-recaptcha/index.tsx b/www/packages/docs-ui/src/hooks/use-recaptcha/index.tsx deleted file mode 100644 index c5ad24a151..0000000000 --- a/www/packages/docs-ui/src/hooks/use-recaptcha/index.tsx +++ /dev/null @@ -1,159 +0,0 @@ -"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( - 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]) - - useEffect(() => { - if (!isExecuteReady) { - return - } - - const recaptchaElm = document.querySelector(".grecaptcha-badge") - if (recaptchaElm?.parentElement) { - recaptchaElm.parentElement.classList.add("absolute") - } - }, [isExecuteReady]) - - 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/Chat/index.tsx b/www/packages/docs-ui/src/providers/AiAssistant/Chat/index.tsx deleted file mode 100644 index 4ef14ac8fa..0000000000 --- a/www/packages/docs-ui/src/providers/AiAssistant/Chat/index.tsx +++ /dev/null @@ -1,389 +0,0 @@ -"use client" - -import React, { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react" -import { useAiAssistant } from ".." -import useResizeObserver from "@react-hook/resize-observer" - -export type AiAssistantChatContextType = { - handleSubmit: (selectedQuestion?: string) => void - getThreadItems: () => AiAssistantThread[] - question: string - setQuestion: React.Dispatch> - inputRef: React.RefObject - contentRef: React.RefObject - loading: boolean - thread: AiAssistantThread[] - answer: string -} - -const AiAssistantChatContext = createContext( - null -) - -export type AiAssistantChunk = { - stream_end: boolean -} & ( - | { - type: "relevant_sources" - content: { - relevant_sources: AiAssistantRelevantSources[] - } - } - | { - type: "partial_answer" - content: AiAssistantPartialAnswer - } - | { - type: "identifiers" - content: AiAssistantIdentifier - } - | { - type: "error" - content: AiAssistantError - } -) - -export type AiAssistantRelevantSources = { - title: string - source_url: string -} - -export type AiAssistantPartialAnswer = { - text: string -} - -export type AiAssistantIdentifier = { - thread_id: string - question_answer_id: string -} - -export type AiAssistantError = { - reason: string -} - -export type AiAssistantThread = { - type: "question" | "answer" | "error" - content: string - question_id?: string - sources?: AiAssistantRelevantSources[] - // for some reason, items in the array get reordered - // sometimes, so this is one way to avoid it - order: number -} - -type AiAssistantChatProvider = { - children: React.ReactNode -} - -export const AiAssistantChatProvider = ({ - children, -}: AiAssistantChatProvider) => { - const [question, setQuestion] = useState("") - // this helps set the `order` field of the threadtype - const [messagesCount, setMessagesCount] = useState(0) - const [thread, setThread] = useState([]) - const [answer, setAnswer] = useState("") - const [answerSources, setAnswerSources] = useState< - AiAssistantRelevantSources[] - >([]) - const [identifiers, setIdentifiers] = useState( - null - ) - const [loading, setLoading] = useState(false) - const [preventAutoScroll, setPreventAutoScroll] = useState(false) - const { getAnswer } = useAiAssistant() - const inputRef = useRef(null) - const contentRef = useRef(null) - - const handleSubmit = (selectedQuestion?: string) => { - if (!selectedQuestion?.length && !question.length) { - return - } - setLoading(true) - setPreventAutoScroll(false) - setAnswer("") - setThread((prevThread) => [ - ...prevThread, - { - type: "question", - content: selectedQuestion || question, - order: getNewOrder(prevThread), - }, - ]) - setMessagesCount((prev) => prev + 1) - } - - const sortThread = (threadArr: AiAssistantThread[]) => { - const sortedThread = [...threadArr] - sortedThread.sort((itemA, itemB) => { - if (itemA.order < itemB.order) { - return -1 - } - - return itemA.order < itemB.order ? 1 : 0 - }) - return sortedThread - } - - const getNewOrder = (prevThread: AiAssistantThread[]) => { - const sortedThread = sortThread(prevThread) - - return sortedThread.length === 0 - ? messagesCount + 1 - : sortedThread[prevThread.length - 1].order + 1 - } - - const setError = (logMessage?: string) => { - if (logMessage?.length) { - console.error(`[AI ERROR]: ${logMessage}`) - } - setThread((prevThread) => [ - ...prevThread, - { - type: "error", - content: - "I'm sorry, but I'm having trouble connecting to my knowledge base. Please try again. If the issue keeps persisting, please consider reporting an issue.", - order: getNewOrder(prevThread), - }, - ]) - setMessagesCount((prev) => prev + 1) - setLoading(false) - setQuestion("") - setAnswer("") - inputRef.current?.focus() - } - - const scrollToBottom = () => { - if (preventAutoScroll) { - return - } - const parent = contentRef.current?.parentElement as HTMLElement - - parent.scrollTop = parent.scrollHeight - } - - const lastAnswerIndex = useMemo(() => { - const index = thread.reverse().findIndex((item) => item.type === "answer") - return index !== -1 ? index : 0 - }, [thread]) - - const process_stream = useCallback(async (response: Response) => { - const reader = response.body?.getReader() - if (!reader) { - return - } - const decoder = new TextDecoder("utf-8") - const delimiter = "\u241E" - const delimiterBytes = new TextEncoder().encode(delimiter) - let buffer = new Uint8Array() - - const findDelimiterIndex = (arr: Uint8Array) => { - for (let i = 0; i < arr.length - delimiterBytes.length + 1; i++) { - let found = true - for (let j = 0; j < delimiterBytes.length; j++) { - if (arr[i + j] !== delimiterBytes[j]) { - found = false - break - } - } - if (found) { - return i - } - } - return -1 - } - - let result - let loop = true - while (loop) { - result = await reader.read() - if (result.done) { - loop = false - continue - } - buffer = new Uint8Array([...buffer, ...result.value]) - let delimiterIndex - while ((delimiterIndex = findDelimiterIndex(buffer)) !== -1) { - const chunkBytes = buffer.slice(0, delimiterIndex) - const chunkText = decoder.decode(chunkBytes) - buffer = buffer.slice(delimiterIndex + delimiterBytes.length) - const chunk = JSON.parse(chunkText).chunk as AiAssistantChunk - - switch (chunk.type) { - case "partial_answer": - setAnswer((prevAnswer) => prevAnswer + chunk.content.text) - break - case "identifiers": - setIdentifiers(chunk.content) - break - case "error": - setError(chunk.content.reason) - loop = false - return - case "relevant_sources": - setAnswerSources((prev) => [ - ...prev, - ...chunk.content.relevant_sources, - ]) - break - } - } - } - - setLoading(false) - setQuestion("") - }, []) - - const fetchAnswer = useCallback(async () => { - try { - const response = await getAnswer(question, identifiers?.thread_id) - - if (response.status === 200) { - await process_stream(response) - } else { - const message = await response.text() - setError(message) - } - } catch (error: any) { - setError(JSON.stringify(error)) - } - }, [question, identifiers, process_stream]) - - useEffect(() => { - if (loading && !answer) { - void fetchAnswer() - } - }, [loading, fetchAnswer]) - - useEffect(() => { - if ( - loading || - !answer.length || - thread[lastAnswerIndex]?.content === answer - ) { - return - } - - const uniqueAnswerSources = answerSources - .filter( - (source, index) => - answerSources.findIndex((s) => s.source_url === source.source_url) === - index - ) - .map((source) => { - const separatorIndex = source.title.indexOf("|") - return { - ...source, - title: - separatorIndex !== -1 - ? source.title.slice(0, separatorIndex) - : source.title, - } - }) - setThread((prevThread) => [ - ...prevThread, - { - type: "answer", - content: answer, - question_id: identifiers?.question_answer_id, - order: getNewOrder(prevThread), - sources: - uniqueAnswerSources.length > 3 - ? uniqueAnswerSources.slice(0, 3) - : uniqueAnswerSources, - }, - ]) - setAnswer("") - setAnswerSources([]) - setMessagesCount((prev) => prev + 1) - inputRef.current?.focus() - scrollToBottom() - }, [loading, answer, thread, lastAnswerIndex, inputRef.current]) - - useResizeObserver(contentRef as React.RefObject, () => { - if (!loading) { - return - } - - scrollToBottom() - }) - - const handleUserScroll = useCallback(() => { - if (!question.length || preventAutoScroll) { - return - } - - setPreventAutoScroll(true) - }, [question, preventAutoScroll]) - - const handleUserScrollEnd = useCallback(() => { - if (preventAutoScroll) { - setPreventAutoScroll(false) - } - }, [preventAutoScroll]) - - useEffect(() => { - if (!contentRef.current?.parentElement) { - return - } - - contentRef.current.parentElement.addEventListener("wheel", handleUserScroll) - contentRef.current.parentElement.addEventListener( - "touchmove", - handleUserScroll - ) - - return () => { - contentRef.current?.parentElement?.removeEventListener( - "wheel", - handleUserScroll - ) - contentRef.current?.parentElement?.removeEventListener( - "touchmove", - handleUserScroll - ) - } - }, [contentRef.current, handleUserScroll]) - - const getThreadItems = useCallback(() => { - return sortThread(thread) - }, [thread]) - - return ( - - {children} - - ) -} - -export const useAiAssistantChat = () => { - const context = useContext(AiAssistantChatContext) - - if (!context) { - throw new Error( - "useAiAssistantChat must be used within a AiAssistantChatContext" - ) - } - - return context -} diff --git a/www/packages/docs-ui/src/providers/AiAssistant/index.tsx b/www/packages/docs-ui/src/providers/AiAssistant/index.tsx index be9825ec03..f314380ee4 100644 --- a/www/packages/docs-ui/src/providers/AiAssistant/index.tsx +++ b/www/packages/docs-ui/src/providers/AiAssistant/index.tsx @@ -1,147 +1,186 @@ "use client" -import React, { createContext, useContext, useEffect, useState } from "react" -import { useAnalytics, useSearch } from "@/providers" -import { AiAssistantIcon, AiAssistantSearchWindow } from "@/components" -import { RecaptchaAction, useRecaptcha } from "../../hooks/use-recaptcha" -import { AiAssistantChatProvider } from "./Chat" - -export type AiAssistantFeedbackType = "upvote" | "downvote" +import { KapaProvider, useChat } from "@kapaai/react-sdk" +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react" +import type { Source } from "@kapaai/react-sdk" +import useResizeObserver from "@react-hook/resize-observer" +import { AiAssistantSearchWindow } from "../../components" export type AiAssistantChatType = "default" | "popover" export type AiAssistantContextType = { - getAnswer: (question: string, thread_id?: string) => Promise - sendFeedback: ( - questionId: string, - reaction: AiAssistantFeedbackType - ) => Promise - version: "v1" | "v2" chatOpened: boolean setChatOpened: React.Dispatch> chatType: AiAssistantChatType + inputRef: React.RefObject + contentRef: React.RefObject + loading: boolean +} + +export type AiAssistantThreadItem = { + type: "question" | "answer" | "error" + content: string + question_id?: string | null + sources?: Source[] } const AiAssistantContext = createContext(null) export type AiAssistantProviderProps = { children?: React.ReactNode - apiUrl: string - recaptchaSiteKey: string - websiteId: string - version?: "v1" | "v2" - type?: "search" | "chat" + integrationId: string chatType?: AiAssistantChatType + type?: "search" | "chat" } -export const AiAssistantProvider = ({ - apiUrl, - recaptchaSiteKey, - websiteId, - version = "v2", +type AiAssistantInnerProviderProps = Omit< + AiAssistantProviderProps, + "integrationId" +> & { + preventAutoScroll: boolean + setPreventAutoScroll: React.Dispatch> + setOnCompleteAction: React.Dispatch void>> +} + +const AiAssistantInnerProvider = ({ children, - type = "chat", chatType = "default", -}: AiAssistantProviderProps) => { + preventAutoScroll, + setPreventAutoScroll, + setOnCompleteAction, + type, +}: AiAssistantInnerProviderProps) => { const [chatOpened, setChatOpened] = useState(false) - const { setCommands, setIsOpen, setCommand } = useSearch() - const { analytics } = useAnalytics() - const { execute: getReCaptchaToken } = useRecaptcha({ - siteKey: recaptchaSiteKey, + const inputRef = useRef(null) + const contentRef = useRef(null) + const { isGeneratingAnswer, isPreparingAnswer } = useChat() + const loading = useMemo( + () => isGeneratingAnswer || isPreparingAnswer, + [isGeneratingAnswer, isPreparingAnswer] + ) + + const scrollToBottom = () => { + if (preventAutoScroll) { + return + } + const parent = contentRef.current?.parentElement as HTMLElement + + parent.scrollTop = parent.scrollHeight + } + + useResizeObserver(contentRef as React.RefObject, () => { + if (!loading) { + return + } + + scrollToBottom() }) - const sendRequest = async ( - apiPath: string, - action: RecaptchaAction, - method = "GET", - headers?: HeadersInit, - body?: BodyInit - ) => { - return await fetch(`${apiUrl}${apiPath}`, { - method, - headers: { - "X-RECAPTCHA-ENTERPRISE-TOKEN": await getReCaptchaToken(action), - "X-WEBSITE-ID": websiteId, - ...headers, - }, - body, - }) - } + const handleUserScroll = useCallback(() => { + if (!loading || preventAutoScroll) { + return + } - const getAnswer = async (question: string, threadId?: string) => { - const questionParam = encodeURI(question) - return await sendRequest( - threadId - ? `/query/v1/thread/${threadId}/stream?query=${questionParam}` - : `/query/v1/stream?query=${questionParam}`, - RecaptchaAction.AskAi - ) - } - - const sendFeedback = async ( - questionId: string, - reaction: AiAssistantFeedbackType - ) => { - return await sendRequest( - `/query/v1/question-answer/${questionId}/feedback`, - RecaptchaAction.FeedbackSubmit, - "POST", - { - "Content-Type": "application/json", - }, - JSON.stringify({ - question_id: questionId, - reaction, - user_identifier: analytics?.user().anonymousId() || "", - }) - ) - } + setPreventAutoScroll(true) + }, [loading, preventAutoScroll]) useEffect(() => { - setCommands((prevCommands) => { - const newCommands = [...prevCommands] + if (!contentRef.current?.parentElement) { + return + } - if (!newCommands.find((c) => c.name === "ai-assistant")) { - newCommands.push({ - name: "ai-assistant", - icon: , - title: "AI Assistant", - badge: { - variant: "blue", - badgeType: "shaded", - children: "Beta", - }, - action: () => { - setIsOpen(false) - setChatOpened(true) - setCommand(null) - }, - }) + contentRef.current.parentElement.addEventListener( + "wheel", + handleUserScroll, + { + passive: true, } + ) + contentRef.current.parentElement.addEventListener( + "touchmove", + handleUserScroll, + { + passive: true, + } + ) - return newCommands + return () => { + contentRef.current?.parentElement?.removeEventListener( + "wheel", + handleUserScroll + ) + contentRef.current?.parentElement?.removeEventListener( + "touchmove", + handleUserScroll + ) + } + }, [contentRef.current, handleUserScroll]) + + useEffect(() => { + setOnCompleteAction(() => { + scrollToBottom() + inputRef.current?.focus({ + preventScroll: true, + }) }) - }, []) + }, [scrollToBottom]) return ( - - {children} - {type === "search" && } - + {children} + {type === "search" && } ) } +export const AiAssistantProvider = ({ + integrationId, + ...props +}: AiAssistantProviderProps) => { + const [preventAutoScroll, setPreventAutoScroll] = useState(false) + const [onCompleteAction, setOnCompleteAction] = useState<() => void>( + () => () => {} + ) + + return ( + { + onCompleteAction?.() + }, + onQuerySubmit: () => setPreventAutoScroll(false), + }, + }} + > + + + ) +} + export const useAiAssistant = () => { const context = useContext(AiAssistantContext) diff --git a/www/yarn.lock b/www/yarn.lock index e3290d84df..ae3be4ce11 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -518,6 +518,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.17.9": + version: 7.27.6 + resolution: "@babel/runtime@npm:7.27.6" + checksum: 89726be83f356f511dcdb74d3ea4d873a5f0cf0017d4530cb53aa27380c01ca102d573eff8b8b77815e624b1f8c24e7f0311834ad4fb632c90a770fda00bd4c8 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.22.5": version: 7.26.10 resolution: "@babel/runtime@npm:7.26.10" @@ -1079,6 +1086,35 @@ __metadata: languageName: node linkType: hard +"@fingerprintjs/fingerprintjs-pro-react@npm:^2.6.3": + version: 2.7.0 + resolution: "@fingerprintjs/fingerprintjs-pro-react@npm:2.7.0" + dependencies: + "@fingerprintjs/fingerprintjs-pro-spa": ^1.3.2 + fast-deep-equal: 3.1.3 + checksum: 8892e325885ba4d5a10ffc48764dc016dcff73c1fc356c9aff245fbdb48c17b5b99c74ad280017f6739d0f035e505ece0006ad74116fc947bcb7c6042591d6a5 + languageName: node + linkType: hard + +"@fingerprintjs/fingerprintjs-pro-spa@npm:^1.3.2": + version: 1.3.2 + resolution: "@fingerprintjs/fingerprintjs-pro-spa@npm:1.3.2" + dependencies: + "@fingerprintjs/fingerprintjs-pro": ^3.11.0 + tslib: ^2.7.0 + checksum: a53fb7be19fe04dcb9705bcbd790ff7a411864ab031044193292046cbd63ec1c80672ed67a5b5326eef0320af35f21853281294fca471148c25ceb1aca24b555 + languageName: node + linkType: hard + +"@fingerprintjs/fingerprintjs-pro@npm:^3.11.0": + version: 3.11.10 + resolution: "@fingerprintjs/fingerprintjs-pro@npm:3.11.10" + dependencies: + tslib: ^2.4.1 + checksum: 981cdeabaf63da38062ed4864193d13df44aa5bd2654135d75866d4f4d06e60cceb8711973d2d881820ebb05b5a05d7ee0ffba1b0990c2f13ae5b5e72e0a3b71 + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.0.0": version: 1.6.0 resolution: "@floating-ui/core@npm:1.6.0" @@ -1190,6 +1226,26 @@ __metadata: languageName: node linkType: hard +"@hcaptcha/loader@npm:^2.0.0": + version: 2.0.0 + resolution: "@hcaptcha/loader@npm:2.0.0" + checksum: c69c8e62ccf41cc5cce8f25721b9bd8284e201da608022476dc6d69afbd70512a6f272dd2bfe558c4b5fdbc0ee20fb5f062033c4d556e6d523ecb2448203298f + languageName: node + linkType: hard + +"@hcaptcha/react-hcaptcha@npm:^1.12.0": + version: 1.12.0 + resolution: "@hcaptcha/react-hcaptcha@npm:1.12.0" + dependencies: + "@babel/runtime": ^7.17.9 + "@hcaptcha/loader": ^2.0.0 + peerDependencies: + react: ">= 16.3.0" + react-dom: ">= 16.3.0" + checksum: 58fddf7cbbeb1c9d784a90a4ee76604efd8d0c968d1d256f72d112d95c8d68bbb1255dd4184148e7e43e38491c5614b3747f4d17393a17d320e28935da47f8e8 + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.0": version: 0.19.0 resolution: "@humanfs/core@npm:0.19.0" @@ -1712,6 +1768,22 @@ __metadata: languageName: node linkType: hard +"@kapaai/react-sdk@npm:^0.1.4": + version: 0.1.4 + resolution: "@kapaai/react-sdk@npm:0.1.4" + dependencies: + "@fingerprintjs/fingerprintjs-pro-react": ^2.6.3 + "@hcaptcha/react-hcaptcha": ^1.12.0 + "@tanstack/react-query": ^5.74.3 + js-cookie: ^3.0.5 + tldts: ^7.0.7 + peerDependencies: + react: ">=17.0.0" + react-dom: ">=17.0.0" + checksum: 235c976c66a4caf031f30501e61dcad9429e302ca217acb7594af20161cd6891b760e8a345866db99ada18551edbb7e7cbfde80a95a74fc08992608c109e9d0c + languageName: node + linkType: hard + "@lukeed/csprng@npm:^1.1.0": version: 1.1.0 resolution: "@lukeed/csprng@npm:1.1.0" @@ -5458,6 +5530,24 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.80.10": + version: 5.80.10 + resolution: "@tanstack/query-core@npm:5.80.10" + checksum: 6391f4439f1f4676f5efeb07271e6b0b1418bd1192779f55cac537f0b35ec31c729eb43a30e0269534328c259c3ed6dd2f31555c42c68b6dd0c9ede410d46eb1 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.74.3": + version: 5.80.10 + resolution: "@tanstack/react-query@npm:5.80.10" + dependencies: + "@tanstack/query-core": 5.80.10 + peerDependencies: + react: ^18 || ^19 + checksum: 1a759b264cc94b389f802f1183183a3fbe1669ec1bb354951cfab6b6cf175276885c5f5c9b3fc00340a87140fdff4b9cf0c688fb7af103d34fb3b209d33a0a95 + languageName: node + linkType: hard + "@tanstack/react-table@npm:8.20.5": version: 8.20.5 resolution: "@tanstack/react-table@npm:8.20.5" @@ -8022,6 +8112,7 @@ __metadata: resolution: "docs-ui@workspace:packages/docs-ui" dependencies: "@emotion/is-prop-valid": ^1.3.1 + "@kapaai/react-sdk": ^0.1.4 "@medusajs/icons": 2.8.4 "@medusajs/ui": 4.0.14 "@next/third-parties": 15.3.1 @@ -9487,7 +9578,7 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": +"fast-deep-equal@npm:3.1.3, fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" checksum: 40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 @@ -11291,6 +11382,13 @@ __metadata: languageName: node linkType: hard +"js-cookie@npm:^3.0.5": + version: 3.0.5 + resolution: "js-cookie@npm:3.0.5" + checksum: 04a0e560407b4489daac3a63e231d35f4e86f78bff9d792011391b49c59f721b513411cd75714c418049c8dc9750b20fcddad1ca5a2ca616c3aca4874cce5b3a + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -16319,6 +16417,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^7.0.9": + version: 7.0.9 + resolution: "tldts-core@npm:7.0.9" + checksum: ff83afc0123fb3cb60036535c8ee5c117393a6fd659d36a247d7b7dface48645cf0b1c89c5223a5d920b41a34a34a2a662086980914ff9db63be37b485626d00 + languageName: node + linkType: hard + +"tldts@npm:^7.0.7": + version: 7.0.9 + resolution: "tldts@npm:7.0.9" + dependencies: + tldts-core: ^7.0.9 + bin: + tldts: bin/cli.js + checksum: 910d121c9c0f3f5a50fd19f1baa430430dac6fb9de389d11ea989526703f66a0f614c65b9b3f1144b3357ba35cedf204e654c88f71a2932409c11aa4401bdcff + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -16541,7 +16657,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.8.0": +"tslib@npm:^2.7.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62