mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
refactor(automations): improve UI consistency by updating alert messages, enhancing task item layout, and refining timezone selection component
This commit is contained in:
parent
eabbfb8c67
commit
75c8063bea
14 changed files with 248 additions and 287 deletions
|
|
@ -63,7 +63,7 @@ export function AutomationsContent({ searchSpaceId }: AutomationsContentProps) {
|
|||
/>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle aria-hidden />
|
||||
<AlertDescription>Couldn't load automations. {error.message}</AlertDescription>
|
||||
<AlertDescription>Couldn't load automations {error.message}</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function AdvancedSection({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Field label="Timeout (seconds)" hint="Wall-clock cap for the whole run.">
|
||||
<Field label="Timeout (seconds)" hint="Wall-clock cap for the whole run">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
|
|
@ -68,7 +68,7 @@ export function AdvancedSection({
|
|||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Max retries" hint="Per-step retry budget.">
|
||||
<Field label="Max retries" hint="Per-step retry budget">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
|
|
@ -86,7 +86,7 @@ export function AdvancedSection({
|
|||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent matchTriggerWidth={false} className="w-auto min-w-48">
|
||||
{BACKOFF_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
|
|
@ -105,7 +105,7 @@ export function AdvancedSection({
|
|||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent matchTriggerWidth={false} className="w-auto min-w-64">
|
||||
{CONCURRENCY_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { AlertCircle, Code2, LayoutList, Save } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { AlertCircle, Code2, LayoutList } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
|
|
@ -15,7 +15,9 @@ import {
|
|||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
type Automation,
|
||||
|
|
@ -36,7 +38,6 @@ import {
|
|||
hasResolvedModels,
|
||||
hydrateForm,
|
||||
} from "@/lib/automations/builder-schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AdvancedSection } from "./advanced-section";
|
||||
import { AutomationModelFields } from "./automation-model-fields";
|
||||
import { BasicsSection } from "./basics-section";
|
||||
|
|
@ -57,6 +58,7 @@ interface AutomationBuilderFormProps {
|
|||
* eligibility itself is now owned by the in-form pickers.
|
||||
*/
|
||||
submitDisabledReason?: string;
|
||||
renderModeSwitcher?: (modeSwitcher: ReactNode) => ReactNode;
|
||||
}
|
||||
|
||||
type Mode = "form" | "json";
|
||||
|
|
@ -79,6 +81,7 @@ export function AutomationBuilderForm({
|
|||
searchSpaceId,
|
||||
automation,
|
||||
submitDisabledReason,
|
||||
renderModeSwitcher,
|
||||
}: AutomationBuilderFormProps) {
|
||||
const router = useRouter();
|
||||
const { mutateAsync: createAutomation } = useAtomValue(createAutomationMutationAtom);
|
||||
|
|
@ -98,7 +101,7 @@ export function AutomationBuilderForm({
|
|||
return {
|
||||
mode: "json" as Mode,
|
||||
form: createEmptyForm(),
|
||||
notice: `This automation ${result.reason}, which the form can't show. Edit it as JSON below.`,
|
||||
notice: `This automation ${result.reason}, which the form can't show. Edit it as JSON below`,
|
||||
};
|
||||
}
|
||||
return { mode: "form" as Mode, form: createEmptyForm(), notice: undefined };
|
||||
|
|
@ -117,11 +120,6 @@ export function AutomationBuilderForm({
|
|||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const cancelHref =
|
||||
mode === "edit" && automation
|
||||
? `/dashboard/${searchSpaceId}/automations/${automation.id}`
|
||||
: `/dashboard/${searchSpaceId}/automations`;
|
||||
|
||||
// Eligible models + the search-space-seeded defaults. Models are chosen per
|
||||
// automation on create; in edit mode the backend preserves the captured
|
||||
// snapshot, so the picker is create-only.
|
||||
|
|
@ -193,7 +191,7 @@ export function AutomationBuilderForm({
|
|||
// form's own validation enforces completeness on submit.
|
||||
const definition = jsonValue.definition;
|
||||
if (!definition || typeof definition !== "object") {
|
||||
return { ok: false, issues: [], notice: "Add a definition before switching to the form." };
|
||||
return { ok: false, issues: [], notice: "Add a definition before switching to the form" };
|
||||
}
|
||||
|
||||
const name =
|
||||
|
|
@ -211,7 +209,7 @@ export function AutomationBuilderForm({
|
|||
const h = hydrateForm(name, description, definition, triggers);
|
||||
return h.formable
|
||||
? { ok: true, form: h.form }
|
||||
: { ok: false, issues: [], notice: `Can't show in the form: it ${h.reason}.` };
|
||||
: { ok: false, issues: [], notice: `Can't show in the form: it ${h.reason}` };
|
||||
}
|
||||
|
||||
function validateForm(): Record<string, string> | null {
|
||||
|
|
@ -329,28 +327,44 @@ export function AutomationBuilderForm({
|
|||
: undefined);
|
||||
// Only gate creation; editing an existing automation isn't blocked here.
|
||||
const submitBlocked = mode === "create" && !!effectiveDisabledReason;
|
||||
const modeSwitcher = (
|
||||
<Tabs
|
||||
value={activeMode}
|
||||
onValueChange={(value) => {
|
||||
if (value === activeMode) return;
|
||||
if (value === "form") switchToForm();
|
||||
else if (value === "json") switchToJson();
|
||||
}}
|
||||
>
|
||||
<TabsList className="h-6 gap-0 rounded-md bg-muted/60 p-0.5 select-none">
|
||||
<TabsTrigger
|
||||
value="form"
|
||||
className="h-5 gap-1 px-1.5 text-[11px] select-none focus-visible:ring-0 focus-visible:ring-offset-0 data-[state=active]:bg-muted-foreground/25 data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||
>
|
||||
<LayoutList className="size-3 shrink-0" />
|
||||
<span className="leading-none">Form</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="json"
|
||||
className="h-5 gap-1 px-1.5 text-[11px] select-none focus-visible:ring-0 focus-visible:ring-offset-0 data-[state=active]:bg-muted-foreground/25 data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||
>
|
||||
<Code2 className="size-3 shrink-0" />
|
||||
<span className="leading-none">Edit as JSON</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="inline-flex rounded-md border border-border/60 p-0.5">
|
||||
<ModeButton
|
||||
active={activeMode === "form"}
|
||||
icon={LayoutList}
|
||||
label="Form"
|
||||
onClick={() => (activeMode === "form" ? undefined : switchToForm())}
|
||||
/>
|
||||
<ModeButton
|
||||
active={activeMode === "json"}
|
||||
icon={Code2}
|
||||
label="Edit as JSON"
|
||||
onClick={() => (activeMode === "json" ? undefined : switchToJson())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{renderModeSwitcher ? (
|
||||
renderModeSwitcher(modeSwitcher)
|
||||
) : (
|
||||
<div className="flex items-center justify-end">{modeSwitcher}</div>
|
||||
)}
|
||||
|
||||
{activeMode === "json" ? (
|
||||
<Card className="border-border/60 bg-accent">
|
||||
<Card className="rounded-md border-accent bg-accent/20">
|
||||
<CardContent className="pt-6">
|
||||
<JsonModePanel
|
||||
value={jsonValue}
|
||||
|
|
@ -362,86 +376,88 @@ export function AutomationBuilderForm({
|
|||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<div className="space-y-4 lg:col-span-2">
|
||||
<Card className="border-border/60 bg-accent">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Basics</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BasicsSection
|
||||
name={form.name}
|
||||
description={form.description}
|
||||
errors={errors}
|
||||
onChange={patchForm}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border/60 bg-accent">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Tasks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<TaskList
|
||||
tasks={form.tasks}
|
||||
errors={errors}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onChange={(tasks) => patchForm({ tasks })}
|
||||
/>
|
||||
<UnattendedToggle
|
||||
checked={form.unattended}
|
||||
onChange={(unattended) => patchForm({ unattended })}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border/60 bg-accent">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Schedule</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScheduleSection
|
||||
schedule={form.schedule}
|
||||
timezone={form.timezone}
|
||||
errors={errors}
|
||||
onScheduleChange={(schedule) => patchForm({ schedule })}
|
||||
onTimezoneChange={(timezone) => patchForm({ timezone })}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border/60 bg-accent">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Models</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AutomationModelFields
|
||||
searchSpaceId={searchSpaceId}
|
||||
value={resolvedModels}
|
||||
onChange={(patch) => patchForm({ models: { ...form.models, ...patch } })}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border/60 bg-accent">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AdvancedSection
|
||||
execution={form.execution}
|
||||
tags={form.tags}
|
||||
onExecutionChange={(patch) =>
|
||||
patchForm({ execution: { ...form.execution, ...patch } })
|
||||
}
|
||||
onTagsChange={(tags) => patchForm({ tags })}
|
||||
/>
|
||||
</CardContent>
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="rounded-md border-accent bg-accent/20">
|
||||
<section>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Basics</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BasicsSection
|
||||
name={form.name}
|
||||
description={form.description}
|
||||
errors={errors}
|
||||
onChange={patchForm}
|
||||
/>
|
||||
</CardContent>
|
||||
</section>
|
||||
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||
<section>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Tasks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<TaskList
|
||||
tasks={form.tasks}
|
||||
errors={errors}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onChange={(tasks) => patchForm({ tasks })}
|
||||
/>
|
||||
<UnattendedToggle
|
||||
checked={form.unattended}
|
||||
onChange={(unattended) => patchForm({ unattended })}
|
||||
/>
|
||||
</CardContent>
|
||||
</section>
|
||||
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||
<section>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Schedule</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScheduleSection
|
||||
schedule={form.schedule}
|
||||
timezone={form.timezone}
|
||||
errors={errors}
|
||||
onScheduleChange={(schedule) => patchForm({ schedule })}
|
||||
onTimezoneChange={(timezone) => patchForm({ timezone })}
|
||||
/>
|
||||
</CardContent>
|
||||
</section>
|
||||
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||
<section>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Models</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AutomationModelFields
|
||||
searchSpaceId={searchSpaceId}
|
||||
value={resolvedModels}
|
||||
onChange={(patch) => patchForm({ models: { ...form.models, ...patch } })}
|
||||
/>
|
||||
</CardContent>
|
||||
</section>
|
||||
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||
<section>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AdvancedSection
|
||||
execution={form.execution}
|
||||
tags={form.tags}
|
||||
onExecutionChange={(patch) =>
|
||||
patchForm({ execution: { ...form.execution, ...patch } })
|
||||
}
|
||||
onTagsChange={(tags) => patchForm({ tags })}
|
||||
/>
|
||||
</CardContent>
|
||||
</section>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="border-border/60 bg-accent lg:sticky lg:top-4">
|
||||
<Card className="rounded-md border-accent bg-accent/20 lg:sticky lg:top-4">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Summary</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -461,9 +477,6 @@ export function AutomationBuilderForm({
|
|||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button asChild type="button" variant="ghost" size="sm">
|
||||
<Link href={cancelHref}>Cancel</Link>
|
||||
</Button>
|
||||
{submitBlocked ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -476,7 +489,6 @@ export function AutomationBuilderForm({
|
|||
className="cursor-not-allowed opacity-50"
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -491,9 +503,7 @@ export function AutomationBuilderForm({
|
|||
>
|
||||
{submitting ? (
|
||||
<Spinner size="xs" className="mr-2" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
) : null}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -502,34 +512,6 @@ export function AutomationBuilderForm({
|
|||
);
|
||||
}
|
||||
|
||||
function ModeButton({
|
||||
active,
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean;
|
||||
icon: typeof Code2;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-[5px] px-2.5 py-1 text-xs font-medium transition-colors",
|
||||
active
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function extractTriggers(raw: unknown): HydratableTrigger[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map((entry) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { TriangleAlert } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { memo, useId } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -120,13 +120,12 @@ const ModelSelectField = memo(function ModelSelectField({
|
|||
<Field label={label}>
|
||||
<Alert variant="warning">
|
||||
<TriangleAlert aria-hidden />
|
||||
<AlertTitle>No eligible models</AlertTitle>
|
||||
<AlertDescription className="block leading-5">
|
||||
<span className="font-medium text-foreground">No eligible models.</span> Automations
|
||||
need a premium or your own (BYOK) model. Set one up in{" "}
|
||||
Use a premium model or your own (BYOK) model in{" "}
|
||||
<Link href={rolesHref} className="font-medium underline underline-offset-2">
|
||||
role settings
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Field>
|
||||
|
|
@ -155,7 +154,7 @@ const ModelSelectField = memo(function ModelSelectField({
|
|||
<SelectValue placeholder="Select a model" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent matchTriggerWidth={false} className="w-auto min-w-80 max-w-[90vw]">
|
||||
{premium.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Premium</SelectLabel>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import { CalendarClock, CheckCircle2, ListOrdered, type LucideIcon, XCircle } from "lucide-react";
|
||||
import { Dot } from "lucide-react";
|
||||
import { type BuilderForm, scheduleToCron } from "@/lib/automations/builder-schema";
|
||||
import { describeCron } from "@/lib/automations/describe-cron";
|
||||
|
||||
|
|
@ -12,85 +12,70 @@ interface BuilderSummaryProps {
|
|||
* chat ``AutomationDraftPreview`` so the two creation paths feel consistent.
|
||||
*/
|
||||
export function BuilderSummary({ form }: BuilderSummaryProps) {
|
||||
const scheduleLabel = form.schedule
|
||||
? `${describeCron(scheduleToCron(form.schedule))} · ${form.timezone}`
|
||||
: "No schedule — won't run automatically";
|
||||
const automationName = form.name.trim() || "Untitled automation";
|
||||
const scheduleDescription = form.schedule ? describeCron(scheduleToCron(form.schedule)) : null;
|
||||
const taskCountLabel = `${form.tasks.length} task${form.tasks.length === 1 ? "" : "s"}`;
|
||||
const visibleTasks = form.tasks.slice(0, 2);
|
||||
const hiddenTaskCount = form.tasks.length - visibleTasks.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">{form.name.trim() || "Untitled automation"}</p>
|
||||
{form.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">{form.description.trim()}</p>
|
||||
)}
|
||||
<div className="flex flex-col gap-4 text-sm">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="truncate text-sm font-semibold text-muted-foreground" title={automationName}>
|
||||
{automationName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Section icon={CalendarClock} label="Schedule">
|
||||
<p className="text-xs text-foreground">{scheduleLabel}</p>
|
||||
</Section>
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<Section
|
||||
icon={ListOrdered}
|
||||
label={`Tasks · ${form.tasks.length} step${form.tasks.length === 1 ? "" : "s"}`}
|
||||
>
|
||||
<ol className="space-y-1.5 text-xs">
|
||||
{form.tasks.map((task, index) => (
|
||||
<li key={task.id} className="flex items-start gap-2">
|
||||
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-muted-foreground shrink-0 mt-0.5">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 space-y-1">
|
||||
<span className="block text-foreground line-clamp-2">
|
||||
{task.query.trim() || (
|
||||
<span className="text-muted-foreground">No instructions yet</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-3">
|
||||
<SummaryRow label="Schedule">
|
||||
{scheduleDescription ? (
|
||||
<span className="flex flex-wrap items-center gap-x-1 gap-y-0.5">
|
||||
<span>{scheduleDescription}</span>
|
||||
<Dot className="size-4 text-muted-foreground" aria-hidden />
|
||||
<span>{form.timezone}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>No schedule — won't run automatically</span>
|
||||
)}
|
||||
</SummaryRow>
|
||||
|
||||
<SummaryRow label={taskCountLabel}>
|
||||
<ol className="ml-4 space-y-1">
|
||||
{visibleTasks.map((task, index) => (
|
||||
<li key={task.id} className="flex gap-2">
|
||||
<span className="shrink-0 text-muted-foreground">{index + 1}.</span>
|
||||
<span className="line-clamp-1 min-w-0">
|
||||
{task.query.trim() || "No instructions yet"}
|
||||
</span>
|
||||
{task.mentions.length > 0 && (
|
||||
<span className="flex flex-wrap gap-1">
|
||||
{task.mentions.map((mention) => (
|
||||
<span
|
||||
key={`${mention.kind}:${mention.id}`}
|
||||
className="inline-flex max-w-[140px] items-center truncate rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary/70"
|
||||
>
|
||||
@{mention.title}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Section>
|
||||
</li>
|
||||
))}
|
||||
{hiddenTaskCount > 0 && (
|
||||
<li className="text-muted-foreground">+{hiddenTaskCount} more tasks</li>
|
||||
)}
|
||||
</ol>
|
||||
</SummaryRow>
|
||||
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{form.unattended ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" aria-hidden />
|
||||
) : (
|
||||
<XCircle className="h-3.5 w-3.5" aria-hidden />
|
||||
)}
|
||||
{form.unattended ? "Runs without approval prompts" : "Will reject approval prompts"}
|
||||
<SummaryRow label="Approvals">
|
||||
{form.unattended ? "Runs without approval prompts" : "Approval prompts are rejected"}
|
||||
</SummaryRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
icon: Icon,
|
||||
function SummaryRow({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
<Icon className="h-3 w-3" aria-hidden />
|
||||
{label}
|
||||
</div>
|
||||
{children}
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="font-medium text-muted-foreground">{label}</div>
|
||||
<div className="text-foreground">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import { CalendarClock, CalendarOff, Plus, X } from "lucide-react";
|
||||
import { CalendarClock, CalendarOff, Dot, Plus, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
|
|
@ -70,11 +70,12 @@ export function ScheduleSection({
|
|||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between gap-3 rounded-md border border-border/60 bg-background px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border/60 bg-accent px-3 py-2">
|
||||
<div className="flex items-center gap-2 text-sm min-w-0">
|
||||
<CalendarClock className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||
<span className="font-medium text-foreground truncate">{label}</span>
|
||||
<span className="text-muted-foreground shrink-0">· {timezone}</span>
|
||||
<Dot className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||
<span className="text-muted-foreground shrink-0">{timezone}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -135,7 +136,7 @@ function PresetEditor({ model, onChange, onSwitchToCron }: PresetEditorProps) {
|
|||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent matchTriggerWidth={false} className="w-auto min-w-64">
|
||||
{FREQUENCY_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"use client";
|
||||
import { ChevronDown, ChevronUp, Trash2 } from "lucide-react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -43,7 +43,7 @@ export function TaskItem({
|
|||
onRemove,
|
||||
}: TaskItemProps) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background p-3 space-y-3">
|
||||
<div className="rounded-md border border-border/60 bg-transparent p-3 space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="inline-flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-foreground">
|
||||
|
|
@ -103,27 +103,30 @@ export function TaskItem({
|
|||
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="advanced" className="border-b-0">
|
||||
<AccordionTrigger className="py-1.5 text-xs text-muted-foreground hover:no-underline">
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger className="group flex flex-1 items-center justify-between rounded-md py-1.5 text-left text-xs font-medium text-muted-foreground outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring/50">
|
||||
Advanced
|
||||
</AccordionTrigger>
|
||||
<ChevronRight className="pointer-events-none size-4 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="pb-1">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Max retries" hint="Leave blank to use the default.">
|
||||
<Field label="Max retries">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
value={task.maxRetries ?? ""}
|
||||
placeholder="default"
|
||||
placeholder="2 retries"
|
||||
onChange={(e) => onChange({ maxRetries: parseOptionalInt(e.target.value) })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Timeout (seconds)" hint="Leave blank to use the default.">
|
||||
<Field label="Timeout (seconds)">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={task.timeoutSeconds ?? ""}
|
||||
placeholder="default"
|
||||
placeholder="600 seconds"
|
||||
onChange={(e) => onChange({ timeoutSeconds: parseOptionalInt(e.target.value) })}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -53,11 +53,10 @@ export function TaskList({ tasks, errors, searchSpaceId, onChange }: TaskListPro
|
|||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange([...tasks, emptyTask()])}
|
||||
>
|
||||
<Plus className="mr-1.5 h-4 w-4" />
|
||||
<Plus className="h-4 w-4" />
|
||||
Add task
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,22 +35,26 @@ export function TimezoneCombobox({ value, onChange }: TimezoneComboboxProps) {
|
|||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between font-normal"
|
||||
className="w-full justify-between border-popover-border bg-transparent font-normal hover:bg-transparent"
|
||||
>
|
||||
<span className="truncate">{value || "Select timezone"}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
||||
<Command>
|
||||
<PopoverContent
|
||||
className="w-[calc(var(--radix-popover-trigger-width)/3)] min-w-72 max-w-[90vw] overflow-hidden border border-popover-border p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command className="bg-popover">
|
||||
<CommandInput placeholder="Search timezone..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No timezone found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandGroup className="p-0">
|
||||
{timezones.map((tz) => (
|
||||
<CommandItem
|
||||
key={tz}
|
||||
value={tz}
|
||||
className="rounded-none px-3"
|
||||
onSelect={() => {
|
||||
onChange(tz);
|
||||
setOpen(false);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"use client";
|
||||
import { Info } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
interface UnattendedToggleProps {
|
||||
checked: boolean;
|
||||
|
|
@ -15,26 +13,15 @@ interface UnattendedToggleProps {
|
|||
*/
|
||||
export function UnattendedToggle({ checked, onChange }: UnattendedToggleProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 rounded-lg border border-border/60 bg-background px-3 py-3">
|
||||
<div className="flex items-start justify-between gap-3 rounded-md bg-transparent">
|
||||
<div className="space-y-0.5 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Run without asking for approvals
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button" aria-label="More info" className="text-muted-foreground">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
Automations run unattended. With this off, any approval the agent asks for is
|
||||
rejected, which can stall a step.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Auto-approve actions the agent would normally pause to confirm.
|
||||
Tasks run automatically without asking for confirmation
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -38,9 +38,12 @@ export function AutomationNewContent({ searchSpaceId }: AutomationNewContentProp
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutomationNewHeader searchSpaceId={searchSpaceId} />
|
||||
<AutomationBuilderForm mode="create" searchSpaceId={searchSpaceId} />
|
||||
</>
|
||||
<AutomationBuilderForm
|
||||
mode="create"
|
||||
searchSpaceId={searchSpaceId}
|
||||
renderModeSwitcher={(modeSwitcher) => (
|
||||
<AutomationNewHeader searchSpaceId={searchSpaceId} modeSwitcher={modeSwitcher} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,38 @@
|
|||
"use client";
|
||||
import { ArrowLeft, MessageSquarePlus } from "lucide-react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface AutomationNewHeaderProps {
|
||||
searchSpaceId: number;
|
||||
modeSwitcher?: ReactNode;
|
||||
}
|
||||
|
||||
export function AutomationNewHeader({ searchSpaceId }: AutomationNewHeaderProps) {
|
||||
export function AutomationNewHeader({ searchSpaceId, modeSwitcher }: AutomationNewHeaderProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Button asChild variant="ghost" size="sm" className="-ml-2 h-auto px-2 py-1">
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/automations`}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
<ArrowLeft className="mr-1.5 h-3.5 w-3.5" />
|
||||
Back to automations
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Button asChild variant="ghost" size="sm" className="-ml-2 h-auto px-2 py-1">
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/automations`}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
<ArrowLeft className="mr-1.5 h-3.5 w-3.5" />
|
||||
Back to automations
|
||||
</Link>
|
||||
</Button>
|
||||
{modeSwitcher ? <div className="shrink-0 md:hidden">{modeSwitcher}</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-xl md:text-2xl font-semibold text-foreground">New automation</h1>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl">
|
||||
Set up a task and a schedule. Prefer natural language? Use chat instead.
|
||||
Configure the task, schedule, and execution settings for this automation.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>
|
||||
<MessageSquarePlus className="mr-2 h-4 w-4" />
|
||||
Switch to chat
|
||||
</Link>
|
||||
</Button>
|
||||
{modeSwitcher ? <div className="ml-auto hidden shrink-0 md:block">{modeSwitcher}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue