Files
khoj/src/interface/web/app/automations/page.tsx

1167 lines
50 KiB
TypeScript

"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:
"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;
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<AutomationsData | null>(
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: <ToastAction altText="Dismiss">Ok</ToastAction>,
});
setToastMessage("");
}
}, [toastMessage, updatedAutomationData, automation, toast]);
if (isDeleted) {
return null;
}
return (
<Card
className={`bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border ${styles.automationCard}`}
>
<CardHeader>
<CardTitle className="line-clamp-2 leading-normal flex justify-between">
{updatedAutomationData?.subject || automation.subject}
<Popover>
<PopoverTrigger asChild>
<Button className="bg-background" variant={"ghost"}>
<DotsThreeVertical className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto grid gap-2 text-left bg-secondary">
{!props.suggestedCard && props.locationData && (
<AutomationComponentWrapper
isMobileWidth={props.isMobileWidth}
callToAction="Edit"
createNew={false}
setIsCreating={setIsEditing}
setShowLoginPrompt={props.setShowLoginPrompt}
setNewAutomationData={setUpdatedAutomationData}
authenticatedData={props.authenticatedData}
isCreating={isEditing}
automation={updatedAutomationData || automation}
ipLocationData={props.locationData}
/>
)}
<ShareLink
buttonTitle="Share"
includeIcon={true}
buttonClassName="justify-start px-4 py-2 h-10"
buttonVariant={"outline" as keyof typeof buttonVariants}
title="Share Automation"
description="Copy the link below and share it with your coworkers or friends."
url={createShareLink(automation)}
onShare={() => {
navigator.clipboard.writeText(createShareLink(automation));
}}
/>
{!props.suggestedCard && (
<Button
variant={"outline"}
className="justify-start"
onClick={() => {
sendAPreview(automation.id.toString(), setToastMessage);
}}
>
<Play className="h-4 w-4 mr-2" />
Run Now
</Button>
)}
<Button
variant={"destructive"}
className="justify-start"
onClick={() => {
if (props.suggestedCard) {
setIsDeleted(true);
return;
}
deleteAutomation(automation.id.toString(), setIsDeleted);
}}
>
<Trash className="h-4 w-4 mr-2" />
Delete
</Button>
</PopoverContent>
</Popover>
</CardTitle>
</CardHeader>
<CardContent className="text-secondary-foreground break-all">
{updatedAutomationData?.query_to_run || automation.query_to_run}
</CardContent>
<CardFooter className="flex flex-col items-start md:flex-row md:justify-between md:items-center gap-2">
<div className="flex gap-2">
<div className="flex items-center bg-blue-50 rounded-lg p-1.5 border-blue-200 border dark:bg-blue-800 dark:border-blue-500">
<CalendarCheck className="h-4 w-4 mr-2 text-blue-700 dark:text-blue-300" />
<div className="text-s text-blue-700 dark:text-blue-300">
{timeRecurrence}
</div>
</div>
<div className="flex items-center bg-purple-50 rounded-lg p-1.5 border-purple-200 border dark:bg-purple-800 dark:border-purple-500">
<ClockAfternoon className="h-4 w-4 mr-2 text-purple-700 dark:text-purple-300" />
<div className="text-s text-purple-700 dark:text-purple-300">
{intervalString}
</div>
</div>
</div>
{props.suggestedCard && props.setNewAutomationData && (
<AutomationComponentWrapper
isMobileWidth={props.isMobileWidth}
callToAction="Add"
createNew={true}
setIsCreating={setIsEditing}
setShowLoginPrompt={props.setShowLoginPrompt}
setNewAutomationData={props.setNewAutomationData}
authenticatedData={props.authenticatedData}
isCreating={isEditing}
automation={automation}
ipLocationData={props.locationData}
/>
)}
</CardFooter>
</Card>
);
}
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 ? (
<AutomationComponentWrapper
isMobileWidth={props.isMobileWidth}
callToAction="Shared"
createNew={true}
setIsCreating={setIsCreating}
setShowLoginPrompt={props.setShowLoginPrompt}
setNewAutomationData={props.setNewAutomationData}
authenticatedData={props.authenticatedData}
isCreating={isCreating}
automation={automation}
ipLocationData={props.locationData}
/>
) : 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<z.infer<typeof EditAutomationSchema>>({
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<typeof EditAutomationSchema>) => {
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) {
updateQueryUrl += `&city=${encodeURIComponent(props.locationData.city)}`;
updateQueryUrl += `&region=${encodeURIComponent(props.locationData.region)}`;
updateQueryUrl += `&country=${encodeURIComponent(props.locationData.country)}`;
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 (
<AutomationModificationForm
authenticatedData={props.authenticatedData}
locationData={props.locationData || null}
form={form}
onSubmit={onSubmit}
create={props.createNew}
isLoggedIn={props.isLoggedIn}
setShowLoginPrompt={props.setShowLoginPrompt}
/>
);
}
interface AutomationModificationFormProps {
form: UseFormReturn<z.infer<typeof EditAutomationSchema>>;
onSubmit: (values: z.infer<typeof EditAutomationSchema>) => 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<HTMLButtonElement>) => void,
) {
return (
<Button
className="text-xs bg-slate-50 dark:bg-slate-950 h-auto p-1.5 m-1 rounded-full"
variant="ghost"
key={recommendationText}
onClick={(event) => {
event.preventDefault();
onChange({ target: { value: recommendationText } }, event);
}}
>
{recommendationText}...
</Button>
);
}
const recommendationPills = [
"Make a picture of",
"Generate a summary of",
"Create a newsletter of",
"Notify me when",
];
return (
<Form {...props.form}>
<form
onSubmit={props.form.handleSubmit((values) => {
props.onSubmit(values);
setIsSaving(true);
})}
className="space-y-6"
>
<FormItem className="space-y-1">
<FormDescription>
Emails will be sent to this address. Timezone and location data will be used
to schedule automations.
{props.locationData &&
metadataMap(props.locationData, props.authenticatedData)}
</FormDescription>
</FormItem>
{!props.create && (
<FormField
control={props.form.control}
name="subject"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Subject</FormLabel>
<FormDescription>
This is the subject of the email you will receive.
</FormDescription>
<FormControl>
<Input
placeholder="Digest of Healthcare AI trends"
{...field}
/>
</FormControl>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.subject?.message}</FormMessage>
)}
</FormItem>
)}
/>
)}
<FormField
control={props.form.control}
name="everyBlah"
render={({ field }) => (
<FormItem className="w-full space-y-1">
<FormLabel>Frequency</FormLabel>
<FormDescription>How often should this automation run?</FormDescription>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-[200px]">
<div className="flex items-center">
<CalendarDots className="h-4 w-4 mr-2 inline" />
Every
</div>
<SelectValue placeholder="" />
</SelectTrigger>
</FormControl>
<SelectContent>
{frequencies.map((frequency) => (
<SelectItem key={frequency} value={frequency}>
{frequency}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.everyBlah?.message}</FormMessage>
)}
</FormItem>
)}
/>
{props.form.watch("everyBlah") === "Week" && (
<FormField
control={props.form.control}
name="dayOfWeek"
render={({ field }) => (
<FormItem className="w-full space-y-1">
<FormDescription>
Every week, on which day should this automation run?
</FormDescription>
<Select
onValueChange={(value) => field.onChange(Number(value))}
defaultValue={String(field.value)}
>
<FormControl>
<SelectTrigger className="w-[200px]">
<div className="flex items-center">
<CalendarDot className="h-4 w-4 mr-2 inline" />
On
</div>
<SelectValue placeholder="" />
</SelectTrigger>
</FormControl>
<SelectContent>
{weekDays.map((day, index) => (
<SelectItem key={day} value={String(index)}>
{day}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.dayOfWeek?.message}</FormMessage>
)}
</FormItem>
)}
/>
)}
{props.form.watch("everyBlah") === "Month" && (
<FormField
control={props.form.control}
name="dayOfMonth"
render={({ field }) => (
<FormItem className="w-full space-y-1">
<FormDescription>
Every month, on which day should the automation run?
</FormDescription>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-[200px]">
<div className="flex items-center">
<CalendarDot className="h-4 w-4 mr-2 inline" />
On the
</div>
<SelectValue placeholder="" />
</SelectTrigger>
</FormControl>
<SelectContent>
{daysOfMonth.map((day) => (
<SelectItem key={day} value={day}>
{day}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.dayOfMonth?.message}</FormMessage>
)}
</FormItem>
)}
/>
)}
{(props.form.watch("everyBlah") === "Day" ||
props.form.watch("everyBlah") == "Week" ||
props.form.watch("everyBlah") == "Month") && (
<FormField
control={props.form.control}
name="timeRecurrence"
render={({ field }) => (
<FormItem className="w-full space-y-1">
<FormLabel>Time</FormLabel>
<FormDescription>
On the days this automation runs, at what time should it run?
</FormDescription>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-[200px]">
<div className="flex items-center">
<ClockAfternoon className="h-4 w-4 mr-2 inline" />
At
</div>
<SelectValue placeholder="" />
</SelectTrigger>
</FormControl>
<SelectContent>
{timeOptions.map((timeOption) => (
<SelectItem key={timeOption} value={timeOption}>
{timeOption}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.timeRecurrence?.message}</FormMessage>
)}
</FormItem>
)}
/>
)}
<FormField
control={props.form.control}
name="queryToRun"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Instructions</FormLabel>
<FormDescription>What do you want Khoj to do?</FormDescription>
{props.create && (
<div>
{recommendationPills.map((recommendation) =>
recommendationPill(recommendation, field.onChange),
)}
</div>
)}
<FormControl>
<Textarea
placeholder="Create a summary of the latest news about AI in healthcare."
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.queryToRun?.message}</FormMessage>
)}
</FormItem>
)}
/>
<fieldset disabled={isSaving}>
{props.isLoggedIn ? (
isSaving ? (
<Button type="submit" disabled>
Saving...
</Button>
) : (
<Button type="submit">Save</Button>
)
) : (
<Button
onClick={(event) => {
event.preventDefault();
props.setShowLoginPrompt(true);
}}
variant={"default"}
>
Login to Save
</Button>
)}
</fieldset>
</form>
</Form>
);
}
function metadataMap(ipLocationData: LocationData, authenticatedData: UserProfile | null) {
return (
<div className="flex flex-wrap gap-2 items-center justify-start">
{authenticatedData ? (
<span className="rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm">
<Envelope className="h-4 w-4 mr-2 inline text-orange-500 shadow-sm" />
{authenticatedData.email}
</span>
) : null}
{ipLocationData && (
<span className="rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm">
<MapPinSimple className="h-4 w-4 mr-2 inline text-purple-500" />
{ipLocationData
? `${ipLocationData.city}, ${ipLocationData.country}`
: "Unknown"}
</span>
)}
{ipLocationData && (
<span className="rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm">
<Clock className="h-4 w-4 mr-2 inline text-green-500" />
{ipLocationData ? `${ipLocationData.timezone}` : "Unknown"}
</span>
)}
</div>
);
}
interface AutomationComponentWrapperProps {
isMobileWidth: boolean;
callToAction: string;
createNew: boolean;
setIsCreating: (completed: boolean) => void;
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
setNewAutomationData: (data: AutomationsData) => void;
authenticatedData: UserProfile | null;
isCreating: boolean;
ipLocationData: LocationData | null | undefined;
automation?: AutomationsData;
}
function AutomationComponentWrapper(props: AutomationComponentWrapperProps) {
return props.isMobileWidth ? (
<Drawer
open={props.isCreating}
onOpenChange={(open) => {
props.setIsCreating(open);
}}
>
<DrawerTrigger asChild>
<Button className="shadow-sm justify-start" variant="outline">
<Plus className="h-4 w-4 mr-2" />
{props.callToAction}
</Button>
</DrawerTrigger>
<DrawerContent className="p-2">
<DrawerTitle>Automation</DrawerTitle>
<EditCard
createNew={props.createNew}
automation={props.automation}
setIsEditing={props.setIsCreating}
isLoggedIn={props.authenticatedData ? true : false}
authenticatedData={props.authenticatedData}
setShowLoginPrompt={props.setShowLoginPrompt}
setUpdatedAutomationData={props.setNewAutomationData}
locationData={props.ipLocationData}
/>
</DrawerContent>
</Drawer>
) : (
<Dialog
open={props.isCreating}
onOpenChange={(open) => {
props.setIsCreating(open);
}}
>
<DialogTrigger asChild>
<Button className="shadow-sm justify-start" variant="outline">
<Plus className="h-4 w-4 mr-2" />
{props.callToAction}
</Button>
</DialogTrigger>
<DialogContent className="max-h-[98vh] overflow-y-auto">
<DialogTitle>Automation</DialogTitle>
<EditCard
automation={props.automation}
createNew={props.createNew}
setIsEditing={props.setIsCreating}
isLoggedIn={props.authenticatedData ? true : false}
authenticatedData={props.authenticatedData}
setShowLoginPrompt={props.setShowLoginPrompt}
setUpdatedAutomationData={props.setNewAutomationData}
locationData={props.ipLocationData}
/>
</DialogContent>
</Dialog>
);
}
export default function Automations() {
const authenticatedData = useAuthenticatedData();
const {
data: personalAutomations,
error,
isLoading,
} = useSWR<AutomationsData[]>(authenticatedData ? "automations" : null, automationsFetcher, {
revalidateOnFocus: false,
});
const [isCreating, setIsCreating] = useState(false);
const [newAutomationData, setNewAutomationData] = useState<AutomationsData | null>(null);
const [allNewAutomations, setAllNewAutomations] = useState<AutomationsData[]>([]);
const [suggestedAutomations, setSuggestedAutomations] = useState<AutomationsData[]>([]);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const isMobileWidth = useIsMobileWidth();
const ipLocationData = useIPLocationData();
useEffect(() => {
if (newAutomationData) {
setAllNewAutomations([...allNewAutomations, newAutomationData]);
setNewAutomationData(null);
}
}, [newAutomationData, allNewAutomations]);
useEffect(() => {
const allAutomations = personalAutomations
? personalAutomations.concat(allNewAutomations)
: allNewAutomations;
if (allAutomations) {
setSuggestedAutomations(
suggestedAutomationsMetadata.filter((suggestedAutomation) => {
return (
allAutomations.find(
(automation) => suggestedAutomation.subject === automation.subject,
) === undefined
);
}),
);
}
}, [personalAutomations, allNewAutomations]);
if (error)
return <InlineLoading message="Oops, something went wrong. Please refresh the page." />;
return (
<main className={`w-full mx-auto`}>
<div className={`grid w-full mx-auto`}>
<div className={`${styles.sidePanel} top-0`}>
<SidePanel
conversationId={null}
uploadedFiles={[]}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={`${styles.pageLayout} w-full`}>
<div className="pt-6 md:pt-8 grid gap-1 md:flex md:justify-between">
<h1 className="text-3xl flex items-center">Automations</h1>
<div className="flex flex-wrap gap-2 items-center justify-start">
{authenticatedData ? (
<span className="rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm">
<Envelope className="h-4 w-4 mr-2 inline text-orange-500 shadow-sm" />
{authenticatedData.email}
</span>
) : null}
{ipLocationData && (
<span className="rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm">
<MapPinSimple className="h-4 w-4 mr-2 inline text-purple-500" />
{ipLocationData
? `${ipLocationData.city}, ${ipLocationData.country}`
: "Unknown"}
</span>
)}
{ipLocationData && (
<span className="rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm">
<Clock className="h-4 w-4 mr-2 inline text-green-500" />
{ipLocationData ? `${ipLocationData.timezone}` : "Unknown"}
</span>
)}
</div>
</div>
{showLoginPrompt && (
<LoginPrompt
loginRedirectMessage={"Create an account to make your own automation"}
onOpenChange={setShowLoginPrompt}
/>
)}
<Alert className="bg-secondary border-none my-4">
<AlertDescription>
<Lightning weight={"fill"} className="h-4 w-4 text-purple-400 inline" />
<span className="font-bold">How it works</span> Automations help you
structure your time by automating tasks you do regularly. Build your
own, or try out our presets. Get results straight to your inbox.
</AlertDescription>
</Alert>
<div className="flex justify-between items-center py-4">
<h3 className="text-xl">Your Creations</h3>
{authenticatedData ? (
<AutomationComponentWrapper
isMobileWidth={isMobileWidth}
callToAction="Create Automation"
createNew={true}
setIsCreating={setIsCreating}
setShowLoginPrompt={setShowLoginPrompt}
setNewAutomationData={setNewAutomationData}
authenticatedData={authenticatedData}
isCreating={isCreating}
ipLocationData={ipLocationData}
/>
) : (
<Button
className="shadow-sm"
onClick={() => setShowLoginPrompt(true)}
variant={"outline"}
>
<Plus className="h-4 w-4 mr-2" />
Create Automation
</Button>
)}
</div>
<Suspense>
<SharedAutomationCard
isMobileWidth={isMobileWidth}
authenticatedData={authenticatedData}
locationData={ipLocationData}
isLoggedIn={authenticatedData ? true : false}
setShowLoginPrompt={setShowLoginPrompt}
setNewAutomationData={setNewAutomationData}
/>
</Suspense>
{(!personalAutomations || personalAutomations.length === 0) &&
allNewAutomations.length == 0 &&
!isLoading && (
<div className="px-4">
So empty! Create your own automation to get started.
<div className="mt-4">
{authenticatedData ? (
<AutomationComponentWrapper
isMobileWidth={isMobileWidth}
callToAction="Design Automation"
createNew={true}
setIsCreating={setIsCreating}
setShowLoginPrompt={setShowLoginPrompt}
setNewAutomationData={setNewAutomationData}
authenticatedData={authenticatedData}
isCreating={isCreating}
ipLocationData={ipLocationData}
/>
) : (
<Button
onClick={() => setShowLoginPrompt(true)}
variant={"default"}
>
Design
</Button>
)}
</div>
</div>
)}
{isLoading && <InlineLoading message="booting up your automations" />}
<div className={`${styles.automationsLayout}`}>
{personalAutomations &&
personalAutomations.map((automation) => (
<AutomationsCard
isMobileWidth={isMobileWidth}
key={automation.id}
authenticatedData={authenticatedData}
automation={automation}
locationData={ipLocationData}
isLoggedIn={authenticatedData ? true : false}
setShowLoginPrompt={setShowLoginPrompt}
/>
))}
{allNewAutomations.map((automation) => (
<AutomationsCard
isMobileWidth={isMobileWidth}
key={automation.id}
authenticatedData={authenticatedData}
automation={automation}
locationData={ipLocationData}
isLoggedIn={authenticatedData ? true : false}
setShowLoginPrompt={setShowLoginPrompt}
/>
))}
</div>
<h3 className="text-xl py-4">Try these out</h3>
<div className={`${styles.automationsLayout}`}>
{suggestedAutomations.map((automation) => (
<AutomationsCard
isMobileWidth={isMobileWidth}
setNewAutomationData={setNewAutomationData}
key={automation.id}
authenticatedData={authenticatedData}
automation={automation}
locationData={ipLocationData}
isLoggedIn={authenticatedData ? true : false}
setShowLoginPrompt={setShowLoginPrompt}
suggestedCard={true}
/>
))}
</div>
</div>
</div>
</main>
);
}