@@ -7,24 +7,21 @@ import {
7
7
} from "@heroicons/react/20/solid" ;
8
8
import { type FeedbackComment , KapaProvider , type QA , useChat } from "@kapaai/react-sdk" ;
9
9
import { useSearchParams } from "@remix-run/react" ;
10
+ import DOMPurify from "dompurify" ;
10
11
import { motion } from "framer-motion" ;
11
12
import { 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" ;
21
15
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon" ;
22
16
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon" ;
17
+ import { useFeatures } from "~/hooks/useFeatures" ;
18
+ import { type loader } from "~/root" ;
23
19
import { Button } from "./primitives/Buttons" ;
24
20
import { Callout } from "./primitives/Callout" ;
25
21
import { Dialog , DialogContent , DialogHeader , DialogTitle } from "./primitives/Dialog" ;
26
22
import { Header2 } from "./primitives/Headers" ;
27
23
import { Paragraph } from "./primitives/Paragraph" ;
24
+ import { ShortcutKey } from "./primitives/ShortcutKey" ;
28
25
import { Spinner } from "./primitives/Spinner" ;
29
26
import {
30
27
SimpleTooltip ,
@@ -33,31 +30,45 @@ import {
33
30
TooltipProvider ,
34
31
TooltipTrigger ,
35
32
} from "./primitives/Tooltip" ;
36
- import DOMPurify from "dompurify " ;
33
+ import { ClientOnly } from "remix-utils/client-only " ;
37
34
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
+ }
44
39
45
- const AskAIContext = createContext < AskAIContextType | null > ( null ) ;
40
+ export function AskAI ( ) {
41
+ const { isManagedCloud } = useFeatures ( ) ;
42
+ const websiteId = useKapaWebsiteId ( ) ;
46
43
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 ;
51
46
}
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
+ ) ;
53
65
}
54
66
55
67
type AskAIProviderProps = {
56
- children : ReactNode ;
57
- websiteId : string | null ;
68
+ websiteId : string ;
58
69
} ;
59
70
60
- export function AskAIProvider ( { children , websiteId } : AskAIProviderProps ) {
71
+ function AskAIProvider ( { websiteId } : AskAIProviderProps ) {
61
72
const [ isOpen , setIsOpen ] = useState ( false ) ;
62
73
const [ initialQuery , setInitialQuery ] = useState < string | undefined > ( ) ;
63
74
const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
@@ -80,59 +91,67 @@ export function AskAIProvider({ children, websiteId }: AskAIProviderProps) {
80
91
useEffect ( ( ) => {
81
92
const aiHelp = searchParams . get ( "aiHelp" ) ;
82
93
if ( aiHelp ) {
83
- const decodedAiHelp = decodeURIComponent ( aiHelp ) ;
84
-
85
94
// Delay to avoid hCaptcha bot detection
86
- const timeoutId = window . setTimeout ( ( ) => openAskAI ( decodedAiHelp ) , 1000 ) ;
95
+ window . setTimeout ( ( ) => openAskAI ( aiHelp ) , 1000 ) ;
87
96
88
97
// Clone instead of mutating in place
89
98
const next = new URLSearchParams ( searchParams ) ;
90
99
next . delete ( "aiHelp" ) ;
91
100
setSearchParams ( next ) ;
92
-
93
- return ( ) => clearTimeout ( timeoutId ) ;
94
101
}
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 ] ) ;
107
103
108
104
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 >
124
144
) ;
125
145
}
126
146
127
147
type AskAIDialogProps = {
128
148
initialQuery ?: string ;
129
149
isOpen : boolean ;
130
150
onOpenChange : ( open : boolean ) => void ;
151
+ closeAskAI : ( ) => void ;
131
152
} ;
132
153
133
- function AskAIDialog ( { initialQuery, isOpen, onOpenChange } : AskAIDialogProps ) {
134
- const { closeAskAI } = useAskAI ( ) ;
135
-
154
+ function AskAIDialog ( { initialQuery, isOpen, onOpenChange, closeAskAI } : AskAIDialogProps ) {
136
155
const handleOpenChange = ( open : boolean ) => {
137
156
if ( ! open ) {
138
157
closeAskAI ( ) ;
@@ -514,29 +533,3 @@ function GradientSpinnerBackground({
514
533
</ div >
515
534
) ;
516
535
}
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