'use client' import useSWR from 'swr'; import Loading, { InlineLoading } from '../components/loading/loading'; import { Card, CardDescription, 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 } 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, ClockCounterClockwise, DotsThreeVertical, Envelope, Info, Lightning, MapPinSimple, Pencil, 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, AlertTitle } from "@/components/ui/alert" import SidePanel from '../components/sidePanel/chatHistorySidePanel'; import NavMenu from '../components/navMenu/navMenu'; 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": "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": "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": "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": "", } ]; 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; 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, props.automation]); useEffect(() => { const toastTitle = `Automation: ${updatedAutomationData?.subject || automation.subject}`; if (toastMessage) { toast({ title: toastTitle, description: toastMessage, action: ( Ok ), }) setToastMessage(''); } }, [toastMessage]); if (isDeleted) { return null; } return ( {updatedAutomationData?.subject || automation.subject} { !props.suggestedCard && ( { setIsEditing(open); }} > Edit Automation ) } { !props.suggestedCard && ( ) } { navigator.clipboard.writeText(createShareLink(automation)); }} /> {updatedAutomationData?.query_to_run || automation.query_to_run}
{timeRecurrence}
{intervalString}
{ props.suggestedCard && props.setNewAutomationData && ( { setIsEditing(open); }} > Add Automation ) }
) } interface SharedAutomationCardProps { locationData?: LocationData | null; setNewAutomationData: (data: AutomationsData) => void; isLoggedIn: boolean; setShowLoginPrompt: (showLoginPrompt: boolean) => void; authenticatedData: UserProfile | null; } 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 ( { setIsCreating(open); }} > Create Automation ) } 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=${values.queryToRun}`; if (automation?.id && !props.createNew) { updateQueryUrl += `&automation_id=${automation.id}`; } if (values.subject) { updateQueryUrl += `&subject=${values.subject}`; } updateQueryUrl += `&crontime=${cronFrequency}`; if (props.locationData) { updateQueryUrl += `&city=${props.locationData.city}`; updateQueryUrl += `®ion=${props.locationData.region}`; updateQueryUrl += `&country=${props.locationData.country}`; updateQueryUrl += `&timezone=${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; const dayOfWeekNumber = dayOfWeek ? 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; console.log(errors); 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-8"> Setup 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)) }
) }