From e6cc8687a5e2b70b9afc228243dbc05da05dfbea Mon Sep 17 00:00:00 2001 From: Andy Hooker <58448663+andrewhooker2@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:13:08 -0500 Subject: [PATCH] feat(builder): Refactor components and types for monitoring page streamlining (#7714) --- rnd/autogpt_builder/src/app/monitor/page.tsx | 739 +----------------- .../src/components/monitor/AgentFlowList.tsx | 187 +++++ .../src/components/monitor/FlowInfo.tsx | 134 ++++ .../src/components/monitor/FlowRunInfo.tsx | 66 ++ .../components/monitor/FlowRunStatusBadge.tsx | 25 + .../src/components/monitor/FlowRunsList.tsx | 68 ++ .../src/components/monitor/FlowRunsStatus.tsx | 114 +++ .../components/monitor/FlowRunsTimeline.tsx | 170 ++++ .../src/components/monitor/index.ts | 6 + rnd/autogpt_builder/src/lib/types.ts | 13 + 10 files changed, 793 insertions(+), 729 deletions(-) create mode 100644 rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/index.ts create mode 100644 rnd/autogpt_builder/src/lib/types.ts diff --git a/rnd/autogpt_builder/src/app/monitor/page.tsx b/rnd/autogpt_builder/src/app/monitor/page.tsx index 2a7f7e998..c88b38609 100644 --- a/rnd/autogpt_builder/src/app/monitor/page.tsx +++ b/rnd/autogpt_builder/src/app/monitor/page.tsx @@ -1,66 +1,20 @@ "use client"; import React, { useEffect, useState } from "react"; -import Link from "next/link"; -import moment from "moment"; -import { - ComposedChart, - DefaultLegendContentProps, - Legend, - Line, - ResponsiveContainer, - Scatter, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; + import AutoGPTServerAPI, { - Graph, GraphMeta, NodeExecutionResult, - safeCopyGraph, } from "@/lib/autogpt-server-api"; + +import { Card } from "@/components/ui/card"; +import { FlowRun } from "@/lib/types"; import { - ChevronDownIcon, - ClockIcon, - EnterIcon, - ExitIcon, - Pencil2Icon, -} from "@radix-ui/react-icons"; -import { cn, exportAsJSONFile, hashString } from "@/lib/utils"; -import { Badge } from "@/components/ui/badge"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { AgentImportForm } from "@/components/agent-import-form"; + AgentFlowList, + FlowInfo, + FlowRunInfo, + FlowRunsList, + FlowRunsStats, +} from "@/components/monitor"; const Monitor = () => { const [flows, setFlows] = useState([]); @@ -165,19 +119,6 @@ const Monitor = () => { ); }; -type FlowRun = { - id: string; - graphID: string; - graphVersion: number; - status: "running" | "waiting" | "success" | "failed"; - startTime: number; // unix timestamp (ms) - endTime: number; // unix timestamp (ms) - duration: number; // seconds - totalRunTime: number; // seconds - - nodeExecutionResults: NodeExecutionResult[]; -}; - function flowRunFromNodeExecutionResults( nodeExecutionResults: NodeExecutionResult[], ): FlowRun { @@ -230,664 +171,4 @@ function flowRunFromNodeExecutionResults( }; } -const AgentFlowList = ({ - flows, - flowRuns, - selectedFlow, - onSelectFlow, - className, -}: { - flows: GraphMeta[]; - flowRuns?: FlowRun[]; - selectedFlow: GraphMeta | null; - onSelectFlow: (f: GraphMeta) => void; - className?: string; -}) => { - const [templates, setTemplates] = useState([]); - const api = new AutoGPTServerAPI(); - useEffect(() => { - api.listTemplates().then((templates) => setTemplates(templates)); - }, []); - - return ( - - - Agents - -
- {/* Split "Create" button */} - - - {/* https://ui.shadcn.com/docs/components/dialog#notes */} - - - - - - - - - Import from file - - - {templates.length > 0 && ( - <> - {/* List of templates */} - - Use a template - {templates.map((template) => ( - { - api - .createGraph(template.id, template.version) - .then((newGraph) => { - window.location.href = `/build?flowID=${newGraph.id}`; - }); - }} - > - {template.name} - - ))} - - )} - - - - - - Import an Agent (template) from a file - - - - -
-
- - - - - - Name - {/* Status */} - {/* Last updated */} - {flowRuns && ( - - # of runs - - )} - {flowRuns && Last run} - - - - {flows - .map((flow) => { - let runCount = 0, - lastRun: FlowRun | null = null; - if (flowRuns) { - const _flowRuns = flowRuns.filter( - (r) => r.graphID == flow.id, - ); - runCount = _flowRuns.length; - lastRun = - runCount == 0 - ? null - : _flowRuns.reduce((a, c) => - a.startTime > c.startTime ? a : c, - ); - } - return { flow, runCount, lastRun }; - }) - .sort((a, b) => { - if (!a.lastRun && !b.lastRun) return 0; - if (!a.lastRun) return 1; - if (!b.lastRun) return -1; - return b.lastRun.startTime - a.lastRun.startTime; - }) - .map(({ flow, runCount, lastRun }) => ( - onSelectFlow(flow)} - data-state={selectedFlow?.id == flow.id ? "selected" : null} - > - {flow.name} - {/* */} - {/* - {flow.updatedAt ?? "???"} - */} - {flowRuns && ( - - {runCount} - - )} - {flowRuns && - (!lastRun ? ( - - ) : ( - - {moment(lastRun.startTime).fromNow()} - - ))} - - ))} - -
-
-
- ); -}; - -const FlowStatusBadge = ({ - status, -}: { - status: "active" | "disabled" | "failing"; -}) => ( - - {status} - -); - -const FlowRunsList: React.FC<{ - flows: GraphMeta[]; - runs: FlowRun[]; - className?: string; - selectedRun?: FlowRun | null; - onSelectRun: (r: FlowRun) => void; -}> = ({ flows, runs, selectedRun, onSelectRun, className }) => ( - - - Runs - - - - - - Agent - Started - Status - Duration - - - - {runs.map((run) => ( - onSelectRun(run)} - data-state={selectedRun?.id == run.id ? "selected" : null} - > - - {flows.find((f) => f.id == run.graphID)!.name} - - {moment(run.startTime).format("HH:mm")} - - - - {formatDuration(run.duration)} - - ))} - -
-
-
-); - -const FlowRunStatusBadge: React.FC<{ - status: FlowRun["status"]; - className?: string; -}> = ({ status, className }) => ( - - {status} - -); - -const FlowInfo: React.FC< - React.HTMLAttributes & { - flow: GraphMeta; - flowRuns: FlowRun[]; - flowVersion?: number | "all"; - } -> = ({ flow, flowRuns, flowVersion, ...props }) => { - const api = new AutoGPTServerAPI(); - - const [flowVersions, setFlowVersions] = useState(null); - const [selectedVersion, setSelectedFlowVersion] = useState( - flowVersion ?? "all", - ); - const selectedFlowVersion: Graph | undefined = flowVersions?.find( - (v) => - v.version == (selectedVersion == "all" ? flow.version : selectedVersion), - ); - - useEffect(() => { - api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result)); - }, [flow.id]); - - return ( - - -
- - {flow.name} v{flow.version} - -

- Agent ID: {flow.id} -

-
-
- {(flowVersions?.length ?? 0) > 1 && ( - - - - - - Choose a version - - - setSelectedFlowVersion( - choice == "all" ? choice : Number(choice), - ) - } - > - - All versions - - {flowVersions?.map((v) => ( - - Version {v.version} - {v.is_active ? " (active)" : ""} - - ))} - - - - )} - - Edit - - -
-
- - - r.graphID == flow.id && - (selectedVersion == "all" || r.graphVersion == selectedVersion), - )} - /> - -
- ); -}; - -const FlowRunInfo: React.FC< - React.HTMLAttributes & { - flow: GraphMeta; - flowRun: FlowRun; - } -> = ({ flow, flowRun, ...props }) => { - if (flowRun.graphID != flow.id) { - throw new Error( - `FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`, - ); - } - - return ( - - -
- - {flow.name} v{flow.version} - -

- Agent ID: {flow.id} -

-

- Run ID: {flowRun.id} -

-
- - Edit Agent - -
- -

- Status:{" "} - -

-

- Started:{" "} - {moment(flowRun.startTime).format("YYYY-MM-DD HH:mm:ss")} -

-

- Finished:{" "} - {moment(flowRun.endTime).format("YYYY-MM-DD HH:mm:ss")} -

-

- Duration (run time): {flowRun.duration} ( - {flowRun.totalRunTime}) seconds -

- {/*

Total cost: €1,23

*/} -
-
- ); -}; - -const FlowRunsStats: React.FC<{ - flows: GraphMeta[]; - flowRuns: FlowRun[]; - title?: string; - className?: string; -}> = ({ flows, flowRuns, title, className }) => { - /* "dateMin": since the first flow in the dataset - * number > 0: custom date (unix timestamp) - * number < 0: offset relative to Date.now() (in seconds) */ - const [statsSince, setStatsSince] = useState(-24 * 3600); - const statsSinceTimestamp = // unix timestamp or null - typeof statsSince == "string" - ? null - : statsSince < 0 - ? Date.now() + statsSince * 1000 - : statsSince; - const filteredFlowRuns = - statsSinceTimestamp != null - ? flowRuns.filter((fr) => fr.startTime > statsSinceTimestamp) - : flowRuns; - - return ( -
-
- {title || "Stats"} -
- - - - - - - - - - - setStatsSince(selectedDay.getTime()) - } - initialFocus - /> - - - -
-
- -
-
-

- Total runs: {filteredFlowRuns.length} -

-

- Total run time:{" "} - {filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)}{" "} - seconds -

- {/*

Total cost: €1,23

*/} -
-
- ); -}; - -const FlowRunsTimeline = ({ - flows, - flowRuns, - dataMin, - className, -}: { - flows: GraphMeta[]; - flowRuns: FlowRun[]; - dataMin: "dataMin" | number; - className?: string; -}) => ( - /* TODO: make logarithmic? */ - - - { - const now = moment(); - const time = moment(unixTime); - return now.diff(time, "hours") < 24 - ? time.format("HH:mm") - : time.format("YYYY-MM-DD HH:mm"); - }} - name="Time" - scale="time" - /> - (s > 90 ? `${Math.round(s / 60)}m` : `${s}s`)} - /> - { - if (payload && payload.length) { - const data: FlowRun & { time: number; _duration: number } = - payload[0].payload; - const flow = flows.find((f) => f.id === data.graphID); - return ( - -

- Agent: {flow ? flow.name : "Unknown"} -

-

- Status:  - -

-

- Started:{" "} - {moment(data.startTime).format("YYYY-MM-DD HH:mm:ss")} -

-

- Duration / run time:{" "} - {formatDuration(data.duration)} /{" "} - {formatDuration(data.totalRunTime)} -

-
- ); - } - return null; - }} - /> - {flows.map((flow) => ( - fr.graphID == flow.id) - .map((fr) => ({ - ...fr, - time: fr.startTime + fr.totalRunTime * 1000, - _duration: fr.totalRunTime, - }))} - name={flow.name} - fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`} - /> - ))} - {flowRuns.map((run) => ( - - ))} - } - wrapperStyle={{ - bottom: 0, - left: 0, - right: 0, - width: "100%", - display: "flex", - justifyContent: "center", - }} - /> -
-
-); - -const ScrollableLegend: React.FC< - DefaultLegendContentProps & { className?: string } -> = ({ payload, className }) => { - return ( -
- {payload.map((entry, index) => { - if (entry.type == "none") return; - return ( - - - {entry.value} - - ); - })} -
- ); -}; - -function formatDuration(seconds: number): string { - return ( - (seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() + - "s" - ); -} - export default Monitor; diff --git a/rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx b/rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx new file mode 100644 index 000000000..0acf6ad5e --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx @@ -0,0 +1,187 @@ +import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api"; +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ChevronDownIcon, EnterIcon } from "@radix-ui/react-icons"; +import { AgentImportForm } from "@/components/agent-import-form"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import moment from "moment/moment"; +import { FlowRun } from "@/lib/types"; + +export const AgentFlowList = ({ + flows, + flowRuns, + selectedFlow, + onSelectFlow, + className, +}: { + flows: GraphMeta[]; + flowRuns?: FlowRun[]; + selectedFlow: GraphMeta | null; + onSelectFlow: (f: GraphMeta) => void; + className?: string; +}) => { + const [templates, setTemplates] = useState([]); + const api = new AutoGPTServerAPI(); + useEffect(() => { + api.listTemplates().then((templates) => setTemplates(templates)); + }, []); + + return ( + + + Agents + +
+ {/* Split "Create" button */} + + + {/* https://ui.shadcn.com/docs/components/dialog#notes */} + + + + + + + + + Import from file + + + {templates.length > 0 && ( + <> + {/* List of templates */} + + Use a template + {templates.map((template) => ( + { + api + .createGraph(template.id, template.version) + .then((newGraph) => { + window.location.href = `/build?flowID=${newGraph.id}`; + }); + }} + > + {template.name} + + ))} + + )} + + + + + + Import an Agent (template) from a file + + + + +
+
+ + + + + + Name + {/* Status */} + {/* Last updated */} + {flowRuns && ( + + # of runs + + )} + {flowRuns && Last run} + + + + {flows + .map((flow) => { + let runCount = 0, + lastRun: FlowRun | null = null; + if (flowRuns) { + const _flowRuns = flowRuns.filter( + (r) => r.graphID == flow.id, + ); + runCount = _flowRuns.length; + lastRun = + runCount == 0 + ? null + : _flowRuns.reduce((a, c) => + a.startTime > c.startTime ? a : c, + ); + } + return { flow, runCount, lastRun }; + }) + .sort((a, b) => { + if (!a.lastRun && !b.lastRun) return 0; + if (!a.lastRun) return 1; + if (!b.lastRun) return -1; + return b.lastRun.startTime - a.lastRun.startTime; + }) + .map(({ flow, runCount, lastRun }) => ( + onSelectFlow(flow)} + data-state={selectedFlow?.id == flow.id ? "selected" : null} + > + {flow.name} + {/* */} + {/* + {flow.updatedAt ?? "???"} + */} + {flowRuns && ( + + {runCount} + + )} + {flowRuns && + (!lastRun ? ( + + ) : ( + + {moment(lastRun.startTime).fromNow()} + + ))} + + ))} + +
+
+
+ ); +}; +export default AgentFlowList; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx b/rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx new file mode 100644 index 000000000..0ec6d2187 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useState } from "react"; +import AutoGPTServerAPI, { + Graph, + GraphMeta, + safeCopyGraph, +} from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { ClockIcon, ExitIcon, Pencil2Icon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import { exportAsJSONFile } from "@/lib/utils"; +import { FlowRunsStats } from "@/components/monitor/index"; + +export const FlowInfo: React.FC< + React.HTMLAttributes & { + flow: GraphMeta; + flowRuns: FlowRun[]; + flowVersion?: number | "all"; + } +> = ({ flow, flowRuns, flowVersion, ...props }) => { + const api = new AutoGPTServerAPI(); + + const [flowVersions, setFlowVersions] = useState(null); + const [selectedVersion, setSelectedFlowVersion] = useState( + flowVersion ?? "all", + ); + const selectedFlowVersion: Graph | undefined = flowVersions?.find( + (v) => + v.version == (selectedVersion == "all" ? flow.version : selectedVersion), + ); + + useEffect(() => { + api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result)); + }, [flow.id]); + + return ( + + +
+ + {flow.name} v{flow.version} + +

+ Agent ID: {flow.id} +

+
+
+ {(flowVersions?.length ?? 0) > 1 && ( + + + + + + Choose a version + + + setSelectedFlowVersion( + choice == "all" ? choice : Number(choice), + ) + } + > + + All versions + + {flowVersions?.map((v) => ( + + Version {v.version} + {v.is_active ? " (active)" : ""} + + ))} + + + + )} + + Edit + + +
+
+ + + r.graphID == flow.id && + (selectedVersion == "all" || r.graphVersion == selectedVersion), + )} + /> + +
+ ); +}; +export default FlowInfo; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx new file mode 100644 index 000000000..1ace5207c --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Link from "next/link"; +import { buttonVariants } from "@/components/ui/button"; +import { Pencil2Icon } from "@radix-ui/react-icons"; +import moment from "moment/moment"; +import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge"; + +export const FlowRunInfo: React.FC< + React.HTMLAttributes & { + flow: GraphMeta; + flowRun: FlowRun; + } +> = ({ flow, flowRun, ...props }) => { + if (flowRun.graphID != flow.id) { + throw new Error( + `FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`, + ); + } + + return ( + + +
+ + {flow.name} v{flow.version} + +

+ Agent ID: {flow.id} +

+

+ Run ID: {flowRun.id} +

+
+ + Edit Agent + +
+ +

+ Status:{" "} + +

+

+ Started:{" "} + {moment(flowRun.startTime).format("YYYY-MM-DD HH:mm:ss")} +

+

+ Finished:{" "} + {moment(flowRun.endTime).format("YYYY-MM-DD HH:mm:ss")} +

+

+ Duration (run time): {flowRun.duration} ( + {flowRun.totalRunTime}) seconds +

+ {/*

Total cost: €1,23

*/} +
+
+ ); +}; +export default FlowRunInfo; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx new file mode 100644 index 000000000..f05478222 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { FlowRun } from "@/lib/types"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export const FlowRunStatusBadge: React.FC<{ + status: FlowRun["status"]; + className?: string; +}> = ({ status, className }) => ( + + {status} + +); diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx new file mode 100644 index 000000000..ed2935556 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import moment from "moment/moment"; +import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge"; + +export const FlowRunsList: React.FC<{ + flows: GraphMeta[]; + runs: FlowRun[]; + className?: string; + selectedRun?: FlowRun | null; + onSelectRun: (r: FlowRun) => void; +}> = ({ flows, runs, selectedRun, onSelectRun, className }) => ( + + + Runs + + + + + + Agent + Started + Status + Duration + + + + {runs.map((run) => ( + onSelectRun(run)} + data-state={selectedRun?.id == run.id ? "selected" : null} + > + + {flows.find((f) => f.id == run.graphID)!.name} + + {moment(run.startTime).format("HH:mm")} + + + + {formatDuration(run.duration)} + + ))} + +
+
+
+); + +function formatDuration(seconds: number): string { + return ( + (seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() + + "s" + ); +} + +export default FlowRunsList; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx new file mode 100644 index 000000000..cf4613494 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx @@ -0,0 +1,114 @@ +import React, { useState } from "react"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Calendar } from "@/components/ui/calendar"; +import { FlowRunsTimeline } from "@/components/monitor/FlowRunsTimeline"; + +export const FlowRunsStatus: React.FC<{ + flows: GraphMeta[]; + flowRuns: FlowRun[]; + title?: string; + className?: string; +}> = ({ flows, flowRuns, title, className }) => { + /* "dateMin": since the first flow in the dataset + * number > 0: custom date (unix timestamp) + * number < 0: offset relative to Date.now() (in seconds) */ + const [statsSince, setStatsSince] = useState(-24 * 3600); + const statsSinceTimestamp = // unix timestamp or null + typeof statsSince == "string" + ? null + : statsSince < 0 + ? Date.now() + statsSince * 1000 + : statsSince; + const filteredFlowRuns = + statsSinceTimestamp != null + ? flowRuns.filter((fr) => fr.startTime > statsSinceTimestamp) + : flowRuns; + + return ( +
+
+ {title || "Stats"} +
+ + + + + + + + + + + setStatsSince(selectedDay.getTime()) + } + initialFocus + /> + + + +
+
+ +
+
+

+ Total runs: {filteredFlowRuns.length} +

+

+ Total run time:{" "} + {filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)}{" "} + seconds +

+ {/*

Total cost: €1,23

*/} +
+
+ ); +}; +export default FlowRunsStatus; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx new file mode 100644 index 000000000..c31efee18 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx @@ -0,0 +1,170 @@ +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { + ComposedChart, + DefaultLegendContentProps, + Legend, + Line, + ResponsiveContainer, + Scatter, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import moment from "moment/moment"; +import { Card } from "@/components/ui/card"; +import { cn, hashString } from "@/lib/utils"; +import React from "react"; +import { FlowRun } from "@/lib/types"; +import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge"; + +export const FlowRunsTimeline = ({ + flows, + flowRuns, + dataMin, + className, +}: { + flows: GraphMeta[]; + flowRuns: FlowRun[]; + dataMin: "dataMin" | number; + className?: string; +}) => ( + /* TODO: make logarithmic? */ + + + { + const now = moment(); + const time = moment(unixTime); + return now.diff(time, "hours") < 24 + ? time.format("HH:mm") + : time.format("YYYY-MM-DD HH:mm"); + }} + name="Time" + scale="time" + /> + (s > 90 ? `${Math.round(s / 60)}m` : `${s}s`)} + /> + { + if (payload && payload.length) { + const data: FlowRun & { time: number; _duration: number } = + payload[0].payload; + const flow = flows.find((f) => f.id === data.graphID); + return ( + +

+ Agent: {flow ? flow.name : "Unknown"} +

+

+ Status:  + +

+

+ Started:{" "} + {moment(data.startTime).format("YYYY-MM-DD HH:mm:ss")} +

+

+ Duration / run time:{" "} + {formatDuration(data.duration)} /{" "} + {formatDuration(data.totalRunTime)} +

+
+ ); + } + return null; + }} + /> + {flows.map((flow) => ( + fr.graphID == flow.id) + .map((fr) => ({ + ...fr, + time: fr.startTime + fr.totalRunTime * 1000, + _duration: fr.totalRunTime, + }))} + name={flow.name} + fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`} + /> + ))} + {flowRuns.map((run) => ( + + ))} + } + wrapperStyle={{ + bottom: 0, + left: 0, + right: 0, + width: "100%", + display: "flex", + justifyContent: "center", + }} + /> +
+
+); + +const ScrollableLegend: React.FC< + DefaultLegendContentProps & { className?: string } +> = ({ payload, className }) => { + return ( +
+ {payload?.map((entry, index) => { + if (entry.type == "none") return; + return ( + + + {entry.value} + + ); + })} +
+ ); +}; + +function formatDuration(seconds: number): string { + return ( + (seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() + + "s" + ); +} diff --git a/rnd/autogpt_builder/src/components/monitor/index.ts b/rnd/autogpt_builder/src/components/monitor/index.ts new file mode 100644 index 000000000..0f8f80287 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/index.ts @@ -0,0 +1,6 @@ +export { default as AgentFlowList } from "./AgentFlowList"; +export { default as FlowRunsList } from "./FlowRunsList"; +export { default as FlowInfo } from "./FlowInfo"; +export { default as FlowRunInfo } from "./FlowRunInfo"; +export { default as FlowRunsStats } from "./FlowRunsStatus"; +export { default as FlowRunsTimeline } from "./FlowRunsTimeline"; diff --git a/rnd/autogpt_builder/src/lib/types.ts b/rnd/autogpt_builder/src/lib/types.ts new file mode 100644 index 000000000..04750a597 --- /dev/null +++ b/rnd/autogpt_builder/src/lib/types.ts @@ -0,0 +1,13 @@ +import { NodeExecutionResult } from "@/lib/autogpt-server-api"; + +export type FlowRun = { + id: string; + graphID: string; + graphVersion: number; + status: "running" | "waiting" | "success" | "failed"; + startTime: number; // unix timestamp (ms) + endTime: number; // unix timestamp (ms) + duration: number; // seconds + totalRunTime: number; // seconds + nodeExecutionResults: NodeExecutionResult[]; +};