"use client"; import useSWR from "swr"; import { InlineLoading } from "../components/loading/loading"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button, buttonVariants } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; interface AutomationsData { id: number; subject: string; query_to_run: string; scheduling_request: string; schedule: string; crontime: string; next: string; } import cronstrue from "cronstrue"; import { zodResolver } from "@hookform/resolvers/zod"; import { UseFormReturn, useForm } from "react-hook-form"; import { z } from "zod"; import { Suspense, useEffect, useState } from "react"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { DialogTitle } from "@radix-ui/react-dialog"; import { Textarea } from "@/components/ui/textarea"; import { LocationData, useIPLocationData, useIsMobileWidth } from "../common/utils"; import styles from "./automations.module.css"; import ShareLink from "../components/shareLink/shareLink"; import { useSearchParams } from "next/navigation"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { CalendarCheck, CalendarDot, CalendarDots, Clock, ClockAfternoon, DotsThreeVertical, Envelope, Lightning, MapPinSimple, Play, Plus, Trash, } from "@phosphor-icons/react"; import { useAuthenticatedData, UserProfile } from "../common/auth"; import LoginPrompt from "../components/loginPrompt/loginPrompt"; import { useToast } from "@/components/ui/use-toast"; import { ToastAction } from "@/components/ui/toast"; import { Alert, AlertDescription } from "@/components/ui/alert"; import SidePanel from "../components/sidePanel/chatHistorySidePanel"; import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; const automationsFetcher = () => window .fetch("/api/automations") .then((res) => res.json()) .catch((err) => console.log(err)); // Standard cron format: minute hour dayOfMonth month dayOfWeek function getEveryBlahFromCron(cron: string) { const cronParts = cron.split(" "); const dayOfMonth = cronParts[2]; const dayOfWeek = cronParts[4]; // If both dayOfMonth and dayOfWeek are '*', it runs every day if (dayOfMonth === "*" && dayOfWeek === "*") { return "Day"; } // If dayOfWeek is not '*', it suggests a specific day of the week, implying a weekly schedule else if (dayOfWeek !== "*") { return "Week"; } // If dayOfMonth is not '*', it suggests a specific day of the month, implying a monthly schedule else if (dayOfMonth !== "*") { return "Month"; } // Default to 'Day' if none of the above conditions are met else { return "Day"; } } function getDayOfWeekFromCron(cron: string) { const cronParts = cron.split(" "); if (cronParts[3] === "*" && cronParts[4] !== "*") { return Number(cronParts[4]); } return undefined; } function getTimeRecurrenceFromCron(cron: string) { const cronParts = cron.split(" "); const hour = cronParts[1]; const minute = cronParts[0]; const period = Number(hour) >= 12 ? "PM" : "AM"; let friendlyHour = Number(hour) > 12 ? Number(hour) - 12 : hour; if (friendlyHour === "00") { friendlyHour = "12"; } let friendlyMinute = minute; if (Number(friendlyMinute) < 10 && friendlyMinute !== "00") { friendlyMinute = `0${friendlyMinute}`; } return `${friendlyHour}:${friendlyMinute} ${period}`; } function getDayOfMonthFromCron(cron: string) { const cronParts = cron.split(" "); return String(cronParts[2]); } function cronToHumanReadableString(cron: string) { return cronstrue.toString(cron); } const frequencies = ["Day", "Week", "Month"]; const daysOfMonth = Array.from({ length: 31 }, (_, i) => String(i + 1)); const weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const timeOptions: string[] = []; const timePeriods = ["AM", "PM"]; // Populate the time selector with options for each hour of the day for (var i = 0; i < timePeriods.length; i++) { for (var hour = 0; hour < 12; hour++) { for (var minute = 0; minute < 60; minute += 15) { // Ensure all minutes are two digits const paddedMinute = String(minute).padStart(2, "0"); const friendlyHour = hour === 0 ? 12 : hour; timeOptions.push(`${friendlyHour}:${paddedMinute} ${timePeriods[i]}`); } } } const timestamp = Date.now(); const suggestedAutomationsMetadata: AutomationsData[] = [ { subject: "Weekly Newsletter", query_to_run: "/research Compile a message including: 1. A recap of news from last week 2. An at-home workout I can do before work 3. A quote to inspire me for the week ahead", schedule: "9AM every Monday", next: "Next run at 9AM on Monday", crontime: "0 9 * * 1", id: timestamp, scheduling_request: "", }, { subject: "Daily Bedtime Story", query_to_run: "Compose a bedtime story that a five-year-old might enjoy. It should not exceed five paragraphs. Appeal to the imagination, but weave in learnings.", schedule: "9PM every night", next: "Next run at 9PM today", crontime: "0 21 * * *", id: timestamp + 1, scheduling_request: "", }, { subject: "Front Page of Hacker News", query_to_run: "/research Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links", schedule: "9PM on every Wednesday", next: "Next run at 9PM on Wednesday", crontime: "0 21 * * 3", id: timestamp + 2, scheduling_request: "", }, { subject: "Market Summary", query_to_run: "/research Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.", schedule: "9AM on every weekday", next: "Next run at 9AM on Monday", crontime: "0 9 * * *", id: timestamp + 3, scheduling_request: "", }, { subject: "Market Crash Notification", query_to_run: "Notify me if the stock market fell by more than 5% today.", schedule: "5PM every evening", next: "Next run at 5PM today", crontime: "0 17 * * *", id: timestamp + 5, scheduling_request: "", }, { subject: "Round-up of research papers about AI in healthcare", query_to_run: "/research Summarize the top 3 research papers about AI in healthcare that were published in the last week. Include links to the full papers.", schedule: "9AM every Friday", next: "Next run at 9AM on Friday", crontime: "0 9 * * 5", id: timestamp + 4, scheduling_request: "", }, ]; function createShareLink(automation: AutomationsData) { const encodedSubject = encodeURIComponent(automation.subject); const encodedQuery = encodeURIComponent(automation.query_to_run); const encodedCrontime = encodeURIComponent(automation.crontime); const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&query=${encodedQuery}&crontime=${encodedCrontime}`; return shareLink; } function deleteAutomation(automationId: string, setIsDeleted: (isDeleted: boolean) => void) { fetch(`/api/automation?automation_id=${automationId}`, { method: "DELETE" }) .then((response) => response.json()) .then((data) => { setIsDeleted(true); }); } function sendAPreview(automationId: string, setToastMessage: (toastMessage: string) => void) { fetch(`/api/trigger/automation?automation_id=${automationId}`, { method: "POST" }) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response; }) .then((automations) => { setToastMessage("Automation triggered. Check your inbox in a few minutes!"); }) .catch((error) => { setToastMessage("Sorry, something went wrong. Try again later."); }); } interface AutomationsCardProps { automation: AutomationsData; isMobileWidth: boolean; locationData?: LocationData | null; suggestedCard?: boolean; setNewAutomationData?: (data: AutomationsData) => void; isLoggedIn: boolean; setShowLoginPrompt: (showLoginPrompt: boolean) => void; authenticatedData: UserProfile | null; } function AutomationsCard(props: AutomationsCardProps) { const [isEditing, setIsEditing] = useState(false); const [updatedAutomationData, setUpdatedAutomationData] = useState( null, ); const [isDeleted, setIsDeleted] = useState(false); const [toastMessage, setToastMessage] = useState(""); const { toast } = useToast(); const automation = props.automation; const [timeRecurrence, setTimeRecurrence] = useState(""); const [intervalString, setIntervalString] = useState(""); useEffect(() => { // The updated automation data, if present, takes priority over the original automation data const automationData = updatedAutomationData || automation; setTimeRecurrence(getTimeRecurrenceFromCron(automationData.crontime)); const frequency = getEveryBlahFromCron(automationData.crontime); if (frequency === "Day") { setIntervalString("Daily"); } else if (frequency === "Week") { const dayOfWeek = getDayOfWeekFromCron(automationData.crontime); if (dayOfWeek === undefined) { setIntervalString("Weekly"); } else { setIntervalString(`${weekDays[dayOfWeek]}`); } } else if (frequency === "Month") { const dayOfMonth = getDayOfMonthFromCron(automationData.crontime); setIntervalString(`Monthly on the ${dayOfMonth}`); } }, [updatedAutomationData, automation]); useEffect(() => { const toastTitle = `Automation: ${updatedAutomationData?.subject || automation.subject}`; if (toastMessage) { toast({ title: toastTitle, description: toastMessage, action: Ok, }); setToastMessage(""); } }, [toastMessage, updatedAutomationData, automation, toast]); if (isDeleted) { return null; } return ( {updatedAutomationData?.subject || automation.subject} {!props.suggestedCard && props.locationData && ( )} { navigator.clipboard.writeText(createShareLink(automation)); }} /> {!props.suggestedCard && ( )} {updatedAutomationData?.query_to_run || automation.query_to_run}
{timeRecurrence}
{intervalString}
{props.suggestedCard && props.setNewAutomationData && ( )}
); } interface SharedAutomationCardProps { locationData?: LocationData | null; setNewAutomationData: (data: AutomationsData) => void; isLoggedIn: boolean; setShowLoginPrompt: (showLoginPrompt: boolean) => void; authenticatedData: UserProfile | null; isMobileWidth: boolean; } function SharedAutomationCard(props: SharedAutomationCardProps) { const searchParams = useSearchParams(); const [isCreating, setIsCreating] = useState(true); const subject = searchParams.get("subject"); const query = searchParams.get("query"); const crontime = searchParams.get("crontime"); if (!subject || !query || !crontime) { return null; } const automation: AutomationsData = { id: 0, subject: decodeURIComponent(subject), query_to_run: decodeURIComponent(query), scheduling_request: "", schedule: cronToHumanReadableString(decodeURIComponent(crontime)), crontime: decodeURIComponent(crontime), next: "", }; return isCreating ? ( ) : null; } const EditAutomationSchema = z.object({ subject: z.optional(z.string()), everyBlah: z.string({ required_error: "Every is required" }), dayOfWeek: z.optional(z.number()), dayOfMonth: z.optional(z.string()), timeRecurrence: z.string({ required_error: "Time Recurrence is required" }), queryToRun: z.string({ required_error: "Query to Run is required" }), }); interface EditCardProps { automation?: AutomationsData; setIsEditing: (completed: boolean) => void; setUpdatedAutomationData: (data: AutomationsData) => void; locationData?: LocationData | null; createNew?: boolean; isLoggedIn: boolean; setShowLoginPrompt: (showLoginPrompt: boolean) => void; authenticatedData: UserProfile | null; } function EditCard(props: EditCardProps) { const automation = props.automation; const form = useForm>({ resolver: zodResolver(EditAutomationSchema), defaultValues: { subject: automation?.subject, everyBlah: automation?.crontime ? getEveryBlahFromCron(automation.crontime) : "Day", dayOfWeek: automation?.crontime ? getDayOfWeekFromCron(automation.crontime) : undefined, timeRecurrence: automation?.crontime ? getTimeRecurrenceFromCron(automation.crontime) : "12:00 PM", dayOfMonth: automation?.crontime ? getDayOfMonthFromCron(automation.crontime) : "1", queryToRun: automation?.query_to_run, }, }); const onSubmit = (values: z.infer) => { const cronFrequency = convertFrequencyToCron( values.everyBlah, values.timeRecurrence, values.dayOfWeek, values.dayOfMonth, ); let updateQueryUrl = `/api/automation?`; updateQueryUrl += `q=${encodeURIComponent(values.queryToRun)}`; if (automation?.id && !props.createNew) { updateQueryUrl += `&automation_id=${encodeURIComponent(automation.id)}`; } if (values.subject) { updateQueryUrl += `&subject=${encodeURIComponent(values.subject)}`; } updateQueryUrl += `&crontime=${encodeURIComponent(cronFrequency)}`; if (props.locationData && props.locationData.city) updateQueryUrl += `&city=${encodeURIComponent(props.locationData.city)}`; if (props.locationData && props.locationData.region) updateQueryUrl += `®ion=${encodeURIComponent(props.locationData.region)}`; if (props.locationData && props.locationData.country) updateQueryUrl += `&country=${encodeURIComponent(props.locationData.country)}`; if (props.locationData && props.locationData.timezone) updateQueryUrl += `&timezone=${encodeURIComponent(props.locationData.timezone)}`; let method = props.createNew ? "POST" : "PUT"; fetch(updateQueryUrl, { method: method }) .then((response) => response.json()) .then((data: AutomationsData) => { props.setIsEditing(false); props.setUpdatedAutomationData({ id: data.id, subject: data.subject || "", query_to_run: data.query_to_run, scheduling_request: data.scheduling_request, schedule: cronToHumanReadableString(data.crontime), crontime: data.crontime, next: data.next, }); }); }; function convertFrequencyToCron( frequency: string, timeRecurrence: string, dayOfWeek?: number, dayOfMonth?: string, ) { let cronString = ""; const minutes = timeRecurrence.split(":")[1].split(" ")[0]; const period = timeRecurrence.split(":")[1].split(" ")[1]; const rawHourAsNumber = Number(timeRecurrence.split(":")[0]); const hours = period === "PM" && rawHourAsNumber < 12 ? String(rawHourAsNumber + 12) : rawHourAsNumber; // Convert Sunday to 0th (from 7th) day of week for server cron format const dayOfWeekNumber = dayOfWeek !== undefined ? (dayOfWeek === 7 ? 0 : dayOfWeek) : "*"; switch (frequency) { case "Day": cronString = `${minutes} ${hours} * * *`; break; case "Week": cronString = `${minutes} ${hours} * * ${dayOfWeekNumber}`; break; case "Month": cronString = `${minutes} ${hours} ${dayOfMonth} * *`; break; } return cronString; } return ( ); } interface AutomationModificationFormProps { form: UseFormReturn>; onSubmit: (values: z.infer) => void; create?: boolean; isLoggedIn: boolean; setShowLoginPrompt: (showLoginPrompt: boolean) => void; authenticatedData: UserProfile | null; locationData: LocationData | null; } function AutomationModificationForm(props: AutomationModificationFormProps) { const [isSaving, setIsSaving] = useState(false); const { errors } = props.form.formState; function recommendationPill( recommendationText: string, onChange: (value: any, event: React.MouseEvent) => void, ) { return ( ); } const recommendationPills = [ "Make a picture of", "Generate a summary of", "Create a newsletter of", "Notify me when", ]; return (
{ props.onSubmit(values); setIsSaving(true); })} className="space-y-6" > Emails will be sent to this address. Timezone and location data will be used to schedule automations. {props.locationData && metadataMap(props.locationData, props.authenticatedData)} {!props.create && ( ( Subject This is the subject of the email you will receive. {errors.subject && ( {errors.subject?.message} )} )} /> )} ( Frequency How often should this automation run? {errors.subject && ( {errors.everyBlah?.message} )} )} /> {props.form.watch("everyBlah") === "Week" && ( ( Every week, on which day should this automation run? {errors.subject && ( {errors.dayOfWeek?.message} )} )} /> )} {props.form.watch("everyBlah") === "Month" && ( ( Every month, on which day should the automation run? {errors.subject && ( {errors.dayOfMonth?.message} )} )} /> )} {(props.form.watch("everyBlah") === "Day" || props.form.watch("everyBlah") == "Week" || props.form.watch("everyBlah") == "Month") && ( ( Time On the days this automation runs, at what time should it run? {errors.subject && ( {errors.timeRecurrence?.message} )} )} /> )} ( Instructions What do you want Khoj to do? {props.create && (
{recommendationPills.map((recommendation) => recommendationPill(recommendation, field.onChange), )}
)}