mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(platform): implement HITL UI redesign with improved review flow (#11529)
## Summary • Redesigned Human-in-the-Loop review interface with yellow warning scheme • Implemented separate approved_data/rejected_data output pins for human_in_the_loop block • Added real-time execution status tracking to legacy flow for review detection • Fixed button loading states and improved UI consistency across flows • Standardized Tailwind CSS usage removing custom values <img width="1500" alt="image" src="https://github.com/user-attachments/assets/4ca6dd98-f3c4-41c0-a06b-92b3bca22490" /> <img width="1500" alt="image" src="https://github.com/user-attachments/assets/0afae211-09f0-465e-b477-c3949f13c876" /> <img width="1500" alt="image" src="https://github.com/user-attachments/assets/05d9d1ed-cd40-4c73-92b8-0dab21713ca9" /> ## Changes Made ### Backend Changes - Modified `human_in_the_loop.py` block to output separate `approved_data` and `rejected_data` pins instead of single reviewed_data with status - Updated block output schema to support better data flow in graph builder ### Frontend UI Changes - Redesigned PendingReviewsList with yellow warning color scheme (replacing orange) - Fixed button loading states to show spinner only on clicked button - Improved FloatingReviewsPanel layout removing redundant headers - Added real-time status tracking to legacy flow using useFlowRealtime hook - Fixed AgentActivityDropdown text overflow and layout issues - Enhanced Safe Mode toggle positioning and toast timing - Standardized all custom Tailwind values to use standard classes ### Design System Updates - Added yellow design tokens (25, 150, 600) for warning states - Unified REVIEW status handling across all components - Improved component composition patterns ## Test Plan - [x] Verify HITL blocks create separate output pins for approved/rejected data - [x] Test review flow works in both new and legacy flow builders - [x] Confirm button loading states work correctly (only clicked button shows spinner) - [x] Validate AgentActivityDropdown properly displays review status - [x] Check Safe Mode toggle positioning matches old flow - [x] Ensure real-time status updates work in legacy flow - [x] Verify yellow warning colors are consistent throughout 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Lluis Agusti <hi@llu.lu>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Graph } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ShieldCheckIcon, ShieldIcon } from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
|
||||
interface Props {
|
||||
graph: GraphModel | LibraryAgent | Graph;
|
||||
className?: string;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export function FloatingSafeModeToggle({
|
||||
graph,
|
||||
className,
|
||||
fullWidth = false,
|
||||
}: Props) {
|
||||
const {
|
||||
currentSafeMode,
|
||||
isPending,
|
||||
shouldShowToggle,
|
||||
isStateUndetermined,
|
||||
handleToggle,
|
||||
} = useAgentSafeMode(graph);
|
||||
|
||||
if (!shouldShowToggle || isStateUndetermined || isPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("fixed z-50", className)}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={currentSafeMode! ? "primary" : "outline"}
|
||||
key={graph.id}
|
||||
size="small"
|
||||
title={
|
||||
currentSafeMode!
|
||||
? "Safe Mode: ON. Human in the loop blocks require manual review"
|
||||
: "Safe Mode: OFF. Human in the loop blocks proceed automatically"
|
||||
}
|
||||
onClick={handleToggle}
|
||||
className={cn(fullWidth ? "w-full" : "")}
|
||||
>
|
||||
{currentSafeMode! ? (
|
||||
<>
|
||||
<ShieldCheckIcon weight="bold" size={16} />
|
||||
<Text variant="body" className="text-zinc-200">
|
||||
Safe Mode: ON
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldIcon weight="bold" size={16} />
|
||||
<Text variant="body" className="text-zinc-600">
|
||||
Safe Mode: OFF
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-center">
|
||||
<div className="font-medium">
|
||||
Safe Mode: {currentSafeMode! ? "ON" : "OFF"}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{currentSafeMode!
|
||||
? "Human in the loop blocks require manual review"
|
||||
: "Human in the loop blocks proceed automatically"}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,12 +16,12 @@ import { useCopyPaste } from "./useCopyPaste";
|
||||
import { FloatingReviewsPanel } from "@/components/organisms/FloatingReviewsPanel/FloatingReviewsPanel";
|
||||
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";
|
||||
import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle";
|
||||
|
||||
export const Flow = () => {
|
||||
const [{ flowID, flowExecutionID }] = useQueryStates({
|
||||
@@ -113,8 +113,7 @@ export const Flow = () => {
|
||||
{graph && (
|
||||
<FloatingSafeModeToggle
|
||||
graph={graph}
|
||||
className="right-4 top-32 p-2"
|
||||
variant="black"
|
||||
className="right-2 top-32 p-2"
|
||||
/>
|
||||
)}
|
||||
</ReactFlow>
|
||||
|
||||
@@ -9,7 +9,7 @@ const statusStyles: Record<AgentExecutionStatus, string> = {
|
||||
INCOMPLETE: "text-slate-700 border-slate-400",
|
||||
QUEUED: "text-blue-700 border-blue-400",
|
||||
RUNNING: "text-amber-700 border-amber-400",
|
||||
REVIEW: "text-orange-700 border-orange-400 bg-orange-50",
|
||||
REVIEW: "text-yellow-700 border-yellow-400 bg-yellow-50",
|
||||
COMPLETED: "text-green-700 border-green-400",
|
||||
TERMINATED: "text-orange-700 border-orange-400",
|
||||
FAILED: "text-red-700 border-red-400",
|
||||
|
||||
@@ -4,7 +4,7 @@ export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
||||
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
||||
QUEUED: " ring-blue-300 bg-blue-300",
|
||||
RUNNING: "ring-amber-300 bg-amber-300",
|
||||
REVIEW: "ring-orange-300 bg-orange-300",
|
||||
REVIEW: "ring-yellow-300 bg-yellow-300",
|
||||
COMPLETED: "ring-green-300 bg-green-300",
|
||||
TERMINATED: "ring-orange-300 bg-orange-300 ",
|
||||
FAILED: "ring-red-300 bg-red-300",
|
||||
|
||||
@@ -65,7 +65,8 @@ import NewControlPanel from "@/app/(platform)/build/components/NewControlPanel/N
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { BuildActionBar } from "../BuildActionBar";
|
||||
import { FloatingReviewsPanel } from "@/components/organisms/FloatingReviewsPanel/FloatingReviewsPanel";
|
||||
import { FloatingSafeModeToggle } from "@/components/molecules/FloatingSafeModeToggle/FloatingSafeModeToggle";
|
||||
import { useFlowRealtime } from "@/app/(platform)/build/components/FlowEditor/Flow/useFlowRealtime";
|
||||
import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle";
|
||||
|
||||
// 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
|
||||
@@ -153,6 +154,9 @@ const FlowEditor: React.FC<{
|
||||
Record<string, { x: number; y: number }>
|
||||
>(Object.fromEntries(nodes.map((node) => [node.id, node.position])));
|
||||
|
||||
// Add realtime execution status tracking for FloatingReviewsPanel
|
||||
useFlowRealtime();
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
@@ -924,8 +928,7 @@ const FlowEditor: React.FC<{
|
||||
{savedAgent && (
|
||||
<FloatingSafeModeToggle
|
||||
graph={savedAgent}
|
||||
className="right-4 top-32 p-2"
|
||||
variant="black"
|
||||
className="right-2 top-32 p-2"
|
||||
/>
|
||||
)}
|
||||
{isNewBlockEnabled ? (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
import { useEffect } from "react";
|
||||
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
|
||||
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
|
||||
import { EmptySchedules } from "./components/other/EmptySchedules";
|
||||
@@ -17,6 +18,7 @@ import { SelectedRunView } from "./components/selected-views/SelectedRunView/Sel
|
||||
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
|
||||
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
|
||||
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
|
||||
import { SelectedSettingsView } from "./components/selected-views/SelectedSettingsView/SelectedSettingsView";
|
||||
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
||||
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
||||
@@ -24,7 +26,6 @@ import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
||||
|
||||
export function NewAgentLibraryView() {
|
||||
const {
|
||||
agentId,
|
||||
agent,
|
||||
ready,
|
||||
activeTemplate,
|
||||
@@ -39,10 +40,17 @@ export function NewAgentLibraryView() {
|
||||
handleCountsChange,
|
||||
handleClearSelectedRun,
|
||||
onRunInitiated,
|
||||
handleSelectSettings,
|
||||
onTriggerSetup,
|
||||
onScheduleCreated,
|
||||
} = useNewAgentLibraryView();
|
||||
|
||||
useEffect(() => {
|
||||
if (agent) {
|
||||
document.title = `${agent.name} - Library - AutoGPT Platform`;
|
||||
}
|
||||
}, [agent]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCard
|
||||
@@ -62,12 +70,14 @@ export function NewAgentLibraryView() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="mx-6 pt-4">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: agent.name, link: `/library/agents/${agentId}` },
|
||||
]}
|
||||
/>
|
||||
<div className="relative flex items-center gap-2">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: agent.name },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<EmptyTasks
|
||||
@@ -121,7 +131,12 @@ export function NewAgentLibraryView() {
|
||||
</SectionWrap>
|
||||
|
||||
{activeItem ? (
|
||||
activeTab === "scheduled" ? (
|
||||
activeItem === "settings" ? (
|
||||
<SelectedSettingsView
|
||||
agent={agent}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
) : activeTab === "scheduled" ? (
|
||||
<SelectedScheduleView
|
||||
agent={agent}
|
||||
scheduleId={activeItem}
|
||||
@@ -148,24 +163,40 @@ export function NewAgentLibraryView() {
|
||||
runId={activeItem}
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
/>
|
||||
)
|
||||
) : sidebarLoading ? (
|
||||
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
|
||||
<LoadingSelectedContent
|
||||
agent={agent}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
/>
|
||||
) : activeTab === "scheduled" ? (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
>
|
||||
<EmptySchedules />
|
||||
</SelectedViewLayout>
|
||||
) : activeTab === "templates" ? (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
>
|
||||
<EmptyTemplates />
|
||||
</SelectedViewLayout>
|
||||
) : activeTab === "triggers" ? (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
>
|
||||
<EmptyTriggers />
|
||||
</SelectedViewLayout>
|
||||
) : (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
>
|
||||
<EmptyTasks
|
||||
agent={agent}
|
||||
onRun={onRunInitiated}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function CredentialRow({
|
||||
</Text>
|
||||
<Text
|
||||
variant="large"
|
||||
className="relative top-1 hidden flex-[0_0_40%] overflow-hidden truncate font-mono tracking-tight md:block"
|
||||
className="lex-[0_0_40%] relative top-1 hidden overflow-hidden whitespace-nowrap font-mono tracking-tight md:block"
|
||||
>
|
||||
{"*".repeat(MASKED_KEY_LENGTH)}
|
||||
</Text>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { GearIcon } from "@phosphor-icons/react";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
onSelectSettings: () => void;
|
||||
}
|
||||
|
||||
export function AgentSettingsButton({ agent, onSelectSettings }: Props) {
|
||||
const { hasHITLBlocks } = useAgentSafeMode(agent);
|
||||
|
||||
if (!hasHITLBlocks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="m-0 min-w-0 rounded-full p-0 px-1"
|
||||
onClick={onSelectSettings}
|
||||
aria-label="Agent Settings"
|
||||
>
|
||||
<GearIcon size={18} className="text-zinc-600" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AnchorLinksWrap({ children }: Props) {
|
||||
return (
|
||||
<div className={cn(AGENT_LIBRARY_SECTION_PADDING_X, "hidden lg:block")}>
|
||||
<nav className="flex gap-8 px-3 pb-1">{children}</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||
import { SelectedViewLayout } from "./SelectedViewLayout";
|
||||
|
||||
interface Props {
|
||||
agentName: string;
|
||||
agentId: string;
|
||||
agent: LibraryAgent;
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function LoadingSelectedContent(props: Props) {
|
||||
return (
|
||||
<SelectedViewLayout agentName={props.agentName} agentId={props.agentId}>
|
||||
<SelectedViewLayout
|
||||
agent={props.agent}
|
||||
onSelectSettings={props.onSelectSettings}
|
||||
selectedSettings={props.selectedSettings}
|
||||
>
|
||||
<div
|
||||
className={cn("flex flex-col gap-4", AGENT_LIBRARY_SECTION_PADDING_X)}
|
||||
>
|
||||
|
||||
@@ -32,6 +32,8 @@ interface Props {
|
||||
runId: string;
|
||||
onSelectRun?: (id: string) => void;
|
||||
onClearSelectedRun?: () => void;
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function SelectedRunView({
|
||||
@@ -39,6 +41,7 @@ export function SelectedRunView({
|
||||
runId,
|
||||
onSelectRun,
|
||||
onClearSelectedRun,
|
||||
onSelectSettings,
|
||||
}: Props) {
|
||||
const { run, preset, isLoading, responseError, httpError } =
|
||||
useSelectedRunView(agent.graph_id, runId);
|
||||
@@ -72,13 +75,13 @@ export function SelectedRunView({
|
||||
}
|
||||
|
||||
if (isLoading && !run) {
|
||||
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
|
||||
return <LoadingSelectedContent agent={agent} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout agent={agent} onSelectSettings={onSelectSettings}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={run} />
|
||||
|
||||
@@ -106,6 +109,11 @@ export function SelectedRunView({
|
||||
className="-mt-2 flex flex-col"
|
||||
>
|
||||
<ScrollableTabsList className="px-4">
|
||||
{withReviews && (
|
||||
<ScrollableTabsTrigger value="reviews">
|
||||
Reviews ({pendingReviews.length})
|
||||
</ScrollableTabsTrigger>
|
||||
)}
|
||||
{withSummary && (
|
||||
<ScrollableTabsTrigger value="summary">
|
||||
Summary
|
||||
@@ -117,13 +125,29 @@ export function SelectedRunView({
|
||||
<ScrollableTabsTrigger value="input">
|
||||
Your input
|
||||
</ScrollableTabsTrigger>
|
||||
{withReviews && (
|
||||
<ScrollableTabsTrigger value="reviews">
|
||||
Reviews ({pendingReviews.length})
|
||||
</ScrollableTabsTrigger>
|
||||
)}
|
||||
</ScrollableTabsList>
|
||||
<div className="my-6 flex flex-col gap-6">
|
||||
{/* Human-in-the-Loop Reviews Section */}
|
||||
{withReviews && (
|
||||
<ScrollableTabsContent value="reviews">
|
||||
<div id="reviews" className="scroll-mt-4 px-4">
|
||||
{reviewsLoading ? (
|
||||
<LoadingSpinner size="small" />
|
||||
) : pendingReviews.length > 0 ? (
|
||||
<PendingReviewsList
|
||||
reviews={pendingReviews}
|
||||
onReviewComplete={refetchReviews}
|
||||
emptyMessage="No pending reviews for this execution"
|
||||
/>
|
||||
) : (
|
||||
<Text variant="body" className="text-zinc-600">
|
||||
No pending reviews for this execution
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</ScrollableTabsContent>
|
||||
)}
|
||||
|
||||
{/* Summary Section */}
|
||||
{withSummary && (
|
||||
<ScrollableTabsContent value="summary">
|
||||
@@ -186,29 +210,6 @@ export function SelectedRunView({
|
||||
</RunDetailCard>
|
||||
</div>
|
||||
</ScrollableTabsContent>
|
||||
|
||||
{/* Reviews Section */}
|
||||
{withReviews && (
|
||||
<ScrollableTabsContent value="reviews">
|
||||
<div className="scroll-mt-4">
|
||||
<RunDetailCard>
|
||||
{reviewsLoading ? (
|
||||
<LoadingSpinner size="small" />
|
||||
) : pendingReviews.length > 0 ? (
|
||||
<PendingReviewsList
|
||||
reviews={pendingReviews}
|
||||
onReviewComplete={refetchReviews}
|
||||
emptyMessage="No pending reviews for this execution"
|
||||
/>
|
||||
) : (
|
||||
<Text variant="body" className="text-zinc-700">
|
||||
No pending reviews for this execution
|
||||
</Text>
|
||||
)}
|
||||
</RunDetailCard>
|
||||
</div>
|
||||
</ScrollableTabsContent>
|
||||
)}
|
||||
</div>
|
||||
</ScrollableTabs>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
EyeIcon,
|
||||
PauseCircleIcon,
|
||||
StopCircleIcon,
|
||||
WarningCircleIcon,
|
||||
WarningIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
@@ -38,9 +38,9 @@ const statusIconMap: Record<AgentExecutionStatus, StatusIconMap> = {
|
||||
textColor: "!text-yellow-700",
|
||||
},
|
||||
REVIEW: {
|
||||
icon: <EyeIcon size={16} className="text-orange-700" weight="bold" />,
|
||||
bgColor: "bg-orange-50",
|
||||
textColor: "!text-orange-700",
|
||||
icon: <WarningIcon size={16} className="text-yellow-700" weight="bold" />,
|
||||
bgColor: "bg-yellow-50",
|
||||
textColor: "!text-yellow-700",
|
||||
},
|
||||
COMPLETED: {
|
||||
icon: (
|
||||
|
||||
@@ -25,7 +25,7 @@ export function RunSummary({ run }: Props) {
|
||||
</p>
|
||||
|
||||
{typeof correctnessScore === "number" && (
|
||||
<div className="flex items-center gap-3 rounded-lg bg-neutral-50 p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-neutral-600">
|
||||
Success Estimate:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Graph } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ShieldCheckIcon, ShieldIcon } from "@phosphor-icons/react";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
|
||||
interface Props {
|
||||
graph: GraphModel | LibraryAgent | Graph;
|
||||
className?: string;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export function SafeModeToggle({ graph }: Props) {
|
||||
const {
|
||||
currentSafeMode,
|
||||
isPending,
|
||||
shouldShowToggle,
|
||||
isStateUndetermined,
|
||||
handleToggle,
|
||||
} = useAgentSafeMode(graph);
|
||||
|
||||
if (!shouldShowToggle || isStateUndetermined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
key={graph.id}
|
||||
size="icon"
|
||||
aria-label={
|
||||
currentSafeMode!
|
||||
? "Safe Mode: ON. Human in the loop blocks require manual review"
|
||||
: "Safe Mode: OFF. Human in the loop blocks proceed automatically"
|
||||
}
|
||||
onClick={handleToggle}
|
||||
className={cn(isPending ? "opacity-0" : "opacity-100")}
|
||||
>
|
||||
{currentSafeMode! ? (
|
||||
<>
|
||||
<ShieldCheckIcon weight="bold" size={16} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldIcon weight="bold" size={16} />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { FloatingSafeModeToggle } from "@/components/molecules/FloatingSafeModeToggle/FloatingSafeModeToggle";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import {
|
||||
ArrowBendLeftUpIcon,
|
||||
@@ -16,6 +15,7 @@ import { SelectedActionsWrap } from "../../../SelectedActionsWrap";
|
||||
import { ShareRunButton } from "../../../ShareRunButton/ShareRunButton";
|
||||
import { CreateTemplateModal } from "../CreateTemplateModal/CreateTemplateModal";
|
||||
import { useSelectedRunActions } from "./useSelectedRunActions";
|
||||
import { SafeModeToggle } from "../SafeModeToggle";
|
||||
|
||||
type Props = {
|
||||
agent: LibraryAgent;
|
||||
@@ -113,7 +113,7 @@ export function SelectedRunActions({
|
||||
shareToken={run.share_token}
|
||||
/>
|
||||
)}
|
||||
<FloatingSafeModeToggle graph={agent} variant="white" fullWidth={false} />
|
||||
<SafeModeToggle graph={agent} fullWidth={false} />
|
||||
{canRunManually && (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -20,12 +20,16 @@ interface Props {
|
||||
agent: LibraryAgent;
|
||||
scheduleId: string;
|
||||
onClearSelectedRun?: () => void;
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function SelectedScheduleView({
|
||||
agent,
|
||||
scheduleId,
|
||||
onClearSelectedRun,
|
||||
onSelectSettings,
|
||||
selectedSettings,
|
||||
}: Props) {
|
||||
const { schedule, isLoading, error } = useSelectedScheduleView(
|
||||
agent.graph_id,
|
||||
@@ -68,13 +72,17 @@ export function SelectedScheduleView({
|
||||
}
|
||||
|
||||
if (isLoading && !schedule) {
|
||||
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
|
||||
return <LoadingSelectedContent agent={agent} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
onSelectSettings={onSelectSettings}
|
||||
selectedSettings={selectedSettings}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-0">
|
||||
<RunDetailHeader
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import { SelectedViewLayout } from "../SelectedViewLayout";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
onClearSelectedRun: () => void;
|
||||
}
|
||||
|
||||
export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||
useAgentSafeMode(agent);
|
||||
|
||||
return (
|
||||
<SelectedViewLayout agent={agent} onSelectSettings={() => {}}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} mb-8 flex items-center gap-3`}
|
||||
>
|
||||
<Button
|
||||
variant="icon"
|
||||
size="small"
|
||||
onClick={onClearSelectedRun}
|
||||
className="w-[2.375rem]"
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Text variant="h2">Agent Settings</Text>
|
||||
</div>
|
||||
|
||||
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
||||
{!hasHITLBlocks ? (
|
||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
This agent doesn't have any human-in-the-loop blocks, so
|
||||
there are no settings to configure.
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">Require human approval</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause and wait for your review before
|
||||
continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSafeMode || false}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectedViewLayout>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export function SelectedTemplateView({
|
||||
}
|
||||
|
||||
if (isLoading && !template) {
|
||||
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
|
||||
return <LoadingSelectedContent agent={agent} />;
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
@@ -100,7 +100,7 @@ export function SelectedTemplateView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout agent={agent}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={undefined} />
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export function SelectedTriggerView({
|
||||
}
|
||||
|
||||
if (isLoading && !trigger) {
|
||||
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
|
||||
return <LoadingSelectedContent agent={agent} />;
|
||||
}
|
||||
|
||||
if (!trigger) {
|
||||
@@ -93,7 +93,7 @@ export function SelectedTriggerView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout agent={agent}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={undefined} />
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { AgentSettingsButton } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||
import { SectionWrap } from "../other/SectionWrap";
|
||||
|
||||
interface Props {
|
||||
agentName: string;
|
||||
agentId: string;
|
||||
agent: LibraryAgent;
|
||||
children: React.ReactNode;
|
||||
additionalBreadcrumb?: { name: string; link?: string };
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function SelectedViewLayout(props: Props) {
|
||||
@@ -14,12 +18,24 @@ export function SelectedViewLayout(props: Props) {
|
||||
<div
|
||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
|
||||
>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: props.agentName, link: `/library/agents/${props.agentId}` },
|
||||
]}
|
||||
/>
|
||||
<div className="relative flex w-fit items-center gap-2">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{
|
||||
name: props.agent.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{props.agent && props.onSelectSettings && (
|
||||
<div className="absolute -right-8">
|
||||
<AgentSettingsButton
|
||||
agent={props.agent}
|
||||
onSelectSettings={props.onSelectSettings}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-visible">
|
||||
{props.children}
|
||||
|
||||
@@ -34,8 +34,8 @@ const statusIconMap: Record<AgentExecutionStatus, React.ReactNode> = {
|
||||
</IconWrapper>
|
||||
),
|
||||
REVIEW: (
|
||||
<IconWrapper className="border-orange-50 bg-orange-50">
|
||||
<PauseCircleIcon size={16} className="text-orange-700" weight="bold" />
|
||||
<IconWrapper className="border-yellow-50 bg-yellow-50">
|
||||
<PauseCircleIcon size={16} className="text-yellow-700" weight="bold" />
|
||||
</IconWrapper>
|
||||
),
|
||||
COMPLETED: (
|
||||
|
||||
@@ -89,10 +89,8 @@ export function useNewAgentLibraryView() {
|
||||
[sidebarCounts],
|
||||
);
|
||||
|
||||
// Show sidebar layout while loading or when there are items
|
||||
const showSidebarLayout = sidebarLoading || hasAnyItems;
|
||||
|
||||
useEffect(() => {
|
||||
// Show sidebar layout while loading or when there are items or settings is selected
|
||||
const showSidebarLayout = useEffect(() => {
|
||||
if (agent) {
|
||||
document.title = `${agent.name} - Library - AutoGPT Platform`;
|
||||
}
|
||||
@@ -134,6 +132,13 @@ export function useNewAgentLibraryView() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleSelectSettings() {
|
||||
setQueryStates({
|
||||
activeItem: "settings",
|
||||
activeTab: "runs", // Reset to runs tab when going to settings
|
||||
});
|
||||
}
|
||||
|
||||
const handleCountsChange = useCallback(
|
||||
(counts: {
|
||||
runsCount: number;
|
||||
@@ -205,6 +210,7 @@ export function useNewAgentLibraryView() {
|
||||
handleCountsChange,
|
||||
handleSelectRun,
|
||||
onRunInitiated,
|
||||
handleSelectSettings,
|
||||
onTriggerSetup,
|
||||
onScheduleCreated,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const statusData: Record<
|
||||
draft: { label: "Draft", variant: "secondary" },
|
||||
stopped: { label: "Stopped", variant: "secondary" },
|
||||
scheduled: { label: "Scheduled", variant: "secondary" },
|
||||
review: { label: "In Review", variant: "orange" },
|
||||
review: { label: "In Review", variant: "warning" },
|
||||
};
|
||||
|
||||
const statusStyles = {
|
||||
@@ -47,8 +47,6 @@ const statusStyles = {
|
||||
destructive: "bg-red-100 text-red-800 hover:bg-red-100 hover:text-red-800",
|
||||
warning:
|
||||
"bg-yellow-100 text-yellow-800 hover:bg-yellow-100 hover:text-yellow-800",
|
||||
orange:
|
||||
"bg-orange-100 text-orange-800 hover:bg-orange-100 hover:text-orange-800",
|
||||
info: "bg-blue-100 text-blue-800 hover:bg-blue-100 hover:text-blue-800",
|
||||
secondary:
|
||||
"bg-slate-100 text-slate-800 hover:bg-slate-100 hover:text-slate-800",
|
||||
|
||||
@@ -54,7 +54,7 @@ export const AgentFlowList = ({
|
||||
|
||||
<div className="flex items-center">
|
||||
{/* Split "Create" button */}
|
||||
<Button variant="outline" className="rounded-r-none" asChild>
|
||||
<Button variant="outline" className="rounded-r-none">
|
||||
<Link href="/build">Create</Link>
|
||||
</Button>
|
||||
<Dialog>
|
||||
|
||||
@@ -48,8 +48,7 @@ export function AgentActivityDropdown() {
|
||||
className="absolute bottom-[-2.5rem] left-1/2 z-50 hidden -translate-x-1/2 transform whitespace-nowrap rounded-small bg-white px-4 py-2 shadow-md group-hover:block"
|
||||
>
|
||||
<Text variant="body-medium">
|
||||
{activeCount} running agent
|
||||
{activeCount > 1 ? "s" : ""}
|
||||
{activeCount} active agent{activeCount > 1 ? "s" : ""}
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -130,7 +130,7 @@ export function ActivityDropdown({
|
||||
{filteredExecutions.length > 0 ? (
|
||||
<List
|
||||
height={listHeight}
|
||||
width={300} // Match dropdown width (w-80 = 20rem = 320px)
|
||||
width={320} // Match dropdown width (w-80 = 20rem = 320px)
|
||||
itemCount={filteredExecutions.length}
|
||||
itemSize={itemHeight}
|
||||
itemData={filteredExecutions}
|
||||
|
||||
@@ -4,13 +4,12 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { formatTimeAgo } from "@/lib/utils/time";
|
||||
import {
|
||||
CheckCircle,
|
||||
CircleDashed,
|
||||
CircleNotch,
|
||||
Clock,
|
||||
Eye,
|
||||
StopCircle,
|
||||
WarningOctagon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
StopCircleIcon,
|
||||
WarningIcon,
|
||||
SpinnerIcon,
|
||||
MinusCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import Link from "next/link";
|
||||
import type { AgentExecutionWithInfo } from "../helpers";
|
||||
@@ -24,68 +23,68 @@ export function ActivityItem({ execution }: Props) {
|
||||
function getStatusIcon() {
|
||||
switch (execution.status) {
|
||||
case AgentExecutionStatus.QUEUED:
|
||||
return <Clock size={18} className="text-purple-500" />;
|
||||
return <ClockIcon size={18} className="text-purple-500" />;
|
||||
case AgentExecutionStatus.RUNNING:
|
||||
return (
|
||||
<CircleNotch
|
||||
size={18}
|
||||
className="animate-spin text-purple-500"
|
||||
weight="bold"
|
||||
/>
|
||||
<SpinnerIcon size={18} className="animate-spin text-purple-500" />
|
||||
);
|
||||
case AgentExecutionStatus.COMPLETED:
|
||||
return (
|
||||
<CheckCircle size={18} weight="fill" className="text-purple-500" />
|
||||
);
|
||||
return <CheckCircleIcon size={18} className="text-purple-500" />;
|
||||
case AgentExecutionStatus.FAILED:
|
||||
return <WarningOctagon size={18} className="text-purple-500" />;
|
||||
return <WarningIcon size={18} className="text-purple-500" />;
|
||||
case AgentExecutionStatus.TERMINATED:
|
||||
return (
|
||||
<StopCircle size={18} className="text-purple-500" weight="fill" />
|
||||
);
|
||||
return <StopCircleIcon size={18} className="text-purple-500" />;
|
||||
case AgentExecutionStatus.INCOMPLETE:
|
||||
return <CircleDashed size={18} className="text-purple-500" />;
|
||||
return <MinusCircleIcon size={18} className="text-purple-500" />;
|
||||
case AgentExecutionStatus.REVIEW:
|
||||
return <Eye size={18} className="text-purple-500" weight="bold" />;
|
||||
return <WarningIcon size={18} className="text-yellow-600" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeDisplay() {
|
||||
function getItemDisplay() {
|
||||
// Handle active statuses (running/queued)
|
||||
const isActiveStatus =
|
||||
execution.status === AgentExecutionStatus.RUNNING ||
|
||||
execution.status === AgentExecutionStatus.QUEUED ||
|
||||
execution.status === AgentExecutionStatus.REVIEW;
|
||||
execution.status === AgentExecutionStatus.QUEUED;
|
||||
|
||||
if (isActiveStatus) {
|
||||
const timeAgo = formatTimeAgo(execution.started_at.toString());
|
||||
let statusText = "running";
|
||||
if (execution.status === AgentExecutionStatus.QUEUED) {
|
||||
statusText = "queued";
|
||||
}
|
||||
return `Started ${timeAgo}, ${getExecutionDuration(execution)} ${statusText}`;
|
||||
const statusText =
|
||||
execution.status === AgentExecutionStatus.QUEUED ? "queued" : "running";
|
||||
return [
|
||||
`Started ${timeAgo}, ${getExecutionDuration(execution)} ${statusText}`,
|
||||
];
|
||||
}
|
||||
|
||||
if (execution.ended_at) {
|
||||
const timeAgo = formatTimeAgo(execution.ended_at.toString());
|
||||
switch (execution.status) {
|
||||
case AgentExecutionStatus.COMPLETED:
|
||||
return `Completed ${timeAgo}`;
|
||||
case AgentExecutionStatus.FAILED:
|
||||
return `Failed ${timeAgo}`;
|
||||
case AgentExecutionStatus.TERMINATED:
|
||||
return `Stopped ${timeAgo}`;
|
||||
case AgentExecutionStatus.INCOMPLETE:
|
||||
return `Incomplete ${timeAgo}`;
|
||||
case AgentExecutionStatus.REVIEW:
|
||||
return `In review ${timeAgo}`;
|
||||
default:
|
||||
return `Ended ${timeAgo}`;
|
||||
}
|
||||
// Handle all other statuses with time display
|
||||
const timeAgo = execution.ended_at
|
||||
? formatTimeAgo(execution.ended_at.toString())
|
||||
: formatTimeAgo(execution.started_at.toString());
|
||||
|
||||
let statusText = "ended";
|
||||
switch (execution.status) {
|
||||
case AgentExecutionStatus.COMPLETED:
|
||||
statusText = "completed";
|
||||
break;
|
||||
case AgentExecutionStatus.FAILED:
|
||||
statusText = "failed";
|
||||
break;
|
||||
case AgentExecutionStatus.TERMINATED:
|
||||
statusText = "stopped";
|
||||
break;
|
||||
case AgentExecutionStatus.INCOMPLETE:
|
||||
statusText = "incomplete";
|
||||
break;
|
||||
case AgentExecutionStatus.REVIEW:
|
||||
statusText = "awaiting approval";
|
||||
break;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
return [
|
||||
`${statusText.charAt(0).toUpperCase() + statusText.slice(1)} ${timeAgo}`,
|
||||
];
|
||||
}
|
||||
|
||||
// Determine the tab based on execution status
|
||||
@@ -101,20 +100,22 @@ export function ActivityItem({ execution }: Props) {
|
||||
{/* Icon + Agent Name */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusIcon()}
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="max-w-[16rem] truncate text-gray-900"
|
||||
>
|
||||
<Text variant="body-medium" className="max-w-44 truncate text-gray-900">
|
||||
{execution.agent_name}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Agent Message - Indented */}
|
||||
<div className="ml-7 pt-1">
|
||||
{/* Time - Indented */}
|
||||
<Text variant="small" className="!text-zinc-500">
|
||||
{getTimeDisplay()}
|
||||
</Text>
|
||||
{getItemDisplay().map((line, index) => (
|
||||
<Text
|
||||
key={index}
|
||||
variant="small"
|
||||
className={index === 0 ? "!text-zinc-600" : "!text-zinc-500"}
|
||||
>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
link: string;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -16,12 +16,18 @@ export function Breadcrumbs({ items }: Props) {
|
||||
<div className="mb-4 flex h-auto flex-wrap items-center justify-start gap-2 md:mb-0 md:gap-2">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Link
|
||||
href={item.link}
|
||||
className="text-[0.75rem] font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
{item.link ? (
|
||||
<Link
|
||||
href={item.link}
|
||||
className="text-[0.75rem] font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-[0.75rem] font-[400] text-zinc-900">
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
{index < items.length - 1 && (
|
||||
<Text variant="small-medium" className="text-zinc-600">
|
||||
/
|
||||
|
||||
@@ -7,6 +7,8 @@ import { cn } from "@/lib/utils";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useGetV1GetExecutionDetails } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
interface FloatingReviewsPanelProps {
|
||||
executionId?: string;
|
||||
@@ -34,6 +36,11 @@ export function FloatingReviewsPanel({
|
||||
const executionStatus =
|
||||
executionDetails?.status === 200 ? executionDetails.data.status : undefined;
|
||||
|
||||
// Get graph execution status from the store (updated via WebSocket)
|
||||
const graphExecutionStatus = useGraphStore(
|
||||
useShallow((state) => state.graphExecutionStatus),
|
||||
);
|
||||
|
||||
const { pendingReviews, isLoading, refetch } = usePendingReviewsForExecution(
|
||||
executionId || "",
|
||||
);
|
||||
@@ -44,6 +51,13 @@ export function FloatingReviewsPanel({
|
||||
}
|
||||
}, [executionStatus, executionId, refetch]);
|
||||
|
||||
// Refetch when graph execution status changes to REVIEW
|
||||
useEffect(() => {
|
||||
if (graphExecutionStatus === AgentExecutionStatus.REVIEW && executionId) {
|
||||
refetch();
|
||||
}
|
||||
}, [graphExecutionStatus, executionId, refetch]);
|
||||
|
||||
if (
|
||||
!executionId ||
|
||||
(!isLoading &&
|
||||
@@ -73,18 +87,17 @@ export function FloatingReviewsPanel({
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden rounded-lg border bg-white shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b bg-gray-50 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon size={20} className="text-orange-600" />
|
||||
<Text variant="h4">Pending Reviews</Text>
|
||||
</div>
|
||||
<Button onClick={() => setIsOpen(false)} variant="icon" size="icon">
|
||||
<XIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex max-h-[80vh] max-w-2xl flex-col overflow-hidden rounded-lg shadow-2xl">
|
||||
<Button
|
||||
onClick={() => setIsOpen(false)}
|
||||
variant="icon"
|
||||
size="icon"
|
||||
className="absolute right-4 top-4 z-10"
|
||||
>
|
||||
<XIcon size={16} />
|
||||
</Button>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
|
||||
@@ -40,18 +40,18 @@ function extractReviewData(payload: unknown): {
|
||||
interface PendingReviewCardProps {
|
||||
review: PendingHumanReviewModel;
|
||||
onReviewDataChange: (nodeExecId: string, data: string) => void;
|
||||
reviewMessage: string;
|
||||
onReviewMessageChange: (nodeExecId: string, message: string) => void;
|
||||
isDisabled: boolean;
|
||||
onToggleDisabled: (nodeExecId: string) => void;
|
||||
reviewMessage?: string;
|
||||
onReviewMessageChange?: (nodeExecId: string, message: string) => void;
|
||||
isDisabled?: boolean;
|
||||
onToggleDisabled?: (nodeExecId: string) => void;
|
||||
}
|
||||
|
||||
export function PendingReviewCard({
|
||||
review,
|
||||
onReviewDataChange,
|
||||
reviewMessage,
|
||||
reviewMessage = "",
|
||||
onReviewMessageChange,
|
||||
isDisabled,
|
||||
isDisabled = false,
|
||||
onToggleDisabled,
|
||||
}: PendingReviewCardProps) {
|
||||
const extractedData = extractReviewData(review.payload);
|
||||
@@ -65,9 +65,12 @@ export function PendingReviewCard({
|
||||
};
|
||||
|
||||
const handleMessageChange = (newMessage: string) => {
|
||||
onReviewMessageChange(review.node_exec_id, newMessage);
|
||||
onReviewMessageChange?.(review.node_exec_id, newMessage);
|
||||
};
|
||||
|
||||
// Show simplified view when no toggle functionality is provided (Screenshot 1 mode)
|
||||
const showSimplified = !onToggleDisabled;
|
||||
|
||||
const renderDataInput = () => {
|
||||
const data = currentData;
|
||||
|
||||
@@ -134,60 +137,80 @@ export function PendingReviewCard({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`space-y-4 rounded-lg border p-4 ${isDisabled ? "bg-muted/50 opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
{isDisabled && (
|
||||
<Text variant="small" className="text-muted-foreground">
|
||||
This item will be rejected
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onToggleDisabled(review.node_exec_id)}
|
||||
variant={isDisabled ? "primary" : "secondary"}
|
||||
size="small"
|
||||
leftIcon={
|
||||
isDisabled ? <EyeSlashIcon size={14} /> : <TrashIcon size={14} />
|
||||
}
|
||||
>
|
||||
{isDisabled ? "Include" : "Exclude"}
|
||||
</Button>
|
||||
</div>
|
||||
// Helper function to get proper field label
|
||||
const getFieldLabel = (instructions?: string) => {
|
||||
if (instructions)
|
||||
return instructions.charAt(0).toUpperCase() + instructions.slice(1);
|
||||
return "Data to Review";
|
||||
};
|
||||
|
||||
{instructions && (
|
||||
<div>
|
||||
<Text variant="body" className="mb-2 font-semibold">
|
||||
Instructions:
|
||||
</Text>
|
||||
<Text variant="body">{instructions}</Text>
|
||||
// Use the existing HITL review interface
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!showSimplified && (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
{isDisabled && (
|
||||
<Text variant="small" className="text-muted-foreground">
|
||||
This item will be rejected
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onToggleDisabled!(review.node_exec_id)}
|
||||
variant={isDisabled ? "primary" : "secondary"}
|
||||
size="small"
|
||||
leftIcon={
|
||||
isDisabled ? <EyeSlashIcon size={14} /> : <TrashIcon size={14} />
|
||||
}
|
||||
>
|
||||
{isDisabled ? "Include" : "Exclude"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text variant="body" className="mb-2 font-semibold">
|
||||
Data to Review:
|
||||
{!isDataEditable && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(Read-only)
|
||||
</span>
|
||||
{/* Show instructions as field label */}
|
||||
{instructions && (
|
||||
<div className="space-y-3">
|
||||
<Text variant="body" className="font-semibold text-gray-900">
|
||||
{getFieldLabel(instructions)}
|
||||
</Text>
|
||||
{isDataEditable && !isDisabled ? (
|
||||
renderDataInput()
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<Text variant="small" className="text-gray-600">
|
||||
{JSON.stringify(currentData, null, 2)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Text>
|
||||
{isDataEditable && !isDisabled ? (
|
||||
renderDataInput()
|
||||
) : (
|
||||
<div className="rounded border bg-muted p-3">
|
||||
<Text variant="small" className="font-mono text-muted-foreground">
|
||||
{JSON.stringify(currentData, null, 2)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDisabled && (
|
||||
{/* If no instructions, show data directly */}
|
||||
{!instructions && (
|
||||
<div className="space-y-3">
|
||||
<Text variant="body" className="font-semibold text-gray-900">
|
||||
Data to Review
|
||||
{!isDataEditable && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(Read-only)
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
{isDataEditable && !isDisabled ? (
|
||||
renderDataInput()
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<Text variant="small" className="text-gray-600">
|
||||
{JSON.stringify(currentData, null, 2)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showSimplified && isDisabled && (
|
||||
<div>
|
||||
<Text variant="body" className="mb-2 font-semibold">
|
||||
Rejection Reason (Optional):
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PendingReviewCard } from "@/components/organisms/PendingReviewCard/Pend
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { ClockIcon, PlayIcon, XIcon, CheckIcon } from "@phosphor-icons/react";
|
||||
import { ClockIcon, WarningIcon } from "@phosphor-icons/react";
|
||||
import { usePostV2ProcessReviewAction } from "@/app/api/__generated__/endpoints/executions/executions";
|
||||
|
||||
interface PendingReviewsListProps {
|
||||
@@ -35,9 +35,10 @@ export function PendingReviewsList({
|
||||
const [reviewMessageMap, setReviewMessageMap] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [disabledReviews, setDisabledReviews] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const [pendingAction, setPendingAction] = useState<
|
||||
"approve" | "reject" | null
|
||||
>(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -69,9 +70,11 @@ export function PendingReviewsList({
|
||||
});
|
||||
}
|
||||
|
||||
setPendingAction(null);
|
||||
onReviewComplete?.();
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
setPendingAction(null);
|
||||
toast({
|
||||
title: "Failed to process reviews",
|
||||
description: error.message || "An error occurred",
|
||||
@@ -89,28 +92,7 @@ export function PendingReviewsList({
|
||||
setReviewMessageMap((prev) => ({ ...prev, [nodeExecId]: message }));
|
||||
}
|
||||
|
||||
function handleToggleDisabled(nodeExecId: string) {
|
||||
setDisabledReviews((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(nodeExecId)) {
|
||||
newSet.delete(nodeExecId);
|
||||
} else {
|
||||
newSet.add(nodeExecId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
function handleApproveAll() {
|
||||
setDisabledReviews(new Set());
|
||||
}
|
||||
|
||||
function handleRejectAll() {
|
||||
const allReviewIds = reviews.map((review) => review.node_exec_id);
|
||||
setDisabledReviews(new Set(allReviewIds));
|
||||
}
|
||||
|
||||
function handleContinue() {
|
||||
function processReviews(approved: boolean) {
|
||||
if (reviews.length === 0) {
|
||||
toast({
|
||||
title: "No reviews to process",
|
||||
@@ -120,34 +102,34 @@ export function PendingReviewsList({
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingAction(approved ? "approve" : "reject");
|
||||
const reviewItems = [];
|
||||
|
||||
for (const review of reviews) {
|
||||
const isApproved = !disabledReviews.has(review.node_exec_id);
|
||||
const reviewData = reviewDataMap[review.node_exec_id];
|
||||
const reviewMessage = reviewMessageMap[review.node_exec_id];
|
||||
|
||||
let parsedData;
|
||||
if (isApproved && review.editable && reviewData) {
|
||||
let parsedData: any = review.payload; // Default to original payload
|
||||
|
||||
// Parse edited data if available and editable
|
||||
if (review.editable && reviewData) {
|
||||
try {
|
||||
parsedData = JSON.parse(reviewData);
|
||||
if (JSON.stringify(parsedData) === JSON.stringify(review.payload)) {
|
||||
parsedData = undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Invalid JSON",
|
||||
description: `Please fix the JSON format in review for node ${review.node_exec_id}: ${error instanceof Error ? error.message : "Invalid syntax"}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
setPendingAction(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
reviewItems.push({
|
||||
node_exec_id: review.node_exec_id,
|
||||
approved: isApproved,
|
||||
reviewed_data: isApproved ? parsedData : undefined,
|
||||
approved,
|
||||
reviewed_data: parsedData,
|
||||
message: reviewMessage || undefined,
|
||||
});
|
||||
}
|
||||
@@ -175,71 +157,67 @@ export function PendingReviewsList({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-7 rounded-xl border border-yellow-150 bg-yellow-25 p-6">
|
||||
{/* Warning Box Header */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-2">
|
||||
<WarningIcon
|
||||
size={28}
|
||||
className="fill-yellow-600 text-white"
|
||||
weight="fill"
|
||||
/>
|
||||
<Text
|
||||
variant="large-semibold"
|
||||
className="overflow-hidden text-ellipsis text-textBlack"
|
||||
>
|
||||
Your review is needed
|
||||
</Text>
|
||||
</div>
|
||||
<Text variant="large" className="text-textGrey">
|
||||
This task is paused until you approve the changes below. Please review
|
||||
and edit if needed.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="space-y-7">
|
||||
{reviews.map((review) => (
|
||||
<PendingReviewCard
|
||||
key={review.node_exec_id}
|
||||
review={review}
|
||||
onReviewDataChange={handleReviewDataChange}
|
||||
reviewMessage={reviewMessageMap[review.node_exec_id] || ""}
|
||||
onReviewMessageChange={handleReviewMessageChange}
|
||||
isDisabled={disabledReviews.has(review.node_exec_id)}
|
||||
onToggleDisabled={handleToggleDisabled}
|
||||
reviewMessage={reviewMessageMap[review.node_exec_id] || ""}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-6">
|
||||
<div className="mb-6 flex justify-center gap-3">
|
||||
<Button
|
||||
onClick={handleApproveAll}
|
||||
disabled={
|
||||
reviewActionMutation.isPending || disabledReviews.size === 0
|
||||
}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
leftIcon={<CheckIcon size={14} />}
|
||||
>
|
||||
Approve All
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRejectAll}
|
||||
disabled={
|
||||
reviewActionMutation.isPending ||
|
||||
disabledReviews.size === reviews.length
|
||||
}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
leftIcon={<XIcon size={14} />}
|
||||
>
|
||||
Reject All
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-7">
|
||||
<Text variant="body" className="text-textGrey">
|
||||
Note: Changes you make here apply only to this task
|
||||
</Text>
|
||||
|
||||
<div className="space-y-4 text-center">
|
||||
<div>
|
||||
<Text variant="small" className="text-muted-foreground">
|
||||
{disabledReviews.size > 0 ? (
|
||||
<>
|
||||
Approve {reviews.length - disabledReviews.size}, reject{" "}
|
||||
{disabledReviews.size} of {reviews.length} items
|
||||
</>
|
||||
) : (
|
||||
<>Approve all {reviews.length} items</>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
onClick={() => processReviews(true)}
|
||||
disabled={reviewActionMutation.isPending || reviews.length === 0}
|
||||
variant="primary"
|
||||
size="large"
|
||||
leftIcon={<PlayIcon size={16} />}
|
||||
className="flex min-w-20 items-center justify-center gap-2 rounded-full px-4 py-3"
|
||||
loading={
|
||||
pendingAction === "approve" && reviewActionMutation.isPending
|
||||
}
|
||||
>
|
||||
{disabledReviews.size === reviews.length
|
||||
? "Continue with Rejections"
|
||||
: "Continue Execution"}
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => processReviews(false)}
|
||||
disabled={reviewActionMutation.isPending || reviews.length === 0}
|
||||
variant="destructive"
|
||||
className="flex min-w-20 items-center justify-center gap-2 rounded-full bg-red-600 px-4 py-3"
|
||||
loading={
|
||||
pendingAction === "reject" && reviewActionMutation.isPending
|
||||
}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,13 +48,15 @@ export const colors = {
|
||||
900: "#6b3900",
|
||||
},
|
||||
yellow: {
|
||||
25: "#FFFCF3",
|
||||
50: "#fef9e6",
|
||||
100: "#fcebb0",
|
||||
150: "#FDEFBF",
|
||||
200: "#fae28a",
|
||||
300: "#f8d554",
|
||||
400: "#f7cd33",
|
||||
500: "#f5c000",
|
||||
600: "#dfaf00",
|
||||
600: "#DFAF00",
|
||||
700: "#ae8800",
|
||||
800: "#876a00",
|
||||
900: "#675100",
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { usePatchV1UpdateGraphSettings } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import {
|
||||
getGetV2GetLibraryAgentQueryOptions,
|
||||
useGetV2GetLibraryAgentByGraphId,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { Graph } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ShieldCheckIcon, ShieldIcon } from "@phosphor-icons/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Graph } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
function getGraphId(graph: GraphModel | LibraryAgent | Graph): string {
|
||||
if ("graph_id" in graph) return graph.graph_id || "";
|
||||
@@ -41,19 +33,7 @@ function isLibraryAgent(
|
||||
return "graph_id" in graph && "settings" in graph;
|
||||
}
|
||||
|
||||
interface FloatingSafeModeToggleProps {
|
||||
graph: GraphModel | LibraryAgent | Graph;
|
||||
className?: string;
|
||||
fullWidth?: boolean;
|
||||
variant?: "white" | "black";
|
||||
}
|
||||
|
||||
export function FloatingSafeModeToggle({
|
||||
graph,
|
||||
className,
|
||||
fullWidth = false,
|
||||
variant = "white",
|
||||
}: FloatingSafeModeToggleProps) {
|
||||
export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -120,6 +100,7 @@ export function FloatingSafeModeToggle({
|
||||
description: newSafeMode
|
||||
? "Human-in-the-loop blocks will require manual review"
|
||||
: "Human-in-the-loop blocks will proceed automatically",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
const isNotFoundError =
|
||||
@@ -154,53 +135,12 @@ export function FloatingSafeModeToggle({
|
||||
toast,
|
||||
]);
|
||||
|
||||
if (!shouldShowToggle || isStateUndetermined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(variant === "black" ? "fixed z-50" : "", className)}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
onClick={handleToggle}
|
||||
disabled={isPending}
|
||||
loading={isPending}
|
||||
className={cn(
|
||||
fullWidth ? "w-full" : "",
|
||||
variant === "black"
|
||||
? "bg-gray-800 text-white hover:bg-gray-700"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{currentSafeMode! ? (
|
||||
<>
|
||||
<ShieldCheckIcon weight="bold" size={16} />
|
||||
Safe Mode: ON
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldIcon weight="bold" size={16} />
|
||||
Safe Mode: OFF
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-center">
|
||||
<div className="font-medium">
|
||||
Safe Mode: {currentSafeMode! ? "ON" : "OFF"}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{currentSafeMode!
|
||||
? "HITL blocks require manual review"
|
||||
: "HITL blocks proceed automatically"}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
currentSafeMode,
|
||||
isPending,
|
||||
shouldShowToggle,
|
||||
isStateUndetermined,
|
||||
handleToggle,
|
||||
hasHITLBlocks: shouldShowToggle,
|
||||
};
|
||||
}
|
||||
@@ -63,21 +63,16 @@ export function useExecutionEvents({
|
||||
if (subscribedIds.has(id)) return;
|
||||
subscribedIds.add(id);
|
||||
|
||||
api
|
||||
.subscribeToGraphExecutions(id as GraphID)
|
||||
.then(() => {
|
||||
console.debug(`Subscribed to execution updates for graph ${id}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`Failed to subscribe to execution updates for graph ${id}:`,
|
||||
error,
|
||||
);
|
||||
Sentry.captureException(error, {
|
||||
tags: { graphId: id },
|
||||
});
|
||||
subscribedIds.delete(id);
|
||||
api.subscribeToGraphExecutions(id as GraphID).catch((error) => {
|
||||
console.error(
|
||||
`Failed to subscribe to execution updates for graph ${id}:`,
|
||||
error,
|
||||
);
|
||||
Sentry.captureException(error, {
|
||||
tags: { graphId: id },
|
||||
});
|
||||
subscribedIds.delete(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user