feat(frontend/library): Add toast on agent execution request failure (#9689)

Currently, when an agent execution fails to be executed, the front-end
does not display any feedback to the user.
The scope of this change is providing that.

### Changes 🏗️

* Extracted `useToastOnFail` from `credits` page into a unified helper
method.
* Uses `useToastOnFail` on agent execution requests on library pages.

<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/2daa0597-eb93-457d-8887-0f00c4db89ac"
/>
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/1a541c98-fb95-424f-8ffe-972332b3ce01"
/>


### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run agent with invalid input 

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:
- [ ] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
This commit is contained in:
Zamil Majdy
2025-03-25 20:51:17 +07:00
committed by GitHub
parent 87f87500cb
commit 33299070d3
5 changed files with 79 additions and 56 deletions

View File

@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import useCredits from "@/hooks/useCredits";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useSearchParams, useRouter } from "next/navigation";
import { useToast } from "@/components/ui/use-toast";
import { useToast, useToastOnFail } from "@/components/ui/use-toast";
import { RefundModal } from "./RefundModal";
import { CreditTransaction } from "@/lib/autogpt-server-api";
@@ -38,20 +38,7 @@ export default function CreditsPage() {
const searchParams = useSearchParams();
const topupStatus = searchParams.get("topup") as "success" | "cancel" | null;
const { toast } = useToast();
const toastOnFail = useCallback(
(action: string, fn: () => Promise<any>) => {
return fn().catch((e) => {
toast({
title: `Unable to ${action}`,
description: e.message,
variant: "destructive",
duration: 10000,
});
});
},
[toast],
);
const toastOnFail = useToastOnFail();
const [isRefundModalOpen, setIsRefundModalOpen] = useState(false);
const [topUpTransactions, setTopUpTransactions] = useState<
@@ -63,42 +50,44 @@ export default function CreditsPage() {
setIsRefundModalOpen(true);
});
};
const refundCredits = async (transaction_key: string, reason: string) =>
toastOnFail("refund transaction", async () => {
const amount = await refundTopUp(transaction_key, reason);
if (amount > 0) {
toast({
title: "Refund approved! 🎉",
description: `Your refund has been automatically processed. Based on your remaining balance, the amount of ${formatCredits(amount)} will be credited to your account.`,
});
} else {
toast({
title: "Refund Request Received",
description:
"We have received your refund request. A member of our team will review it and reach out via email shortly.",
});
}
});
const refundCredits = (transaction_key: string, reason: string) =>
refundTopUp(transaction_key, reason)
.then((amount) => {
if (amount > 0) {
toast({
title: "Refund approved! 🎉",
description: `Your refund has been automatically processed. Based on your remaining balance, the amount of ${formatCredits(amount)} will be credited to your account.`,
});
} else {
toast({
title: "Refund Request Received",
description:
"We have received your refund request. A member of our team will review it and reach out via email shortly.",
});
}
})
.catch(toastOnFail("refund transaction"));
useEffect(() => {
if (api && topupStatus === "success") {
toastOnFail("fulfill checkout", () => api.fulfillCheckout());
api.fulfillCheckout().catch(toastOnFail("fulfill checkout"));
}
}, [api, topupStatus, toastOnFail]);
const openBillingPortal = async () => {
toastOnFail("open billing portal", async () => {
const portal = await api.getUserPaymentPortalLink();
router.push(portal.url);
});
};
const openBillingPortal = () =>
api
.getUserPaymentPortalLink()
.then((portal) => {
router.push(portal.url);
})
.catch(toastOnFail("open billing portal"));
const submitTopUp = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const amount =
parseInt(new FormData(form).get("topUpAmount") as string) * 100;
toastOnFail("request top-up", () => requestTopUp(amount));
requestTopUp(amount).catch(toastOnFail("request top-up"));
};
const submitAutoTopUpConfig = (e: React.FormEvent<HTMLFormElement>) => {
@@ -107,11 +96,11 @@ export default function CreditsPage() {
const formData = new FormData(form);
const amount = parseInt(formData.get("topUpAmount") as string) * 100;
const threshold = parseInt(formData.get("threshold") as string) * 100;
toastOnFail("update auto top-up config", () =>
updateAutoTopUpConfig(amount, threshold).then(() => {
updateAutoTopUpConfig(amount, threshold)
.then(() => {
toast({ title: "Auto top-up config updated! 🎉" });
}),
);
})
.catch(toastOnFail("update auto top-up config"));
};
return (

View File

@@ -12,6 +12,7 @@ import {
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { IconRefresh, IconSquare } from "@/components/ui/icons";
import { useToastOnFail } from "@/components/ui/use-toast";
import { Button } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
@@ -38,6 +39,8 @@ export default function AgentRunDetailsView({
[run],
);
const toastOnFail = useToastOnFail();
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
if (!run) return [];
return [
@@ -79,14 +82,16 @@ export default function AgentRunDetailsView({
const runAgain = useCallback(
() =>
agentRunInputs &&
api.executeGraph(
graph.id,
graph.version,
Object.fromEntries(
Object.entries(agentRunInputs).map(([k, v]) => [k, v.value]),
),
),
[api, graph, agentRunInputs],
api
.executeGraph(
graph.id,
graph.version,
Object.fromEntries(
Object.entries(agentRunInputs).map(([k, v]) => [k, v.value]),
),
)
.catch(toastOnFail("execute agent")),
[api, graph, agentRunInputs, toastOnFail],
);
const stopRun = useCallback(

View File

@@ -7,6 +7,7 @@ import { GraphExecutionID, GraphMeta } from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LocalValuedInput } from "@/components/ui/input";
import { useToastOnFail } from "@/components/ui/use-toast";
import { Pencil2Icon } from "@radix-ui/react-icons";
import { Textarea } from "@/components/ui/textarea";
import { IconPlay } from "@/components/ui/icons";
@@ -28,6 +29,7 @@ export default function AgentRunDraftView({
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();
const toastOnFail = useToastOnFail();
const agentInputs = graph.input_schema.properties;
const [inputValues, setInputValues] = useState<Record<string, any>>({});
@@ -57,8 +59,9 @@ export default function AgentRunDraftView({
() =>
api
.executeGraph(graph.id, graph.version, inputValues)
.then((newRun) => onRun(newRun.graph_exec_id)),
[api, graph, inputValues, onRun],
.then((newRun) => onRun(newRun.graph_exec_id))
.catch(toastOnFail("execute agent")),
[api, graph, inputValues, onRun, toastOnFail],
);
const runActions: ButtonAction[] = useMemo(

View File

@@ -11,6 +11,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AgentRunStatus } from "@/components/agents/agent-run-status-chip";
import { useToastOnFail } from "@/components/ui/use-toast";
import { Button } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
@@ -29,6 +30,8 @@ export default function AgentScheduleDetailsView({
const selectedRunStatus: AgentRunStatus = "scheduled";
const toastOnFail = useToastOnFail();
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
return [
{
@@ -67,8 +70,9 @@ export default function AgentScheduleDetailsView({
() =>
api
.executeGraph(graph.id, graph.version, schedule.input_data)
.then((run) => onForcedRun(run.graph_exec_id)),
[api, graph, schedule, onForcedRun],
.then((run) => onForcedRun(run.graph_exec_id))
.catch(toastOnFail("execute agent")),
[api, graph, schedule, onForcedRun, toastOnFail],
);
const runActions: { label: string; callback: () => void }[] = useMemo(

View File

@@ -188,4 +188,26 @@ function useToast() {
};
}
export { useToast, toast };
interface ToastOnFailOptions {
rethrow?: boolean;
}
function useToastOnFail() {
return React.useCallback(
(action: string, { rethrow = false }: ToastOnFailOptions = {}) =>
(error: any) => {
toast({
title: `Unable to ${action}`,
description: (error as Error)?.message ?? "Something went wrong",
variant: "destructive",
duration: 10000,
});
if (rethrow) {
throw error;
}
},
[],
);
}
export { useToast, toast, useToastOnFail };