From 5a7193cfb7c987b0ac3f071c00bcb4895bd30ea5 Mon Sep 17 00:00:00 2001 From: Bently Date: Mon, 16 Sep 2024 12:05:07 +0100 Subject: [PATCH] Feat(Builder): Add Runner input and ouput screens (#8038) * Feat(Builder): Add Runner input and ouput screens * Fix run button not working * prettier * prettier again -- forgot flow * fix input scaling + auto close on run * removed "Runner Input" button to make it auto open runner input if input block is + Fixed issue with output not showing in output UI * replaced runner output icon and added a new icon for it * replaced IconOutput icon with LogOut from lucide-react * prettier * fix type safety issue + add error handling for formatOutput * Updates based on comments * prettier for utils --- rnd/autogpt_builder/src/components/Flow.tsx | 27 +++- .../src/components/RunnerUIWrapper.tsx | 141 ++++++++++++++++++ .../components/runner-ui/RunnerInputBlock.tsx | 61 ++++++++ .../components/runner-ui/RunnerInputList.tsx | 33 ++++ .../components/runner-ui/RunnerInputUI.tsx | 74 +++++++++ .../components/runner-ui/RunnerOutputUI.tsx | 94 ++++++++++++ rnd/autogpt_builder/src/lib/utils.ts | 7 + 7 files changed, 434 insertions(+), 3 deletions(-) create mode 100644 rnd/autogpt_builder/src/components/RunnerUIWrapper.tsx create mode 100644 rnd/autogpt_builder/src/components/runner-ui/RunnerInputBlock.tsx create mode 100644 rnd/autogpt_builder/src/components/runner-ui/RunnerInputList.tsx create mode 100644 rnd/autogpt_builder/src/components/runner-ui/RunnerInputUI.tsx create mode 100644 rnd/autogpt_builder/src/components/runner-ui/RunnerOutputUI.tsx diff --git a/rnd/autogpt_builder/src/components/Flow.tsx b/rnd/autogpt_builder/src/components/Flow.tsx index a794cb0164..2a6aca2715 100644 --- a/rnd/autogpt_builder/src/components/Flow.tsx +++ b/rnd/autogpt_builder/src/components/Flow.tsx @@ -27,7 +27,7 @@ import "@xyflow/react/dist/style.css"; import { CustomNode } from "./CustomNode"; import "./flow.css"; import { Link } from "@/lib/autogpt-server-api"; -import { getTypeColor } from "@/lib/utils"; +import { getTypeColor, filterBlocksByType } from "@/lib/utils"; import { history } from "./history"; import { CustomEdge } from "./CustomEdge"; import ConnectionLine from "./ConnectionLine"; @@ -36,14 +36,19 @@ import { SaveControl } from "@/components/edit/control/SaveControl"; import { BlocksControl } from "@/components/edit/control/BlocksControl"; import { IconPlay, + IconUndo2, IconRedo2, IconSquare, - IconUndo2, + IconOutput, } from "@/components/ui/icons"; import { startTutorial } from "./tutorial"; import useAgentGraph from "@/hooks/useAgentGraph"; import { v4 as uuidv4 } from "uuid"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; +import { LogOut } from "lucide-react"; +import RunnerUIWrapper, { + RunnerUIWrapperRef, +} from "@/components/RunnerUIWrapper"; // This is for the history, this is the minimum distance a block must move before it is logged // It helps to prevent spamming the history with small movements especially when pressing on a input in a block @@ -101,6 +106,8 @@ const FlowEditor: React.FC<{ // State to control if blocks menu should be pinned open const [pinBlocksPopover, setPinBlocksPopover] = useState(false); + const runnerUIRef = useRef(null); + useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -552,7 +559,14 @@ const FlowEditor: React.FC<{ { label: !isRunning ? "Run" : "Stop", icon: !isRunning ? : , - onClick: !isRunning ? requestSaveAndRun : requestStopRun, + onClick: !isRunning + ? () => runnerUIRef.current?.runOrOpenInput() + : requestStopRun, + }, + { + label: "Runner Output", + icon: , + onClick: () => runnerUIRef.current?.openRunnerOutput(), }, ]; @@ -596,6 +610,13 @@ const FlowEditor: React.FC<{ + ); }; diff --git a/rnd/autogpt_builder/src/components/RunnerUIWrapper.tsx b/rnd/autogpt_builder/src/components/RunnerUIWrapper.tsx new file mode 100644 index 0000000000..bd9b6f29db --- /dev/null +++ b/rnd/autogpt_builder/src/components/RunnerUIWrapper.tsx @@ -0,0 +1,141 @@ +import React, { + useState, + useCallback, + forwardRef, + useImperativeHandle, +} from "react"; +import RunnerInputUI from "./runner-ui/RunnerInputUI"; +import RunnerOutputUI from "./runner-ui/RunnerOutputUI"; +import { Node } from "@xyflow/react"; +import { filterBlocksByType } from "@/lib/utils"; +import { BlockIORootSchema } from "@/lib/autogpt-server-api/types"; + +interface RunnerUIWrapperProps { + nodes: Node[]; + setNodes: React.Dispatch>; + isRunning: boolean; + requestSaveAndRun: () => void; +} + +export interface RunnerUIWrapperRef { + openRunnerInput: () => void; + openRunnerOutput: () => void; + runOrOpenInput: () => void; +} + +const RunnerUIWrapper = forwardRef( + ({ nodes, setNodes, isRunning, requestSaveAndRun }, ref) => { + const [isRunnerInputOpen, setIsRunnerInputOpen] = useState(false); + const [isRunnerOutputOpen, setIsRunnerOutputOpen] = useState(false); + + const getBlockInputsAndOutputs = useCallback(() => { + const inputBlocks = filterBlocksByType( + nodes, + (node) => node.data.block_id === "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b", + ); + + const outputBlocks = filterBlocksByType( + nodes, + (node) => node.data.block_id === "363ae599-353e-4804-937e-b2ee3cef3da4", + ); + + const inputs = inputBlocks.map((node) => ({ + id: node.id, + type: "input" as const, + inputSchema: node.data.inputSchema as BlockIORootSchema, + hardcodedValues: { + name: (node.data.hardcodedValues as any).name || "", + description: (node.data.hardcodedValues as any).description || "", + value: (node.data.hardcodedValues as any).value, + placeholder_values: + (node.data.hardcodedValues as any).placeholder_values || [], + limit_to_placeholder_values: + (node.data.hardcodedValues as any).limit_to_placeholder_values || + false, + }, + })); + + const outputs = outputBlocks.map((node) => ({ + id: node.id, + type: "output" as const, + outputSchema: node.data.outputSchema as BlockIORootSchema, + hardcodedValues: { + name: (node.data.hardcodedValues as any).name || "Output", + description: + (node.data.hardcodedValues as any).description || + "Output from the agent", + value: (node.data.hardcodedValues as any).value, + }, + result: (node.data.executionResults as any)?.at(-1)?.data?.output, + })); + + return { inputs, outputs }; + }, [nodes]); + + const handleInputChange = useCallback( + (nodeId: string, field: string, value: string) => { + setNodes((nds) => + nds.map((node) => { + if (node.id === nodeId) { + return { + ...node, + data: { + ...node.data, + hardcodedValues: { + ...(node.data.hardcodedValues as any), + [field]: value, + }, + }, + }; + } + return node; + }), + ); + }, + [setNodes], + ); + + const openRunnerInput = () => setIsRunnerInputOpen(true); + const openRunnerOutput = () => setIsRunnerOutputOpen(true); + + const runOrOpenInput = () => { + const { inputs } = getBlockInputsAndOutputs(); + if (inputs.length > 0) { + openRunnerInput(); + } else { + requestSaveAndRun(); + } + }; + + useImperativeHandle(ref, () => ({ + openRunnerInput, + openRunnerOutput, + runOrOpenInput, + })); + + return ( + <> + setIsRunnerInputOpen(false)} + blockInputs={getBlockInputsAndOutputs().inputs} + onInputChange={handleInputChange} + onRun={() => { + setIsRunnerInputOpen(false); + requestSaveAndRun(); + }} + isRunning={isRunning} + /> + setIsRunnerOutputOpen(false)} + blockOutputs={getBlockInputsAndOutputs().outputs} + /> + + ); + }, +); + +RunnerUIWrapper.displayName = "RunnerUIWrapper"; + +export default RunnerUIWrapper; diff --git a/rnd/autogpt_builder/src/components/runner-ui/RunnerInputBlock.tsx b/rnd/autogpt_builder/src/components/runner-ui/RunnerInputBlock.tsx new file mode 100644 index 0000000000..67d08bada8 --- /dev/null +++ b/rnd/autogpt_builder/src/components/runner-ui/RunnerInputBlock.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface InputBlockProps { + id: string; + name: string; + description?: string; + value: string; + placeholder_values?: any[]; + onInputChange: (id: string, field: string, value: string) => void; +} + +export function InputBlock({ + id, + name, + description, + value, + placeholder_values, + onInputChange, +}: InputBlockProps) { + return ( +
+

{name || "Unnamed Input"}

+ {description &&

{description}

} +
+ {placeholder_values && placeholder_values.length > 1 ? ( + + ) : ( + onInputChange(id, "value", e.target.value)} + placeholder={placeholder_values?.[0]?.toString() || "Enter value"} + className="w-full" + /> + )} +
+
+ ); +} diff --git a/rnd/autogpt_builder/src/components/runner-ui/RunnerInputList.tsx b/rnd/autogpt_builder/src/components/runner-ui/RunnerInputList.tsx new file mode 100644 index 0000000000..abf0523979 --- /dev/null +++ b/rnd/autogpt_builder/src/components/runner-ui/RunnerInputList.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { InputBlock } from "./RunnerInputBlock"; +import { BlockInput } from "./RunnerInputUI"; + +interface InputListProps { + blockInputs: BlockInput[]; + onInputChange: (nodeId: string, field: string, value: string) => void; +} + +export function InputList({ blockInputs, onInputChange }: InputListProps) { + return ( + +
+ {blockInputs && blockInputs.length > 0 ? ( + blockInputs.map((block) => ( + + )) + ) : ( +

No input blocks available.

+ )} +
+
+ ); +} diff --git a/rnd/autogpt_builder/src/components/runner-ui/RunnerInputUI.tsx b/rnd/autogpt_builder/src/components/runner-ui/RunnerInputUI.tsx new file mode 100644 index 0000000000..4e0077c8f6 --- /dev/null +++ b/rnd/autogpt_builder/src/components/runner-ui/RunnerInputUI.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { BlockIORootSchema } from "@/lib/autogpt-server-api/types"; +import { InputList } from "./RunnerInputList"; + +export interface BlockInput { + id: string; + inputSchema: BlockIORootSchema; + hardcodedValues: { + name: string; + description: string; + value: any; + placeholder_values?: any[]; + limit_to_placeholder_values?: boolean; + }; +} + +interface RunSettingsUiProps { + isOpen: boolean; + onClose: () => void; + blockInputs: BlockInput[]; + onInputChange: (nodeId: string, field: string, value: string) => void; + onRun: () => void; + isRunning: boolean; +} + +export function RunnerInputUI({ + isOpen, + onClose, + blockInputs, + onInputChange, + onRun, + isRunning, +}: RunSettingsUiProps) { + const handleRun = () => { + onRun(); + onClose(); + }; + + return ( + + + + Run Settings + + Configure settings for running your agent. + + +
+ +
+ + + +
+
+ ); +} + +export default RunnerInputUI; diff --git a/rnd/autogpt_builder/src/components/runner-ui/RunnerOutputUI.tsx b/rnd/autogpt_builder/src/components/runner-ui/RunnerOutputUI.tsx new file mode 100644 index 0000000000..8e39eb3d48 --- /dev/null +++ b/rnd/autogpt_builder/src/components/runner-ui/RunnerOutputUI.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, +} from "@/components/ui/sheet"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { BlockIORootSchema } from "@/lib/autogpt-server-api/types"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +interface BlockOutput { + id: string; + outputSchema: BlockIORootSchema; + hardcodedValues: { + name: string; + description: string; + }; + result?: any; +} + +interface OutputModalProps { + isOpen: boolean; + onClose: () => void; + blockOutputs: BlockOutput[]; +} + +const formatOutput = (output: any): string => { + if (typeof output === "object") { + try { + return JSON.stringify(output, null, 2); + } catch (error) { + return `Error formatting output: ${(error as Error).message}`; + } + } + return String(output); +}; + +export function RunnerOutputUI({ + isOpen, + onClose, + blockOutputs, +}: OutputModalProps) { + return ( + + + + Run Outputs + + View the outputs from your agent run. + + +
+ +
+ {blockOutputs && blockOutputs.length > 0 ? ( + blockOutputs.map((block) => ( +
+ + + {block.hardcodedValues.description && ( + + )} + +
+