diff --git a/apps/web/app/(app)/automation/ProcessingPromptFileDialog.tsx b/apps/web/app/(app)/automation/ProcessingPromptFileDialog.tsx index 3ae2a8cf6..0397a6823 100644 --- a/apps/web/app/(app)/automation/ProcessingPromptFileDialog.tsx +++ b/apps/web/app/(app)/automation/ProcessingPromptFileDialog.tsx @@ -1,8 +1,6 @@ -import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; import Image from "next/image"; -import { Loader2 } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; -import { useQueryState } from "nuqs"; +import Link from "next/link"; import { Dialog, DialogContent, @@ -10,322 +8,247 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel"; -import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Loading } from "@/components/Loading"; +import { pluralize } from "@/utils/string"; + +type StepProps = { + back?: () => void; + next?: () => void; +}; -/* -When the modal first opens we'll tell them the AI is processing their prompt file. -And that they can learn about the AI assistant in the meantime. -When completed, we'll show them the test view and automatically start testing -the rules. -If they notice a mistake, they can mark an error. -*/ +type StepContentProps = StepProps & { + title: string; + children: React.ReactNode; +}; + +type ResultProps = { + createdRules: number; + editedRules: number; + removedRules: number; +}; export function ProcessingPromptFileDialog({ open, onOpenChange, result, - isLoading, + setViewedProcessingPromptFileDialog, }: { open: boolean; onOpenChange: (open: boolean) => void; - result?: { - createdRules: number; - editedRules: number; - removedRules: number; - }; - isLoading: boolean; + result?: ResultProps; + setViewedProcessingPromptFileDialog: (viewed: boolean) => void; }) { - const [modal, setModal] = useQueryState("modal"); const [currentStep, setCurrentStep] = useState(0); - // useEffect(() => { - // if (!isLoading && result && currentStep < 4) { - // setCurrentStep(4); - // } - // }, [isLoading, result, currentStep]); + const back = useCallback(() => { + setCurrentStep((currentStep) => Math.max(0, currentStep - 1)); + }, []); + + const next = useCallback(() => { + setCurrentStep((currentStep) => Math.min(4, currentStep + 1)); + }, []); useEffect(() => { - // reset modal state on close - if (!open) { - setCurrentStep(0); - setModal(null); + if (currentStep > 3) { + setViewedProcessingPromptFileDialog(true); } - }, [open, setModal]); - - const showRules = modal === "rules"; + }, [currentStep, setViewedProcessingPromptFileDialog]); return ( - {/* */} - - {currentStep === 0 && ( - setCurrentStep(1)} /> - )} - - {currentStep === 1 && ( - setCurrentStep(2)} /> - )} - {currentStep === 2 && ( - setCurrentStep(3)} /> - )} - {currentStep === 3 && ( - setCurrentStep(4)} /> - )} - {currentStep === 4 && ( - setCurrentStep(5)} /> - )} - - {/* - - {isLoading ? ( -
- - Processing your prompt file... -
- ) : ( - "Rules Generated" - )} -
- - {isLoading - ? "In the meantime, learn more about the AI assistant and how it works." - : "Your rules have been created. You can now test and improve them."} - -
*/} - - {/* {showRules ? ( - - ) : ( -
- + {currentStep === 0 && } + {currentStep === 1 && } + {currentStep === 2 && } + {currentStep === 3 && } + {currentStep > 3 && + (result ? ( + onOpenChange(false)} + result={result} /> -
- -
- -
- )} */} + ) : ( + + ))}
); } -function OnboardingContent({ onNext }: { onNext: () => void }) { +function StepNavigation({ back, next }: StepProps) { return ( - - - Processing prompt file... - - Get to know our AI assistant while we process your rules! - -
- -
-
+ )} + {next && } + ); } -function ResultContent({ - result, -}: { - result?: { - createdRules: number; - editedRules: number; - removedRules: number; - }; -}) { - if (!result) { - return null; - } +function Step({ back, next, title, children }: StepContentProps) { + return ( + <> + + {title} + + {children} + + +
+ +
+ + ); +} +function IntroStep({ next }: StepProps) { return ( -
-

Rules saved!

- - -
+ <> + + + Processing... + + This will take a minute. +
+ In the meantime, get to know our AI assistant better! +
+
+
+ +
+ ); } -function ProcessingPromptFileDialogStep1({ onNext }: { onNext: () => void }) { +function Step1({ back, next }: StepProps) { return ( -
+ +

We're converting your prompt into specific, actionable rules.

+

+ These are more reliable and give you fine-grained control over your + email automation. +

+ Analyzing prompt file -

- First, our AI analyzes your prompt file and extracts rules from it. -

- -
+ ); } -function ProcessingPromptFileDialogStep2({ onNext }: { onNext: () => void }) { +function Step2({ back, next }: StepProps) { return ( -
+ +

Once created, you can fine-tune each rule to your needs.

Saving rules -
-

Next, you can click a rule to edit it.

-

Each rule is made up of two parts:

-
    -
  1. A condition
  2. -
  3. An action
  4. -
-

- Conditions need to be met for actions to happen. For example, "apply - this to marketing emails". -

-

Example actions include:

-
    -
  • Drafting an email
  • -
  • Labeling
  • -
  • Archiving
  • -
-
- -
+ ); } -function ProcessingPromptFileDialogStep3({ onNext }: { onNext: () => void }) { +function Step3({ back, next }: StepProps) { return ( -
+ +

Shortly, you'll be taken to the "Test" tab. Here you can:

+ + Saving rules -

Next, you can click on a rule to edit it even further.

-

Each rule is made up of two parts: a condition and an action.

-

Our AI sets these up for you, but you can adjust them as needed.

- -
+ ); } -function ProcessingPromptFileDialogStep4({ onNext }: { onNext: () => void }) { +function FinalStepWaiting({ back }: StepProps) { return ( -
- Testing rules -

Test the rules to see how they perform.

-

- This allows you to ensure the rules work as expected before applying - them. -

- -
+ <> + + + Almost done! + + We're almost done. + + +
+ +
+ ); } -function ProcessingPromptFileDialogCarousel({ - currentStep, - onStepChange, -}: { - currentStep: number; - onStepChange: (step: number) => void; +function FinalStepReady({ + back, + next, + result, +}: StepProps & { + result: ResultProps; }) { - const [api, setApi] = useState(); - - useEffect(() => { - if (api) { - api.scrollTo(currentStep); + function getDescription() { + let message = ""; + + if (result.createdRules > 0) { + message += `We've created ${result.createdRules} ${pluralize( + result.createdRules, + "rule", + )} for you.`; } - }, [api, currentStep]); - useEffect(() => { - if (!api) return; + if (result.editedRules > 0) { + message += ` We edited ${result.editedRules} ${pluralize( + result.editedRules, + "rule", + )}.`; + } - api.on("select", () => { - onStepChange(api.selectedScrollSnap()); - }); - }, [api, onStepChange]); + if (result.removedRules > 0) { + message += ` We removed ${result.removedRules} ${pluralize( + result.removedRules, + "rule", + )}.`; + } - const steps = useMemo( - () => [ - {}} />, - {}} />, - {}} />, - {}} />, - ], - [], - ); + return message; + } return ( - - - {steps.map((step, index) => ( - -
- - - {/* {step} */} - {step} - - -
-
- ))} -
- - -
+ <> + + All done! + + {getDescription()} + + + +
+ + +
+ ); } diff --git a/apps/web/app/(app)/automation/RulesPrompt.tsx b/apps/web/app/(app)/automation/RulesPrompt.tsx index 94cb618f7..47d94848b 100644 --- a/apps/web/app/(app)/automation/RulesPrompt.tsx +++ b/apps/web/app/(app)/automation/RulesPrompt.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { useLocalStorage } from "usehooks-ts"; import { SparklesIcon, UserPenIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; @@ -36,7 +37,7 @@ import { AutomationOnboarding } from "@/app/(app)/automation/AutomationOnboardin import { examplePrompts, personas } from "@/app/(app)/automation/examples"; import { PersonaDialog } from "@/app/(app)/automation/PersonaDialog"; import { useModal } from "@/components/Modal"; -// import { ProcessingPromptFileDialog } from "@/app/(app)/automation/ProcessingPromptFileDialog"; +import { ProcessingPromptFileDialog } from "@/app/(app)/automation/ProcessingPromptFileDialog"; export function RulesPrompt() { const { data, isLoading, error, mutate } = useSWR< @@ -44,9 +45,10 @@ export function RulesPrompt() { { error: string } >("/api/user/rules/prompt"); const { isModalOpen, setIsModalOpen } = useModal(); - const onOpenPersonaDialog = useCallback(() => { - setIsModalOpen(true); - }, [setIsModalOpen]); + const onOpenPersonaDialog = useCallback( + () => setIsModalOpen(true), + [setIsModalOpen], + ); const [persona, setPersona] = useState(null); @@ -91,12 +93,18 @@ function RulesPromptForm({ }) { const [isSubmitting, setIsSubmitting] = useState(false); const [isGenerating, setIsGenerating] = useState(false); - // const [isDialogOpen, setIsDialogOpen] = useState(false); - // const [result, setResult] = useState<{ - // createdRules: number; - // editedRules: number; - // removedRules: number; - // }>(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [result, setResult] = useState<{ + createdRules: number; + editedRules: number; + removedRules: number; + }>(); + + const [ + viewedProcessingPromptFileDialog, + setViewedProcessingPromptFileDialog, + ] = useLocalStorage("viewedProcessingPromptFileDialog", false); + const { register, handleSubmit, @@ -133,20 +141,25 @@ function RulesPromptForm({ throw new Error(result.error); } - router.push("/automation?tab=test"); + if (viewedProcessingPromptFileDialog) { + router.push("/automation?tab=test"); + } + mutate(); setIsSubmitting(false); return result; }; - // setIsDialogOpen(true); - // setResult(undefined); + if (!viewedProcessingPromptFileDialog) { + setIsDialogOpen(true); + } + setResult(undefined); toast.promise(() => saveRulesPromise(data), { loading: "Saving rules... This may take a while to process...", success: (result) => { - // setResult(result); + setResult(result); const { createdRules, editedRules, removedRules } = result || {}; const message = [ @@ -164,7 +177,7 @@ function RulesPromptForm({ }, }); }, - [router, mutate], + [mutate, router, viewedProcessingPromptFileDialog], ); const addExamplePrompt = useCallback( @@ -183,12 +196,14 @@ function RulesPromptForm({
- {/* */} + @@ -290,7 +305,7 @@ Let me know if you're interested!
-
+
Examples diff --git a/apps/web/components/ui/carousel.tsx b/apps/web/components/ui/carousel.tsx deleted file mode 100644 index a98fa6b9c..000000000 --- a/apps/web/components/ui/carousel.tsx +++ /dev/null @@ -1,263 +0,0 @@ -"use client"; - -import * as React from "react"; -import useEmblaCarousel, { - type UseEmblaCarouselType, -} from "embla-carousel-react"; -import { ArrowLeft, ArrowRight } from "lucide-react"; - -import { cn } from "@/utils"; -import { Button } from "@/components/ui/button"; - -type CarouselApi = UseEmblaCarouselType[1]; -type UseCarouselParameters = Parameters; -type CarouselOptions = UseCarouselParameters[0]; -type CarouselPlugin = UseCarouselParameters[1]; - -type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugin; - orientation?: "horizontal" | "vertical"; - setApi?: (api: CarouselApi) => void; -}; - -type CarouselContextProps = { - carouselRef: ReturnType[0]; - api: ReturnType[1]; - scrollPrev: () => void; - scrollNext: () => void; - canScrollPrev: boolean; - canScrollNext: boolean; -} & CarouselProps; - -const CarouselContext = React.createContext(null); - -function useCarousel() { - const context = React.useContext(CarouselContext); - - if (!context) { - throw new Error("useCarousel must be used within a "); - } - - return context; -} - -const Carousel = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & CarouselProps ->( - ( - { - orientation = "horizontal", - opts, - setApi, - plugins, - className, - children, - ...props - }, - ref, - ) => { - const [carouselRef, api] = useEmblaCarousel( - { - ...opts, - axis: orientation === "horizontal" ? "x" : "y", - }, - plugins, - ); - const [canScrollPrev, setCanScrollPrev] = React.useState(false); - const [canScrollNext, setCanScrollNext] = React.useState(false); - - const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) { - return; - } - - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }, []); - - const scrollPrev = React.useCallback(() => { - api?.scrollPrev(); - }, [api]); - - const scrollNext = React.useCallback(() => { - api?.scrollNext(); - }, [api]); - - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], - ); - - React.useEffect(() => { - if (!api || !setApi) { - return; - } - - setApi(api); - }, [api, setApi]); - - React.useEffect(() => { - if (!api) { - return; - } - - onSelect(api); - api.on("reInit", onSelect); - api.on("select", onSelect); - - return () => { - api?.off("select", onSelect); - }; - }, [api, onSelect]); - - return ( - -
- role="region" - aria-roledescription="carousel" - {...props} - > - {children} -
-
- ); - }, -); -Carousel.displayName = "Carousel"; - -const CarouselContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const { carouselRef, orientation } = useCarousel(); - - return ( -
-
-
- ); -}); -CarouselContent.displayName = "CarouselContent"; - -const CarouselItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const { orientation } = useCarousel(); - - return ( -
- ); -}); -CarouselItem.displayName = "CarouselItem"; - -const CarouselPrevious = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollPrev, canScrollPrev } = useCarousel(); - - return ( - - ); -}); -CarouselPrevious.displayName = "CarouselPrevious"; - -const CarouselNext = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollNext, canScrollNext } = useCarousel(); - - return ( - - ); -}); -CarouselNext.displayName = "CarouselNext"; - -export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, -}; diff --git a/apps/web/package.json b/apps/web/package.json index 2f88bed9e..e2a142fb8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -79,7 +79,6 @@ "crisp-sdk-web": "^1.0.25", "date-fns": "^3.6.0", "diff": "^7.0.0", - "embla-carousel-react": "^8.5.1", "encoding": "^0.1.13", "eslint": "^8.57.1", "eslint-config-next": "14.2.15", diff --git a/apps/web/public/images/automation/process.png b/apps/web/public/images/automation/process.png new file mode 100644 index 000000000..f45e30ac5 Binary files /dev/null and b/apps/web/public/images/automation/process.png differ diff --git a/apps/web/public/images/automation/rule-edit.png b/apps/web/public/images/automation/rule-edit.png new file mode 100644 index 000000000..7884a2913 Binary files /dev/null and b/apps/web/public/images/automation/rule-edit.png differ diff --git a/apps/web/public/images/automation/rules.png b/apps/web/public/images/automation/rules.png new file mode 100644 index 000000000..fd9139279 Binary files /dev/null and b/apps/web/public/images/automation/rules.png differ diff --git a/apps/web/utils/string.ts b/apps/web/utils/string.ts index 8e617f765..4b3e4f57c 100644 --- a/apps/web/utils/string.ts +++ b/apps/web/utils/string.ts @@ -36,3 +36,11 @@ export function generalizeSubject(subject = "") { .trim() ); } + +export function pluralize( + count: number, + singular: string, + plural = `${singular}s`, +) { + return count === 1 ? singular : plural; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6381d2764..f81e88919 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -282,9 +282,6 @@ importers: diff: specifier: ^7.0.0 version: 7.0.0 - embla-carousel-react: - specifier: ^8.5.1 - version: 8.5.1(react@18.3.1) encoding: specifier: ^0.1.13 version: 0.1.13 @@ -7197,19 +7194,6 @@ packages: electron-to-chromium@1.5.73: resolution: {integrity: sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==} - embla-carousel-react@8.5.1: - resolution: {integrity: sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==} - peerDependencies: - react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - - embla-carousel-reactive-utils@8.5.1: - resolution: {integrity: sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==} - peerDependencies: - embla-carousel: 8.5.1 - - embla-carousel@8.5.1: - resolution: {integrity: sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==} - emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -20128,18 +20112,6 @@ snapshots: electron-to-chromium@1.5.73: {} - embla-carousel-react@8.5.1(react@18.3.1): - dependencies: - embla-carousel: 8.5.1 - embla-carousel-reactive-utils: 8.5.1(embla-carousel@8.5.1) - react: 18.3.1 - - embla-carousel-reactive-utils@8.5.1(embla-carousel@8.5.1): - dependencies: - embla-carousel: 8.5.1 - - embla-carousel@8.5.1: {} - emoji-regex@10.3.0: {} emoji-regex@8.0.0: {}