docs: use Kapa React SDK (#12792)
This commit is contained in:
1 parent
77d0f00929
commit
ab21ac57ca
27 files changed
+462
-875
No files matched your search
@@ -15,7 +15,5 @@ NEXT_PUBLIC_DOCS_V1_URL=
|
|||||||
NEXT_PUBLIC_API_V1_URL=
|
NEXT_PUBLIC_API_V1_URL=
|
||||||
ALGOLIA_WRITE_API_KEY=
|
ALGOLIA_WRITE_API_KEY=
|
||||||
ANALYZE_BUNDLE=
|
ANALYZE_BUNDLE=
|
||||||
NEXT_PUBLIC_AI_ASSISTANT_URL=
|
NEXT_PUBLIC_INTEGRATION_ID=
|
||||||
NEXT_PUBLIC_AI_WEBSITE_ID=
|
|
||||||
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
|
||||||
NEXT_PUBLIC_GA_ID=
|
NEXT_PUBLIC_GA_ID=
|
||||||
@@ -24,12 +24,8 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<MainNavProvider>
|
<MainNavProvider>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<AiAssistantProvider
|
<AiAssistantProvider
|
||||||
apiUrl={process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"}
|
integrationId={
|
||||||
websiteId={process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"}
|
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
|
||||||
recaptchaSiteKey={
|
|
||||||
process.env
|
|
||||||
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
|
|
||||||
"temp"
|
|
||||||
}
|
}
|
||||||
chatType="popover"
|
chatType="popover"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ NEXT_PUBLIC_API_URL=
|
|||||||
NEXT_PUBLIC_DOCS_V1_URL=
|
NEXT_PUBLIC_DOCS_V1_URL=
|
||||||
ALGOLIA_WRITE_API_KEY=
|
ALGOLIA_WRITE_API_KEY=
|
||||||
ANALYZE_BUNDLE=
|
ANALYZE_BUNDLE=
|
||||||
NEXT_PUBLIC_AI_ASSISTANT_URL=
|
|
||||||
NEXT_PUBLIC_AI_WEBSITE_ID=
|
|
||||||
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
|
||||||
CLOUDINARY_CLOUD_NAME=
|
CLOUDINARY_CLOUD_NAME=
|
||||||
NEXT_PUBLIC_BASE_PATH=
|
NEXT_PUBLIC_BASE_PATH=
|
||||||
NEXT_PUBLIC_GA_ID=
|
NEXT_PUBLIC_GA_ID=
|
||||||
NEXT_PUBLIC_PROD_BASE_URL=
|
NEXT_PUBLIC_PROD_BASE_URL=
|
||||||
|
NEXT_PUBLIC_INTEGRATION_ID=
|
||||||
@@ -34,16 +34,8 @@ const Providers = ({ children, aiAssistantProps = {} }: ProvidersProps) => {
|
|||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<AiAssistantProvider
|
<AiAssistantProvider
|
||||||
{...aiAssistantProps}
|
{...aiAssistantProps}
|
||||||
apiUrl={
|
integrationId={
|
||||||
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"
|
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
|
||||||
}
|
|
||||||
websiteId={
|
|
||||||
process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"
|
|
||||||
}
|
|
||||||
recaptchaSiteKey={
|
|
||||||
process.env
|
|
||||||
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
|
|
||||||
"temp"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HooksLoader
|
<HooksLoader
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ NEXT_PUBLIC_API_URL=
|
|||||||
NEXT_PUBLIC_USER_GUIDE_URL=
|
NEXT_PUBLIC_USER_GUIDE_URL=
|
||||||
ALGOLIA_WRITE_API_KEY=
|
ALGOLIA_WRITE_API_KEY=
|
||||||
ANALYZE_BUNDLE=
|
ANALYZE_BUNDLE=
|
||||||
NEXT_PUBLIC_AI_ASSISTANT_URL=
|
|
||||||
NEXT_PUBLIC_AI_WEBSITE_ID=
|
|
||||||
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
|
||||||
CLOUDINARY_CLOUD_NAME=
|
CLOUDINARY_CLOUD_NAME=
|
||||||
NEXT_PUBLIC_GA_ID=
|
NEXT_PUBLIC_GA_ID=
|
||||||
|
NEXT_PUBLIC_INTEGRATION_ID=
|
||||||
@@ -37,17 +37,8 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<MainNavProvider>
|
<MainNavProvider>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<AiAssistantProvider
|
<AiAssistantProvider
|
||||||
apiUrl={
|
integrationId={
|
||||||
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL ||
|
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HooksLoader
|
<HooksLoader
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ NEXT_PUBLIC_AI_ASSISTANT_URL=
|
|||||||
NEXT_PUBLIC_AI_WEBSITE_ID=
|
NEXT_PUBLIC_AI_WEBSITE_ID=
|
||||||
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
||||||
CLOUDINARY_CLOUD_NAME=
|
CLOUDINARY_CLOUD_NAME=
|
||||||
NEXT_PUBLIC_GA_ID=
|
NEXT_PUBLIC_GA_ID=
|
||||||
|
NEXT_PUBLIC_INTEGRATION_ID=
|
||||||
@@ -33,16 +33,8 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<MainNavProvider>
|
<MainNavProvider>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<AiAssistantProvider
|
<AiAssistantProvider
|
||||||
apiUrl={
|
integrationId={
|
||||||
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"
|
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
|
||||||
}
|
|
||||||
websiteId={
|
|
||||||
process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"
|
|
||||||
}
|
|
||||||
recaptchaSiteKey={
|
|
||||||
process.env
|
|
||||||
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY ||
|
|
||||||
"temp"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HooksLoader
|
<HooksLoader
|
||||||
|
|||||||
@@ -6,7 +6,5 @@ NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME=
|
|||||||
NEXT_PUBLIC_ALGOLIA_API_KEY=
|
NEXT_PUBLIC_ALGOLIA_API_KEY=
|
||||||
NEXT_PUBLIC_ALGOLIA_APP_ID=
|
NEXT_PUBLIC_ALGOLIA_APP_ID=
|
||||||
NEXT_PUBLIC_SEGMENT_API_KEY=
|
NEXT_PUBLIC_SEGMENT_API_KEY=
|
||||||
NEXT_PUBLIC_AI_ASSISTANT_URL=
|
NEXT_PUBLIC_GA_ID=
|
||||||
NEXT_PUBLIC_AI_WEBSITE_ID=
|
NEXT_PUBLIC_INTEGRATION_ID=
|
||||||
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
|
||||||
NEXT_PUBLIC_GA_ID=
|
|
||||||
@@ -25,11 +25,8 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<MainNavProvider>
|
<MainNavProvider>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<AiAssistantProvider
|
<AiAssistantProvider
|
||||||
apiUrl={process.env.NEXT_PUBLIC_AI_ASSISTANT_URL || "temp"}
|
integrationId={
|
||||||
websiteId={process.env.NEXT_PUBLIC_AI_WEBSITE_ID || "temp"}
|
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
|
||||||
recaptchaSiteKey={
|
|
||||||
process.env
|
|
||||||
.NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY || "temp"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ NEXT_PUBLIC_API_URL=
|
|||||||
NEXT_PUBLIC_CLOUD_URL=
|
NEXT_PUBLIC_CLOUD_URL=
|
||||||
ALGOLIA_WRITE_API_KEY=
|
ALGOLIA_WRITE_API_KEY=
|
||||||
ANALYZE_BUNDLE=
|
ANALYZE_BUNDLE=
|
||||||
NEXT_PUBLIC_AI_ASSISTANT_URL=
|
|
||||||
NEXT_PUBLIC_AI_WEBSITE_ID=
|
|
||||||
NEXT_PUBLIC_AI_API_ASSISTANT_RECAPTCHA_SITE_KEY=
|
|
||||||
CLOUDINARY_CLOUD_NAME=
|
CLOUDINARY_CLOUD_NAME=
|
||||||
NEXT_PUBLIC_GA_ID=
|
NEXT_PUBLIC_GA_ID=
|
||||||
|
NEXT_PUBLIC_INTEGRATION_ID=
|
||||||
@@ -37,17 +37,8 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<MainNavProvider>
|
<MainNavProvider>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<AiAssistantProvider
|
<AiAssistantProvider
|
||||||
apiUrl={
|
integrationId={
|
||||||
process.env.NEXT_PUBLIC_AI_ASSISTANT_URL ||
|
process.env.NEXT_PUBLIC_INTEGRATION_ID || "temp"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HooksLoader
|
<HooksLoader
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
|
"@kapaai/react-sdk": "^0.1.4",
|
||||||
"@medusajs/icons": "2.8.4",
|
"@medusajs/icons": "2.8.4",
|
||||||
"@medusajs/ui": "4.0.14",
|
"@medusajs/ui": "4.0.14",
|
||||||
"@next/third-parties": "15.3.1",
|
"@next/third-parties": "15.3.1",
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import React, { useEffect, useRef } from "react"
|
import React, { useEffect, useRef } from "react"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { useAiAssistantChat } from "../../../../providers/AiAssistant/Chat"
|
|
||||||
import { ArrowUpCircleSolid } from "@medusajs/icons"
|
import { ArrowUpCircleSolid } from "@medusajs/icons"
|
||||||
import { useAiAssistant } from "../../../../providers"
|
import { useAiAssistant } from "../../../../providers"
|
||||||
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
import { useAiAssistantChatNavigation } from "../../../../hooks"
|
||||||
|
|
||||||
export const AiAssistantChatWindowInput = () => {
|
type AiAssistantChatWindowInputProps = {
|
||||||
const {
|
chatWindowRef: React.RefObject<HTMLDivElement | null>
|
||||||
inputRef,
|
}
|
||||||
question,
|
|
||||||
setQuestion,
|
export const AiAssistantChatWindowInput = ({
|
||||||
handleSubmit: submitQuestion,
|
chatWindowRef,
|
||||||
loading,
|
}: AiAssistantChatWindowInputProps) => {
|
||||||
getThreadItems,
|
const { chatOpened, inputRef, loading } = useAiAssistant()
|
||||||
} = useAiAssistantChat()
|
const { submitQuery, conversation } = useChat()
|
||||||
const { chatOpened } = useAiAssistant()
|
const [question, setQuestion] = React.useState("")
|
||||||
const formRef = useRef<HTMLFormElement | null>(null)
|
const formRef = useRef<HTMLFormElement | null>(null)
|
||||||
|
|
||||||
const onSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
|
||||||
e?.preventDefault()
|
e?.preventDefault()
|
||||||
submitQuestion()
|
submitQuery(question)
|
||||||
|
setQuestion("")
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyboardDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyboardDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === "ArrowUp" && !question) {
|
if (e.key === "ArrowUp" && !question) {
|
||||||
const lastQuestion = getThreadItems()
|
const lastQuestion = conversation.getLatest()?.question
|
||||||
.reverse()
|
|
||||||
.find((item) => item.type === "question")
|
|
||||||
if (lastQuestion) {
|
if (lastQuestion) {
|
||||||
setQuestion(lastQuestion.content)
|
setQuestion(lastQuestion)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,16 @@ export const AiAssistantChatWindowInput = () => {
|
|||||||
}
|
}
|
||||||
}, [chatOpened, inputRef.current])
|
}, [chatOpened, inputRef.current])
|
||||||
|
|
||||||
|
useAiAssistantChatNavigation({
|
||||||
|
getChatWindowElm: () => chatWindowRef.current as HTMLElement | null,
|
||||||
|
getInputElm: () => inputRef.current as HTMLTextAreaElement | null,
|
||||||
|
focusInput: () =>
|
||||||
|
inputRef.current?.focus({
|
||||||
|
preventScroll: true,
|
||||||
|
}),
|
||||||
|
question,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -103,7 +113,7 @@ export const AiAssistantChatWindowInput = () => {
|
|||||||
"appearance-none text-base md:text-small placeholder:text-medusa-fg-muted",
|
"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",
|
"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]",
|
"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}
|
value={question}
|
||||||
onChange={(e) => setQuestion(e.target.value)}
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
|
|||||||
@@ -1,31 +1,37 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import clsx from "clsx"
|
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 { useAiAssistant, useIsBrowser } from "../../../providers"
|
||||||
import { AiAssistantChatWindowHeader } from "./Header"
|
import { AiAssistantChatWindowHeader } from "./Header"
|
||||||
import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat"
|
|
||||||
import { AiAssistantSuggestions } from "../Suggestions"
|
import { AiAssistantSuggestions } from "../Suggestions"
|
||||||
import { AiAssistantThreadItem } from "../ThreadItem"
|
import { AiAssistantThreadItem } from "../ThreadItem"
|
||||||
import { AiAssistantChatWindowInput } from "./Input"
|
import { AiAssistantChatWindowInput } from "./Input"
|
||||||
import { useAiAssistantChatNavigation, useKeyboardShortcut } from "../../.."
|
import { useKeyboardShortcut } from "../../.."
|
||||||
import { AiAssistantChatWindowFooter } from "./Footer"
|
import { AiAssistantChatWindowFooter } from "./Footer"
|
||||||
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
|
||||||
const DEFAULT_HEIGHT = "calc(100% - 8px)"
|
const DEFAULT_HEIGHT = "calc(100% - 8px)"
|
||||||
|
|
||||||
export const AiAssistantChatWindow = () => {
|
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 [height, setHeight] = useState(DEFAULT_HEIGHT)
|
||||||
const [showFade, setShowFade] = useState(false)
|
const [showFade, setShowFade] = useState(false)
|
||||||
const { isBrowser } = useIsBrowser()
|
const { isBrowser } = useIsBrowser()
|
||||||
const {
|
|
||||||
inputRef,
|
|
||||||
thread,
|
|
||||||
getThreadItems: getChatThreadItems,
|
|
||||||
answer,
|
|
||||||
loading,
|
|
||||||
contentRef,
|
|
||||||
} = useAiAssistantChat()
|
|
||||||
const chatWindowRef = useRef<HTMLDivElement | null>(null)
|
const chatWindowRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,21 +45,27 @@ export const AiAssistantChatWindow = () => {
|
|||||||
}, [chatOpened])
|
}, [chatOpened])
|
||||||
|
|
||||||
const getThreadItems = useCallback(() => {
|
const getThreadItems = useCallback(() => {
|
||||||
const sortedThread = getChatThreadItems()
|
return conversation.map((item, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
return sortedThread.map((item, index) => (
|
<AiAssistantThreadItem
|
||||||
<AiAssistantThreadItem item={item} key={index} />
|
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])
|
||||||
|
|
||||||
useAiAssistantChatNavigation({
|
|
||||||
getChatWindowElm: () => chatWindowRef.current as HTMLElement | null,
|
|
||||||
getInputElm: () => inputRef.current as HTMLTextAreaElement | null,
|
|
||||||
focusInput: () =>
|
|
||||||
inputRef.current?.focus({
|
|
||||||
preventScroll: true,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
useKeyboardShortcut({
|
useKeyboardShortcut({
|
||||||
metakey: false,
|
metakey: false,
|
||||||
@@ -75,7 +87,8 @@ export const AiAssistantChatWindow = () => {
|
|||||||
}
|
}
|
||||||
setShowFade(
|
setShowFade(
|
||||||
!loading &&
|
!loading &&
|
||||||
parentElm.offsetHeight + parentElm.scrollTop < parentElm.scrollHeight
|
parentElm.offsetHeight + parentElm.scrollTop <
|
||||||
|
parentElm.scrollHeight - 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +181,13 @@ export const AiAssistantChatWindow = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div ref={contentRef}>
|
<div ref={contentRef}>
|
||||||
{!thread.length && <AiAssistantSuggestions />}
|
{!conversation.length && <AiAssistantSuggestions />}
|
||||||
{getThreadItems()}
|
{getThreadItems()}
|
||||||
{(answer.length || loading) && (
|
{error?.length && (
|
||||||
<AiAssistantThreadItem
|
<AiAssistantThreadItem
|
||||||
item={{
|
item={{
|
||||||
type: "answer",
|
type: "error",
|
||||||
content: answer,
|
content: error,
|
||||||
order: 0,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -189,7 +201,7 @@ export const AiAssistantChatWindow = () => {
|
|||||||
)}
|
)}
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<AiAssistantChatWindowInput />
|
<AiAssistantChatWindowInput chatWindowRef={chatWindowRef} />
|
||||||
<AiAssistantChatWindowFooter />
|
<AiAssistantChatWindowFooter />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,41 +1,52 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useCallback } from "react"
|
import React, { Fragment, useCallback, useState } from "react"
|
||||||
import { Badge, Button, InputText, Kbd, Tooltip, Link } from "@/components"
|
import { Badge, Button, InputText, Kbd, Tooltip, Link } from "@/components"
|
||||||
import { useSearch } from "@/providers"
|
import { useAiAssistant, useSearch } from "@/providers"
|
||||||
import { ArrowUturnLeft } from "@medusajs/icons"
|
import { ArrowUturnLeft } from "@medusajs/icons"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { AiAssistantThreadItem } from "../ThreadItem"
|
import { AiAssistantThreadItem } from "../ThreadItem"
|
||||||
import { AiAssistantSuggestions } from "../Suggestions"
|
import { AiAssistantSuggestions } from "../Suggestions"
|
||||||
import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat"
|
|
||||||
import { useSearchNavigation } from "../../.."
|
import { useSearchNavigation } from "../../.."
|
||||||
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
|
||||||
export const AiAssistantSearchWindow = () => {
|
export const AiAssistantSearchWindow = () => {
|
||||||
const {
|
const { conversation, submitQuery, error } = useChat()
|
||||||
handleSubmit,
|
const { inputRef, contentRef, loading } = useAiAssistant()
|
||||||
getThreadItems: getChatThreadItems,
|
const [question, setQuestion] = useState("")
|
||||||
question,
|
|
||||||
setQuestion,
|
|
||||||
inputRef,
|
|
||||||
contentRef,
|
|
||||||
loading,
|
|
||||||
thread,
|
|
||||||
answer,
|
|
||||||
} = useAiAssistantChat()
|
|
||||||
const { setCommand } = useSearch()
|
const { setCommand } = useSearch()
|
||||||
|
|
||||||
const getThreadItems = useCallback(() => {
|
const getThreadItems = useCallback(() => {
|
||||||
const sortedThread = getChatThreadItems()
|
return conversation.map((item, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
return sortedThread.map((item, index) => (
|
<AiAssistantThreadItem
|
||||||
<AiAssistantThreadItem item={item} key={index} />
|
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({
|
useSearchNavigation({
|
||||||
getInputElm: () => inputRef.current as HTMLInputElement | null,
|
getInputElm: () => inputRef.current as HTMLInputElement | null,
|
||||||
focusInput: () => inputRef.current?.focus(),
|
focusInput: () => inputRef.current?.focus(),
|
||||||
handleSubmit,
|
handleSubmit: () => {
|
||||||
|
if (question.length > 0) {
|
||||||
|
submitQuery(question)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,14 +112,15 @@ export const AiAssistantSearchWindow = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-[calc(100%-95px)] lg:max-h-[calc(100%-140px)] lg:min-h-[calc(100%-140px)] overflow-auto">
|
<div className="h-[calc(100%-95px)] lg:max-h-[calc(100%-140px)] lg:min-h-[calc(100%-140px)] overflow-auto">
|
||||||
<div ref={contentRef}>
|
<div ref={contentRef}>
|
||||||
{!thread.length && <AiAssistantSuggestions className="mx-docs_0.5" />}
|
{!conversation.length && (
|
||||||
|
<AiAssistantSuggestions className="mx-docs_0.5" />
|
||||||
|
)}
|
||||||
{getThreadItems()}
|
{getThreadItems()}
|
||||||
{(answer.length || loading) && (
|
{error?.length && (
|
||||||
<AiAssistantThreadItem
|
<AiAssistantThreadItem
|
||||||
item={{
|
item={{
|
||||||
type: "answer",
|
type: "error",
|
||||||
content: answer,
|
content: error,
|
||||||
order: 0,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -123,7 +135,7 @@ export const AiAssistantSearchWindow = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-docs_0.75">
|
<div className="flex items-center gap-docs_0.75">
|
||||||
<div className="flex items-center gap-docs_0.5">
|
<div className="flex items-center gap-docs_0.5">
|
||||||
{thread.length === 0 && (
|
{conversation.length === 0 && (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -153,7 +165,7 @@ export const AiAssistantSearchWindow = () => {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{thread.length > 0 && (
|
{conversation.length > 0 && (
|
||||||
<span
|
<span
|
||||||
className={clsx("text-medusa-fg-muted", "text-compact-x-small")}
|
className={clsx("text-medusa-fg-muted", "text-compact-x-small")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,56 +1,36 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useMemo } from "react"
|
import React from "react"
|
||||||
import { SearchSuggestionType } from "../../Search/Suggestions"
|
import { SearchSuggestionType } from "../../Search/Suggestions"
|
||||||
import { useAiAssistant } from "../../../providers"
|
|
||||||
import { SearchHitGroupName } from "../../Search/Hits/GroupName"
|
import { SearchHitGroupName } from "../../Search/Hits/GroupName"
|
||||||
import { SearchSuggestionItem } from "../../Search/Suggestions/Item"
|
import { SearchSuggestionItem } from "../../Search/Suggestions/Item"
|
||||||
import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat"
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
|
||||||
type AiAssistantSuggestionsProps = React.AllHTMLAttributes<HTMLDivElement>
|
type AiAssistantSuggestionsProps = React.AllHTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
export const AiAssistantSuggestions = (props: AiAssistantSuggestionsProps) => {
|
export const AiAssistantSuggestions = (props: AiAssistantSuggestionsProps) => {
|
||||||
const { version } = useAiAssistant()
|
const { submitQuery } = useChat()
|
||||||
const { setQuestion, handleSubmit } = useAiAssistantChat()
|
const suggestions: SearchSuggestionType[] = [
|
||||||
const suggestions: SearchSuggestionType[] = useMemo(() => {
|
{
|
||||||
return version === "v2"
|
title: "FAQ",
|
||||||
? [
|
items: [
|
||||||
{
|
"What is Medusa?",
|
||||||
title: "FAQ",
|
"How can I create a module?",
|
||||||
items: [
|
"How can I create a data model?",
|
||||||
"What is Medusa?",
|
"How do I create a workflow?",
|
||||||
"How can I create a module?",
|
"How can I extend a data model in the Product 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?",
|
||||||
title: "Recipes",
|
"How do I build digital products with Medusa?",
|
||||||
items: [
|
"How do I build subscription-based purchases with Medusa?",
|
||||||
"How do I build a marketplace with Medusa?",
|
"What other recipes are available in the Medusa documentation?",
|
||||||
"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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
@@ -60,8 +40,7 @@ export const AiAssistantSuggestions = (props: AiAssistantSuggestionsProps) => {
|
|||||||
{suggestion.items.map((item, itemIndex) => (
|
{suggestion.items.map((item, itemIndex) => (
|
||||||
<SearchSuggestionItem
|
<SearchSuggestionItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuestion(item)
|
submitQuery(item)
|
||||||
handleSubmit(item)
|
|
||||||
}}
|
}}
|
||||||
key={itemIndex}
|
key={itemIndex}
|
||||||
tabIndex={itemIndex}
|
tabIndex={itemIndex}
|
||||||
|
|||||||
@@ -2,33 +2,29 @@ import React, { useState } from "react"
|
|||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { Badge, Button, Link, type ButtonProps } from "@/components"
|
import { Badge, Button, Link, type ButtonProps } from "@/components"
|
||||||
import { ThumbDown, ThumbUp } from "@medusajs/icons"
|
import { ThumbDown, ThumbUp } from "@medusajs/icons"
|
||||||
import { AiAssistantFeedbackType, useAiAssistant } from "@/providers"
|
import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../../providers"
|
||||||
import { AiAssistantThread } from "../../../../providers/AiAssistant/Chat"
|
import { Reaction, useChat } from "@kapaai/react-sdk"
|
||||||
|
|
||||||
export type AiAssistantThreadItemActionsProps = {
|
export type AiAssistantThreadItemActionsProps = {
|
||||||
item: AiAssistantThread
|
item: AiAssistantThreadItemType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AiAssistantThreadItemActions = ({
|
export const AiAssistantThreadItemActions = ({
|
||||||
item,
|
item,
|
||||||
}: AiAssistantThreadItemActionsProps) => {
|
}: AiAssistantThreadItemActionsProps) => {
|
||||||
const [feedback, setFeedback] = useState<AiAssistantFeedbackType | null>(null)
|
const [feedback, setFeedback] = useState<Reaction | null>(null)
|
||||||
const { sendFeedback } = useAiAssistant()
|
const { addFeedback } = useChat()
|
||||||
|
|
||||||
const handleFeedback = async (
|
const handleFeedback = async (
|
||||||
reaction: AiAssistantFeedbackType,
|
reaction: Reaction,
|
||||||
question_id?: string
|
question_id?: string | null
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!question_id || feedback) {
|
if (!question_id || feedback) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setFeedback(reaction)
|
setFeedback(reaction)
|
||||||
const response = await sendFeedback(question_id, reaction)
|
addFeedback(question_id, reaction)
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
console.error("Error sending feedback:", response.status)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending feedback:", error)
|
console.error("Error sending feedback:", error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import React from "react"
|
import React, { useMemo } from "react"
|
||||||
import {
|
import {
|
||||||
AiAssistantIcon,
|
AiAssistantIcon,
|
||||||
CodeMdx,
|
CodeMdx,
|
||||||
@@ -9,13 +9,22 @@ import {
|
|||||||
MDXComponents,
|
MDXComponents,
|
||||||
} from "@/components"
|
} from "@/components"
|
||||||
import { AiAssistantThreadItemActions } from "./Actions"
|
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 = {
|
export type AiAssistantThreadItemProps = {
|
||||||
item: AiAssistantThread
|
item: AiAssistantThreadItemType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -84,15 +93,23 @@ export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => {
|
|||||||
)}
|
)}
|
||||||
{item.type === "answer" && (
|
{item.type === "answer" && (
|
||||||
<div className="flex flex-col gap-docs_0.75">
|
<div className="flex flex-col gap-docs_0.75">
|
||||||
{!item.question_id && item.content.length === 0 && <DotsLoading />}
|
{showLoading && <DotsLoading />}
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
className="[&>*:last-child]:mb-0"
|
className="[&>*:last-child]:mb-0"
|
||||||
components={{
|
components={{
|
||||||
...MDXComponents,
|
...MDXComponents,
|
||||||
code: (props: CodeMdxProps) => {
|
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}
|
{item.content}
|
||||||
</MarkdownContent>
|
</MarkdownContent>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useAiAssistant, useSearch, useSiteConfig } from "../../../providers"
|
|||||||
import { useKeyboardShortcut } from "../../../hooks"
|
import { useKeyboardShortcut } from "../../../hooks"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
|
||||||
// const AI_ASSISTANT_ICON = "/images/ai-assistent-luminosity.png"
|
|
||||||
const AI_ASSISTANT_ICON_ACTIVE = "/images/ai-assistent.png"
|
const AI_ASSISTANT_ICON_ACTIVE = "/images/ai-assistent.png"
|
||||||
|
|
||||||
export const AiAssistantTriggerButton = () => {
|
export const AiAssistantTriggerButton = () => {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useAiAssistant, useSiteConfig } from "../../../../providers"
|
import { useAiAssistant, useSiteConfig } from "../../../../providers"
|
||||||
import { useAiAssistantChat } from "../../../../providers/AiAssistant/Chat"
|
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { Tooltip } from "../../../Tooltip"
|
import { Tooltip } from "../../../Tooltip"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
|
||||||
export type CodeBlockCopyActionProps = {
|
export type CodeBlockCopyActionProps = {
|
||||||
source: string
|
source: string
|
||||||
@@ -16,15 +16,15 @@ export const CodeBlockAskAiAction = ({
|
|||||||
source,
|
source,
|
||||||
inHeader,
|
inHeader,
|
||||||
}: CodeBlockCopyActionProps) => {
|
}: CodeBlockCopyActionProps) => {
|
||||||
const { setChatOpened } = useAiAssistant()
|
const { setChatOpened, loading } = useAiAssistant()
|
||||||
const { setQuestion, loading } = useAiAssistantChat()
|
const { submitQuery } = useChat()
|
||||||
const { config } = useSiteConfig()
|
const { config } = useSiteConfig()
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return
|
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)
|
setChatOpened(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import React from "react"
|
import React, { useMemo } from "react"
|
||||||
import { MarkdownIcon } from "../../Icons/Markdown"
|
import { MarkdownIcon } from "../../Icons/Markdown"
|
||||||
import { useAiAssistant, useSiteConfig } from "../../../providers"
|
import { useAiAssistant, useSiteConfig } from "../../../providers"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { BroomSparkle } from "@medusajs/icons"
|
import { BroomSparkle } from "@medusajs/icons"
|
||||||
import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat"
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
|
||||||
export const ContentMenuActions = () => {
|
export const ContentMenuActions = () => {
|
||||||
const {
|
const {
|
||||||
@@ -14,14 +14,18 @@ export const ContentMenuActions = () => {
|
|||||||
} = useSiteConfig()
|
} = useSiteConfig()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { setChatOpened } = useAiAssistant()
|
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 pageUrl = `${baseUrl}${basePath}${pathname}`
|
||||||
|
|
||||||
const handleAiAssistantClick = () => {
|
const handleAiAssistantClick = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setQuestion(`Explain the page ${pageUrl}`)
|
submitQuery(`Explain the page ${pageUrl}`)
|
||||||
setChatOpened(true)
|
setChatOpened(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import {
|
|||||||
useKeyboardShortcut,
|
useKeyboardShortcut,
|
||||||
type useKeyboardShortcutOptions,
|
type useKeyboardShortcutOptions,
|
||||||
} from "../use-keyboard-shortcut"
|
} from "../use-keyboard-shortcut"
|
||||||
import { useAiAssistantChat } from "../../providers/AiAssistant/Chat"
|
|
||||||
|
|
||||||
export type UseAiAssistantChatNavigationProps = {
|
export type UseAiAssistantChatNavigationProps = {
|
||||||
getChatWindowElm: () => HTMLElement | null
|
getChatWindowElm: () => HTMLElement | null
|
||||||
getInputElm: () => HTMLTextAreaElement | null
|
getInputElm: () => HTMLTextAreaElement | null
|
||||||
focusInput: () => void
|
focusInput: () => void
|
||||||
keyboardProps?: Partial<useKeyboardShortcutOptions>
|
keyboardProps?: Partial<useKeyboardShortcutOptions>
|
||||||
|
question: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAiAssistantChatNavigation = ({
|
export const useAiAssistantChatNavigation = ({
|
||||||
@@ -21,10 +21,10 @@ export const useAiAssistantChatNavigation = ({
|
|||||||
focusInput,
|
focusInput,
|
||||||
keyboardProps,
|
keyboardProps,
|
||||||
getChatWindowElm,
|
getChatWindowElm,
|
||||||
|
question,
|
||||||
}: UseAiAssistantChatNavigationProps) => {
|
}: UseAiAssistantChatNavigationProps) => {
|
||||||
const shortcutKeys = useMemo(() => ["ArrowUp", "ArrowDown", "Enter"], [])
|
const shortcutKeys = useMemo(() => ["ArrowUp", "ArrowDown", "Enter"], [])
|
||||||
const { chatOpened } = useAiAssistant()
|
const { chatOpened } = useAiAssistant()
|
||||||
const { question } = useAiAssistantChat()
|
|
||||||
|
|
||||||
const handleKeyAction = (e: KeyboardEvent) => {
|
const handleKeyAction = (e: KeyboardEvent) => {
|
||||||
const chatElm = getChatWindowElm()
|
const chatElm = getChatWindowElm()
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,147 +1,186 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react"
|
import { KapaProvider, useChat } from "@kapaai/react-sdk"
|
||||||
import { useAnalytics, useSearch } from "@/providers"
|
import React, {
|
||||||
import { AiAssistantIcon, AiAssistantSearchWindow } from "@/components"
|
createContext,
|
||||||
import { RecaptchaAction, useRecaptcha } from "../../hooks/use-recaptcha"
|
useCallback,
|
||||||
import { AiAssistantChatProvider } from "./Chat"
|
useContext,
|
||||||
|
useEffect,
|
||||||
export type AiAssistantFeedbackType = "upvote" | "downvote"
|
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 AiAssistantChatType = "default" | "popover"
|
||||||
|
|
||||||
export type AiAssistantContextType = {
|
export type AiAssistantContextType = {
|
||||||
getAnswer: (question: string, thread_id?: string) => Promise<Response>
|
|
||||||
sendFeedback: (
|
|
||||||
questionId: string,
|
|
||||||
reaction: AiAssistantFeedbackType
|
|
||||||
) => Promise<Response>
|
|
||||||
version: "v1" | "v2"
|
|
||||||
chatOpened: boolean
|
chatOpened: boolean
|
||||||
setChatOpened: React.Dispatch<React.SetStateAction<boolean>>
|
setChatOpened: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
chatType: AiAssistantChatType
|
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)
|
const AiAssistantContext = createContext<AiAssistantContextType | null>(null)
|
||||||
|
|
||||||
export type AiAssistantProviderProps = {
|
export type AiAssistantProviderProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
apiUrl: string
|
integrationId: string
|
||||||
recaptchaSiteKey: string
|
|
||||||
websiteId: string
|
|
||||||
version?: "v1" | "v2"
|
|
||||||
type?: "search" | "chat"
|
|
||||||
chatType?: AiAssistantChatType
|
chatType?: AiAssistantChatType
|
||||||
|
type?: "search" | "chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AiAssistantProvider = ({
|
type AiAssistantInnerProviderProps = Omit<
|
||||||
apiUrl,
|
AiAssistantProviderProps,
|
||||||
recaptchaSiteKey,
|
"integrationId"
|
||||||
websiteId,
|
> & {
|
||||||
version = "v2",
|
preventAutoScroll: boolean
|
||||||
|
setPreventAutoScroll: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setOnCompleteAction: React.Dispatch<React.SetStateAction<() => void>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AiAssistantInnerProvider = ({
|
||||||
children,
|
children,
|
||||||
type = "chat",
|
|
||||||
chatType = "default",
|
chatType = "default",
|
||||||
}: AiAssistantProviderProps) => {
|
preventAutoScroll,
|
||||||
|
setPreventAutoScroll,
|
||||||
|
setOnCompleteAction,
|
||||||
|
type,
|
||||||
|
}: AiAssistantInnerProviderProps) => {
|
||||||
const [chatOpened, setChatOpened] = useState(false)
|
const [chatOpened, setChatOpened] = useState(false)
|
||||||
const { setCommands, setIsOpen, setCommand } = useSearch()
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
||||||
const { analytics } = useAnalytics()
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
const { execute: getReCaptchaToken } = useRecaptcha({
|
const { isGeneratingAnswer, isPreparingAnswer } = useChat()
|
||||||
siteKey: recaptchaSiteKey,
|
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 (
|
const handleUserScroll = useCallback(() => {
|
||||||
apiPath: string,
|
if (!loading || preventAutoScroll) {
|
||||||
action: RecaptchaAction,
|
return
|
||||||
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 getAnswer = async (question: string, threadId?: string) => {
|
setPreventAutoScroll(true)
|
||||||
const questionParam = encodeURI(question)
|
}, [loading, preventAutoScroll])
|
||||||
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() || "",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCommands((prevCommands) => {
|
if (!contentRef.current?.parentElement) {
|
||||||
const newCommands = [...prevCommands]
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!newCommands.find((c) => c.name === "ai-assistant")) {
|
contentRef.current.parentElement.addEventListener(
|
||||||
newCommands.push({
|
"wheel",
|
||||||
name: "ai-assistant",
|
handleUserScroll,
|
||||||
icon: <AiAssistantIcon />,
|
{
|
||||||
title: "AI Assistant",
|
passive: true,
|
||||||
badge: {
|
|
||||||
variant: "blue",
|
|
||||||
badgeType: "shaded",
|
|
||||||
children: "Beta",
|
|
||||||
},
|
|
||||||
action: () => {
|
|
||||||
setIsOpen(false)
|
|
||||||
setChatOpened(true)
|
|
||||||
setCommand(null)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
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 (
|
return (
|
||||||
<AiAssistantContext.Provider
|
<AiAssistantContext.Provider
|
||||||
value={{
|
value={{
|
||||||
getAnswer,
|
|
||||||
sendFeedback,
|
|
||||||
version,
|
|
||||||
chatOpened,
|
chatOpened,
|
||||||
setChatOpened,
|
setChatOpened,
|
||||||
chatType,
|
chatType,
|
||||||
|
inputRef,
|
||||||
|
contentRef,
|
||||||
|
loading,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AiAssistantChatProvider>
|
{children}
|
||||||
{children}
|
{type === "search" && <AiAssistantSearchWindow />}
|
||||||
{type === "search" && <AiAssistantSearchWindow />}
|
|
||||||
</AiAssistantChatProvider>
|
|
||||||
</AiAssistantContext.Provider>
|
</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 = () => {
|
export const useAiAssistant = () => {
|
||||||
const context = useContext(AiAssistantContext)
|
const context = useContext(AiAssistantContext)
|
||||||
|
|
||||||
|
|||||||
+118
-2
@@ -518,6 +518,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@babel/runtime@npm:^7.22.5":
|
||||||
version: 7.26.10
|
version: 7.26.10
|
||||||
resolution: "@babel/runtime@npm:7.26.10"
|
resolution: "@babel/runtime@npm:7.26.10"
|
||||||
@@ -1079,6 +1086,35 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@floating-ui/core@npm:^1.0.0":
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
resolution: "@floating-ui/core@npm:1.6.0"
|
resolution: "@floating-ui/core@npm:1.6.0"
|
||||||
@@ -1190,6 +1226,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@humanfs/core@npm:^0.19.0":
|
||||||
version: 0.19.0
|
version: 0.19.0
|
||||||
resolution: "@humanfs/core@npm:0.19.0"
|
resolution: "@humanfs/core@npm:0.19.0"
|
||||||
@@ -1712,6 +1768,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@lukeed/csprng@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@lukeed/csprng@npm:1.1.0"
|
resolution: "@lukeed/csprng@npm:1.1.0"
|
||||||
@@ -5458,6 +5530,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@tanstack/react-table@npm:8.20.5":
|
||||||
version: 8.20.5
|
version: 8.20.5
|
||||||
resolution: "@tanstack/react-table@npm:8.20.5"
|
resolution: "@tanstack/react-table@npm:8.20.5"
|
||||||
@@ -8022,6 +8112,7 @@ __metadata:
|
|||||||
resolution: "docs-ui@workspace:packages/docs-ui"
|
resolution: "docs-ui@workspace:packages/docs-ui"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/is-prop-valid": ^1.3.1
|
"@emotion/is-prop-valid": ^1.3.1
|
||||||
|
"@kapaai/react-sdk": ^0.1.4
|
||||||
"@medusajs/icons": 2.8.4
|
"@medusajs/icons": 2.8.4
|
||||||
"@medusajs/ui": 4.0.14
|
"@medusajs/ui": 4.0.14
|
||||||
"@next/third-parties": 15.3.1
|
"@next/third-parties": 15.3.1
|
||||||
@@ -9487,7 +9578,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 3.1.3
|
||||||
resolution: "fast-deep-equal@npm:3.1.3"
|
resolution: "fast-deep-equal@npm:3.1.3"
|
||||||
checksum: 40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0
|
checksum: 40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0
|
||||||
@@ -11291,6 +11382,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "js-tokens@npm:4.0.0"
|
resolution: "js-tokens@npm:4.0.0"
|
||||||
@@ -16319,6 +16417,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"to-fast-properties@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "to-fast-properties@npm:2.0.0"
|
resolution: "to-fast-properties@npm:2.0.0"
|
||||||
@@ -16541,7 +16657,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tslib@npm:^2.8.0":
|
"tslib@npm:^2.7.0, tslib@npm:^2.8.0":
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
resolution: "tslib@npm:2.8.1"
|
resolution: "tslib@npm:2.8.1"
|
||||||
checksum: 9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
checksum: 9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
||||||
|
|||||||
Reference in new issue
Block a user