import { DEFAULT_AUTOMATION_SCHEDULE_DRAFT, AUTOMATION_WEEKDAYS, automationScheduleToCron, formatAutomationTime, type AutomationScheduleDraft, type AutomationSchedulePreset, type AutomationScheduleUnit, } from "@agent-native/core/client/agent-page/automation-schedule"; import { TimezoneSelect, browserTimezone, } from "@agent-native/core/client/agent-page/timezone-select"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconBolt, IconCalendarEvent, IconClock, IconCode, IconLoader2, } from "@tabler/icons-react"; import { useEffect, useState } from "react"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./ui/dialog"; import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Textarea } from "./ui/textarea"; type TriggerType = "schedule" | "event" | "webhook"; interface AutomationEvent { name: string; description?: string; } interface CreateAutomationInput { operation: "create"; name: string; scope: "personal" | "organization"; triggerType: TriggerType; body: string; schedule?: string; timezone?: string; event?: string; } interface CreateAutomationResult { created: true; name: string; triggerType: TriggerType; webhookPath: string | null; } const PRESETS: { value: AutomationSchedulePreset; labelKey: string; detailKey: string; label: string; detail: string; draft: Partial; }[] = [ { value: "hourly", labelKey: "jobs.schedulePreset.hourly", detailKey: "jobs.schedulePreset.hourlyDetail", label: "Every hour", detail: "At the top of the hour", draft: { preset: "hourly" }, }, { value: "daily-midnight", labelKey: "jobs.schedulePreset.dailyMidnight", detailKey: "jobs.schedulePreset.dailyMidnightDetail", label: "Every day at midnight", detail: "12:00 AM", draft: { preset: "daily-midnight" }, }, { value: "daily-noon", labelKey: "jobs.schedulePreset.dailyNoon", detailKey: "jobs.schedulePreset.dailyNoonDetail", label: "Every day at noon", detail: "12:00 PM", draft: { preset: "daily-noon" }, }, { value: "weekdays", labelKey: "jobs.schedulePreset.weekdays", detailKey: "jobs.schedulePreset.weekdaysDetail", label: "Every weekday", detail: "Monday to Friday at 9:00 AM", draft: { preset: "weekdays", time: "09:00" }, }, { value: "weekly", labelKey: "jobs.schedulePreset.weekly", detailKey: "jobs.schedulePreset.weeklyDetail", label: "Every week", detail: "Sunday at 9:00 AM", draft: { preset: "weekly", weekday: 0, time: "09:00" }, }, ]; function presetClass(selected: boolean): string { return ( "rounded-lg border px-3 py-2 text-left transition-colors " + (selected ? "border-primary bg-primary/5 text-foreground" : "border-border/70 bg-background hover:bg-muted/50") ); } function scheduleIsValid(value: string): boolean { return value.trim().split(/\s+/).length === 5; } function ScheduleFields({ draft, saving, onChange, }: { draft: AutomationScheduleDraft; saving: boolean; onChange: (patch: Partial) => void; }) { const t = useT(); const customMinute = Number(draft.time.split(":")[1] ?? 0); return (
{draft.unit === "hour" ? ( ) : null} {draft.unit === "week" ? ( ) : null} {draft.unit === "month" ? ( ) : null} {draft.unit !== "hour" ? ( ) : null}
); } function ScheduleBuilder({ saving, timezone, onTimezoneChange, onChange, }: { saving: boolean; timezone: string; onTimezoneChange: (value: string) => void; onChange: (value: string) => void; }) { const t = useT(); const [draft, setDraft] = useState( DEFAULT_AUTOMATION_SCHEDULE_DRAFT, ); const [advanced, setAdvanced] = useState(false); const [advancedValue, setAdvancedValue] = useState("0 * * * *"); const friendly = automationScheduleToCron(draft); const activeSchedule = advanced ? advancedValue.trim() : (friendly.schedule ?? ""); const valid = scheduleIsValid(activeSchedule) && (!friendly.error || advanced); useEffect(() => { onChange(valid ? activeSchedule : ""); }, [activeSchedule, onChange, valid]); function choosePreset( value: AutomationSchedulePreset, presetDraft: Partial, ) { setDraft((current) => ({ ...current, ...presetDraft, preset: value })); setAdvanced(false); } return (
{t("jobs.repeat", { defaultValue: "Repeat" })}
{PRESETS.map((preset) => ( ))}
{!advanced && draft.preset === "custom" ? ( setDraft((current) => ({ ...current, ...patch })) } /> ) : null} {!advanced && friendly.schedule ? (

{t("jobs.schedulePreview", { defaultValue: "Runs {{time}}.", time: draft.preset === "hourly" ? t("jobs.schedulePreview.hourly", { defaultValue: "at the start of every hour", }) : draft.preset === "daily-midnight" ? t("jobs.schedulePreview.dailyMidnight", { defaultValue: "every day at midnight", }) : draft.preset === "daily-noon" ? t("jobs.schedulePreview.dailyNoon", { defaultValue: "every day at noon", }) : t("jobs.schedulePreview.atTime", { defaultValue: "at {{time}}", time: formatAutomationTime(draft.time), }), })}

) : null} {!advanced && friendly.error ? (

{friendly.error === "daily-interval" ? t("jobs.dailyIntervalAdvanced", { defaultValue: "Every few days needs the Advanced cron editor below.", }) : t("jobs.weeklyIntervalAdvanced", { defaultValue: "Custom weekly intervals beyond one week are available under Advanced.", })}

) : null}
{ const next = event.currentTarget.open; if (next && !advanced && friendly.schedule) { setAdvancedValue(friendly.schedule); } setAdvanced(next); }} > {t("jobs.advancedSchedule", { defaultValue: "Advanced - cron expression", })} {t("jobs.show", { defaultValue: "Show" })} {t("jobs.hide", { defaultValue: "Hide" })}
setAdvanced(true)} onChange={(event) => { setAdvanced(true); setAdvancedValue(event.target.value); }} />

{t("jobs.cronFormatHint", { defaultValue: "minute hour day-of-month month day-of-week", })}

{activeSchedule && !valid ? (

{t("jobs.cronFieldCount", { defaultValue: "A cron expression needs exactly 5 fields.", })}

) : null}
); } function EventFields({ open, event, onChange, }: { open: boolean; event: string; onChange: (value: string) => void; }) { const t = useT(); const eventsQuery = useActionQuery( "list-automation-events", {}, { enabled: open, staleTime: 60_000 }, ); return ( <> {eventsQuery.data?.find((item) => item.name === event)?.description ? (

{eventsQuery.data.find((item) => item.name === event)?.description}

) : null} {eventsQuery.error ? (

{t("jobs.loadEventTypesError", { defaultValue: "Could not load event types.", })}

) : null} ); } export interface AutomationCreateDialogProps { open: boolean; initialScope?: "personal" | "organization"; onOpenChange: (open: boolean) => void; onCreated: (result: CreateAutomationResult) => void; } export function AutomationCreateDialog({ open, initialScope = "personal", onOpenChange, onCreated, }: AutomationCreateDialogProps) { const t = useT(); const [name, setName] = useState(""); const [scope, setScope] = useState<"personal" | "organization">(initialScope); const [triggerType, setTriggerType] = useState("schedule"); const [schedule, setSchedule] = useState("0 * * * *"); const [timezone, setTimezone] = useState(browserTimezone()); const [event, setEvent] = useState(""); const [body, setBody] = useState(""); const create = useActionMutation< CreateAutomationResult, CreateAutomationInput >("manage-automation", { onSuccess: onCreated, }); const resetCreate = create.reset; useEffect(() => { if (!open) return; setName(""); setScope(initialScope); setTriggerType("schedule"); setSchedule("0 * * * *"); setTimezone(browserTimezone()); setEvent(""); setBody(""); resetCreate(); }, [initialScope, open, resetCreate]); const canSubmit = name.trim().length > 0 && body.trim().length > 0 && (triggerType !== "event" || event.length > 0) && (triggerType !== "schedule" || scheduleIsValid(schedule)); function submit() { if (!canSubmit) return; create.mutate({ operation: "create", name: name.trim(), scope, triggerType, body: body.trim(), ...(triggerType === "schedule" ? { schedule, timezone } : triggerType === "event" ? { event } : {}), }); } return ( {t("jobs.newAutomation", { defaultValue: "New automation" })} {t("jobs.createAutomationDescription", { defaultValue: "Choose what starts it, then describe the work in plain language.", })}
setTriggerType(value as TriggerType)} > {t("jobs.schedule", { defaultValue: "Schedule" })} {t("jobs.webhook", { defaultValue: "Webhook" })} {t("jobs.appEvent", { defaultValue: "App event" })}

{t("jobs.webhookTriggerDescription", { defaultValue: "Run when a service sends an HTTP POST", })}

{t("jobs.webhookSetupDescription", { defaultValue: "After you save, Dispatch gives you a private URL to paste into GitHub, Stripe, or another webhook provider.", })}