Skip to content

Commit c3dca58

Browse files
authored
Fix for the Kapa widget not working with SSR (#2170)
* Move all the logic into a single component, preparing for client only * Use ClientOnly * Fix for search param not working
1 parent 20a8e7c commit c3dca58

File tree

3 files changed

+85
-125
lines changed

3 files changed

+85
-125
lines changed

apps/webapp/app/components/AskAI.tsx

Lines changed: 79 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,21 @@ import {
77
} from "@heroicons/react/20/solid";
88
import { type FeedbackComment, KapaProvider, type QA, useChat } from "@kapaai/react-sdk";
99
import { useSearchParams } from "@remix-run/react";
10+
import DOMPurify from "dompurify";
1011
import { motion } from "framer-motion";
1112
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";
2115
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
2216
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
17+
import { useFeatures } from "~/hooks/useFeatures";
18+
import { type loader } from "~/root";
2319
import { Button } from "./primitives/Buttons";
2420
import { Callout } from "./primitives/Callout";
2521
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog";
2622
import { Header2 } from "./primitives/Headers";
2723
import { Paragraph } from "./primitives/Paragraph";
24+
import { ShortcutKey } from "./primitives/ShortcutKey";
2825
import { Spinner } from "./primitives/Spinner";
2926
import {
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

5567
type 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

127147
type 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-
}

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

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
import { useNavigation } from "@remix-run/react";
2222
import { useEffect, useRef, useState, type ReactNode } from "react";
2323
import simplur from "simplur";
24-
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
2524
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
2625
import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon";
2726
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
@@ -63,7 +62,7 @@ import {
6362
v3UsagePath,
6463
v3WaitpointTokensPath,
6564
} from "~/utils/pathBuilder";
66-
import { useAskAI } from "../AskAI";
65+
import { AskAI } from "../AskAI";
6766
import { FreePlanUsage } from "../billing/FreePlanUsage";
6867
import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence";
6968
import { ImpersonationBanner } from "../ImpersonationBanner";
@@ -77,7 +76,6 @@ import {
7776
PopoverMenuItem,
7877
PopoverTrigger,
7978
} from "../primitives/Popover";
80-
import { ShortcutKey } from "../primitives/ShortcutKey";
8179
import { TextLink } from "../primitives/TextLink";
8280
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
8381
import { ShortcutsAutoOpen } from "../Shortcuts";
@@ -587,40 +585,12 @@ function SelectorDivider() {
587585

588586
function HelpAndAI() {
589587
const features = useFeatures();
590-
const { openAskAI, websiteId } = useAskAI();
591-
const isKapaEnabled = features.isManagedCloud && websiteId;
592588

593589
return (
594590
<>
595591
<ShortcutsAutoOpen />
596592
<HelpAndFeedback />
597-
{isKapaEnabled && (
598-
<TooltipProvider disableHoverableContent>
599-
<Tooltip>
600-
<TooltipTrigger asChild>
601-
<div className="inline-flex">
602-
<Button
603-
variant="small-menu-item"
604-
data-action="ask-ai"
605-
shortcut={{ modifiers: ["mod"], key: "/", enabledOnInputElements: true }}
606-
hideShortcutKey
607-
data-modal-override-open-class-ask-ai="true"
608-
onClick={() => openAskAI()}
609-
>
610-
<AISparkleIcon className="size-5" />
611-
</Button>
612-
</div>
613-
</TooltipTrigger>
614-
<TooltipContent
615-
side="top"
616-
className="flex items-center gap-1 py-1.5 pl-2.5 pr-2 text-xs"
617-
>
618-
Ask AI
619-
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
620-
</TooltipContent>
621-
</Tooltip>
622-
</TooltipProvider>
623-
)}
593+
<AskAI />
624594
</>
625595
);
626596
}

apps/webapp/app/root.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ 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";
109
import { RouteErrorDisplay } from "./components/ErrorDisplay";
1110
import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout";
1211
import { ShortcutsProvider } from "./components/primitives/ShortcutsProvider";
@@ -108,12 +107,10 @@ export default function App() {
108107
<Links />
109108
</head>
110109
<body className="h-full overflow-hidden bg-background-dimmed">
111-
<AskAIProvider websiteId={kapa.websiteId || null}>
112-
<ShortcutsProvider>
113-
<Outlet />
114-
<Toast />
115-
</ShortcutsProvider>
116-
</AskAIProvider>
110+
<ShortcutsProvider>
111+
<Outlet />
112+
<Toast />
113+
</ShortcutsProvider>
117114
<ScrollRestoration />
118115
<ExternalScripts />
119116
<Scripts />

0 commit comments

Comments
 (0)