Skip to content

Commit 5e874c9

Browse files
committed
AskAI component is now a hook that can take a question
1 parent f8f68d4 commit 5e874c9

File tree

3 files changed

+192
-11
lines changed

3 files changed

+192
-11
lines changed

apps/webapp/app/components/AskAI.tsx

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import { KapaProvider, useChat } from "@kapaai/react-sdk";
99
import { useSearchParams } from "@remix-run/react";
1010
import { motion } from "framer-motion";
1111
import { 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";
1321
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
1422
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
1523
import { 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+
30155
type 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+
}

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import simplur from "simplur";
2424
import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon";
2525
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
2626
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
27-
import { AskAI } from "~/components/AskAI";
27+
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
2828
import { Avatar } from "~/components/primitives/Avatar";
2929
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
3030
import { useFeatures } from "~/hooks/useFeatures";
@@ -81,6 +81,7 @@ import { TextLink } from "../primitives/TextLink";
8181
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
8282
import { ShortcutsAutoOpen } from "../Shortcuts";
8383
import { UserProfilePhoto } from "../UserProfilePhoto";
84+
import { useAskAI } from "../AskAI";
8485
import { EnvironmentSelector } from "./EnvironmentSelector";
8586
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
8687
import { SideMenuHeader } from "./SideMenuHeader";
@@ -578,14 +579,39 @@ function HelpAndAI() {
578579
id: "root",
579580
matches,
580581
});
581-
const isKapaEnabled = features.isManagedCloud && routeMatch?.kapa?.websiteId;
582+
const { openAskAI, websiteId } = useAskAI();
583+
const isKapaEnabled = features.isManagedCloud && websiteId;
582584

583585
return (
584586
<>
585587
<ShortcutsAutoOpen />
586588
<HelpAndFeedback />
587-
{isKapaEnabled && routeMatch.kapa.websiteId && (
588-
<AskAI websiteId={routeMatch.kapa.websiteId} />
589+
{isKapaEnabled && (
590+
<TooltipProvider disableHoverableContent>
591+
<Tooltip>
592+
<TooltipTrigger asChild>
593+
<div className="inline-flex">
594+
<Button
595+
variant="small-menu-item"
596+
data-action="ask-ai"
597+
shortcut={{ modifiers: ["mod"], key: "/", enabledOnInputElements: true }}
598+
hideShortcutKey
599+
data-modal-override-open-class-ask-ai="true"
600+
onClick={() => openAskAI()}
601+
>
602+
<AISparkleIcon className="size-5" />
603+
</Button>
604+
</div>
605+
</TooltipTrigger>
606+
<TooltipContent
607+
side="top"
608+
className="flex items-center gap-1 py-1.5 pl-2.5 pr-2 text-xs"
609+
>
610+
Ask AI
611+
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
612+
</TooltipContent>
613+
</Tooltip>
614+
</TooltipProvider>
589615
)}
590616
</>
591617
);

apps/webapp/app/root.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ExternalScripts } from "remix-utils/external-scripts";
66
import type { ToastMessage } from "~/models/message.server";
77
import { commitSession, getSession } from "~/models/message.server";
88
import tailwindStylesheetUrl from "~/tailwind.css";
9+
import { AskAIProvider } from "./components/AskAI";
910
import { RouteErrorDisplay } from "./components/ErrorDisplay";
1011
import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout";
1112
import { ShortcutsProvider } from "./components/primitives/ShortcutsProvider";
@@ -96,7 +97,7 @@ export function ErrorBoundary() {
9697
}
9798

9899
export default function App() {
99-
const { posthogProjectKey } = useTypedLoaderData<typeof loader>();
100+
const { posthogProjectKey, kapa } = useTypedLoaderData<typeof loader>();
100101
usePostHog(posthogProjectKey);
101102

102103
return (
@@ -105,12 +106,15 @@ export default function App() {
105106
<head>
106107
<Meta />
107108
<Links />
109+
{kapa.websiteId && <script src="https://js.hcaptcha.com/1/api.js"></script>}
108110
</head>
109111
<body className="h-full overflow-hidden bg-background-dimmed">
110-
<ShortcutsProvider>
111-
<Outlet />
112-
<Toast />
113-
</ShortcutsProvider>
112+
<AskAIProvider websiteId={kapa.websiteId || null}>
113+
<ShortcutsProvider>
114+
<Outlet />
115+
<Toast />
116+
</ShortcutsProvider>
117+
</AskAIProvider>
114118
<ScrollRestoration />
115119
<ExternalScripts />
116120
<Scripts />

0 commit comments

Comments
 (0)