From fbbd222405a342632588ad3ff22f46252df8e71c Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 7 Apr 2026 22:57:21 +0700 Subject: [PATCH 01/97] feat(frontend/builder): add chat panel for interactive agent editing Add a collapsible right-side chat panel to the flow builder that lets users ask questions about their agent and request modifications via chat. --- .../BuilderChatPanel/BuilderChatPanel.tsx | 272 ++++++++++++++++ .../__tests__/BuilderChatPanel.test.tsx | 303 ++++++++++++++++++ .../components/BuilderChatPanel/helpers.ts | 85 +++++ .../BuilderChatPanel/useBuilderChatPanel.ts | 149 +++++++++ .../build/components/FlowEditor/Flow/Flow.tsx | 2 + 5 files changed, 811 insertions(+) create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/BuilderChatPanel.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/__tests__/BuilderChatPanel.test.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/helpers.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/useBuilderChatPanel.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/BuilderChatPanel.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/BuilderChatPanel.tsx new file mode 100644 index 0000000000..7420b189a6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderChatPanel/BuilderChatPanel.tsx @@ -0,0 +1,272 @@ +"use client"; + +import { Button } from "@/components/atoms/Button/Button"; +import { cn } from "@/lib/utils"; +import { + ChatCircle, + PaperPlaneTilt, + SpinnerGap, + StopCircle, + X, +} from "@phosphor-icons/react"; +import { KeyboardEvent, useRef, useState } from "react"; +import { GraphAction } from "./helpers"; +import { useBuilderChatPanel } from "./useBuilderChatPanel"; + +interface Props { + className?: string; +} + +export function BuilderChatPanel({ className }: Props) { + const { + isOpen, + handleToggle, + messages, + sendMessage, + stop, + status, + isCreatingSession, + parsedActions, + handleApplyAction, + } = useBuilderChatPanel(); + + const [inputValue, setInputValue] = useState(""); + const messagesEndRef = useRef(null); + const isStreaming = status === "streaming" || status === "submitted"; + + function handleSend() { + const text = inputValue.trim(); + if (!text || isStreaming) return; + setInputValue(""); + sendMessage({ text }); + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, 50); + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + } + + return ( +
+ {isOpen && ( +
+ + + + + +
+ )} + + +
+ ); +} + +function PanelHeader({ onClose }: { onClose: () => void }) { + return ( +
+
+ + + Chat with Builder + +
+ +
+ ); +} + +interface MessageListProps { + messages: ReturnType["messages"]; + isCreatingSession: boolean; + parsedActions: GraphAction[]; + onApplyAction: (action: GraphAction) => void; + messagesEndRef: React.RefObject; +} + +function MessageList({ + messages, + isCreatingSession, + parsedActions, + onApplyAction, + messagesEndRef, +}: MessageListProps) { + return ( +
+ {isCreatingSession && ( +
+ + Setting up chat session… +
+ )} + + {messages.map((msg) => { + const textParts = msg.parts + .filter( + (p): p is Extract => p.type === "text", + ) + .map((p) => p.text) + .join(""); + + if (!textParts) return null; + + return ( +
+ {textParts} +
+ ); + })} + + {parsedActions.length > 0 && ( +
+

+ Suggested changes +

+ {parsedActions.map((action, i) => ( + onApplyAction(action)} + /> + ))} +
+ )} + +
+
+ ); +} + +function ActionItem({ + action, + onApply, +}: { + action: GraphAction; + onApply: () => void; +}) { + const [applied, setApplied] = useState(false); + + function handleApply() { + onApply(); + setApplied(true); + } + + const label = + action.type === "update_node_input" + ? `Set node ${action.nodeId} "${action.key}" = ${JSON.stringify(action.value)}` + : `Connect node ${action.source} → ${action.target}`; + + return ( +
+ {label} + +
+ ); +} + +interface PanelInputProps { + value: string; + onChange: (v: string) => void; + onKeyDown: (e: KeyboardEvent) => void; + onSend: () => void; + onStop: () => void; + isStreaming: boolean; +} + +function PanelInput({ + value, + onChange, + onKeyDown, + onSend, + onStop, + isStreaming, +}: PanelInputProps) { + return ( +
+
+