@@ -9,7 +9,15 @@ import { KapaProvider, useChat } from "@kapaai/react-sdk";
99import { useSearchParams } from "@remix-run/react" ;
1010import { motion } from "framer-motion" ;
1111import { marked } from "marked" ;
12- import { useCallback , useEffect , useRef , useState } from "react" ;
12+ import {
13+ useCallback ,
14+ useEffect ,
15+ useRef ,
16+ useState ,
17+ createContext ,
18+ useContext ,
19+ type ReactNode ,
20+ } from "react" ;
1321import { AISparkleIcon } from "~/assets/icons/AISparkleIcon" ;
1422import { SparkleListIcon } from "~/assets/icons/SparkleListIcon" ;
1523import { Button } from "./primitives/Buttons" ;
@@ -27,6 +35,123 @@ import {
2735 TooltipTrigger ,
2836} from "./primitives/Tooltip" ;
2937
38+ type AskAIContextType = {
39+ isOpen : boolean ;
40+ openAskAI : ( question ?: string ) => void ;
41+ closeAskAI : ( ) => void ;
42+ websiteId : string | null ;
43+ } ;
44+
45+ const AskAIContext = createContext < AskAIContextType | null > ( null ) ;
46+
47+ export function useAskAI ( ) {
48+ const context = useContext ( AskAIContext ) ;
49+ if ( ! context ) {
50+ throw new Error ( "useAskAI must be used within an AskAIProvider" ) ;
51+ }
52+ return context ;
53+ }
54+
55+ type AskAIProviderProps = {
56+ children : ReactNode ;
57+ websiteId : string | null ;
58+ } ;
59+
60+ export function AskAIProvider ( { children, websiteId } : AskAIProviderProps ) {
61+ const [ isOpen , setIsOpen ] = useState ( false ) ;
62+ const [ initialQuery , setInitialQuery ] = useState < string | undefined > ( ) ;
63+ const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
64+
65+ const openAskAI = useCallback ( ( question ?: string ) => {
66+ if ( question ) {
67+ setInitialQuery ( question ) ;
68+ } else {
69+ setInitialQuery ( undefined ) ;
70+ }
71+ setIsOpen ( true ) ;
72+ } , [ ] ) ;
73+
74+ const closeAskAI = useCallback ( ( ) => {
75+ setIsOpen ( false ) ;
76+ setInitialQuery ( undefined ) ;
77+ } , [ ] ) ;
78+
79+ // Handle URL param functionality
80+ useEffect ( ( ) => {
81+ const aiHelp = searchParams . get ( "aiHelp" ) ;
82+ if ( aiHelp ) {
83+ setSearchParams ( ( prev ) => {
84+ prev . delete ( "aiHelp" ) ;
85+ return prev ;
86+ } ) ;
87+
88+ const decodedAiHelp = decodeURIComponent ( aiHelp ) ;
89+ openAskAI ( decodedAiHelp ) ;
90+ }
91+ } , [ searchParams , setSearchParams , openAskAI ] ) ;
92+
93+ const contextValue : AskAIContextType = {
94+ isOpen,
95+ openAskAI,
96+ closeAskAI,
97+ websiteId,
98+ } ;
99+
100+ if ( ! websiteId ) {
101+ return < AskAIContext . Provider value = { contextValue } > { children } </ AskAIContext . Provider > ;
102+ }
103+
104+ return (
105+ < AskAIContext . Provider value = { contextValue } >
106+ < KapaProvider
107+ integrationId = { websiteId }
108+ callbacks = { {
109+ askAI : {
110+ onQuerySubmit : ( ) => openAskAI ( ) ,
111+ onAnswerGenerationCompleted : ( ) => openAskAI ( ) ,
112+ } ,
113+ } }
114+ botProtectionMechanism = "hcaptcha"
115+ >
116+ { children }
117+ < AskAIDialog initialQuery = { initialQuery } isOpen = { isOpen } onOpenChange = { setIsOpen } />
118+ </ KapaProvider >
119+ </ AskAIContext . Provider >
120+ ) ;
121+ }
122+
123+ type AskAIDialogProps = {
124+ initialQuery ?: string ;
125+ isOpen : boolean ;
126+ onOpenChange : ( open : boolean ) => void ;
127+ } ;
128+
129+ function AskAIDialog ( { initialQuery, isOpen, onOpenChange } : AskAIDialogProps ) {
130+ const { closeAskAI } = useAskAI ( ) ;
131+
132+ const handleOpenChange = ( open : boolean ) => {
133+ if ( ! open ) {
134+ closeAskAI ( ) ;
135+ } else {
136+ onOpenChange ( open ) ;
137+ }
138+ } ;
139+
140+ return (
141+ < Dialog open = { isOpen } onOpenChange = { handleOpenChange } >
142+ < DialogContent className = "animated-gradient-glow flex max-h-[90vh] min-h-fit w-full flex-col justify-between gap-0 px-0 pb-0 pt-0 sm:max-w-prose" >
143+ < DialogHeader className = "flex h-[2.75rem] items-start justify-center rounded-t-md bg-background-bright pl-3" >
144+ < div className = "flex items-center gap-1" >
145+ < AISparkleIcon className = "size-5" />
146+ < DialogTitle className = "text-sm font-medium text-text-bright" > Ask AI</ DialogTitle >
147+ </ div >
148+ </ DialogHeader >
149+ < ChatInterface initialQuery = { initialQuery } />
150+ </ DialogContent >
151+ </ Dialog >
152+ ) ;
153+ }
154+
30155type KapaChatProps = {
31156 websiteId : string ;
32157 onOpen ?: ( ) => void ;
@@ -400,7 +525,7 @@ export function AskAI({ websiteId, onOpen }: KapaChatProps) {
400525 onAnswerGenerationCompleted : ( ) => handleOpen ( ) ,
401526 } ,
402527 } }
403- botProtectionMechanism = "recaptcha "
528+ botProtectionMechanism = "hcaptcha "
404529 >
405530 < div className = "relative" >
406531 < TooltipProvider disableHoverableContent >
@@ -468,3 +593,29 @@ function GradientSpinnerBackground({
468593 </ div >
469594 ) ;
470595}
596+
597+ export function AskAIButton ( { question } : { question ?: string } ) {
598+ const { openAskAI } = useAskAI ( ) ;
599+
600+ return (
601+ < TooltipProvider disableHoverableContent >
602+ < Tooltip >
603+ < TooltipTrigger asChild >
604+ < div className = "inline-flex" >
605+ < Button
606+ variant = "minimal/small"
607+ onClick = { ( ) => openAskAI ( question ) }
608+ className = "pl-0.5 pr-1"
609+ data-action = "ask-ai"
610+ >
611+ < AISparkleIcon className = "size-5" />
612+ </ Button >
613+ </ div >
614+ </ TooltipTrigger >
615+ < TooltipContent side = "top" className = "flex items-center gap-1 px-2 py-1.5 text-xs" >
616+ Ask AI
617+ </ TooltipContent >
618+ </ Tooltip >
619+ </ TooltipProvider >
620+ ) ;
621+ }
0 commit comments