mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend/library): Add preset/trigger support to Library v3 - EOD: 80%
This commit is contained in:
@@ -11,6 +11,7 @@ import { RunAgentModal } from "./components/RunAgentModal/RunAgentModal";
|
||||
import { RunsSidebar } from "./components/RunsSidebar/RunsSidebar";
|
||||
import { SelectedRunView } from "./components/SelectedRunView/SelectedRunView";
|
||||
import { SelectedScheduleView } from "./components/SelectedScheduleView/SelectedScheduleView";
|
||||
import { SelectedTemplateView } from "./components/SelectedTemplateView/SelectedTemplateView";
|
||||
import { useAgentRunsView } from "./useAgentRunsView";
|
||||
|
||||
export function AgentRunsView() {
|
||||
@@ -99,6 +100,23 @@ export function AgentRunsView() {
|
||||
scheduleId={selectedRun.replace("schedule:", "")}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
) : selectedRun.startsWith("preset:") ? (
|
||||
<SelectedTemplateView
|
||||
agent={agent}
|
||||
presetID={selectedRun.replace("preset:", "")}
|
||||
onDelete={(templateId) => {
|
||||
// TODO: Implement template deletion
|
||||
console.log("Delete template:", templateId);
|
||||
}}
|
||||
onRun={(templateId) => {
|
||||
// TODO: Implement template execution
|
||||
console.log("Run template:", templateId);
|
||||
}}
|
||||
onCreateSchedule={(templateId) => {
|
||||
// TODO: Implement schedule creation from template
|
||||
console.log("Create schedule from template:", templateId);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SelectedRunView
|
||||
agent={agent}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useRunsSidebar } from "./useRunsSidebar";
|
||||
import { RunListItem } from "./components/RunListItem";
|
||||
import { ScheduleListItem } from "./components/ScheduleListItem";
|
||||
import { TemplateListItem } from "./components/TemplateListItem";
|
||||
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { InfiniteList } from "@/components/molecules/InfiniteList/InfiniteList";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
@@ -23,6 +24,7 @@ interface RunsSidebarProps {
|
||||
onCountsChange?: (info: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
presetsCount: number;
|
||||
loading?: boolean;
|
||||
}) => void;
|
||||
}
|
||||
@@ -36,8 +38,11 @@ export function RunsSidebar({
|
||||
const {
|
||||
runs,
|
||||
schedules,
|
||||
presets,
|
||||
runsCount,
|
||||
schedulesCount,
|
||||
triggersCount,
|
||||
regularPresetsCount,
|
||||
error,
|
||||
loading,
|
||||
fetchMoreRuns,
|
||||
@@ -69,13 +74,15 @@ export function RunsSidebar({
|
||||
<TabsLine
|
||||
value={tabValue}
|
||||
onValueChange={(v) => {
|
||||
const value = v as "runs" | "scheduled";
|
||||
const value = v as "runs" | "scheduled" | "templates";
|
||||
setTabValue(value);
|
||||
if (value === "runs") {
|
||||
if (runs && runs.length) onSelectRun(runs[0].id);
|
||||
} else {
|
||||
} else if (value === "scheduled") {
|
||||
if (schedules && schedules.length)
|
||||
onSelectRun(`schedule:${schedules[0].id}`);
|
||||
} else if (value === "templates") {
|
||||
if (presets && presets.length) onSelectRun(`preset:${presets[0].id}`);
|
||||
}
|
||||
}}
|
||||
className="min-w-0 overflow-hidden"
|
||||
@@ -87,6 +94,18 @@ export function RunsSidebar({
|
||||
<TabsLineTrigger value="scheduled">
|
||||
Scheduled <span className="ml-3 inline-block">{schedulesCount}</span>
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="templates">
|
||||
Templates{" "}
|
||||
<span className="ml-3 inline-block">
|
||||
{triggersCount > 0 && regularPresetsCount > 0
|
||||
? `${triggersCount} + ${regularPresetsCount}`
|
||||
: triggersCount > 0
|
||||
? `${triggersCount} trigger${triggersCount !== 1 ? "s" : ""}`
|
||||
: regularPresetsCount > 0
|
||||
? `${regularPresetsCount} preset${regularPresetsCount !== 1 ? "s" : ""}`
|
||||
: "0"}
|
||||
</span>
|
||||
</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
|
||||
<>
|
||||
@@ -123,6 +142,36 @@ export function RunsSidebar({
|
||||
))}
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="templates">
|
||||
<div className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-3 lg:overflow-x-hidden">
|
||||
{/* Triggers first */}
|
||||
{presets
|
||||
.filter((preset) => !!preset.webhook)
|
||||
.sort((a, b) => b.updated_at.getTime() - a.updated_at.getTime())
|
||||
.map((preset) => (
|
||||
<div className="w-[15rem] lg:w-full" key={preset.id}>
|
||||
<TemplateListItem
|
||||
preset={preset}
|
||||
selected={selectedRunId === `preset:${preset.id}`}
|
||||
onClick={() => onSelectRun(`preset:${preset.id}`)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{/* Regular presets second */}
|
||||
{presets
|
||||
.filter((preset) => !preset.webhook)
|
||||
.sort((a, b) => b.updated_at.getTime() - a.updated_at.getTime())
|
||||
.map((preset) => (
|
||||
<div className="w-[15rem] lg:w-full" key={preset.id}>
|
||||
<TemplateListItem
|
||||
preset={preset}
|
||||
selected={selectedRunId === `preset:${preset.id}`}
|
||||
onClick={() => onSelectRun(`preset:${preset.id}`)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</>
|
||||
</TabsLine>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ interface RunListItemProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
statusBadge?: React.ReactNode;
|
||||
selected?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
@@ -16,6 +17,7 @@ export function RunSidebarCard({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
statusBadge,
|
||||
selected,
|
||||
onClick,
|
||||
}: RunListItemProps) {
|
||||
@@ -30,12 +32,15 @@ export function RunSidebarCard({
|
||||
<div className="flex min-w-0 items-center justify-start gap-3">
|
||||
{icon}
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start justify-between">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="block w-full truncate text-ellipsis"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="block truncate text-ellipsis"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{statusBadge}
|
||||
</div>
|
||||
<Text variant="small" className="!text-zinc-500">
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import { RunSidebarCard } from "./RunSidebarCard";
|
||||
import { IconWrapper } from "./RunIconWrapper";
|
||||
import { LinkIcon, PushPinIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface TemplateListItemProps {
|
||||
preset: LibraryAgentPreset;
|
||||
selected?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function TemplateListItem({
|
||||
preset,
|
||||
selected,
|
||||
onClick,
|
||||
}: TemplateListItemProps) {
|
||||
const isTrigger = !!preset.webhook;
|
||||
const isActive = preset.is_active ?? false;
|
||||
|
||||
return (
|
||||
<RunSidebarCard
|
||||
title={preset.name}
|
||||
description={preset.description || "No description"}
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
icon={
|
||||
<IconWrapper
|
||||
className={
|
||||
isTrigger
|
||||
? isActive
|
||||
? "border-green-50 bg-green-50"
|
||||
: "border-gray-50 bg-gray-50"
|
||||
: "border-blue-50 bg-blue-50"
|
||||
}
|
||||
>
|
||||
{isTrigger ? (
|
||||
<LinkIcon
|
||||
size={16}
|
||||
className={isActive ? "text-green-700" : "text-gray-700"}
|
||||
weight="bold"
|
||||
/>
|
||||
) : (
|
||||
<PushPinIcon size={16} className="text-blue-700" weight="bold" />
|
||||
)}
|
||||
</IconWrapper>
|
||||
}
|
||||
statusBadge={
|
||||
isTrigger ? (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs ${
|
||||
isActive
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{isActive ? "Active" : "Inactive"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-800">
|
||||
Preset
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
|
||||
import { useGetV2ListPresets } from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import {
|
||||
@@ -20,6 +23,7 @@ type Args = {
|
||||
onCountsChange?: (info: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
presetsCount: number;
|
||||
loading?: boolean;
|
||||
}) => void;
|
||||
};
|
||||
@@ -27,7 +31,9 @@ type Args = {
|
||||
export function useRunsSidebar({ graphId, onSelectRun, onCountsChange }: Args) {
|
||||
const params = useSearchParams();
|
||||
const existingRunId = params.get("executionId") as string | undefined;
|
||||
const [tabValue, setTabValue] = useState<"runs" | "scheduled">("runs");
|
||||
const [tabValue, setTabValue] = useState<"runs" | "scheduled" | "templates">(
|
||||
"runs",
|
||||
);
|
||||
|
||||
const runsQuery = useGetV1ListGraphExecutionsInfinite(
|
||||
graphId || "",
|
||||
@@ -54,23 +60,42 @@ export function useRunsSidebar({ graphId, onSelectRun, onCountsChange }: Args) {
|
||||
},
|
||||
);
|
||||
|
||||
const presetsQuery = useGetV2ListPresets(
|
||||
{ graph_id: graphId || null, page: 1, page_size: 100 },
|
||||
{
|
||||
query: {
|
||||
enabled: !!graphId,
|
||||
select: (r) => okData<LibraryAgentPresetResponse>(r)?.presets ?? [],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const runs = useMemo(
|
||||
() => extractRunsFromPages(runsQuery.data),
|
||||
[runsQuery.data],
|
||||
);
|
||||
|
||||
const schedules = schedulesQuery.data || [];
|
||||
const presets = presetsQuery.data || [];
|
||||
|
||||
const runsCount = computeRunsCount(runsQuery.data, runs.length);
|
||||
const schedulesCount = schedules.length;
|
||||
const loading = !schedulesQuery.isSuccess || !runsQuery.isSuccess;
|
||||
const presetsCount = presets.length;
|
||||
const triggersCount = presets.filter((preset) => !!preset.webhook).length;
|
||||
const regularPresetsCount = presets.filter(
|
||||
(preset) => !preset.webhook,
|
||||
).length;
|
||||
const loading =
|
||||
!schedulesQuery.isSuccess ||
|
||||
!runsQuery.isSuccess ||
|
||||
!presetsQuery.isSuccess;
|
||||
|
||||
// Notify parent about counts and loading state
|
||||
useEffect(() => {
|
||||
if (onCountsChange) {
|
||||
onCountsChange({ runsCount, schedulesCount, loading });
|
||||
onCountsChange({ runsCount, schedulesCount, presetsCount, loading });
|
||||
}
|
||||
}, [runsCount, schedulesCount, loading, onCountsChange]);
|
||||
}, [runsCount, schedulesCount, presetsCount, loading, onCountsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (runs.length > 0) {
|
||||
@@ -85,25 +110,37 @@ export function useRunsSidebar({ graphId, onSelectRun, onCountsChange }: Args) {
|
||||
useEffect(() => {
|
||||
if (existingRunId && existingRunId.startsWith("schedule:"))
|
||||
setTabValue("scheduled");
|
||||
else if (existingRunId && existingRunId.startsWith("preset:"))
|
||||
setTabValue("templates");
|
||||
else setTabValue("runs");
|
||||
}, [existingRunId]);
|
||||
|
||||
// If there are no runs but there are schedules, and nothing is selected, auto-select the first schedule
|
||||
// If there are no runs but there are schedules or presets, auto-select the first available
|
||||
useEffect(() => {
|
||||
if (!existingRunId && runs.length === 0 && schedules.length > 0)
|
||||
onSelectRun(`schedule:${schedules[0].id}`);
|
||||
}, [existingRunId, runs.length, schedules, onSelectRun]);
|
||||
if (!existingRunId && runs.length === 0) {
|
||||
if (schedules.length > 0) {
|
||||
onSelectRun(`schedule:${schedules[0].id}`);
|
||||
} else if (presets.length > 0) {
|
||||
onSelectRun(`preset:${presets[0].id}`);
|
||||
}
|
||||
}
|
||||
}, [existingRunId, runs.length, schedules, presets, onSelectRun]);
|
||||
|
||||
return {
|
||||
runs,
|
||||
schedules,
|
||||
error: schedulesQuery.error || runsQuery.error,
|
||||
presets,
|
||||
error: schedulesQuery.error || runsQuery.error || presetsQuery.error,
|
||||
loading,
|
||||
runsQuery,
|
||||
presetsQuery,
|
||||
tabValue,
|
||||
setTabValue,
|
||||
runsCount,
|
||||
schedulesCount,
|
||||
presetsCount,
|
||||
triggersCount,
|
||||
regularPresetsCount,
|
||||
fetchMoreRuns: runsQuery.fetchNextPage,
|
||||
hasMoreRuns: runsQuery.hasNextPage,
|
||||
isFetchingMoreRuns: runsQuery.isFetchingNextPage,
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
PlayIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
CalendarIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
TabsLine,
|
||||
TabsLineContent,
|
||||
TabsLineList,
|
||||
TabsLineTrigger,
|
||||
} from "@/components/molecules/TabsLine/TabsLine";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import {
|
||||
useGetV2ListPresets,
|
||||
useDeleteV2DeleteAPreset,
|
||||
usePostV2ExecuteAPreset,
|
||||
getGetV2ListPresetsQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
|
||||
import { getGetV1ListGraphExecutionsInfiniteQueryOptions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { AgentInputsReadOnly } from "../AgentInputsReadOnly/AgentInputsReadOnly";
|
||||
import { EditTemplateModal } from "./components/EditTemplateModal";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
presetID: string;
|
||||
onDelete?: (presetID: string) => void;
|
||||
onRun?: (presetID: string) => void;
|
||||
onCreateSchedule?: (presetID: string) => void;
|
||||
}
|
||||
|
||||
export function SelectedTemplateView({
|
||||
agent,
|
||||
presetID,
|
||||
onDelete,
|
||||
onRun,
|
||||
onCreateSchedule,
|
||||
}: Props) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
// Fetch preset data
|
||||
const presetsQuery = useGetV2ListPresets(
|
||||
{ graph_id: agent.graph_id, page: 1, page_size: 100 },
|
||||
{
|
||||
query: {
|
||||
enabled: !!agent.graph_id && !!presetID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const preset = useMemo(() => {
|
||||
const response = okData<LibraryAgentPresetResponse>(presetsQuery.data);
|
||||
const presets = response?.presets ?? [];
|
||||
return presets.find((preset) => preset.id === presetID) || null;
|
||||
}, [presetsQuery.data, presetID]);
|
||||
|
||||
// Delete preset mutation
|
||||
const deleteTemplateMutation = useDeleteV2DeleteAPreset({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Template deleted successfully",
|
||||
variant: "default",
|
||||
});
|
||||
// Invalidate presets list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
|
||||
});
|
||||
setIsDeleting(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Failed to delete template",
|
||||
description: String(error),
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsDeleting(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Execute preset mutation
|
||||
const executeTemplateMutation = usePostV2ExecuteAPreset({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
if (response.status === 200) {
|
||||
toast({
|
||||
title: "Template execution started",
|
||||
variant: "default",
|
||||
});
|
||||
// Invalidate runs list for this graph
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
|
||||
agent.graph_id,
|
||||
).queryKey,
|
||||
});
|
||||
}
|
||||
setIsRunning(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Failed to run template",
|
||||
description: String(error),
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsRunning(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteTemplate = async () => {
|
||||
setIsDeleting(true);
|
||||
deleteTemplateMutation.mutate({ presetId: presetID });
|
||||
};
|
||||
|
||||
const handleRunTemplate = async () => {
|
||||
setIsRunning(true);
|
||||
executeTemplateMutation.mutate({
|
||||
presetId: presetID,
|
||||
data: {
|
||||
inputs: {},
|
||||
credential_inputs: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTemplateSaved = () => {
|
||||
// Invalidate presets list to refresh data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateSchedule = () => {
|
||||
// TODO: Implement schedule creation from preset
|
||||
toast({
|
||||
title: "Schedule creation",
|
||||
description:
|
||||
"Schedule creation from template will be implemented in the next phase",
|
||||
variant: "default",
|
||||
});
|
||||
};
|
||||
|
||||
const isLoading = presetsQuery.isLoading;
|
||||
const error = presetsQuery.error;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCard
|
||||
responseError={
|
||||
error
|
||||
? {
|
||||
message: String(
|
||||
(error as unknown as { message?: string })?.message ||
|
||||
"Failed to load template",
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
httpError={
|
||||
(error as any)?.status
|
||||
? {
|
||||
status: (error as any).status,
|
||||
statusText: (error as any).statusText,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
context="template"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading && !preset) {
|
||||
return (
|
||||
<div className="flex-1 space-y-4">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full flex-col gap-0">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text variant="h2" className="!text-2xl font-bold">
|
||||
{preset?.name || "Loading..."}
|
||||
</Text>
|
||||
<Text variant="body-medium" className="!text-zinc-500">
|
||||
Template • {agent.name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{preset ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleRunTemplate();
|
||||
onRun?.(presetID);
|
||||
}}
|
||||
disabled={isRunning || isDeleting}
|
||||
leftIcon={<PlayIcon size={16} />}
|
||||
>
|
||||
{isRunning ? "Running..." : "Run Template"}
|
||||
</Button>
|
||||
<EditTemplateModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
leftIcon={<PencilIcon size={16} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
preset={preset}
|
||||
onSaved={handleTemplateSaved}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleCreateSchedule();
|
||||
onCreateSchedule?.(presetID);
|
||||
}}
|
||||
leftIcon={<CalendarIcon size={16} />}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleDeleteTemplate();
|
||||
onDelete?.(presetID);
|
||||
}}
|
||||
disabled={isRunning || isDeleting}
|
||||
leftIcon={<TrashIcon size={16} />}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsLine defaultValue="input">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="input">Your input</TabsLineTrigger>
|
||||
<TabsLineTrigger value="details">Template details</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
|
||||
<TabsLineContent value="input">
|
||||
<RunDetailCard>
|
||||
<div className="relative">
|
||||
<AgentInputsReadOnly
|
||||
agent={agent}
|
||||
inputs={preset?.inputs}
|
||||
credentialInputs={preset?.credentials}
|
||||
/>
|
||||
</div>
|
||||
</RunDetailCard>
|
||||
</TabsLineContent>
|
||||
|
||||
<TabsLineContent value="details">
|
||||
<RunDetailCard>
|
||||
{isLoading || !preset ? (
|
||||
<div className="text-neutral-500">Loading…</div>
|
||||
) : (
|
||||
<div className="relative flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Text variant="body-medium" className="!text-black">
|
||||
Name
|
||||
</Text>
|
||||
<p className="text-sm text-zinc-600">{preset.name}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Text variant="body-medium" className="!text-black">
|
||||
Description
|
||||
</Text>
|
||||
<p className="text-sm text-zinc-600">
|
||||
{preset.description || "No description provided"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Text variant="body-medium" className="!text-black">
|
||||
Created
|
||||
</Text>
|
||||
<p className="text-sm text-zinc-600">
|
||||
{new Date(preset.created_at).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Text variant="body-medium" className="!text-black">
|
||||
Last Updated
|
||||
</Text>
|
||||
<p className="text-sm text-zinc-600">
|
||||
{new Date(preset.updated_at).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</RunDetailCard>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { usePatchV2UpdateAnExistingPreset } from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import { ModalHeader } from "../../RunAgentModal/components/ModalHeader/ModalHeader";
|
||||
import { AgentCostSection } from "../../RunAgentModal/components/AgentCostSection/AgentCostSection";
|
||||
import { AgentSectionHeader } from "../../RunAgentModal/components/AgentSectionHeader/AgentSectionHeader";
|
||||
import { ModalRunSection } from "../../RunAgentModal/components/ModalRunSection/ModalRunSection";
|
||||
import { RunAgentModalContextProvider } from "../../RunAgentModal/context";
|
||||
import { AgentDetails } from "../../RunAgentModal/components/AgentDetails/AgentDetails";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
|
||||
interface Props {
|
||||
triggerSlot: React.ReactNode;
|
||||
agent: LibraryAgent;
|
||||
preset: LibraryAgentPreset;
|
||||
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
|
||||
}
|
||||
|
||||
export function EditTemplateModal({
|
||||
triggerSlot,
|
||||
agent,
|
||||
preset,
|
||||
onSaved,
|
||||
}: Props) {
|
||||
const { toast } = useToast();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [presetName, setPresetName] = useState(preset.name);
|
||||
const [presetDescription, setPresetDescription] = useState(
|
||||
preset.description,
|
||||
);
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>(
|
||||
preset.inputs || {},
|
||||
);
|
||||
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
|
||||
preset.credentials || {},
|
||||
);
|
||||
|
||||
// Reset form when preset changes
|
||||
useEffect(() => {
|
||||
setPresetName(preset.name);
|
||||
setPresetDescription(preset.description);
|
||||
setInputValues(preset.inputs || {});
|
||||
setInputCredentials(preset.credentials || {});
|
||||
}, [preset]);
|
||||
|
||||
// Update preset mutation
|
||||
const updatePresetMutation = usePatchV2UpdateAnExistingPreset({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
if (response.status === 200) {
|
||||
toast({
|
||||
title: "Template updated successfully",
|
||||
variant: "default",
|
||||
});
|
||||
setIsOpen(false);
|
||||
onSaved?.(response.data as unknown as LibraryAgentPreset);
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: "Failed to update template",
|
||||
description: error.message || "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Input schema validation (reusing logic from useAgentRunModal)
|
||||
const agentInputSchema = agent.input_schema || {
|
||||
properties: {},
|
||||
required: [],
|
||||
};
|
||||
const agentInputFields = (() => {
|
||||
if (
|
||||
!agentInputSchema ||
|
||||
typeof agentInputSchema !== "object" ||
|
||||
!("properties" in agentInputSchema) ||
|
||||
!agentInputSchema.properties
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
const properties = agentInputSchema.properties as Record<string, any>;
|
||||
return Object.fromEntries(
|
||||
Object.entries(properties).filter(
|
||||
([_, subSchema]: [string, any]) => !subSchema.hidden,
|
||||
),
|
||||
);
|
||||
})();
|
||||
|
||||
const agentCredentialsInputFields = (() => {
|
||||
if (
|
||||
!agent.credentials_input_schema ||
|
||||
typeof agent.credentials_input_schema !== "object" ||
|
||||
!("properties" in agent.credentials_input_schema) ||
|
||||
!agent.credentials_input_schema.properties
|
||||
) {
|
||||
return {} as Record<string, any>;
|
||||
}
|
||||
return agent.credentials_input_schema.properties as Record<string, any>;
|
||||
})();
|
||||
|
||||
const hasAnySetupFields =
|
||||
Object.keys(agentInputFields || {}).length > 0 ||
|
||||
Object.keys(agentCredentialsInputFields || {}).length > 0;
|
||||
|
||||
function handleInputChange(key: string, value: string) {
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleCredentialsChange(key: string, value: any | undefined) {
|
||||
setInputCredentials((prev) => {
|
||||
const next = { ...prev } as Record<string, any>;
|
||||
if (value === undefined) {
|
||||
delete next[key];
|
||||
return next;
|
||||
}
|
||||
next[key] = value;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function handleSetOpen(open: boolean) {
|
||||
setIsOpen(open);
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
updatePresetMutation.mutate({
|
||||
presetId: preset.id,
|
||||
data: {
|
||||
name: presetName,
|
||||
description: presetDescription,
|
||||
inputs: inputValues,
|
||||
credentials: inputCredentials,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const hasChanges =
|
||||
presetName !== preset.name ||
|
||||
presetDescription !== preset.description ||
|
||||
JSON.stringify(inputValues) !== JSON.stringify(preset.inputs || {}) ||
|
||||
JSON.stringify(inputCredentials) !==
|
||||
JSON.stringify(preset.credentials || {});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{ isOpen, set: handleSetOpen }}
|
||||
styling={{ maxWidth: "600px", maxHeight: "90vh" }}
|
||||
>
|
||||
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div className="flex h-full flex-col pb-4">
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0">
|
||||
<ModalHeader agent={agent} />
|
||||
<AgentCostSection flowId={agent.graph_id} />
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
|
||||
{/* Template Info Section */}
|
||||
<div className="mt-10">
|
||||
<AgentSectionHeader title="Template Information" />
|
||||
<div className="mb-10 mt-4 space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">Template Name</label>
|
||||
<Input
|
||||
id="template_name"
|
||||
label="Template Name"
|
||||
size="small"
|
||||
hideLabel
|
||||
value={presetName}
|
||||
placeholder="Enter template name"
|
||||
onChange={(e) => setPresetName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Template Description
|
||||
</label>
|
||||
<Input
|
||||
id="template_description"
|
||||
label="Template Description"
|
||||
size="small"
|
||||
hideLabel
|
||||
value={presetDescription}
|
||||
placeholder="Enter template description"
|
||||
onChange={(e) => setPresetDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Setup Section */}
|
||||
{hasAnySetupFields ? (
|
||||
<div className="mt-8">
|
||||
<RunAgentModalContextProvider
|
||||
value={{
|
||||
agent,
|
||||
defaultRunType: "manual", // Always manual for templates
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
inputValues,
|
||||
setInputValue: handleInputChange,
|
||||
agentInputFields,
|
||||
inputCredentials,
|
||||
setInputCredentialsValue: handleCredentialsChange,
|
||||
agentCredentialsInputFields,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<AgentSectionHeader title="Template Setup" />
|
||||
<ModalRunSection />
|
||||
</>
|
||||
</RunAgentModalContextProvider>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Agent Details Section */}
|
||||
<div className="mt-8">
|
||||
<AgentSectionHeader title="Agent Details" />
|
||||
<AgentDetails agent={agent} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer
|
||||
className="fixed bottom-1 left-0 z-10 w-full bg-white p-4"
|
||||
style={{ boxShadow: "0px -8px 10px white" }}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsOpen(false)}
|
||||
disabled={updatePresetMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
!hasChanges ||
|
||||
updatePresetMutation.isPending ||
|
||||
!presetName.trim()
|
||||
}
|
||||
>
|
||||
{updatePresetMutation.isPending ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
PlayIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
CalendarIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
interface Props {
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onRun?: () => void;
|
||||
onCreateSchedule?: () => void;
|
||||
isRunning?: boolean;
|
||||
isDeleting?: boolean;
|
||||
}
|
||||
|
||||
export function TemplateActions({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onRun,
|
||||
onCreateSchedule,
|
||||
isRunning = false,
|
||||
isDeleting = false,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{onRun && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onRun}
|
||||
disabled={isRunning || isDeleting}
|
||||
leftIcon={<PlayIcon size={16} />}
|
||||
>
|
||||
{isRunning ? "Running..." : "Run Template"}
|
||||
</Button>
|
||||
)}
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={onEdit}
|
||||
leftIcon={<PencilIcon size={16} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
{onCreateSchedule && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={onCreateSchedule}
|
||||
leftIcon={<CalendarIcon size={16} />}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="small"
|
||||
onClick={onDelete}
|
||||
disabled={isRunning || isDeleting}
|
||||
leftIcon={<TrashIcon size={16} />}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,13 +24,15 @@ export function useAgentRunsView() {
|
||||
const [sidebarCounts, setSidebarCounts] = useState({
|
||||
runsCount: 0,
|
||||
schedulesCount: 0,
|
||||
presetsCount: 0,
|
||||
});
|
||||
const [sidebarLoading, setSidebarLoading] = useState(true);
|
||||
|
||||
const hasAnyItems = useMemo(
|
||||
() =>
|
||||
(sidebarCounts.runsCount ?? 0) > 0 ||
|
||||
(sidebarCounts.schedulesCount ?? 0) > 0,
|
||||
(sidebarCounts.schedulesCount ?? 0) > 0 ||
|
||||
(sidebarCounts.presetsCount ?? 0) > 0,
|
||||
[sidebarCounts],
|
||||
);
|
||||
|
||||
@@ -49,11 +51,13 @@ export function useAgentRunsView() {
|
||||
(counts: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
presetsCount: number;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
setSidebarCounts({
|
||||
runsCount: counts.runsCount,
|
||||
schedulesCount: counts.schedulesCount,
|
||||
presetsCount: counts.presetsCount,
|
||||
});
|
||||
if (counts.loading !== undefined) {
|
||||
setSidebarLoading(counts.loading);
|
||||
|
||||
Reference in New Issue
Block a user