diff --git a/apps/webapp/app/assets/icons/AISparkleIcon.tsx b/apps/webapp/app/assets/icons/AISparkleIcon.tsx index ee0924bbfd..46f7429e77 100644 --- a/apps/webapp/app/assets/icons/AISparkleIcon.tsx +++ b/apps/webapp/app/assets/icons/AISparkleIcon.tsx @@ -1,53 +1,31 @@ export function AISparkleIcon({ className }: { className?: string }) { return ( - + - - - - - - - - - - - - - - ); } diff --git a/apps/webapp/app/assets/icons/KeyboardEnterIcon.tsx b/apps/webapp/app/assets/icons/KeyboardEnterIcon.tsx new file mode 100644 index 0000000000..b634191272 --- /dev/null +++ b/apps/webapp/app/assets/icons/KeyboardEnterIcon.tsx @@ -0,0 +1,12 @@ +export function KeyboardEnterIcon({ className }: { className?: string }) { + return ( + + + + + ); +} diff --git a/apps/webapp/app/components/Shortcuts.tsx b/apps/webapp/app/components/Shortcuts.tsx index d44359bb4e..8349ed970f 100644 --- a/apps/webapp/app/components/Shortcuts.tsx +++ b/apps/webapp/app/components/Shortcuts.tsx @@ -11,6 +11,8 @@ import { } from "./primitives/SheetV3"; import { ShortcutKey } from "./primitives/ShortcutKey"; import { Button } from "./primitives/Buttons"; +import { useState } from "react"; +import { useShortcutKeys } from "~/hooks/useShortcutKeys"; export function Shortcuts() { return ( @@ -23,121 +25,147 @@ export function Shortcuts() { data-action="shortcuts" fullWidth textAlignLeft - shortcut={{ modifiers: ["shift"], key: "?" }} + shortcut={{ modifiers: ["shift"], key: "?", enabled: false }} className="gap-x-0 pl-0.5" iconSpacing="gap-x-0.5" > Shortcuts - - - -
- - - Keyboard shortcuts - -
- -
-
- General - - - - - - - - - - - - - - to - - - - - - - - - - - - -
-
- Runs page - - - - - - - - - -
-
- Run page - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - to - - - -
-
- Schedules page - - - -
-
- Alerts page - - - -
-
- - + ); } +export function ShortcutsAutoOpen() { + const [isOpen, setIsOpen] = useState(false); + + useShortcutKeys({ + shortcut: { modifiers: ["shift"], key: "?" }, + action: () => { + setIsOpen(true); + }, + }); + + return ( + + + + ); +} + +function ShortcutContent() { + return ( + + + +
+ + + Keyboard shortcuts + +
+
+
+
+ General + + + + + + + + + + + + + + + + + to + + + + + + + + + + + + +
+
+ Runs page + + + + + + + + + +
+
+ Run page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + to + + + +
+
+ Schedules page + + + +
+
+ Alerts page + + + +
+
+
+
+ ); +} + function Shortcut({ children, name }: { children: React.ReactNode; name: string }) { return (
diff --git a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx index f595ea1dbd..a788e74233 100644 --- a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx +++ b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx @@ -2,9 +2,9 @@ import { ArrowUpRightIcon, BookOpenIcon, CalendarDaysIcon, - ChatBubbleLeftEllipsisIcon, EnvelopeIcon, LightBulbIcon, + QuestionMarkCircleIcon, SignalIcon, StarIcon, } from "@heroicons/react/20/solid"; @@ -12,6 +12,7 @@ import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons"; import { Fragment, useState } from "react"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; import { Feedback } from "../Feedback"; +import { Shortcuts } from "../Shortcuts"; import { StepContentContainer } from "../StepContentContainer"; import { Button } from "../primitives/Buttons"; import { ClipboardField } from "../primitives/ClipboardField"; @@ -21,16 +22,21 @@ import { Paragraph } from "../primitives/Paragraph"; import { Popover, PopoverContent, PopoverSideMenuTrigger } from "../primitives/Popover"; import { StepNumber } from "../primitives/StepNumber"; import { MenuCount, SideMenuItem } from "./SideMenuItem"; -import { Shortcuts } from "../Shortcuts"; -export function HelpAndFeedback() { + +export function HelpAndFeedback({ disableShortcut = false }: { disableShortcut?: boolean }) { const [isHelpMenuOpen, setHelpMenuOpen] = useState(false); const currentPlan = useCurrentPlan(); return ( setHelpMenuOpen(open)}> - +
- + Help & Feedback
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 6a56281873..ec1b20406b 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -17,9 +17,10 @@ import { ServerStackIcon, Squares2X2Icon, } from "@heroicons/react/20/solid"; -import { useNavigation } from "@remix-run/react"; +import { useLocation, useNavigation } from "@remix-run/react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import simplur from "simplur"; +import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon"; import { TaskIconSmall } from "~/assets/icons/TaskIcon"; import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon"; @@ -55,6 +56,7 @@ import { v3UsagePath, v3WaitpointTokensPath, } from "~/utils/pathBuilder"; +import { useKapaWidget } from "../../hooks/useKapaWidget"; import { FreePlanUsage } from "../billing/FreePlanUsage"; import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence"; import { ImpersonationBanner } from "../ImpersonationBanner"; @@ -68,8 +70,10 @@ 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"; import { UserProfilePhoto } from "../UserProfilePhoto"; import { EnvironmentSelector } from "./EnvironmentSelector"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; @@ -276,7 +280,9 @@ export function SideMenu({
- +
+ +
{isFreeUser && ( ); } + +function HelpAndAI() { + const { isKapaEnabled, openKapa, isKapaOpen } = useKapaWidget(); + const location = useLocation(); + + // If the location searchParams contains `aiHelp="A question to ask AI"` we should get the value, clear it from the searchParams and open Kapa with the question + const searchParams = new URLSearchParams(location.search); + + useEffect(() => { + const aiHelp = searchParams.get("aiHelp"); + if (aiHelp) { + searchParams.delete("aiHelp"); + console.log("aiHelp", aiHelp); + openKapa(aiHelp); + } + }, [location.search]); + + return ( + <> + + + {isKapaEnabled && ( + + + +
+ +
+
+ + Ask AI + + +
+
+ )} + + ); +} diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 2adddb40cc..ed94d46f95 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -177,6 +177,7 @@ export type ButtonContentPropsType = { shortcutPosition?: "before-trailing-icon" | "after-trailing-icon"; tooltip?: ReactNode; iconSpacing?: string; + hideShortcutKey?: boolean; }; export function ButtonContent(props: ButtonContentPropsType) { @@ -192,6 +193,7 @@ export function ButtonContent(props: ButtonContentPropsType) { className, tooltip, iconSpacing, + hideShortcutKey, } = props; const variation = allVariants.variant[props.variant]; @@ -202,7 +204,8 @@ export function ButtonContent(props: ButtonContentPropsType) { const textColorClassName = variation.textColor; const renderShortcutKey = () => - shortcut && ( + shortcut && + !hideShortcutKey && ( Enter; + return isMac ? ( + + ) : ( + Enter + ); case "esc": return Esc; case "del": diff --git a/apps/webapp/app/components/primitives/ShortcutsProvider.tsx b/apps/webapp/app/components/primitives/ShortcutsProvider.tsx new file mode 100644 index 0000000000..bcbed9b562 --- /dev/null +++ b/apps/webapp/app/components/primitives/ShortcutsProvider.tsx @@ -0,0 +1,39 @@ +import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react"; + +type ShortcutsContextType = { + areShortcutsEnabled: boolean; + disableShortcuts: () => void; + enableShortcuts: () => void; +}; + +const ShortcutsContext = createContext(null); + +type ShortcutsProviderProps = { + children: ReactNode; +}; + +export function ShortcutsProvider({ children }: ShortcutsProviderProps) { + const [areShortcutsEnabled, setAreShortcutsEnabled] = useState(true); + + const disableShortcuts = useCallback(() => setAreShortcutsEnabled(false), []); + const enableShortcuts = useCallback(() => setAreShortcutsEnabled(true), []); + + const value = useMemo( + () => ({ + areShortcutsEnabled, + disableShortcuts, + enableShortcuts, + }), + [areShortcutsEnabled, disableShortcuts, enableShortcuts] + ); + + return {children}; +} + +const throwIfNoProvider = () => { + throw new Error("useShortcuts must be used within a ShortcutsProvider"); +}; + +export const useShortcuts = () => { + return useContext(ShortcutsContext) ?? throwIfNoProvider(); +}; diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 66ea233fec..25dddd221c 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -710,6 +710,9 @@ const EnvironmentSchema = z.object({ QUEUE_SSE_AUTORELOAD_INTERVAL_MS: z.coerce.number().int().default(5_000), QUEUE_SSE_AUTORELOAD_TIMEOUT_MS: z.coerce.number().int().default(60_000), + + // kapa.ai + KAPA_AI_WEBSITE_ID: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/hooks/useKapaWidget.tsx b/apps/webapp/app/hooks/useKapaWidget.tsx new file mode 100644 index 0000000000..d89db74b6c --- /dev/null +++ b/apps/webapp/app/hooks/useKapaWidget.tsx @@ -0,0 +1,148 @@ +import { useMatches, useSearchParams } from "@remix-run/react"; +import { useCallback, useEffect, useState } from "react"; +import { useFeatures } from "~/hooks/useFeatures"; +import { type loader } from "~/root"; +import { useShortcuts } from "../components/primitives/ShortcutsProvider"; +import { useTypedMatchesData } from "./useTypedMatchData"; + +type OpenOptions = { mode: string; query: string; submit: boolean }; + +declare global { + interface Window { + Kapa: ( + command: string, + options?: (() => void) | { onRender?: () => void } | OpenOptions, + remove?: string | { onRender?: () => void } + ) => void; + } +} + +export function KapaScripts({ websiteId }: { websiteId?: string }) { + if (!websiteId) return null; + + return ( + <> + +