mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-08 13:55:06 -05:00
Compare commits
7 Commits
fix/execut
...
pwuts/open
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1562ee0f6 | ||
|
|
d785a89f2e | ||
|
|
c6ce7b9fe9 | ||
|
|
a72d49cd15 | ||
|
|
0252afbef7 | ||
|
|
0778d78e84 | ||
|
|
a0604b1a06 |
@@ -41,6 +41,12 @@ export default defineConfig({
|
||||
useInfiniteQueryParam: "page",
|
||||
},
|
||||
},
|
||||
"getV2List presets": {
|
||||
query: {
|
||||
useInfinite: true,
|
||||
useInfiniteQueryParam: "page",
|
||||
},
|
||||
},
|
||||
"getV1List graph executions": {
|
||||
query: {
|
||||
useInfinite: true,
|
||||
|
||||
@@ -18,7 +18,6 @@ import { parseAsString, useQueryStates } from "nuqs";
|
||||
import { CustomControls } from "./components/CustomControl";
|
||||
import { FloatingSafeModeToggle } from "@/components/molecules/FloatingSafeModeToggle/FloatingSafeModeToggle";
|
||||
import { useGetV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { TriggerAgentBanner } from "./components/TriggerAgentBanner";
|
||||
import { resolveCollisions } from "./helpers/resolve-collision";
|
||||
@@ -34,7 +33,7 @@ export const Flow = () => {
|
||||
{},
|
||||
{
|
||||
query: {
|
||||
select: okData<GraphModel>,
|
||||
select: okData,
|
||||
enabled: !!flowID,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
||||
import { SectionWrap } from "./components/other/SectionWrap";
|
||||
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
|
||||
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
|
||||
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
|
||||
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
|
||||
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
||||
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
||||
@@ -84,7 +85,6 @@ export function NewAgentLibraryView() {
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
agentId={agent.id.toString()}
|
||||
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
|
||||
onScheduleCreated={(schedule) =>
|
||||
handleSelectRun(schedule.id, "scheduled")
|
||||
@@ -109,6 +109,16 @@ export function NewAgentLibraryView() {
|
||||
scheduleId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
) : activeTab === "templates" ? (
|
||||
<SelectedTemplateView
|
||||
agent={agent}
|
||||
presetID={activeItem}
|
||||
onCreateRun={(runId) => handleSelectRun(runId, "runs")}
|
||||
onCreateSchedule={(scheduleId) =>
|
||||
handleSelectRun(scheduleId, "scheduled")
|
||||
}
|
||||
onDelete={handleClearSelectedRun}
|
||||
/>
|
||||
) : (
|
||||
<SelectedRunView
|
||||
agent={agent}
|
||||
|
||||
@@ -13,9 +13,11 @@ export function getCredentialTypeDisplayName(type: string): string {
|
||||
}
|
||||
|
||||
export function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
|
||||
const schema = agent.input_schema as unknown as {
|
||||
properties?: Record<string, any>;
|
||||
} | null;
|
||||
const schema = agent.trigger_setup_info
|
||||
? agent.trigger_setup_info.config_schema
|
||||
: (agent.input_schema as unknown as {
|
||||
properties?: Record<string, any>;
|
||||
} | null);
|
||||
if (!schema || !schema.properties) return {};
|
||||
const properties = schema.properties as Record<string, any>;
|
||||
const visibleEntries = Object.entries(properties).filter(
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { AlarmIcon } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
@@ -20,17 +22,30 @@ import { useAgentRunModal } from "./useAgentRunModal";
|
||||
interface Props {
|
||||
triggerSlot: React.ReactNode;
|
||||
agent: LibraryAgent;
|
||||
agentId: string;
|
||||
agentVersion?: number;
|
||||
initialInputValues?: Record<string, any>;
|
||||
initialInputCredentials?: Record<string, any>;
|
||||
initialPresetName?: string;
|
||||
initialPresetDescription?: string;
|
||||
onRunCreated?: (execution: GraphExecutionMeta) => void;
|
||||
onTriggerSetup?: (preset: LibraryAgentPreset) => void;
|
||||
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
|
||||
editMode?: {
|
||||
preset: LibraryAgentPreset;
|
||||
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function RunAgentModal({
|
||||
triggerSlot,
|
||||
agent,
|
||||
initialInputValues,
|
||||
initialInputCredentials,
|
||||
initialPresetName,
|
||||
initialPresetDescription,
|
||||
onRunCreated,
|
||||
onTriggerSetup,
|
||||
onScheduleCreated,
|
||||
editMode,
|
||||
}: Props) {
|
||||
const {
|
||||
// UI state
|
||||
@@ -54,6 +69,11 @@ export function RunAgentModal({
|
||||
setPresetName,
|
||||
setPresetDescription,
|
||||
|
||||
// Edit mode
|
||||
hasChanges,
|
||||
isUpdatingPreset,
|
||||
handleSave,
|
||||
|
||||
// Validation/readiness
|
||||
allRequiredInputsAreSet,
|
||||
|
||||
@@ -69,6 +89,13 @@ export function RunAgentModal({
|
||||
handleRun,
|
||||
} = useAgentRunModal(agent, {
|
||||
onRun: onRunCreated,
|
||||
onSetupTrigger: onTriggerSetup,
|
||||
onScheduleCreated,
|
||||
initialInputValues,
|
||||
initialInputCredentials,
|
||||
initialPresetName,
|
||||
initialPresetDescription,
|
||||
editMode,
|
||||
});
|
||||
|
||||
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
||||
@@ -114,6 +141,8 @@ export function RunAgentModal({
|
||||
onScheduleCreated?.(schedule);
|
||||
}
|
||||
|
||||
const templateOrTrigger = agent.trigger_setup_info ? "Trigger" : "Template";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
@@ -131,23 +160,64 @@ export function RunAgentModal({
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
|
||||
{/* Template Info Section (Edit Mode Only) */}
|
||||
{editMode && (
|
||||
<div className="mt-10">
|
||||
<AgentSectionHeader
|
||||
title={`${templateOrTrigger} Information`}
|
||||
/>
|
||||
<div className="mb-10 mt-4 space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
{templateOrTrigger} 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">
|
||||
{templateOrTrigger} 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 */}
|
||||
<div className="mt-10">
|
||||
<div className={editMode ? "mt-8" : "mt-10"}>
|
||||
{hasAnySetupFields ? (
|
||||
<RunAgentModalContextProvider
|
||||
value={{
|
||||
agent,
|
||||
defaultRunType,
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
inputValues,
|
||||
setInputValue: handleInputChange,
|
||||
agentInputFields,
|
||||
inputCredentials,
|
||||
setInputCredentialsValue: handleCredentialsChange,
|
||||
agentCredentialsInputFields,
|
||||
presetEditMode: Boolean(
|
||||
editMode || agent.trigger_setup_info,
|
||||
),
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
@@ -176,32 +246,62 @@ export function RunAgentModal({
|
||||
style={{ boxShadow: "0px -8px 10px white" }}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleOpenScheduleModal}
|
||||
disabled={
|
||||
isExecuting || isSettingUpTrigger || !allRequiredInputsAreSet
|
||||
}
|
||||
>
|
||||
<AlarmIcon size={16} />
|
||||
Schedule Agent
|
||||
</Button>
|
||||
<RunActions
|
||||
defaultRunType={defaultRunType}
|
||||
onRun={handleRun}
|
||||
isExecuting={isExecuting}
|
||||
isSettingUpTrigger={isSettingUpTrigger}
|
||||
isRunReady={allRequiredInputsAreSet}
|
||||
/>
|
||||
{editMode ? (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsOpen(false)}
|
||||
disabled={isUpdatingPreset}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
!hasChanges || isUpdatingPreset || !presetName.trim()
|
||||
}
|
||||
>
|
||||
{isUpdatingPreset ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{(defaultRunType == "manual" ||
|
||||
defaultRunType == "schedule") && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleOpenScheduleModal}
|
||||
disabled={
|
||||
isExecuting ||
|
||||
isSettingUpTrigger ||
|
||||
!allRequiredInputsAreSet
|
||||
}
|
||||
>
|
||||
<AlarmIcon size={16} />
|
||||
Schedule Agent
|
||||
</Button>
|
||||
)}
|
||||
<RunActions
|
||||
defaultRunType={defaultRunType}
|
||||
onRun={handleRun}
|
||||
isExecuting={isExecuting}
|
||||
isSettingUpTrigger={isSettingUpTrigger}
|
||||
isRunReady={allRequiredInputsAreSet}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ScheduleAgentModal
|
||||
isOpen={isScheduleModalOpen}
|
||||
onClose={handleCloseScheduleModal}
|
||||
agent={agent}
|
||||
inputValues={inputValues}
|
||||
inputCredentials={inputCredentials}
|
||||
onScheduleCreated={handleScheduleCreated}
|
||||
/>
|
||||
{(defaultRunType == "manual" || defaultRunType == "schedule") && (
|
||||
<ScheduleAgentModal
|
||||
isOpen={isScheduleModalOpen}
|
||||
onClose={handleCloseScheduleModal}
|
||||
agent={agent}
|
||||
inputValues={inputValues}
|
||||
inputCredentials={inputCredentials}
|
||||
onScheduleCreated={handleScheduleCreated}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,24 +13,25 @@ export function ModalRunSection() {
|
||||
const {
|
||||
agent,
|
||||
defaultRunType,
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
inputValues,
|
||||
setInputValue,
|
||||
agentInputFields,
|
||||
inputCredentials,
|
||||
setInputCredentialsValue,
|
||||
agentCredentialsInputFields,
|
||||
presetEditMode,
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
} = useRunAgentModalContext();
|
||||
|
||||
return (
|
||||
<div className="mb-10 mt-4">
|
||||
<div className="mb-10 mt-4 flex flex-col gap-4">
|
||||
{defaultRunType === "automatic-trigger" && <WebhookTriggerBanner />}
|
||||
|
||||
{/* Preset/Trigger fields */}
|
||||
{defaultRunType === "automatic-trigger" && (
|
||||
{presetEditMode && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
@@ -119,7 +120,7 @@ export function ModalRunSection() {
|
||||
|
||||
{/* Selected Credentials Preview */}
|
||||
{Object.keys(inputCredentials).length > 0 && (
|
||||
<div className="mt-6 flex flex-col gap-6">
|
||||
<div className="mt-2 flex flex-col gap-6">
|
||||
{Object.entries(agentCredentialsInputFields || {}).map(
|
||||
([key, _sub]) => {
|
||||
const credential = inputCredentials[key];
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
export function WebhookTriggerBanner() {
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function WebhookTriggerBanner({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border border-blue-200 bg-blue-50 p-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<svg
|
||||
|
||||
@@ -4,14 +4,9 @@ import React, { createContext, useContext } from "react";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { RunVariant } from "./useAgentRunModal";
|
||||
|
||||
export interface RunAgentModalContextValue {
|
||||
export type RunAgentModalContextValue = {
|
||||
agent: LibraryAgent;
|
||||
defaultRunType: RunVariant;
|
||||
// Preset / Trigger
|
||||
presetName: string;
|
||||
setPresetName: (value: string) => void;
|
||||
presetDescription: string;
|
||||
setPresetDescription: (value: string) => void;
|
||||
// Inputs
|
||||
inputValues: Record<string, any>;
|
||||
setInputValue: (key: string, value: any) => void;
|
||||
@@ -20,7 +15,14 @@ export interface RunAgentModalContextValue {
|
||||
inputCredentials: Record<string, any>;
|
||||
setInputCredentialsValue: (key: string, value: any | undefined) => void;
|
||||
agentCredentialsInputFields: Record<string, any>;
|
||||
}
|
||||
|
||||
// Trigger / Preset fields
|
||||
presetEditMode: boolean; // determines whether to show the name and description fields
|
||||
presetName: string;
|
||||
setPresetName: (value: string) => void;
|
||||
presetDescription: string;
|
||||
setPresetDescription: (value: string) => void;
|
||||
};
|
||||
|
||||
const RunAgentModalContext = createContext<RunAgentModalContextValue | null>(
|
||||
null,
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
usePostV1CreateExecutionSchedule as useCreateSchedule,
|
||||
getGetV1ListExecutionSchedulesForAGraphQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/schedules/schedules";
|
||||
import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import {
|
||||
getGetV2ListPresetsQueryKey,
|
||||
usePostV2SetupTrigger,
|
||||
usePatchV2UpdateAnExistingPreset,
|
||||
} from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
|
||||
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
@@ -26,8 +30,16 @@ export type RunVariant =
|
||||
|
||||
interface UseAgentRunModalCallbacks {
|
||||
onRun?: (execution: GraphExecutionMeta) => void;
|
||||
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
|
||||
onSetupTrigger?: (preset: LibraryAgentPreset) => void;
|
||||
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
|
||||
initialInputValues?: Record<string, any>;
|
||||
initialInputCredentials?: Record<string, any>;
|
||||
initialPresetName?: string;
|
||||
initialPresetDescription?: string;
|
||||
editMode?: {
|
||||
preset: LibraryAgentPreset;
|
||||
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function useAgentRunModal(
|
||||
@@ -38,12 +50,22 @@ export function useAgentRunModal(
|
||||
const queryClient = useQueryClient();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showScheduleView, setShowScheduleView] = useState(false);
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>({});
|
||||
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
|
||||
{},
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>(
|
||||
callbacks?.initialInputValues || callbacks?.editMode?.preset?.inputs || {},
|
||||
);
|
||||
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
|
||||
callbacks?.initialInputCredentials ||
|
||||
callbacks?.editMode?.preset?.credentials ||
|
||||
{},
|
||||
);
|
||||
const [presetName, setPresetName] = useState<string>(
|
||||
callbacks?.initialPresetName || callbacks?.editMode?.preset?.name || "",
|
||||
);
|
||||
const [presetDescription, setPresetDescription] = useState<string>(
|
||||
callbacks?.initialPresetDescription ||
|
||||
callbacks?.editMode?.preset?.description ||
|
||||
"",
|
||||
);
|
||||
const [presetName, setPresetName] = useState<string>("");
|
||||
const [presetDescription, setPresetDescription] = useState<string>("");
|
||||
const defaultScheduleName = useMemo(() => `Run ${agent.name}`, [agent.name]);
|
||||
const [scheduleName, setScheduleName] = useState(defaultScheduleName);
|
||||
const [cronExpression, setCronExpression] = useState(
|
||||
@@ -70,7 +92,7 @@ export function useAgentRunModal(
|
||||
toast({
|
||||
title: "Agent execution started",
|
||||
});
|
||||
callbacks?.onRun?.(response.data as unknown as GraphExecutionMeta);
|
||||
callbacks?.onRun?.(response.data);
|
||||
// Invalidate runs list for this graph
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
|
||||
@@ -105,7 +127,7 @@ export function useAgentRunModal(
|
||||
toast({
|
||||
title: "Schedule created",
|
||||
});
|
||||
callbacks?.onCreateSchedule?.(response.data);
|
||||
callbacks?.onScheduleCreated?.(response.data);
|
||||
// Invalidate schedules list for this graph
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
|
||||
@@ -132,18 +154,51 @@ export function useAgentRunModal(
|
||||
|
||||
const setupTriggerMutation = usePostV2SetupTrigger({
|
||||
mutation: {
|
||||
onSuccess: (response: any) => {
|
||||
onSuccess: (response) => {
|
||||
if (response.status === 200) {
|
||||
toast({
|
||||
title: "Trigger setup complete",
|
||||
});
|
||||
callbacks?.onSetupTrigger?.(response.data);
|
||||
// Invalidate preset queries to show the newly created trigger
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListPresetsQueryKey({
|
||||
graph_id: response.data.graph_id,
|
||||
}),
|
||||
});
|
||||
analytics.sendDatafastEvent("setup_trigger", {
|
||||
name: agent.name,
|
||||
id: agent.graph_id,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "❌ Failed to setup trigger",
|
||||
description: String(error) || "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Edit mode mutation for updating presets
|
||||
const updatePresetMutation = usePatchV2UpdateAnExistingPreset({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
if (response.status === 200) {
|
||||
toast({
|
||||
title: "Template updated successfully",
|
||||
variant: "default",
|
||||
});
|
||||
setIsOpen(false);
|
||||
callbacks?.editMode?.onSaved?.(response.data);
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: "❌ Failed to setup trigger",
|
||||
title: "Failed to update template",
|
||||
description: error.message || "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
@@ -151,11 +206,13 @@ export function useAgentRunModal(
|
||||
},
|
||||
});
|
||||
|
||||
// Input schema validation
|
||||
const agentInputSchema = useMemo(
|
||||
() => agent.input_schema || { properties: {}, required: [] },
|
||||
[agent.input_schema],
|
||||
);
|
||||
// Input schema validation (use trigger schema for triggered agents)
|
||||
const agentInputSchema = useMemo(() => {
|
||||
if (agent.trigger_setup_info?.config_schema) {
|
||||
return agent.trigger_setup_info.config_schema;
|
||||
}
|
||||
return agent.input_schema || { properties: {}, required: [] };
|
||||
}, [agent.input_schema, agent.trigger_setup_info]);
|
||||
|
||||
const agentInputFields = useMemo(() => {
|
||||
if (
|
||||
@@ -259,9 +316,10 @@ export function useAgentRunModal(
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: add support for "manual-trigger"
|
||||
if (defaultRunType === "automatic-trigger") {
|
||||
// Setup trigger
|
||||
if (!scheduleName.trim()) {
|
||||
if (!presetName.trim()) {
|
||||
toast({
|
||||
title: "⚠️ Trigger name required",
|
||||
description: "Please provide a name for your trigger.",
|
||||
@@ -272,7 +330,7 @@ export function useAgentRunModal(
|
||||
|
||||
setupTriggerMutation.mutate({
|
||||
data: {
|
||||
name: presetName || scheduleName,
|
||||
name: presetName,
|
||||
description: presetDescription || `Trigger for ${agent.name}`,
|
||||
graph_id: agent.graph_id,
|
||||
graph_version: agent.graph_version,
|
||||
@@ -295,11 +353,10 @@ export function useAgentRunModal(
|
||||
}, [
|
||||
allRequiredInputsAreSet,
|
||||
defaultRunType,
|
||||
scheduleName,
|
||||
presetName,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
agent,
|
||||
presetName,
|
||||
presetDescription,
|
||||
notifyMissingRequirements,
|
||||
setupTriggerMutation,
|
||||
@@ -345,6 +402,7 @@ export function useAgentRunModal(
|
||||
createScheduleMutation,
|
||||
toast,
|
||||
userTimezone,
|
||||
presetName,
|
||||
]);
|
||||
|
||||
function handleShowSchedule() {
|
||||
@@ -371,6 +429,47 @@ export function useAgentRunModal(
|
||||
setCronExpression(expression);
|
||||
}
|
||||
|
||||
// Edit mode save handler
|
||||
const handleSave = useCallback(() => {
|
||||
if (!callbacks?.editMode?.preset) return;
|
||||
|
||||
updatePresetMutation.mutate({
|
||||
presetId: callbacks.editMode.preset.id,
|
||||
data: {
|
||||
name: presetName,
|
||||
description: presetDescription,
|
||||
inputs: inputValues,
|
||||
credentials: inputCredentials,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
callbacks?.editMode?.preset,
|
||||
presetName,
|
||||
presetDescription,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
updatePresetMutation,
|
||||
]);
|
||||
|
||||
// Check if there are changes in edit mode
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!callbacks?.editMode?.preset) return false;
|
||||
const preset = callbacks.editMode.preset;
|
||||
return (
|
||||
presetName !== preset.name ||
|
||||
presetDescription !== preset.description ||
|
||||
JSON.stringify(inputValues) !== JSON.stringify(preset.inputs || {}) ||
|
||||
JSON.stringify(inputCredentials) !==
|
||||
JSON.stringify(preset.credentials || {})
|
||||
);
|
||||
}, [
|
||||
callbacks?.editMode?.preset,
|
||||
presetName,
|
||||
presetDescription,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
]);
|
||||
|
||||
const hasInputFields = useMemo(() => {
|
||||
return Object.keys(agentInputFields).length > 0;
|
||||
}, [agentInputFields]);
|
||||
@@ -382,7 +481,7 @@ export function useAgentRunModal(
|
||||
showScheduleView,
|
||||
|
||||
// Run mode
|
||||
defaultRunType,
|
||||
defaultRunType: defaultRunType as RunVariant,
|
||||
|
||||
// Form: regular inputs
|
||||
inputValues,
|
||||
@@ -415,6 +514,7 @@ export function useAgentRunModal(
|
||||
isExecuting: executeGraphMutation.isPending,
|
||||
isCreatingSchedule: createScheduleMutation.isPending,
|
||||
isSettingUpTrigger: setupTriggerMutation.isPending,
|
||||
isUpdatingPreset: updatePresetMutation.isPending,
|
||||
|
||||
// Actions
|
||||
handleRun,
|
||||
@@ -423,5 +523,7 @@ export function useAgentRunModal(
|
||||
handleGoBack,
|
||||
handleSetScheduleName,
|
||||
handleSetCronExpression,
|
||||
handleSave,
|
||||
hasChanges,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ export function EmptyTasks({ agent }: Props) {
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
agentId={agent.id.toString()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, 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 {
|
||||
PencilIcon,
|
||||
PlayIcon,
|
||||
StopIcon,
|
||||
TrashIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
TabsLine,
|
||||
TabsLineContent,
|
||||
TabsLineList,
|
||||
TabsLineTrigger,
|
||||
} from "@/components/molecules/TabsLine/TabsLine";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import {
|
||||
useDeleteV2DeleteAPreset,
|
||||
getGetV2ListPresetsQueryKey,
|
||||
useGetV2GetASpecificPreset,
|
||||
getGetV2GetASpecificPresetQueryKey,
|
||||
usePatchV2UpdateAnExistingPreset,
|
||||
} from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import { getGetV1ListGraphExecutionsQueryKey } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { getGetV1ListExecutionSchedulesForAGraphQueryKey } from "@/app/api/__generated__/endpoints/schedules/schedules";
|
||||
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
|
||||
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
|
||||
import { RunAgentModal } from "../../modals/RunAgentModal/RunAgentModal";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
|
||||
interface SelectedTemplateViewProps {
|
||||
agent: LibraryAgent;
|
||||
presetID: string;
|
||||
onDelete?: (presetID: string) => void;
|
||||
onCreateRun?: (runId: string) => void;
|
||||
onCreateSchedule?: (scheduleId: string) => void;
|
||||
}
|
||||
|
||||
export function SelectedTemplateView({
|
||||
agent,
|
||||
presetID,
|
||||
onDelete,
|
||||
onCreateRun: _onCreateRun,
|
||||
onCreateSchedule: _onCreateSchedule,
|
||||
}: SelectedTemplateViewProps) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const templateOrTrigger = agent.trigger_setup_info ? "Trigger" : "Template";
|
||||
|
||||
const presetQuery = useGetV2GetASpecificPreset(presetID, {
|
||||
query: {
|
||||
enabled: !!agent.graph_id && !!presetID,
|
||||
// select: okData,
|
||||
},
|
||||
});
|
||||
const preset = useMemo(() => okData(presetQuery.data), [presetQuery.data]);
|
||||
|
||||
// Delete preset mutation
|
||||
const deleteTemplateMutation = useDeleteV2DeleteAPreset({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: `${templateOrTrigger} 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 ${templateOrTrigger.toLowerCase()}`,
|
||||
description: String(error),
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsDeleting(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const doDeleteTemplate = async () => {
|
||||
setIsDeleting(true);
|
||||
deleteTemplateMutation.mutate({ presetId: presetID });
|
||||
};
|
||||
|
||||
// Toggle trigger active status mutation
|
||||
const toggleTriggerStatusMutation = usePatchV2UpdateAnExistingPreset({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
if (response.status === 200) {
|
||||
toast({
|
||||
title: `Trigger ${preset?.is_active ? "disabled" : "enabled"} successfully`,
|
||||
variant: "default",
|
||||
});
|
||||
// Invalidate preset queries to refresh data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2GetASpecificPresetQueryKey(presetID),
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: `Failed to ${preset?.is_active ? "disable" : "enable"} trigger`,
|
||||
description: String(error),
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const doToggleTriggerStatus = () => {
|
||||
if (!preset) return;
|
||||
toggleTriggerStatusMutation.mutate({
|
||||
presetId: presetID,
|
||||
data: {
|
||||
is_active: !preset.is_active,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
// Invalidate preset queries to refresh data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2GetASpecificPresetQueryKey(presetID),
|
||||
});
|
||||
}, [queryClient, agent.graph_id, presetID]);
|
||||
|
||||
const onCreateRun = useCallback(
|
||||
(execution: GraphExecutionMeta) => {
|
||||
// Invalidate runs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
|
||||
});
|
||||
_onCreateRun?.(execution.id);
|
||||
},
|
||||
[queryClient, agent.graph_id, _onCreateRun],
|
||||
);
|
||||
|
||||
const onCreateSchedule = useCallback(
|
||||
(schedule: GraphExecutionJobInfo) => {
|
||||
// Invalidate schedules list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
|
||||
agent.graph_id,
|
||||
),
|
||||
});
|
||||
_onCreateSchedule?.(schedule.id);
|
||||
},
|
||||
[queryClient, agent.graph_id, _onCreateSchedule],
|
||||
);
|
||||
|
||||
const isLoading = presetQuery.isLoading;
|
||||
const error = presetQuery.error;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCard
|
||||
responseError={
|
||||
error
|
||||
? {
|
||||
message: String(
|
||||
(error as unknown as { message?: string })?.message ||
|
||||
`Failed to load ${templateOrTrigger.toLowerCase()}`,
|
||||
),
|
||||
}
|
||||
: 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">
|
||||
{templateOrTrigger} • {agent.name}
|
||||
</Text> */}
|
||||
</div>
|
||||
</div>
|
||||
{preset ? (
|
||||
<div className="flex gap-2">
|
||||
{!agent.has_external_trigger ? (
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
leftIcon={<PlayIcon size={16} />}
|
||||
>
|
||||
Run {templateOrTrigger}
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
initialInputValues={preset.inputs || {}}
|
||||
initialInputCredentials={preset.credentials || {}}
|
||||
initialPresetName={preset.name}
|
||||
initialPresetDescription={preset.description}
|
||||
onRunCreated={onCreateRun}
|
||||
onScheduleCreated={onCreateSchedule}
|
||||
/>
|
||||
) : null}
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
leftIcon={<PencilIcon size={16} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
editMode={{
|
||||
preset,
|
||||
onSaved: onSave,
|
||||
}}
|
||||
/>
|
||||
{/* Enable/Disable Trigger Button - only for triggered presets */}
|
||||
{preset.webhook && (
|
||||
<Button
|
||||
variant={preset.is_active ? "destructive" : "primary"}
|
||||
size="small"
|
||||
onClick={doToggleTriggerStatus}
|
||||
disabled={toggleTriggerStatusMutation.isPending}
|
||||
leftIcon={
|
||||
preset.is_active ? (
|
||||
<StopIcon size={16} />
|
||||
) : (
|
||||
<PlayIcon size={16} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{toggleTriggerStatusMutation.isPending
|
||||
? preset.is_active
|
||||
? "Disabling..."
|
||||
: "Enabling..."
|
||||
: preset.is_active
|
||||
? "Disable Trigger"
|
||||
: "Enable Trigger"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
// TODO: add confirmation modal before deleting
|
||||
variant="destructive"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
doDeleteTemplate();
|
||||
onDelete?.(presetID);
|
||||
}}
|
||||
disabled={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">
|
||||
{templateOrTrigger} 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,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>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||
import { RunListItem } from "./components/RunListItem";
|
||||
import { ScheduleListItem } from "./components/ScheduleListItem";
|
||||
import { TemplateListItem } from "./components/TemplateListItem";
|
||||
import { useSidebarRunsList } from "./useSidebarRunsList";
|
||||
|
||||
interface Props {
|
||||
@@ -27,6 +28,7 @@ interface Props {
|
||||
onCountsChange?: (info: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
presetsCount: number;
|
||||
loading?: boolean;
|
||||
}) => void;
|
||||
}
|
||||
@@ -42,13 +44,18 @@ export function SidebarRunsList({
|
||||
const {
|
||||
runs,
|
||||
schedules,
|
||||
presets,
|
||||
runsCount,
|
||||
schedulesCount,
|
||||
presetsCount,
|
||||
error,
|
||||
loading,
|
||||
fetchMoreRuns,
|
||||
hasMoreRuns,
|
||||
fetchMoreRuns,
|
||||
isFetchingMoreRuns,
|
||||
hasMorePresets,
|
||||
fetchMorePresets,
|
||||
isFetchingMorePresets,
|
||||
tabValue,
|
||||
} = useSidebarRunsList({
|
||||
graphId: agent.graph_id,
|
||||
@@ -94,7 +101,11 @@ export function SidebarRunsList({
|
||||
onClearSelectedRun?.();
|
||||
}
|
||||
} else if (value === "templates") {
|
||||
onClearSelectedRun?.();
|
||||
if (presets && presets.length) {
|
||||
onSelectRun(`preset:${presets[0].id}`);
|
||||
} else {
|
||||
onClearSelectedRun?.();
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="flex min-h-0 flex-col overflow-hidden"
|
||||
@@ -107,7 +118,8 @@ export function SidebarRunsList({
|
||||
Scheduled <span className="ml-3 inline-block">{schedulesCount}</span>
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="templates">
|
||||
Templates <span className="ml-3 inline-block">0</span>
|
||||
{agent.trigger_setup_info ? "Triggers" : "Templates"}{" "}
|
||||
<span className="ml-3 inline-block">{presetsCount}</span>
|
||||
</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
|
||||
@@ -168,17 +180,29 @@ export function SidebarRunsList({
|
||||
<TabsLineContent
|
||||
value="templates"
|
||||
className={cn(
|
||||
"mt-0 flex min-h-0 flex-1 flex-col",
|
||||
"flex min-h-0 flex-1 flex-col",
|
||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden">
|
||||
<div className="flex min-h-[50vh] flex-col items-center justify-center">
|
||||
<Text variant="large" className="text-zinc-700">
|
||||
No templates saved
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteList
|
||||
items={presets}
|
||||
hasMore={!!hasMorePresets}
|
||||
isFetchingMore={isFetchingMorePresets}
|
||||
onEndReached={fetchMorePresets}
|
||||
className="flex max-h-[76vh] flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden"
|
||||
itemWrapperClassName="w-auto lg:w-full"
|
||||
renderItem={(preset) => (
|
||||
<div className="w-[15rem] lg:w-full">
|
||||
<TemplateListItem
|
||||
preset={preset}
|
||||
selected={selectedRunId === `preset:${preset.id}`}
|
||||
onClick={() =>
|
||||
onSelectRun && onSelectRun(`preset:${preset.id}`)
|
||||
}
|
||||
/>
|
||||
</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 gap-0">
|
||||
<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="body" className="leading-tight !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">
|
||||
Template
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,21 @@
|
||||
import type { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated";
|
||||
import { Pagination } from "@/app/api/__generated__/models/pagination";
|
||||
import type { InfiniteData } from "@tanstack/react-query";
|
||||
|
||||
function hasValidExecutionsData(
|
||||
page: unknown,
|
||||
): page is { data: GraphExecutionsPaginated } {
|
||||
return (
|
||||
typeof page === "object" &&
|
||||
page !== null &&
|
||||
"data" in page &&
|
||||
typeof (page as { data: unknown }).data === "object" &&
|
||||
(page as { data: unknown }).data !== null &&
|
||||
"executions" in (page as { data: GraphExecutionsPaginated }).data
|
||||
);
|
||||
}
|
||||
|
||||
export function computeRunsCount(
|
||||
export function getPaginatedTotalCount(
|
||||
infiniteData: InfiniteData<unknown> | undefined,
|
||||
runsLength: number,
|
||||
): number {
|
||||
const lastPage = infiniteData?.pages.at(-1);
|
||||
if (!hasValidExecutionsData(lastPage)) return runsLength;
|
||||
if (!hasValidPaginationInfo(lastPage)) return runsLength;
|
||||
return lastPage.data.pagination?.total_items || runsLength;
|
||||
}
|
||||
|
||||
export function getNextRunsPageParam(lastPage: unknown): number | undefined {
|
||||
if (!hasValidExecutionsData(lastPage)) return undefined;
|
||||
export function getPaginationNextPageNumber(
|
||||
lastPage:
|
||||
| { data: { pagination?: Pagination; [key: string]: any } }
|
||||
| undefined,
|
||||
): number | undefined {
|
||||
if (!hasValidPaginationInfo(lastPage)) return undefined;
|
||||
|
||||
const { pagination } = lastPage.data;
|
||||
const hasMore =
|
||||
@@ -32,13 +23,56 @@ export function getNextRunsPageParam(lastPage: unknown): number | undefined {
|
||||
return hasMore ? pagination.current_page + 1 : undefined;
|
||||
}
|
||||
|
||||
export function extractRunsFromPages(
|
||||
infiniteData: InfiniteData<unknown> | undefined,
|
||||
) {
|
||||
export function unpaginate<
|
||||
TItemData extends object,
|
||||
TPageDataKey extends string,
|
||||
>(
|
||||
infiniteData: InfiniteData<{
|
||||
status: number;
|
||||
data: { [key in TPageDataKey]: TItemData[] } | Record<string, any>;
|
||||
}>,
|
||||
pageListKey: TPageDataKey &
|
||||
keyof (typeof infiniteData)["pages"][number]["data"],
|
||||
): TItemData[] {
|
||||
return (
|
||||
infiniteData?.pages.flatMap((page) => {
|
||||
if (!hasValidExecutionsData(page)) return [];
|
||||
return page.data.executions || [];
|
||||
if (!hasValidListPage<TItemData, TPageDataKey>(page, pageListKey))
|
||||
return [];
|
||||
return page.data[pageListKey] || [];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
function hasValidListPage<TItemData extends object, TKey extends string>(
|
||||
page: unknown,
|
||||
pageListKey: TKey,
|
||||
): page is { data: { [key in TKey]: TItemData[] } } {
|
||||
return (
|
||||
typeof page === "object" &&
|
||||
page !== null &&
|
||||
"data" in page &&
|
||||
typeof page.data === "object" &&
|
||||
page.data !== null &&
|
||||
pageListKey in page.data &&
|
||||
Array.isArray((page.data as Record<string, unknown>)[pageListKey])
|
||||
);
|
||||
}
|
||||
|
||||
function hasValidPaginationInfo(
|
||||
page: unknown,
|
||||
): page is { data: { pagination: Pagination; [key: string]: any } } {
|
||||
return (
|
||||
typeof page === "object" &&
|
||||
page !== null &&
|
||||
"data" in page &&
|
||||
typeof page.data === "object" &&
|
||||
page.data !== null &&
|
||||
"pagination" in page.data &&
|
||||
typeof page.data.pagination === "object" &&
|
||||
page.data.pagination !== null &&
|
||||
"total_items" in page.data.pagination &&
|
||||
"total_pages" in page.data.pagination &&
|
||||
"current_page" in page.data.pagination &&
|
||||
"page_size" in page.data.pagination
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import { useEffect, useMemo } from "react";
|
||||
|
||||
import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
|
||||
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { useGetV2ListPresetsInfinite } from "@/app/api/__generated__/endpoints/presets/presets";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useExecutionEvents } from "@/hooks/useExecutionEvents";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { parseAsString, useQueryStates } from "nuqs";
|
||||
import {
|
||||
computeRunsCount,
|
||||
extractRunsFromPages,
|
||||
getNextRunsPageParam,
|
||||
getPaginatedTotalCount,
|
||||
getPaginationNextPageNumber,
|
||||
unpaginate,
|
||||
} from "./helpers";
|
||||
|
||||
function parseTab(value: string | null): "runs" | "scheduled" | "templates" {
|
||||
@@ -28,6 +28,7 @@ type Args = {
|
||||
onCountsChange?: (info: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
presetsCount: number;
|
||||
loading?: boolean;
|
||||
}) => void;
|
||||
};
|
||||
@@ -52,7 +53,18 @@ export function useSidebarRunsList({
|
||||
query: {
|
||||
enabled: !!graphId,
|
||||
refetchOnWindowFocus: false,
|
||||
getNextPageParam: getNextRunsPageParam,
|
||||
getNextPageParam: getPaginationNextPageNumber,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const presetsQuery = useGetV2ListPresetsInfinite(
|
||||
{ graph_id: graphId || null, page: 1, page_size: 100 },
|
||||
{
|
||||
query: {
|
||||
enabled: !!graphId,
|
||||
refetchOnWindowFocus: false,
|
||||
getNextPageParam: getPaginationNextPageNumber,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -62,21 +74,31 @@ export function useSidebarRunsList({
|
||||
{
|
||||
query: {
|
||||
enabled: !!graphId,
|
||||
select: (r) => okData<GraphExecutionJobInfo[]>(r) ?? [],
|
||||
select: (r) => okData(r) ?? [],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const runs = useMemo(
|
||||
() => extractRunsFromPages(runsQuery.data),
|
||||
() => (runsQuery.data ? unpaginate(runsQuery.data, "executions") : []),
|
||||
[runsQuery.data],
|
||||
);
|
||||
|
||||
const presets = useMemo(
|
||||
() => (presetsQuery.data ? unpaginate(presetsQuery.data, "presets") : []),
|
||||
[presetsQuery.data],
|
||||
);
|
||||
const schedules = schedulesQuery.data || [];
|
||||
|
||||
const runsCount = computeRunsCount(runsQuery.data, runs.length);
|
||||
const runsCount = getPaginatedTotalCount(runsQuery.data, runs.length);
|
||||
const presetsCount = getPaginatedTotalCount(
|
||||
presetsQuery.data,
|
||||
presets.length,
|
||||
);
|
||||
const schedulesCount = schedules.length;
|
||||
const loading = !schedulesQuery.isSuccess || !runsQuery.isSuccess;
|
||||
const loading =
|
||||
!schedulesQuery.isSuccess ||
|
||||
!runsQuery.isSuccess ||
|
||||
!presetsQuery.isSuccess;
|
||||
|
||||
// Update query cache when execution events arrive via websocket
|
||||
useExecutionEvents({
|
||||
@@ -94,9 +116,9 @@ export function useSidebarRunsList({
|
||||
// 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 && tabValue === "runs" && !activeItem) {
|
||||
@@ -104,24 +126,34 @@ export function useSidebarRunsList({
|
||||
}
|
||||
}, [runs, activeItem, tabValue, onSelectRun]);
|
||||
|
||||
// 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 (!activeItem && runs.length === 0 && schedules.length > 0) {
|
||||
onSelectRun(schedules[0].id, "scheduled");
|
||||
if (!activeItem && runs.length === 0) {
|
||||
if (schedules.length > 0) {
|
||||
onSelectRun(`schedule:${schedules[0].id}`);
|
||||
} else if (presets.length > 0) {
|
||||
onSelectRun(`preset:${presets[0].id}`);
|
||||
}
|
||||
}
|
||||
}, [activeItem, runs.length, schedules, onSelectRun]);
|
||||
|
||||
return {
|
||||
runs,
|
||||
presets,
|
||||
schedules,
|
||||
error: schedulesQuery.error || runsQuery.error,
|
||||
error: schedulesQuery.error || runsQuery.error || presetsQuery.error,
|
||||
loading,
|
||||
runsQuery,
|
||||
presetsQuery,
|
||||
tabValue,
|
||||
runsCount,
|
||||
presetsCount,
|
||||
schedulesCount,
|
||||
fetchMoreRuns: runsQuery.fetchNextPage,
|
||||
hasMoreRuns: runsQuery.hasNextPage,
|
||||
fetchMoreRuns: runsQuery.fetchNextPage,
|
||||
isFetchingMoreRuns: runsQuery.isFetchingNextPage,
|
||||
hasMorePresets: presetsQuery.hasNextPage,
|
||||
fetchMorePresets: presetsQuery.fetchNextPage,
|
||||
isFetchingMorePresets: presetsQuery.isFetchingNextPage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useParams } from "next/navigation";
|
||||
import { parseAsString, useQueryStates } from "nuqs";
|
||||
@@ -17,14 +16,10 @@ export function useNewAgentLibraryView() {
|
||||
const agentId = id as string;
|
||||
|
||||
const {
|
||||
data: response,
|
||||
data: agent,
|
||||
isSuccess,
|
||||
error,
|
||||
} = useGetV2GetLibraryAgent(agentId, {
|
||||
query: {
|
||||
select: okData<LibraryAgent>,
|
||||
},
|
||||
});
|
||||
} = useGetV2GetLibraryAgent(agentId, { query: { select: okData } });
|
||||
|
||||
const [{ activeItem, activeTab: activeTabRaw }, setQueryStates] =
|
||||
useQueryStates({
|
||||
@@ -45,6 +40,7 @@ export function useNewAgentLibraryView() {
|
||||
const [sidebarCounts, setSidebarCounts] = useState({
|
||||
runsCount: 0,
|
||||
schedulesCount: 0,
|
||||
presetsCount: 0,
|
||||
});
|
||||
|
||||
const [sidebarLoading, setSidebarLoading] = useState(true);
|
||||
@@ -52,7 +48,8 @@ export function useNewAgentLibraryView() {
|
||||
const hasAnyItems = useMemo(
|
||||
() =>
|
||||
(sidebarCounts.runsCount ?? 0) > 0 ||
|
||||
(sidebarCounts.schedulesCount ?? 0) > 0,
|
||||
(sidebarCounts.schedulesCount ?? 0) > 0 ||
|
||||
(sidebarCounts.presetsCount ?? 0) > 0,
|
||||
[sidebarCounts],
|
||||
);
|
||||
|
||||
@@ -60,10 +57,10 @@ export function useNewAgentLibraryView() {
|
||||
const showSidebarLayout = sidebarLoading || hasAnyItems;
|
||||
|
||||
useEffect(() => {
|
||||
if (response) {
|
||||
document.title = `${response.name} - Library - AutoGPT Platform`;
|
||||
if (agent) {
|
||||
document.title = `${agent.name} - Library - AutoGPT Platform`;
|
||||
}
|
||||
}, [response]);
|
||||
}, [agent]);
|
||||
|
||||
function handleSelectRun(id: string, tab?: "runs" | "scheduled") {
|
||||
setQueryStates({
|
||||
@@ -88,11 +85,13 @@ export function useNewAgentLibraryView() {
|
||||
(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);
|
||||
@@ -105,7 +104,7 @@ export function useNewAgentLibraryView() {
|
||||
agentId: id,
|
||||
ready: isSuccess,
|
||||
error,
|
||||
agent: response,
|
||||
agent,
|
||||
hasAnyItems,
|
||||
showSidebarLayout,
|
||||
activeItem,
|
||||
|
||||
@@ -9,13 +9,15 @@ import {
|
||||
* Usage with React Query select:
|
||||
* ```ts
|
||||
* const { data: agent } = useGetV2GetLibraryAgent(agentId, {
|
||||
* query: { select: okData<LibraryAgent> },
|
||||
* query: { select: okData },
|
||||
* });
|
||||
*
|
||||
* data // is now properly typed as LibraryAgent | undefined
|
||||
* ```
|
||||
*/
|
||||
export function okData<T>(res: unknown): T | undefined {
|
||||
export function okData<TResponse extends { status: number; data?: object }>(
|
||||
res: TResponse | undefined,
|
||||
): (TResponse & { status: 200 })["data"] | undefined {
|
||||
if (!res || typeof res !== "object") return undefined;
|
||||
|
||||
// status must exist and be exactly 200
|
||||
@@ -26,7 +28,7 @@ export function okData<T>(res: unknown): T | undefined {
|
||||
// check presence to safely return it as T; the generic T is enforced at call sites.
|
||||
if (!("data" in (res as Record<string, unknown>))) return undefined;
|
||||
|
||||
return (res as { data: T }).data;
|
||||
return res.data;
|
||||
}
|
||||
|
||||
type ResponseWithData = { status: number; data: unknown };
|
||||
|
||||
Reference in New Issue
Block a user