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">
|
<Alert variant="destructive">
|
||||||
<AlertCircle aria-hidden />
|
<AlertCircle aria-hidden />
|
||||||
<AlertDescription>Couldn't load automations. {error.message}</AlertDescription>
|
<AlertDescription>Couldn't load automations {error.message}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function AdvancedSection({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<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
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
|
|
@ -68,7 +68,7 @@ export function AdvancedSection({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Max retries" hint="Per-step retry budget.">
|
<Field label="Max retries" hint="Per-step retry budget">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -86,7 +86,7 @@ export function AdvancedSection({
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent matchTriggerWidth={false} className="w-auto min-w-48">
|
||||||
{BACKOFF_OPTIONS.map((option) => (
|
{BACKOFF_OPTIONS.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
@ -105,7 +105,7 @@ export function AdvancedSection({
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent matchTriggerWidth={false} className="w-auto min-w-64">
|
||||||
{CONCURRENCY_OPTIONS.map((option) => (
|
{CONCURRENCY_OPTIONS.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { AlertCircle, Code2, LayoutList, Save } from "lucide-react";
|
import { AlertCircle, Code2, LayoutList } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import {
|
import {
|
||||||
|
|
@ -15,7 +15,9 @@ import {
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
type Automation,
|
type Automation,
|
||||||
|
|
@ -36,7 +38,6 @@ import {
|
||||||
hasResolvedModels,
|
hasResolvedModels,
|
||||||
hydrateForm,
|
hydrateForm,
|
||||||
} from "@/lib/automations/builder-schema";
|
} from "@/lib/automations/builder-schema";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { AdvancedSection } from "./advanced-section";
|
import { AdvancedSection } from "./advanced-section";
|
||||||
import { AutomationModelFields } from "./automation-model-fields";
|
import { AutomationModelFields } from "./automation-model-fields";
|
||||||
import { BasicsSection } from "./basics-section";
|
import { BasicsSection } from "./basics-section";
|
||||||
|
|
@ -57,6 +58,7 @@ interface AutomationBuilderFormProps {
|
||||||
* eligibility itself is now owned by the in-form pickers.
|
* eligibility itself is now owned by the in-form pickers.
|
||||||
*/
|
*/
|
||||||
submitDisabledReason?: string;
|
submitDisabledReason?: string;
|
||||||
|
renderModeSwitcher?: (modeSwitcher: ReactNode) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mode = "form" | "json";
|
type Mode = "form" | "json";
|
||||||
|
|
@ -79,6 +81,7 @@ export function AutomationBuilderForm({
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
automation,
|
automation,
|
||||||
submitDisabledReason,
|
submitDisabledReason,
|
||||||
|
renderModeSwitcher,
|
||||||
}: AutomationBuilderFormProps) {
|
}: AutomationBuilderFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync: createAutomation } = useAtomValue(createAutomationMutationAtom);
|
const { mutateAsync: createAutomation } = useAtomValue(createAutomationMutationAtom);
|
||||||
|
|
@ -98,7 +101,7 @@ export function AutomationBuilderForm({
|
||||||
return {
|
return {
|
||||||
mode: "json" as Mode,
|
mode: "json" as Mode,
|
||||||
form: createEmptyForm(),
|
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 };
|
return { mode: "form" as Mode, form: createEmptyForm(), notice: undefined };
|
||||||
|
|
@ -117,11 +120,6 @@ export function AutomationBuilderForm({
|
||||||
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
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
|
// Eligible models + the search-space-seeded defaults. Models are chosen per
|
||||||
// automation on create; in edit mode the backend preserves the captured
|
// automation on create; in edit mode the backend preserves the captured
|
||||||
// snapshot, so the picker is create-only.
|
// snapshot, so the picker is create-only.
|
||||||
|
|
@ -193,7 +191,7 @@ export function AutomationBuilderForm({
|
||||||
// form's own validation enforces completeness on submit.
|
// form's own validation enforces completeness on submit.
|
||||||
const definition = jsonValue.definition;
|
const definition = jsonValue.definition;
|
||||||
if (!definition || typeof definition !== "object") {
|
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 =
|
const name =
|
||||||
|
|
@ -211,7 +209,7 @@ export function AutomationBuilderForm({
|
||||||
const h = hydrateForm(name, description, definition, triggers);
|
const h = hydrateForm(name, description, definition, triggers);
|
||||||
return h.formable
|
return h.formable
|
||||||
? { ok: true, form: h.form }
|
? { 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 {
|
function validateForm(): Record<string, string> | null {
|
||||||
|
|
@ -329,28 +327,44 @@ export function AutomationBuilderForm({
|
||||||
: undefined);
|
: undefined);
|
||||||
// Only gate creation; editing an existing automation isn't blocked here.
|
// Only gate creation; editing an existing automation isn't blocked here.
|
||||||
const submitBlocked = mode === "create" && !!effectiveDisabledReason;
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-end">
|
{renderModeSwitcher ? (
|
||||||
<div className="inline-flex rounded-md border border-border/60 p-0.5">
|
renderModeSwitcher(modeSwitcher)
|
||||||
<ModeButton
|
) : (
|
||||||
active={activeMode === "form"}
|
<div className="flex items-center justify-end">{modeSwitcher}</div>
|
||||||
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>
|
|
||||||
|
|
||||||
{activeMode === "json" ? (
|
{activeMode === "json" ? (
|
||||||
<Card className="border-border/60 bg-accent">
|
<Card className="rounded-md border-accent bg-accent/20">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<JsonModePanel
|
<JsonModePanel
|
||||||
value={jsonValue}
|
value={jsonValue}
|
||||||
|
|
@ -362,86 +376,88 @@ export function AutomationBuilderForm({
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||||
<div className="space-y-4 lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<Card className="border-border/60 bg-accent">
|
<Card className="rounded-md border-accent bg-accent/20">
|
||||||
<CardHeader className="pb-3">
|
<section>
|
||||||
<CardTitle className="text-sm font-semibold">Basics</CardTitle>
|
<CardHeader className="pb-3">
|
||||||
</CardHeader>
|
<CardTitle className="text-sm font-semibold">Basics</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<BasicsSection
|
<CardContent>
|
||||||
name={form.name}
|
<BasicsSection
|
||||||
description={form.description}
|
name={form.name}
|
||||||
errors={errors}
|
description={form.description}
|
||||||
onChange={patchForm}
|
errors={errors}
|
||||||
/>
|
onChange={patchForm}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
</CardContent>
|
||||||
|
</section>
|
||||||
<Card className="border-border/60 bg-accent">
|
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||||
<CardHeader className="pb-3">
|
<section>
|
||||||
<CardTitle className="text-sm font-semibold">Tasks</CardTitle>
|
<CardHeader className="pb-3">
|
||||||
</CardHeader>
|
<CardTitle className="text-sm font-semibold">Tasks</CardTitle>
|
||||||
<CardContent className="space-y-4">
|
</CardHeader>
|
||||||
<TaskList
|
<CardContent className="space-y-4">
|
||||||
tasks={form.tasks}
|
<TaskList
|
||||||
errors={errors}
|
tasks={form.tasks}
|
||||||
searchSpaceId={searchSpaceId}
|
errors={errors}
|
||||||
onChange={(tasks) => patchForm({ tasks })}
|
searchSpaceId={searchSpaceId}
|
||||||
/>
|
onChange={(tasks) => patchForm({ tasks })}
|
||||||
<UnattendedToggle
|
/>
|
||||||
checked={form.unattended}
|
<UnattendedToggle
|
||||||
onChange={(unattended) => patchForm({ unattended })}
|
checked={form.unattended}
|
||||||
/>
|
onChange={(unattended) => patchForm({ unattended })}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
</CardContent>
|
||||||
|
</section>
|
||||||
<Card className="border-border/60 bg-accent">
|
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||||
<CardHeader className="pb-3">
|
<section>
|
||||||
<CardTitle className="text-sm font-semibold">Schedule</CardTitle>
|
<CardHeader className="pb-3">
|
||||||
</CardHeader>
|
<CardTitle className="text-sm font-semibold">Schedule</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<ScheduleSection
|
<CardContent>
|
||||||
schedule={form.schedule}
|
<ScheduleSection
|
||||||
timezone={form.timezone}
|
schedule={form.schedule}
|
||||||
errors={errors}
|
timezone={form.timezone}
|
||||||
onScheduleChange={(schedule) => patchForm({ schedule })}
|
errors={errors}
|
||||||
onTimezoneChange={(timezone) => patchForm({ timezone })}
|
onScheduleChange={(schedule) => patchForm({ schedule })}
|
||||||
/>
|
onTimezoneChange={(timezone) => patchForm({ timezone })}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
</CardContent>
|
||||||
|
</section>
|
||||||
<Card className="border-border/60 bg-accent">
|
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||||
<CardHeader className="pb-3">
|
<section>
|
||||||
<CardTitle className="text-sm font-semibold">Models</CardTitle>
|
<CardHeader className="pb-3">
|
||||||
</CardHeader>
|
<CardTitle className="text-sm font-semibold">Models</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<AutomationModelFields
|
<CardContent>
|
||||||
searchSpaceId={searchSpaceId}
|
<AutomationModelFields
|
||||||
value={resolvedModels}
|
searchSpaceId={searchSpaceId}
|
||||||
onChange={(patch) => patchForm({ models: { ...form.models, ...patch } })}
|
value={resolvedModels}
|
||||||
/>
|
onChange={(patch) => patchForm({ models: { ...form.models, ...patch } })}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
</CardContent>
|
||||||
|
</section>
|
||||||
<Card className="border-border/60 bg-accent">
|
<Separator className="mx-auto data-[orientation=horizontal]:w-[calc(100%-6rem)]" />
|
||||||
<CardHeader className="pb-3">
|
<section>
|
||||||
<CardTitle className="text-sm font-semibold">Settings</CardTitle>
|
<CardHeader className="pb-3">
|
||||||
</CardHeader>
|
<CardTitle className="text-sm font-semibold">Settings</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<AdvancedSection
|
<CardContent>
|
||||||
execution={form.execution}
|
<AdvancedSection
|
||||||
tags={form.tags}
|
execution={form.execution}
|
||||||
onExecutionChange={(patch) =>
|
tags={form.tags}
|
||||||
patchForm({ execution: { ...form.execution, ...patch } })
|
onExecutionChange={(patch) =>
|
||||||
}
|
patchForm({ execution: { ...form.execution, ...patch } })
|
||||||
onTagsChange={(tags) => patchForm({ tags })}
|
}
|
||||||
/>
|
onTagsChange={(tags) => patchForm({ tags })}
|
||||||
</CardContent>
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</section>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-1">
|
<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">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-sm font-semibold">Summary</CardTitle>
|
<CardTitle className="text-sm font-semibold">Summary</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -461,9 +477,6 @@ export function AutomationBuilderForm({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button asChild type="button" variant="ghost" size="sm">
|
|
||||||
<Link href={cancelHref}>Cancel</Link>
|
|
||||||
</Button>
|
|
||||||
{submitBlocked ? (
|
{submitBlocked ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -476,7 +489,6 @@ export function AutomationBuilderForm({
|
||||||
className="cursor-not-allowed opacity-50"
|
className="cursor-not-allowed opacity-50"
|
||||||
onClick={(event) => event.preventDefault()}
|
onClick={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
@ -491,9 +503,7 @@ export function AutomationBuilderForm({
|
||||||
>
|
>
|
||||||
{submitting ? (
|
{submitting ? (
|
||||||
<Spinner size="xs" className="mr-2" />
|
<Spinner size="xs" className="mr-2" />
|
||||||
) : (
|
) : null}
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</Button>
|
</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[] {
|
function extractTriggers(raw: unknown): HydratableTrigger[] {
|
||||||
if (!Array.isArray(raw)) return [];
|
if (!Array.isArray(raw)) return [];
|
||||||
return raw.map((entry) => {
|
return raw.map((entry) => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { TriangleAlert } from "lucide-react";
|
import { TriangleAlert } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { memo, useId } from "react";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -120,13 +120,12 @@ const ModelSelectField = memo(function ModelSelectField({
|
||||||
<Field label={label}>
|
<Field label={label}>
|
||||||
<Alert variant="warning">
|
<Alert variant="warning">
|
||||||
<TriangleAlert aria-hidden />
|
<TriangleAlert aria-hidden />
|
||||||
|
<AlertTitle>No eligible models</AlertTitle>
|
||||||
<AlertDescription className="block leading-5">
|
<AlertDescription className="block leading-5">
|
||||||
<span className="font-medium text-foreground">No eligible models.</span> Automations
|
Use a premium model or your own (BYOK) model in{" "}
|
||||||
need a premium or your own (BYOK) model. Set one up in{" "}
|
|
||||||
<Link href={rolesHref} className="font-medium underline underline-offset-2">
|
<Link href={rolesHref} className="font-medium underline underline-offset-2">
|
||||||
role settings
|
role settings
|
||||||
</Link>
|
</Link>
|
||||||
.
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
@ -155,7 +154,7 @@ const ModelSelectField = memo(function ModelSelectField({
|
||||||
<SelectValue placeholder="Select a model" />
|
<SelectValue placeholder="Select a model" />
|
||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent matchTriggerWidth={false} className="w-auto min-w-80 max-w-[90vw]">
|
||||||
{premium.length > 0 ? (
|
{premium.length > 0 ? (
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>Premium</SelectLabel>
|
<SelectLabel>Premium</SelectLabel>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"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 { type BuilderForm, scheduleToCron } from "@/lib/automations/builder-schema";
|
||||||
import { describeCron } from "@/lib/automations/describe-cron";
|
import { describeCron } from "@/lib/automations/describe-cron";
|
||||||
|
|
||||||
|
|
@ -12,85 +12,70 @@ interface BuilderSummaryProps {
|
||||||
* chat ``AutomationDraftPreview`` so the two creation paths feel consistent.
|
* chat ``AutomationDraftPreview`` so the two creation paths feel consistent.
|
||||||
*/
|
*/
|
||||||
export function BuilderSummary({ form }: BuilderSummaryProps) {
|
export function BuilderSummary({ form }: BuilderSummaryProps) {
|
||||||
const scheduleLabel = form.schedule
|
const automationName = form.name.trim() || "Untitled automation";
|
||||||
? `${describeCron(scheduleToCron(form.schedule))} · ${form.timezone}`
|
const scheduleDescription = form.schedule ? describeCron(scheduleToCron(form.schedule)) : null;
|
||||||
: "No schedule — won't run automatically";
|
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 (
|
return (
|
||||||
<div className="space-y-4 text-sm">
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
<div className="space-y-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-foreground">{form.name.trim() || "Untitled automation"}</p>
|
<p className="truncate text-sm font-semibold text-muted-foreground" title={automationName}>
|
||||||
{form.description?.trim() && (
|
{automationName}
|
||||||
<p className="text-xs text-muted-foreground">{form.description.trim()}</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section icon={CalendarClock} label="Schedule">
|
<div className="h-px bg-border/60" />
|
||||||
<p className="text-xs text-foreground">{scheduleLabel}</p>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
<div className="flex flex-col gap-3">
|
||||||
icon={ListOrdered}
|
<SummaryRow label="Schedule">
|
||||||
label={`Tasks · ${form.tasks.length} step${form.tasks.length === 1 ? "" : "s"}`}
|
{scheduleDescription ? (
|
||||||
>
|
<span className="flex flex-wrap items-center gap-x-1 gap-y-0.5">
|
||||||
<ol className="space-y-1.5 text-xs">
|
<span>{scheduleDescription}</span>
|
||||||
{form.tasks.map((task, index) => (
|
<Dot className="size-4 text-muted-foreground" aria-hidden />
|
||||||
<li key={task.id} className="flex items-start gap-2">
|
<span>{form.timezone}</span>
|
||||||
<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">
|
</span>
|
||||||
{index + 1}
|
) : (
|
||||||
</span>
|
<span>No schedule — won't run automatically</span>
|
||||||
<span className="min-w-0 flex-1 space-y-1">
|
)}
|
||||||
<span className="block text-foreground line-clamp-2">
|
</SummaryRow>
|
||||||
{task.query.trim() || (
|
|
||||||
<span className="text-muted-foreground">No instructions yet</span>
|
<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>
|
</span>
|
||||||
{task.mentions.length > 0 && (
|
</li>
|
||||||
<span className="flex flex-wrap gap-1">
|
))}
|
||||||
{task.mentions.map((mention) => (
|
{hiddenTaskCount > 0 && (
|
||||||
<span
|
<li className="text-muted-foreground">+{hiddenTaskCount} more tasks</li>
|
||||||
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"
|
</ol>
|
||||||
>
|
</SummaryRow>
|
||||||
@{mention.title}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<SummaryRow label="Approvals">
|
||||||
{form.unattended ? (
|
{form.unattended ? "Runs without approval prompts" : "Approval prompts are rejected"}
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" aria-hidden />
|
</SummaryRow>
|
||||||
) : (
|
|
||||||
<XCircle className="h-3.5 w-3.5" aria-hidden />
|
|
||||||
)}
|
|
||||||
{form.unattended ? "Runs without approval prompts" : "Will reject approval prompts"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Section({
|
function SummaryRow({
|
||||||
icon: Icon,
|
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
label: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="flex flex-col gap-1 text-xs">
|
||||||
<div className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
<div className="font-medium text-muted-foreground">{label}</div>
|
||||||
<Icon className="h-3 w-3" aria-hidden />
|
<div className="text-foreground">{children}</div>
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
|
|
@ -70,11 +70,12 @@ export function ScheduleSection({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<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">
|
<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 />
|
<CalendarClock className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
<span className="font-medium text-foreground truncate">{label}</span>
|
<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>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -135,7 +136,7 @@ function PresetEditor({ model, onChange, onSwitchToCron }: PresetEditorProps) {
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent matchTriggerWidth={false} className="w-auto min-w-64">
|
||||||
{FREQUENCY_OPTIONS.map((option) => (
|
{FREQUENCY_OPTIONS.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"use client";
|
"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 {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -43,7 +43,7 @@ export function TaskItem({
|
||||||
onRemove,
|
onRemove,
|
||||||
}: TaskItemProps) {
|
}: TaskItemProps) {
|
||||||
return (
|
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">
|
<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 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">
|
<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>
|
<Accordion type="single" collapsible>
|
||||||
<AccordionItem value="advanced" className="border-b-0">
|
<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
|
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">
|
<AccordionContent className="pb-1">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Field label="Max retries" hint="Leave blank to use the default.">
|
<Field label="Max retries">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={10}
|
max={10}
|
||||||
value={task.maxRetries ?? ""}
|
value={task.maxRetries ?? ""}
|
||||||
placeholder="default"
|
placeholder="2 retries"
|
||||||
onChange={(e) => onChange({ maxRetries: parseOptionalInt(e.target.value) })}
|
onChange={(e) => onChange({ maxRetries: parseOptionalInt(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Timeout (seconds)" hint="Leave blank to use the default.">
|
<Field label="Timeout (seconds)">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={task.timeoutSeconds ?? ""}
|
value={task.timeoutSeconds ?? ""}
|
||||||
placeholder="default"
|
placeholder="600 seconds"
|
||||||
onChange={(e) => onChange({ timeoutSeconds: parseOptionalInt(e.target.value) })}
|
onChange={(e) => onChange({ timeoutSeconds: parseOptionalInt(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,10 @@ export function TaskList({ tasks, errors, searchSpaceId, onChange }: TaskListPro
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onChange([...tasks, emptyTask()])}
|
onClick={() => onChange([...tasks, emptyTask()])}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add task
|
Add task
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,22 +35,26 @@ export function TimezoneCombobox({ value, onChange }: TimezoneComboboxProps) {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
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>
|
<span className="truncate">{value || "Select timezone"}</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
<PopoverContent
|
||||||
<Command>
|
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..." />
|
<CommandInput placeholder="Search timezone..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No timezone found.</CommandEmpty>
|
<CommandEmpty>No timezone found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup className="p-0">
|
||||||
{timezones.map((tz) => (
|
{timezones.map((tz) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={tz}
|
key={tz}
|
||||||
value={tz}
|
value={tz}
|
||||||
|
className="rounded-none px-3"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
onChange(tz);
|
onChange(tz);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Info } from "lucide-react";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
interface UnattendedToggleProps {
|
interface UnattendedToggleProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
|
@ -15,26 +13,15 @@ interface UnattendedToggleProps {
|
||||||
*/
|
*/
|
||||||
export function UnattendedToggle({ checked, onChange }: UnattendedToggleProps) {
|
export function UnattendedToggle({ checked, onChange }: UnattendedToggleProps) {
|
||||||
return (
|
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="space-y-0.5 min-w-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
Run without asking for approvals
|
Run without asking for approvals
|
||||||
</span>
|
</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>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,12 @@ export function AutomationNewContent({ searchSpaceId }: AutomationNewContentProp
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AutomationBuilderForm
|
||||||
<AutomationNewHeader searchSpaceId={searchSpaceId} />
|
mode="create"
|
||||||
<AutomationBuilderForm mode="create" searchSpaceId={searchSpaceId} />
|
searchSpaceId={searchSpaceId}
|
||||||
</>
|
renderModeSwitcher={(modeSwitcher) => (
|
||||||
|
<AutomationNewHeader searchSpaceId={searchSpaceId} modeSwitcher={modeSwitcher} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,38 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { ArrowLeft, MessageSquarePlus } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface AutomationNewHeaderProps {
|
interface AutomationNewHeaderProps {
|
||||||
searchSpaceId: number;
|
searchSpaceId: number;
|
||||||
|
modeSwitcher?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AutomationNewHeader({ searchSpaceId }: AutomationNewHeaderProps) {
|
export function AutomationNewHeader({ searchSpaceId, modeSwitcher }: AutomationNewHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Button asChild variant="ghost" size="sm" className="-ml-2 h-auto px-2 py-1">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Link
|
<Button asChild variant="ghost" size="sm" className="-ml-2 h-auto px-2 py-1">
|
||||||
href={`/dashboard/${searchSpaceId}/automations`}
|
<Link
|
||||||
className="text-xs text-muted-foreground"
|
href={`/dashboard/${searchSpaceId}/automations`}
|
||||||
>
|
className="text-xs text-muted-foreground"
|
||||||
<ArrowLeft className="mr-1.5 h-3.5 w-3.5" />
|
>
|
||||||
Back to automations
|
<ArrowLeft className="mr-1.5 h-3.5 w-3.5" />
|
||||||
</Link>
|
Back to automations
|
||||||
</Button>
|
</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="flex items-start justify-between gap-4 flex-wrap">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-xl md:text-2xl font-semibold text-foreground">New automation</h1>
|
<h1 className="text-xl md:text-2xl font-semibold text-foreground">New automation</h1>
|
||||||
<p className="text-sm text-muted-foreground max-w-2xl">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" size="sm">
|
{modeSwitcher ? <div className="ml-auto hidden shrink-0 md:block">{modeSwitcher}</div> : null}
|
||||||
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>
|
|
||||||
<MessageSquarePlus className="mr-2 h-4 w-4" />
|
|
||||||
Switch to chat
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -221,18 +221,15 @@ export function SidebarUserProfile({
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
|
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="px-2 py-1 font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="min-w-0">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
{/* <p className="truncate text-sm font-medium">{displayName}</p> */}
|
||||||
<div className="flex-1 min-w-0">
|
<p className="truncate text-xs font-semibold leading-tight text-muted-foreground">
|
||||||
<p className="truncate text-sm font-medium">{displayName}</p>
|
{user.email}
|
||||||
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onUserSettings}>
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
<UserCog className="h-4 w-4" />
|
<UserCog className="h-4 w-4" />
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
|
|
@ -327,7 +324,7 @@ export function SidebarUserProfile({
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator />
|
<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}
|
v{APP_VERSION}
|
||||||
</p>
|
</p>
|
||||||
</DropdownMenuSubContent>
|
</DropdownMenuSubContent>
|
||||||
|
|
@ -406,18 +403,15 @@ export function SidebarUserProfile({
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
|
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="px-2 py-1 font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="min-w-0">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<p className="truncate text-sm font-medium">{displayName}</p>
|
||||||
<div className="flex-1 min-w-0">
|
<p className="truncate text-xs font-semibold leading-tight text-muted-foreground">
|
||||||
<p className="truncate text-sm font-medium">{displayName}</p>
|
{user.email}
|
||||||
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onUserSettings}>
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
<UserCog className="h-4 w-4" />
|
<UserCog className="h-4 w-4" />
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
|
|
@ -512,7 +506,7 @@ export function SidebarUserProfile({
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator />
|
<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}
|
v{APP_VERSION}
|
||||||
</p>
|
</p>
|
||||||
</DropdownMenuSubContent>
|
</DropdownMenuSubContent>
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,12 @@ function SelectTrigger({
|
||||||
function SelectContent({
|
function SelectContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
matchTriggerWidth = true,
|
||||||
position = "popper",
|
position = "popper",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
||||||
|
matchTriggerWidth?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
|
|
@ -64,6 +67,7 @@ function SelectContent({
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
|
matchTriggerWidth &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue