docs: use Kapa React SDK (#12792)

This commit is contained in:
Shahed Nasser
2025-06-20 18:24:14 +03:00
committed by GitHub
parent 77d0f00929
commit ab21ac57ca
27 changed files with 462 additions and 875 deletions

View File

@@ -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=

View File

@@ -24,12 +24,8 @@ const Providers = ({ children }: ProvidersProps) => {
<MainNavProvider>
<SearchProvider>
<AiAssistantProvider
apiUrl={process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"}
websiteId={process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"}
recaptchaSiteKey={
process.env
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
"temp"
integrationId={
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
}
chatType="popover"
>

View File

@@ -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=
NEXT_PUBLIC_PROD_BASE_URL=
NEXT_PUBLIC_INTEGRATION_ID=

View File

@@ -34,16 +34,8 @@ const Providers = ({ children, aiAssistantProps = {} }: ProvidersProps) => {
<SearchProvider>
<AiAssistantProvider
{...aiAssistantProps}
apiUrl={
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"
}
websiteId={
process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"
}
recaptchaSiteKey={
process.env
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
"temp"
integrationId={
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
}
>
<HooksLoader

View File

@@ -13,8 +13,6 @@ NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_USER_GUIDE_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=
NEXT_PUBLIC_GA_ID=
NEXT_PUBLIC_INTEGRATION_ID=

View File

@@ -37,17 +37,8 @@ const Providers = ({ children }: ProvidersProps) => {
<MainNavProvider>
<SearchProvider>
<AiAssistantProvider
apiUrl={
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL ||
"temp"
}
websiteId={
process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"
}
recaptchaSiteKey={
// eslint-disable-next-line prettier/prettier, max-len
process.env.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
"temp"
integrationId={
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
}
>
<HooksLoader

View File

@@ -18,4 +18,5 @@ 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=
NEXT_PUBLIC_GA_ID=
NEXT_PUBLIC_INTEGRATION_ID=

View File

@@ -33,16 +33,8 @@ const Providers = ({ children }: ProvidersProps) => {
<MainNavProvider>
<SearchProvider>
<AiAssistantProvider
apiUrl={
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"
}
websiteId={
process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"
}
recaptchaSiteKey={
process.env
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
"temp"
integrationId={
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
}
>
<HooksLoader

View File

@@ -6,7 +6,5 @@ NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME=
NEXT_PUBLIC_ALGOLIA_API_KEY=
NEXT_PUBLIC_ALGOLIA_APP_ID=
NEXT_PUBLIC_SEGMENT_API_KEY=
NEXT_PUBLIC_AI_ASSISTANT_URL=
NEXT_PUBLIC_AI_WEBSITE_ID=
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
NEXT_PUBLIC_GA_ID=
NEXT_PUBLIC_GA_ID=
NEXT_PUBLIC_INTEGRATION_ID=

View File

@@ -25,11 +25,8 @@ const Providers = ({ children }: ProvidersProps) => {
<MainNavProvider>
<SearchProvider>
<AiAssistantProvider
apiUrl={process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"}
websiteId={process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"}
recaptchaSiteKey={
process.env
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY || "temp"
integrationId={
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
}
>
<TooltipProvider>{children}</TooltipProvider>

View File

@@ -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=
NEXT_PUBLIC_GA_ID=
NEXT_PUBLIC_INTEGRATION_ID=

View File

@@ -37,17 +37,8 @@ const Providers = ({ children }: ProvidersProps) => {
<MainNavProvider>
<SearchProvider>
<AiAssistantProvider
apiUrl={
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL ||
"temp"
}
websiteId={
process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"
}
recaptchaSiteKey={
// eslint-disable-next-line prettier/prettier, max-len
process.env.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
"temp"
integrationId={
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
}
>
<HooksLoader

View File

@@ -57,6 +57,7 @@
},
"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",

View File

@@ -1,33 +1,33 @@
import React, { useEffect, useRef } from "react"
import clsx from "clsx"
import { useAiAssistantChat } from "../../../../providers/AiAssistant/Chat"
import { ArrowUpCircleSolid } from "@medusajs/icons"
import { useAiAssistant } from "../../../../providers"
import { useChat } from "@kapaai/react-sdk"
import { useAiAssistantChatNavigation } from "../../../../hooks"
export const AiAssistantChatWindowInput = () => {
const {
inputRef,
question,
setQuestion,
handleSubmit: submitQuestion,
loading,
getThreadItems,
} = useAiAssistantChat()
const { chatOpened } = useAiAssistant()
type AiAssistantChatWindowInputProps = {
chatWindowRef: React.RefObject<HTMLDivElement | null>
}
export const AiAssistantChatWindowInput = ({
chatWindowRef,
}: AiAssistantChatWindowInputProps) => {
const { chatOpened, inputRef, loading } = useAiAssistant()
const { submitQuery, conversation } = useChat()
const [question, setQuestion] = React.useState("")
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
e?.preventDefault()
submitQuestion()
submitQuery(question)
setQuestion("")
}
const handleKeyboardDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
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 (
<div
className={clsx(
@@ -103,7 +113,7 @@ export const AiAssistantChatWindowInput = () => {
"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)}

View File

@@ -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<HTMLDivElement | null>(null)
useEffect(() => {
@@ -39,21 +45,27 @@ export const AiAssistantChatWindow = () => {
}, [chatOpened])
const getThreadItems = useCallback(() => {
const sortedThread = getChatThreadItems()
return sortedThread.map((item, index) => (
<AiAssistantThreadItem item={item} key={index} />
return conversation.map((item, index) => (
<Fragment key={index}>
<AiAssistantThreadItem
item={{
type: "question",
content: item.question,
sources: item.sources,
question_id: item.id,
}}
/>
<AiAssistantThreadItem
item={{
type: "answer",
content: item.answer,
sources: item.sources,
question_id: item.id,
}}
/>
</Fragment>
))
}, [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 = () => {
)}
>
<div ref={contentRef}>
{!thread.length && <AiAssistantSuggestions />}
{!conversation.length && <AiAssistantSuggestions />}
{getThreadItems()}
{(answer.length || loading) && (
{error?.length && (
<AiAssistantThreadItem
item={{
type: "answer",
content: answer,
order: 0,
type: "error",
content: error,
}}
/>
)}
@@ -189,7 +201,7 @@ export const AiAssistantChatWindow = () => {
)}
></span>
</div>
<AiAssistantChatWindowInput />
<AiAssistantChatWindowInput chatWindowRef={chatWindowRef} />
<AiAssistantChatWindowFooter />
</div>
</>

View File

@@ -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) => (
<AiAssistantThreadItem item={item} key={index} />
return conversation.map((item, index) => (
<Fragment key={index}>
<AiAssistantThreadItem
item={{
type: "question",
content: item.question,
sources: item.sources,
question_id: item.id,
}}
/>
<AiAssistantThreadItem
item={{
type: "answer",
content: item.answer,
sources: item.sources,
question_id: item.id,
}}
/>
</Fragment>
))
}, [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 = () => {
</div>
<div className="h-[calc(100%-95px)] lg:max-h-[calc(100%-140px)] lg:min-h-[calc(100%-140px)] overflow-auto">
<div ref={contentRef}>
{!thread.length && <AiAssistantSuggestions className="mx-docs_0.5" />}
{!conversation.length && (
<AiAssistantSuggestions className="mx-docs_0.5" />
)}
{getThreadItems()}
{(answer.length || loading) && (
{error?.length && (
<AiAssistantThreadItem
item={{
type: "answer",
content: answer,
order: 0,
type: "error",
content: error,
}}
/>
)}
@@ -123,7 +135,7 @@ export const AiAssistantSearchWindow = () => {
>
<div className="flex items-center gap-docs_0.75">
<div className="flex items-center gap-docs_0.5">
{thread.length === 0 && (
{conversation.length === 0 && (
<>
<span
className={clsx(
@@ -153,7 +165,7 @@ export const AiAssistantSearchWindow = () => {
</span>
</>
)}
{thread.length > 0 && (
{conversation.length > 0 && (
<span
className={clsx("text-medusa-fg-muted", "text-compact-x-small")}
>

View File

@@ -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<HTMLDivElement>
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 (
<div {...props}>
@@ -60,8 +40,7 @@ export const AiAssistantSuggestions = (props: AiAssistantSuggestionsProps) => {
{suggestion.items.map((item, itemIndex) => (
<SearchSuggestionItem
onClick={() => {
setQuestion(item)
handleSubmit(item)
submitQuery(item)
}}
key={itemIndex}
tabIndex={itemIndex}

View File

@@ -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<AiAssistantFeedbackType | null>(null)
const { sendFeedback } = useAiAssistant()
const [feedback, setFeedback] = useState<Reaction | null>(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)
}

View File

@@ -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 (
<div
className={clsx(
@@ -84,15 +93,23 @@ export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => {
)}
{item.type === "answer" && (
<div className="flex flex-col gap-docs_0.75">
{!item.question_id && item.content.length === 0 && <DotsLoading />}
{showLoading && <DotsLoading />}
<MarkdownContent
className="[&>*:last-child]:mb-0"
components={{
...MDXComponents,
code: (props: CodeMdxProps) => {
return <CodeMdx {...props} noReport noAskAi />
return (
<CodeMdx
{...props}
noReport
noAskAi
wrapperClassName="mt-docs_1"
/>
)
},
}}
disallowedElements={["h1", "h2", "h3", "h4", "h5", "h6"]}
>
{item.content}
</MarkdownContent>

View File

@@ -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 = () => {

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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<useKeyboardShortcutOptions>
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()

View File

@@ -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<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])
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<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 }
}

View File

@@ -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<React.SetStateAction<string>>
inputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>
contentRef: React.RefObject<HTMLDivElement | null>
loading: boolean
thread: AiAssistantThread[]
answer: string
}
const AiAssistantChatContext = createContext<AiAssistantChatContextType | null>(
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<AiAssistantThread[]>([])
const [answer, setAnswer] = useState("")
const [answerSources, setAnswerSources] = useState<
AiAssistantRelevantSources[]
>([])
const [identifiers, setIdentifiers] = useState<AiAssistantIdentifier | null>(
null
)
const [loading, setLoading] = useState(false)
const [preventAutoScroll, setPreventAutoScroll] = useState(false)
const { getAnswer } = useAiAssistant()
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
const contentRef = useRef<HTMLDivElement>(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<HTMLDivElement>, () => {
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 (
<AiAssistantChatContext.Provider
value={{
handleSubmit,
getThreadItems,
question,
setQuestion,
inputRef,
contentRef,
loading,
thread,
answer,
}}
>
{children}
</AiAssistantChatContext.Provider>
)
}
export const useAiAssistantChat = () => {
const context = useContext(AiAssistantChatContext)
if (!context) {
throw new Error(
"useAiAssistantChat must be used within a AiAssistantChatContext"
)
}
return context
}

View File

@@ -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<Response>
sendFeedback: (
questionId: string,
reaction: AiAssistantFeedbackType
) => Promise<Response>
version: "v1" | "v2"
chatOpened: boolean
setChatOpened: React.Dispatch<React.SetStateAction<boolean>>
chatType: AiAssistantChatType
inputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>
contentRef: React.RefObject<HTMLDivElement | null>
loading: boolean
}
export type AiAssistantThreadItem = {
type: "question" | "answer" | "error"
content: string
question_id?: string | null
sources?: Source[]
}
const AiAssistantContext = createContext<AiAssistantContextType | null>(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<React.SetStateAction<boolean>>
setOnCompleteAction: React.Dispatch<React.SetStateAction<() => 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<HTMLInputElement | HTMLTextAreaElement>(null)
const contentRef = useRef<HTMLDivElement>(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<HTMLDivElement>, () => {
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: <AiAssistantIcon />,
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 (
<AiAssistantContext.Provider
value={{
getAnswer,
sendFeedback,
version,
chatOpened,
setChatOpened,
chatType,
inputRef,
contentRef,
loading,
}}
>
<AiAssistantChatProvider>
{children}
{type === "search" && <AiAssistantSearchWindow />}
</AiAssistantChatProvider>
{children}
{type === "search" && <AiAssistantSearchWindow />}
</AiAssistantContext.Provider>
)
}
export const AiAssistantProvider = ({
integrationId,
...props
}: AiAssistantProviderProps) => {
const [preventAutoScroll, setPreventAutoScroll] = useState(false)
const [onCompleteAction, setOnCompleteAction] = useState<() => void>(
() => () => {}
)
return (
<KapaProvider
integrationId={integrationId}
callbacks={{
askAI: {
onAnswerGenerationCompleted: () => {
onCompleteAction?.()
},
onQuerySubmit: () => setPreventAutoScroll(false),
},
}}
>
<AiAssistantInnerProvider
{...props}
preventAutoScroll={preventAutoScroll}
setPreventAutoScroll={setPreventAutoScroll}
setOnCompleteAction={setOnCompleteAction}
/>
</KapaProvider>
)
}
export const useAiAssistant = () => {
const context = useContext(AiAssistantContext)

View File

@@ -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