import { useCallback, useEffect, useState } from "react"; import { Clock, Wand2 } from "lucide-react"; import { Button } from "@nous-research/ui/ui/components/button"; import { Select, SelectOption } from "@nous-research/ui/ui/components/select"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { Card, CardContent } from "@nous-research/ui/ui/components/card"; import { Input } from "@nous-research/ui/ui/components/input"; import { Label } from "@nous-research/ui/ui/components/label"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { useToast } from "@nous-research/ui/hooks/use-toast"; import { Toast } from "@nous-research/ui/ui/components/toast"; import { api } from "@/lib/api"; import type { AutomationBlueprint, AutomationBlueprintField } from "@/lib/api"; import { cn, themedBody } from "@/lib/utils"; interface AutomationBlueprintsProps { profile: string; /** Called after a blueprint is instantiated so the parent can refresh its job list. */ onCreated?: () => void; } /** Initial form values for a blueprint = each field's default (or ""). */ function initialValues(blueprint: AutomationBlueprint): Record { const out: Record = {}; for (const f of blueprint.fields) out[f.name] = f.default ?? ""; return out; } function FieldInput({ field, value, onChange, }: { field: AutomationBlueprintField; value: string; onChange: (v: string) => void; }) { if (field.type === "enum" || field.type === "weekdays") { return ( ); } if (field.type === "time") { return ( onChange(e.target.value)} /> ); } // text return ( onChange(e.target.value)} /> ); } function BlueprintCard({ blueprint, profile, showToast, onCreated, }: { blueprint: AutomationBlueprint; profile: string; showToast: (message: string, type: "error" | "success") => void; onCreated?: () => void; }) { const [open, setOpen] = useState(false); const [values, setValues] = useState>(() => initialValues(blueprint)); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const submit = useCallback(async () => { setSubmitting(true); setError(null); try { const job = await api.instantiateAutomationBlueprint({ blueprint: blueprint.key, values }, profile); const when = job.schedule_display ? ` — ${job.schedule_display}` : ""; showToast(`${blueprint.title} scheduled${when}`, "success"); setOpen(false); setValues(initialValues(blueprint)); onCreated?.(); } catch (e) { // 422 from the API carries the slot-level validation message. const msg = e instanceof Error ? e.message : String(e); setError(msg.replace(/^\d+:\s*/, "")); } finally { setSubmitting(false); } }, [blueprint, values, profile, showToast, onCreated]); return (
{blueprint.title}

{blueprint.description}

{blueprint.tags.map((t) => ( {t} ))}
{open && (
{blueprint.fields.map((f) => (
setValues((prev) => ({ ...prev, [f.name]: v }))} /> {f.help && f.type !== "text" ? (

{f.help}

) : null}
))} {error ? (

{error}

) : null}
)}
); } /** * Automation Blueprints gallery — the form-where-there's-a-screen surface. Each blueprint * card expands into an inline form (one field per typed slot); submitting POSTs * to /api/cron/blueprints/instantiate which fills the blueprint and creates the job * via the same create_job path as everything else. */ export function AutomationBlueprints({ profile, onCreated }: AutomationBlueprintsProps) { const { toast, showToast } = useToast(); const [blueprints, setBlueprints] = useState(null); const [loadError, setLoadError] = useState(null); useEffect(() => { let cancelled = false; api .getAutomationBlueprints() .then((r) => { if (!cancelled) setBlueprints(r.blueprints); }) .catch((e) => { if (!cancelled) setLoadError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; }, []); if (loadError) { return

Couldn't load blueprints: {loadError}

; } if (blueprints === null) { return (
Loading blueprints…
); } if (blueprints.length === 0) { return

No automation blueprints available.

; } return ( <>
{blueprints.map((r) => ( ))}
); } export default AutomationBlueprints;