From 69de2530a0b5ab4adbb892fa1ae04a409191ccff Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Jun 2025 18:39:32 +0100 Subject: [PATCH 1/3] Move all the logic into a single component, preparing for client only --- apps/webapp/app/components/AskAI.tsx | 140 ++++++++---------- .../app/components/navigation/SideMenu.tsx | 34 +---- apps/webapp/app/root.tsx | 11 +- 3 files changed, 66 insertions(+), 119 deletions(-) diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index 23b575b771..e464a46e3a 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -7,24 +7,21 @@ import { } from "@heroicons/react/20/solid"; import { type FeedbackComment, KapaProvider, type QA, useChat } from "@kapaai/react-sdk"; import { useSearchParams } from "@remix-run/react"; +import DOMPurify from "dompurify"; import { motion } from "framer-motion"; import { marked } from "marked"; -import { - createContext, - type ReactNode, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTypedRouteLoaderData } from "remix-typedjson"; import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; import { SparkleListIcon } from "~/assets/icons/SparkleListIcon"; +import { useFeatures } from "~/hooks/useFeatures"; +import { type loader } from "~/root"; import { Button } from "./primitives/Buttons"; import { Callout } from "./primitives/Callout"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog"; import { Header2 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; +import { ShortcutKey } from "./primitives/ShortcutKey"; import { Spinner } from "./primitives/Spinner"; import { SimpleTooltip, @@ -33,31 +30,28 @@ import { TooltipProvider, TooltipTrigger, } from "./primitives/Tooltip"; -import DOMPurify from "dompurify"; -type AskAIContextType = { - isOpen: boolean; - openAskAI: (question?: string) => void; - closeAskAI: () => void; - websiteId: string | null; -}; +function useKapaWebsiteId() { + const routeMatch = useTypedRouteLoaderData("root"); + return routeMatch?.kapa.websiteId; +} -const AskAIContext = createContext(null); +export function AskAI() { + const { isManagedCloud } = useFeatures(); + const websiteId = useKapaWebsiteId(); -export function useAskAI() { - const context = useContext(AskAIContext); - if (!context) { - throw new Error("useAskAI must be used within an AskAIProvider"); + if (!isManagedCloud || !websiteId) { + return null; } - return context; + + return ; } type AskAIProviderProps = { - children: ReactNode; - websiteId: string | null; + websiteId: string; }; -export function AskAIProvider({ children, websiteId }: AskAIProviderProps) { +function AskAIProvider({ websiteId }: AskAIProviderProps) { const [isOpen, setIsOpen] = useState(false); const [initialQuery, setInitialQuery] = useState(); const [searchParams, setSearchParams] = useSearchParams(); @@ -94,33 +88,46 @@ export function AskAIProvider({ children, websiteId }: AskAIProviderProps) { } }, [searchParams.toString(), openAskAI]); - const contextValue: AskAIContextType = { - isOpen, - openAskAI, - closeAskAI, - websiteId, - }; - - if (!websiteId) { - return {children}; - } - return ( - - openAskAI(), - onAnswerGenerationCompleted: () => openAskAI(), - }, - }} - botProtectionMechanism="hcaptcha" - > - {children} - - - + openAskAI(), + onAnswerGenerationCompleted: () => openAskAI(), + }, + }} + botProtectionMechanism="hcaptcha" + > + + + +
+ +
+
+ + Ask AI + + +
+
+ +
); } @@ -128,11 +135,10 @@ type AskAIDialogProps = { initialQuery?: string; isOpen: boolean; onOpenChange: (open: boolean) => void; + closeAskAI: () => void; }; -function AskAIDialog({ initialQuery, isOpen, onOpenChange }: AskAIDialogProps) { - const { closeAskAI } = useAskAI(); - +function AskAIDialog({ initialQuery, isOpen, onOpenChange, closeAskAI }: AskAIDialogProps) { const handleOpenChange = (open: boolean) => { if (!open) { closeAskAI(); @@ -514,29 +520,3 @@ function GradientSpinnerBackground({ ); } - -export function AskAIButton({ question }: { question?: string }) { - const { openAskAI } = useAskAI(); - - return ( - - - -
- -
-
- - Ask AI - -
-
- ); -} diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 0cf70c0153..5ee7badc35 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -21,7 +21,6 @@ import { import { useNavigation } from "@remix-run/react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import simplur from "simplur"; -import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon"; import { TaskIconSmall } from "~/assets/icons/TaskIcon"; @@ -63,7 +62,7 @@ import { v3UsagePath, v3WaitpointTokensPath, } from "~/utils/pathBuilder"; -import { useAskAI } from "../AskAI"; +import { AskAI } from "../AskAI"; import { FreePlanUsage } from "../billing/FreePlanUsage"; import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence"; import { ImpersonationBanner } from "../ImpersonationBanner"; @@ -77,7 +76,6 @@ import { PopoverMenuItem, PopoverTrigger, } from "../primitives/Popover"; -import { ShortcutKey } from "../primitives/ShortcutKey"; import { TextLink } from "../primitives/TextLink"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; import { ShortcutsAutoOpen } from "../Shortcuts"; @@ -587,40 +585,12 @@ function SelectorDivider() { function HelpAndAI() { const features = useFeatures(); - const { openAskAI, websiteId } = useAskAI(); - const isKapaEnabled = features.isManagedCloud && websiteId; return ( <> - {isKapaEnabled && ( - - - -
- -
-
- - Ask AI - - -
-
- )} + ); } diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index ac556e0f2c..c6f0929eb8 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -6,7 +6,6 @@ import { ExternalScripts } from "remix-utils/external-scripts"; import type { ToastMessage } from "~/models/message.server"; import { commitSession, getSession } from "~/models/message.server"; import tailwindStylesheetUrl from "~/tailwind.css"; -import { AskAIProvider } from "./components/AskAI"; import { RouteErrorDisplay } from "./components/ErrorDisplay"; import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout"; import { ShortcutsProvider } from "./components/primitives/ShortcutsProvider"; @@ -108,12 +107,10 @@ export default function App() { - - - - - - + + + + From 00a6270a99b10ce59975f45ba59725cffb0961f9 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Jun 2025 19:34:22 +0100 Subject: [PATCH 2/3] Use ClientOnly --- apps/webapp/app/components/AskAI.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index e464a46e3a..198bdbf832 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -30,6 +30,7 @@ import { TooltipProvider, TooltipTrigger, } from "./primitives/Tooltip"; +import { ClientOnly } from "remix-utils/client-only"; function useKapaWebsiteId() { const routeMatch = useTypedRouteLoaderData("root"); @@ -44,7 +45,23 @@ export function AskAI() { return null; } - return ; + return ( + + + + } + > + {() => } + + ); } type AskAIProviderProps = { From cb6c54f4c688ca55de1d10e77caf79f62c942043 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Jun 2025 19:36:30 +0100 Subject: [PATCH 3/3] Fix for search param not working --- apps/webapp/app/components/AskAI.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index 198bdbf832..39cc4cdaaf 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -91,19 +91,15 @@ function AskAIProvider({ websiteId }: AskAIProviderProps) { useEffect(() => { const aiHelp = searchParams.get("aiHelp"); if (aiHelp) { - const decodedAiHelp = decodeURIComponent(aiHelp); - // Delay to avoid hCaptcha bot detection - const timeoutId = window.setTimeout(() => openAskAI(decodedAiHelp), 1000); + window.setTimeout(() => openAskAI(aiHelp), 1000); // Clone instead of mutating in place const next = new URLSearchParams(searchParams); next.delete("aiHelp"); setSearchParams(next); - - return () => clearTimeout(timeoutId); } - }, [searchParams.toString(), openAskAI]); + }, [searchParams, openAskAI]); return (