@@ -7,24 +7,21 @@ import {
77} from "@heroicons/react/20/solid" ;
88import { type FeedbackComment , KapaProvider , type QA , useChat } from "@kapaai/react-sdk" ;
99import { useSearchParams } from "@remix-run/react" ;
10+ import DOMPurify from "dompurify" ;
1011import { motion } from "framer-motion" ;
1112import { marked } from "marked" ;
12- import {
13- createContext ,
14- type ReactNode ,
15- useCallback ,
16- useContext ,
17- useEffect ,
18- useRef ,
19- useState ,
20- } from "react" ;
13+ import { useCallback , useEffect , useRef , useState } from "react" ;
14+ import { useTypedRouteLoaderData } from "remix-typedjson" ;
2115import { AISparkleIcon } from "~/assets/icons/AISparkleIcon" ;
2216import { SparkleListIcon } from "~/assets/icons/SparkleListIcon" ;
17+ import { useFeatures } from "~/hooks/useFeatures" ;
18+ import { type loader } from "~/root" ;
2319import { Button } from "./primitives/Buttons" ;
2420import { Callout } from "./primitives/Callout" ;
2521import { Dialog , DialogContent , DialogHeader , DialogTitle } from "./primitives/Dialog" ;
2622import { Header2 } from "./primitives/Headers" ;
2723import { Paragraph } from "./primitives/Paragraph" ;
24+ import { ShortcutKey } from "./primitives/ShortcutKey" ;
2825import { Spinner } from "./primitives/Spinner" ;
2926import {
3027 SimpleTooltip ,
@@ -33,31 +30,45 @@ import {
3330 TooltipProvider ,
3431 TooltipTrigger ,
3532} from "./primitives/Tooltip" ;
36- import DOMPurify from "dompurify " ;
33+ import { ClientOnly } from "remix-utils/client-only " ;
3734
38- type AskAIContextType = {
39- isOpen : boolean ;
40- openAskAI : ( question ?: string ) => void ;
41- closeAskAI : ( ) => void ;
42- websiteId : string | null ;
43- } ;
35+ function useKapaWebsiteId ( ) {
36+ const routeMatch = useTypedRouteLoaderData < typeof loader > ( "root" ) ;
37+ return routeMatch ?. kapa . websiteId ;
38+ }
4439
45- const AskAIContext = createContext < AskAIContextType | null > ( null ) ;
40+ export function AskAI ( ) {
41+ const { isManagedCloud } = useFeatures ( ) ;
42+ const websiteId = useKapaWebsiteId ( ) ;
4643
47- export function useAskAI ( ) {
48- const context = useContext ( AskAIContext ) ;
49- if ( ! context ) {
50- throw new Error ( "useAskAI must be used within an AskAIProvider" ) ;
44+ if ( ! isManagedCloud || ! websiteId ) {
45+ return null ;
5146 }
52- return context ;
47+
48+ return (
49+ < ClientOnly
50+ fallback = {
51+ < Button
52+ variant = "small-menu-item"
53+ data-action = "ask-ai"
54+ hideShortcutKey
55+ data-modal-override-open-class-ask-ai = "true"
56+ disabled
57+ >
58+ < AISparkleIcon className = "size-5" />
59+ </ Button >
60+ }
61+ >
62+ { ( ) => < AskAIProvider websiteId = { websiteId } /> }
63+ </ ClientOnly >
64+ ) ;
5365}
5466
5567type AskAIProviderProps = {
56- children : ReactNode ;
57- websiteId : string | null ;
68+ websiteId : string ;
5869} ;
5970
60- export function AskAIProvider ( { children , websiteId } : AskAIProviderProps ) {
71+ function AskAIProvider ( { websiteId } : AskAIProviderProps ) {
6172 const [ isOpen , setIsOpen ] = useState ( false ) ;
6273 const [ initialQuery , setInitialQuery ] = useState < string | undefined > ( ) ;
6374 const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
@@ -80,59 +91,67 @@ export function AskAIProvider({ children, websiteId }: AskAIProviderProps) {
8091 useEffect ( ( ) => {
8192 const aiHelp = searchParams . get ( "aiHelp" ) ;
8293 if ( aiHelp ) {
83- const decodedAiHelp = decodeURIComponent ( aiHelp ) ;
84-
8594 // Delay to avoid hCaptcha bot detection
86- const timeoutId = window . setTimeout ( ( ) => openAskAI ( decodedAiHelp ) , 1000 ) ;
95+ window . setTimeout ( ( ) => openAskAI ( aiHelp ) , 1000 ) ;
8796
8897 // Clone instead of mutating in place
8998 const next = new URLSearchParams ( searchParams ) ;
9099 next . delete ( "aiHelp" ) ;
91100 setSearchParams ( next ) ;
92-
93- return ( ) => clearTimeout ( timeoutId ) ;
94101 }
95- } , [ searchParams . toString ( ) , openAskAI ] ) ;
96-
97- const contextValue : AskAIContextType = {
98- isOpen,
99- openAskAI,
100- closeAskAI,
101- websiteId,
102- } ;
103-
104- if ( ! websiteId ) {
105- return < AskAIContext . Provider value = { contextValue } > { children } </ AskAIContext . Provider > ;
106- }
102+ } , [ searchParams , openAskAI ] ) ;
107103
108104 return (
109- < AskAIContext . Provider value = { contextValue } >
110- < KapaProvider
111- integrationId = { websiteId }
112- callbacks = { {
113- askAI : {
114- onQuerySubmit : ( ) => openAskAI ( ) ,
115- onAnswerGenerationCompleted : ( ) => openAskAI ( ) ,
116- } ,
117- } }
118- botProtectionMechanism = "hcaptcha"
119- >
120- { children }
121- < AskAIDialog initialQuery = { initialQuery } isOpen = { isOpen } onOpenChange = { setIsOpen } />
122- </ KapaProvider >
123- </ AskAIContext . Provider >
105+ < KapaProvider
106+ integrationId = { websiteId }
107+ callbacks = { {
108+ askAI : {
109+ onQuerySubmit : ( ) => openAskAI ( ) ,
110+ onAnswerGenerationCompleted : ( ) => openAskAI ( ) ,
111+ } ,
112+ } }
113+ botProtectionMechanism = "hcaptcha"
114+ >
115+ < TooltipProvider disableHoverableContent >
116+ < Tooltip >
117+ < TooltipTrigger asChild >
118+ < div className = "inline-flex" >
119+ < Button
120+ variant = "small-menu-item"
121+ data-action = "ask-ai"
122+ shortcut = { { modifiers : [ "mod" ] , key : "/" , enabledOnInputElements : true } }
123+ hideShortcutKey
124+ data-modal-override-open-class-ask-ai = "true"
125+ onClick = { ( ) => openAskAI ( ) }
126+ >
127+ < AISparkleIcon className = "size-5" />
128+ </ Button >
129+ </ div >
130+ </ TooltipTrigger >
131+ < TooltipContent side = "top" className = "flex items-center gap-1 py-1.5 pl-2.5 pr-2 text-xs" >
132+ Ask AI
133+ < ShortcutKey shortcut = { { modifiers : [ "mod" ] , key : "/" } } variant = "medium/bright" />
134+ </ TooltipContent >
135+ </ Tooltip >
136+ </ TooltipProvider >
137+ < AskAIDialog
138+ initialQuery = { initialQuery }
139+ isOpen = { isOpen }
140+ onOpenChange = { setIsOpen }
141+ closeAskAI = { closeAskAI }
142+ />
143+ </ KapaProvider >
124144 ) ;
125145}
126146
127147type AskAIDialogProps = {
128148 initialQuery ?: string ;
129149 isOpen : boolean ;
130150 onOpenChange : ( open : boolean ) => void ;
151+ closeAskAI : ( ) => void ;
131152} ;
132153
133- function AskAIDialog ( { initialQuery, isOpen, onOpenChange } : AskAIDialogProps ) {
134- const { closeAskAI } = useAskAI ( ) ;
135-
154+ function AskAIDialog ( { initialQuery, isOpen, onOpenChange, closeAskAI } : AskAIDialogProps ) {
136155 const handleOpenChange = ( open : boolean ) => {
137156 if ( ! open ) {
138157 closeAskAI ( ) ;
@@ -514,29 +533,3 @@ function GradientSpinnerBackground({
514533 </ div >
515534 ) ;
516535}
517-
518- export function AskAIButton ( { question } : { question ?: string } ) {
519- const { openAskAI } = useAskAI ( ) ;
520-
521- return (
522- < TooltipProvider disableHoverableContent >
523- < Tooltip >
524- < TooltipTrigger asChild >
525- < div className = "inline-flex" >
526- < Button
527- variant = "minimal/small"
528- onClick = { ( ) => openAskAI ( question ) }
529- className = "pl-0.5 pr-1"
530- data-action = "ask-ai"
531- >
532- < AISparkleIcon className = "size-5" />
533- </ Button >
534- </ div >
535- </ TooltipTrigger >
536- < TooltipContent side = "top" className = "flex items-center gap-1 px-2 py-1.5 text-xs" >
537- Ask AI
538- </ TooltipContent >
539- </ Tooltip >
540- </ TooltipProvider >
541- ) ;
542- }
0 commit comments