refactor(automations): improve UI consistency by updating alert messages, enhancing task item layout, and refining timezone selection component

This commit is contained in:
Anish Sarkar 2026-06-03 03:41:03 +05:30
parent eabbfb8c67
commit 75c8063bea
14 changed files with 248 additions and 287 deletions

View file

@ -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>
</>
);

View file

@ -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}

View file

@ -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) => {

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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

View file

@ -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} />
)}
/>
);
}

View file

@ -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>
);

View file

@ -221,18 +221,15 @@ export function SidebarUserProfile({
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-medium">{displayName}</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
<DropdownMenuLabel className="px-2 py-1 font-normal">
<div className="min-w-0">
{/* <p className="truncate text-sm font-medium">{displayName}</p> */}
<p className="truncate text-xs font-semibold leading-tight text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onUserSettings}>
<UserCog className="h-4 w-4" />
{t("user_settings")}
@ -327,7 +324,7 @@ export function SidebarUserProfile({
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<p className="select-none px-2 py-1.5 text-xs text-muted-foreground/50">
<p className="select-none px-2 py-1 text-xs leading-tight text-muted-foreground/50">
v{APP_VERSION}
</p>
</DropdownMenuSubContent>
@ -406,18 +403,15 @@ export function SidebarUserProfile({
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-medium">{displayName}</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
<DropdownMenuLabel className="px-2 py-1 font-normal">
<div className="min-w-0">
<p className="truncate text-sm font-medium">{displayName}</p>
<p className="truncate text-xs font-semibold leading-tight text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onUserSettings}>
<UserCog className="h-4 w-4" />
{t("user_settings")}
@ -512,7 +506,7 @@ export function SidebarUserProfile({
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<p className="select-none px-2 py-1.5 text-xs text-muted-foreground/50">
<p className="select-none px-2 py-1 text-xs leading-tight text-muted-foreground/50">
v{APP_VERSION}
</p>
</DropdownMenuSubContent>

View file

@ -43,9 +43,12 @@ function SelectTrigger({
function SelectContent({
className,
children,
matchTriggerWidth = true,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
matchTriggerWidth?: boolean;
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
@ -64,6 +67,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
matchTriggerWidth &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>