feat(frontend): implement new actions sidebar + summary (#11545)

## Changes 🏗️

<img width="800" height="767" alt="Screenshot 2025-12-04 at 17 40 10"
src="https://github.com/user-attachments/assets/37036246-bcdb-46eb-832c-f91fddfd9014"
/>

<img width="800" height="492" alt="Screenshot 2025-12-04 at 17 40 16"
src="https://github.com/user-attachments/assets/ba547e54-016a-403c-9ab6-99465d01af6b"
/>

On the new Agent Library page:

- Implement the new actions sidebar ( main change... ) 
- Refactor the layout/components to accommodate that
- Implement the missing "Summary" functionality 
- Update icon buttons in Design system with new designs

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally and test it
This commit is contained in:
Ubbe
2025-12-04 22:46:42 +07:00
committed by GitHub
parent 3ccc712463
commit f7a8e372dd
23 changed files with 942 additions and 634 deletions

View File

@@ -1,6 +1,5 @@
"use client";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { Button } from "@/components/atoms/Button/Button";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
@@ -12,8 +11,10 @@ import { EmptySchedules } from "./components/other/EmptySchedules";
import { EmptyTasks } from "./components/other/EmptyTasks";
import { EmptyTemplates } from "./components/other/EmptyTemplates";
import { SectionWrap } from "./components/other/SectionWrap";
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
@@ -101,49 +102,36 @@ export function NewAgentLibraryView() {
/>
</SectionWrap>
<SectionWrap className="mb-3">
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} border-b border-zinc-100 pb-4`}
>
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
{ name: agent.name, link: `/library/agents/${agentId}` },
]}
{activeItem ? (
activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
/>
</div>
<div className="flex min-h-0 flex-1 flex-col">
{activeItem ? (
activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
/>
) : (
<SelectedRunView
agent={agent}
runId={activeItem}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
/>
)
) : sidebarLoading ? (
<div className="flex flex-col gap-4">
<Skeleton className="h-8 w-full bg-slate-100" />
<Skeleton className="h-12 w-full bg-slate-100" />
<Skeleton className="h-64 w-full bg-slate-100" />
<Skeleton className="h-32 w-full bg-slate-100" />
</div>
) : activeTab === "scheduled" ? (
<EmptySchedules />
) : activeTab === "templates" ? (
<EmptyTemplates />
) : (
<EmptyTasks agent={agent} />
)}
</div>
</SectionWrap>
) : (
<SelectedRunView
agent={agent}
runId={activeItem}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
/>
)
) : sidebarLoading ? (
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
) : activeTab === "scheduled" ? (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptySchedules />
</SelectedViewLayout>
) : activeTab === "templates" ? (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTemplates />
</SelectedViewLayout>
) : (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTasks agent={agent} />
</SelectedViewLayout>
)}
</div>
);
}

View File

@@ -1,46 +1,83 @@
"use client";
import React, { useState } from "react";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
getV1GetGraphVersion,
useDeleteV1DeleteGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2ListLibraryAgentsQueryKey,
useDeleteV2DeleteLibraryAgent,
} from "@/app/api/__generated__/endpoints/library/library";
import {
getGetV1ListExecutionSchedulesForAGraphQueryOptions,
useDeleteV1DeleteExecutionSchedule,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import Link from "next/link";
import {
FileArrowDownIcon,
PencilSimpleIcon,
TrashIcon,
} from "@phosphor-icons/react";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { getV1GetGraphVersion } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { exportAsJSONFile } from "@/lib/utils";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { exportAsJSONFile } from "@/lib/utils";
import { DotsThreeIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useDeleteV2DeleteLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
import { Text } from "@/components/atoms/Text/Text";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
scheduleId?: string;
run?: GraphExecution;
agentGraphId?: string;
onClearSelectedRun?: () => void;
}
export function AgentActionsDropdown({ agent }: Props) {
export function AgentActionsDropdown({
agent,
run,
agentGraphId,
scheduleId,
onClearSelectedRun,
}: Props) {
const { toast } = useToast();
const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent();
const router = useRouter();
const [isDeleting, setIsDeleting] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
async function handleDelete() {
const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent();
const { mutateAsync: deleteRun, isPending: isDeletingRun } =
useDeleteV1DeleteGraphExecution();
const queryClient = useQueryClient();
const router = useRouter();
const [isDeletingAgent, setIsDeletingAgent] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showDeleteRunDialog, setShowDeleteRunDialog] = useState(false);
const { mutateAsync: deleteSchedule } = useDeleteV1DeleteExecutionSchedule();
const [isDeletingSchedule, setIsDeletingSchedule] = useState(false);
const [showDeleteScheduleDialog, setShowDeleteScheduleDialog] =
useState(false);
async function handleDeleteAgent() {
if (!agent.id) return;
setIsDeleting(true);
setIsDeletingAgent(true);
try {
await deleteAgent({ libraryAgentId: agent.id });
await queryClient.refetchQueries({
queryKey: getGetV2ListLibraryAgentsQueryKey(),
});
toast({ title: "Agent deleted" });
setShowDeleteDialog(false);
router.push("/library");
@@ -54,7 +91,7 @@ export function AgentActionsDropdown({ agent }: Props) {
variant: "destructive",
});
} finally {
setIsDeleting(false);
setIsDeletingAgent(false);
}
}
@@ -81,39 +118,145 @@ export function AgentActionsDropdown({ agent }: Props) {
}
}
async function handleDeleteRun() {
if (!run?.id || !agentGraphId) return;
try {
await deleteRun({ graphExecId: run.id });
toast({ title: "Task deleted" });
await queryClient.refetchQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
});
if (onClearSelectedRun) onClearSelectedRun();
setShowDeleteRunDialog(false);
} catch (error: unknown) {
toast({
title: "Failed to delete task",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
async function handleDeleteSchedule() {
setIsDeletingSchedule(true);
try {
await deleteSchedule({ scheduleId: scheduleId ?? "" });
toast({ title: "Schedule deleted" });
await queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryOptions(
agentGraphId ?? "",
).queryKey,
});
setShowDeleteDialog(false);
} catch (error: unknown) {
toast({
title: "Failed to delete schedule",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
} finally {
setIsDeletingSchedule(false);
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="small" className="min-w-fit">
<Button
variant="icon"
size="icon"
aria-label="More actions"
className="min-w-fit"
>
<DotsThreeIcon size={18} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{run ? (
<>
<DropdownMenuItem
onClick={() => setShowDeleteRunDialog(true)}
className="flex items-center gap-2"
>
Delete this task
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
) : null}
<DropdownMenuItem asChild>
<Link
href={`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`}
target="_blank"
className="flex items-center gap-2"
>
<PencilSimpleIcon size={16} /> Edit agent
Edit agent
</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleExport}
className="flex items-center gap-2"
>
<FileArrowDownIcon size={16} /> Export agent
Export agent to file
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setShowDeleteDialog(true)}
className="flex items-center gap-2"
>
<TrashIcon size={16} /> Delete agent
Delete agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteRunDialog,
set: setShowDeleteRunDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete task"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this task? This action cannot be
undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeletingRun}
onClick={() => setShowDeleteRunDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteRun}
loading={isDeletingRun}
>
Delete Task
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
<Dialog
controlled={{
isOpen: showDeleteDialog,
@@ -131,17 +274,51 @@ export function AgentActionsDropdown({ agent }: Props) {
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
disabled={isDeletingAgent}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
onClick={handleDeleteAgent}
loading={isDeletingAgent}
>
Delete
Delete Agent
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
<Dialog
controlled={{
isOpen: showDeleteScheduleDialog,
set: setShowDeleteScheduleDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete schedule"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this schedule? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeletingSchedule}
onClick={() => setShowDeleteScheduleDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteSchedule}
loading={isDeletingSchedule}
>
Delete Schedule
</Button>
</Dialog.Footer>
</div>

View File

@@ -0,0 +1,24 @@
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { cn } from "@/lib/utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
import { SelectedViewLayout } from "./SelectedViewLayout";
interface Props {
agentName: string;
agentId: string;
}
export function LoadingSelectedContent(props: Props) {
return (
<SelectedViewLayout agentName={props.agentName} agentId={props.agentId}>
<div
className={cn("flex flex-col gap-4", AGENT_LIBRARY_SECTION_PADDING_X)}
>
<Skeleton className="h-8 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-32 w-full" />
</div>
</SelectedViewLayout>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { OutputRenderer, OutputMetadata } from "../types";
import { Text } from "@/components/atoms/Text/Text";
import { OutputMetadata, OutputRenderer } from "../types";
interface OutputItemProps {
value: any;
@@ -19,7 +19,9 @@ export function OutputItem({
return (
<div className="relative">
{label && (
<label className="mb-1.5 block text-sm font-medium">{label}</label>
<Text variant="large-medium" className="capitalize">
{label}
</Text>
)}
<div className="relative">{renderer.render(value, metadata)}</div>

View File

@@ -1,18 +1,21 @@
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
type Props = {
children: React.ReactNode;
className?: string;
title?: string;
};
export function RunDetailCard({ children, className }: Props) {
export function RunDetailCard({ children, className, title }: Props) {
return (
<div
className={cn(
"mx-4 min-h-20 rounded-medium border border-zinc-100 bg-white p-6",
"relative mx-4 flex min-h-20 flex-col gap-4 rounded-medium border border-zinc-100 bg-white p-6",
className,
)}
>
{title && <Text variant="lead-semibold">{title}</Text>}
{children}
</div>
);

View File

@@ -1,22 +1,9 @@
import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { FloatingSafeModeToggle } from "@/components/molecules/FloatingSafeModeToggle/FloatingSafeModeToggle";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import {
ArrowSquareOutIcon,
PlayIcon,
StopIcon,
TrashIcon,
} from "@phosphor-icons/react";
import moment from "moment";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentActionsDropdown } from "../AgentActionsDropdown";
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
import { ShareRunButton } from "../ShareRunButton/ShareRunButton";
import { useRunDetailHeader } from "./useRunDetailHeader";
type Props = {
agent: LibraryAgent;
@@ -26,29 +13,7 @@ type Props = {
onClearSelectedRun?: () => void;
};
export function RunDetailHeader({
agent,
run,
scheduleRecurrence,
onSelectRun,
onClearSelectedRun,
}: Props) {
const shareExecutionResultsEnabled = useGetFlag(Flag.SHARE_EXECUTION_RESULTS);
const {
canStop,
isStopping,
isDeleting,
isRunning,
isRunningAgain,
openInBuilderHref,
showDeleteDialog,
handleStopRun,
handleRunAgain,
handleDeleteRun,
handleShowDeleteDialog,
} = useRunDetailHeader(agent.graph_id, run, onSelectRun, onClearSelectedRun);
export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
return (
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<div className="flex w-full items-center justify-between">
@@ -60,62 +25,6 @@ export function RunDetailHeader({
{agent.name}
</Text>
</div>
{run ? (
<div className="my-4 flex flex-wrap items-center gap-2 md:my-2 lg:my-0">
<Button
variant="secondary"
size="small"
onClick={handleRunAgain}
loading={isRunningAgain}
>
<PlayIcon size={16} /> Run again
</Button>
{shareExecutionResultsEnabled && (
<ShareRunButton
graphId={agent.graph_id}
executionId={run.id}
isShared={run.is_shared}
shareToken={run.share_token}
/>
)}
<FloatingSafeModeToggle
graph={agent}
variant="white"
fullWidth={false}
/>
{!isRunning ? (
<Button
variant="secondary"
size="small"
onClick={() => handleShowDeleteDialog(true)}
>
<TrashIcon size={16} /> Delete run
</Button>
) : null}
{openInBuilderHref ? (
<Button
variant="secondary"
size="small"
as="NextLink"
href={openInBuilderHref}
target="_blank"
>
<ArrowSquareOutIcon size={16} /> Edit run
</Button>
) : null}
{canStop ? (
<Button
variant="destructive"
size="small"
onClick={handleStopRun}
disabled={isStopping}
>
<StopIcon size={14} /> Stop agent
</Button>
) : null}
<AgentActionsDropdown agent={agent} />
</div>
) : null}
</div>
{run ? (
<div className="mt-1 flex flex-wrap items-center gap-2 gap-y-1 text-zinc-400">
@@ -167,40 +76,6 @@ export function RunDetailHeader({
) : null}
</div>
</div>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: handleShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete run"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this run? This action cannot be
undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => handleShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteRun}
loading={isDeleting}
>
Delete
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</div>
);
}

View File

@@ -2,25 +2,26 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import {
TabsLine,
TabsLineContent,
TabsLineList,
TabsLineTrigger,
} from "@/components/molecules/TabsLine/TabsLine";
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
import { parseAsString, useQueryState } from "nuqs";
import { useEffect } from "react";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
import { SelectedViewLayout } from "../SelectedViewLayout";
import { RunOutputs } from "./components/RunOutputs";
import { RunSummary } from "./components/RunSummary";
import { SelectedRunActions } from "./components/SelectedRunActions/SelectedRunActions";
import { useSelectedRunView } from "./useSelectedRunView";
const anchorStyles =
"border-b-2 border-transparent pb-1 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 hover:border-slate-900";
interface Props {
agent: LibraryAgent;
runId: string;
@@ -45,18 +46,22 @@ export function SelectedRunView({
refetch: refetchReviews,
} = usePendingReviewsForExecution(runId);
// Tab state management
const [activeTab, setActiveTab] = useQueryState(
"tab",
parseAsString.withDefault("output"),
);
useEffect(() => {
if (run?.status === AgentExecutionStatus.REVIEW && runId) {
refetchReviews();
}
}, [run?.status, runId, refetchReviews]);
const withSummary = run?.stats?.activity_status;
const withReviews = run?.status === AgentExecutionStatus.REVIEW;
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
if (responseError || httpError) {
return (
<ErrorCard
@@ -68,79 +73,118 @@ export function SelectedRunView({
}
if (isLoading && !run) {
return (
<div className="flex-1 space-y-4 px-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
}
return (
<div className="flex flex-col gap-4">
<RunDetailHeader
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
<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}>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={run} />
{/* Content */}
<TabsLine value={activeTab} onValueChange={setActiveTab}>
<TabsLineList className={AGENT_LIBRARY_SECTION_PADDING_X}>
<TabsLineTrigger value="output">Output</TabsLineTrigger>
<TabsLineTrigger value="input">Your input</TabsLineTrigger>
{run?.status === AgentExecutionStatus.REVIEW && (
<TabsLineTrigger value="reviews">
Reviews ({pendingReviews.length})
</TabsLineTrigger>
)}
</TabsLineList>
{/* Navigation Links */}
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<nav className="flex gap-8 px-3 pb-1">
{withSummary && (
<button
onClick={() => scrollToSection("summary")}
className={anchorStyles}
>
Summary
</button>
)}
<button
onClick={() => scrollToSection("output")}
className={anchorStyles}
>
Output
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
{withReviews && (
<button
onClick={() => scrollToSection("reviews")}
className={anchorStyles}
>
Reviews ({pendingReviews.length})
</button>
)}
</nav>
</div>
<TabsLineContent value="output">
<RunDetailCard>
{isLoading ? (
<div className="text-neutral-500">Loading</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<div className="text-neutral-600">No output from this run.</div>
{/* Summary Section */}
{withSummary && (
<div id="summary" className="scroll-mt-4">
<RunDetailCard title="Summary">
<RunSummary run={run} />
</RunDetailCard>
</div>
)}
</RunDetailCard>
</TabsLineContent>
<TabsLineContent value="input">
<RunDetailCard>
<AgentInputsReadOnly
agent={agent}
inputs={(run as any)?.inputs}
credentialInputs={(run as any)?.credential_inputs}
/>
</RunDetailCard>
</TabsLineContent>
{/* Output Section */}
<div id="output" className="scroll-mt-4">
<RunDetailCard title="Output">
{isLoading ? (
<div className="text-neutral-500">
<LoadingSpinner />
</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<Text variant="body" className="text-neutral-600">
No output from this run.
</Text>
)}
</RunDetailCard>
</div>
{run?.status === AgentExecutionStatus.REVIEW && (
<TabsLineContent value="reviews">
<RunDetailCard>
{reviewsLoading ? (
<div className="text-neutral-500">Loading reviews</div>
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
{/* Input Section */}
<div id="input" className="scroll-mt-4">
<RunDetailCard title="Your input">
<AgentInputsReadOnly
agent={agent}
inputs={(run as any)?.inputs}
credentialInputs={(run as any)?.credential_inputs}
/>
) : (
<div className="text-neutral-600">
No pending reviews for this execution
</div>
)}
</RunDetailCard>
</TabsLineContent>
)}
</TabsLine>
</RunDetailCard>
</div>
{/* Reviews Section */}
{withReviews && (
<div id="reviews" className="scroll-mt-4">
<RunDetailCard>
{reviewsLoading ? (
<div className="text-neutral-500">Loading reviews</div>
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
/>
) : (
<div className="text-neutral-600">
No pending reviews for this execution
</div>
)}
</RunDetailCard>
</div>
)}
</div>
</SelectedViewLayout>
</div>
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
</div>
</div>
);
}

View File

@@ -83,8 +83,8 @@ export function RunOutputs({ outputs }: RunOutputsProps) {
}
return (
<div className="relative flex flex-col gap-4">
<div className="absolute -top-3 right-0 z-10">
<div className="flex flex-col gap-4">
<div className="absolute right-3 top-3 z-10">
<OutputActions
items={items.map((item) => ({
value: item.value,

View File

@@ -0,0 +1,100 @@
"use client";
import type { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
import { IconCircleAlert } from "@/components/__legacy__/ui/icons";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { RunDetailCard } from "../../RunDetailCard/RunDetailCard";
interface Props {
run: GetV1GetExecutionDetails200;
}
export function RunSummary({ run }: Props) {
if (!run.stats?.activity_status) return null;
const correctnessScore = run.stats.correctness_score;
return (
<RunDetailCard>
<div className="space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">Task Summary</h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<IconCircleAlert className="size-4 cursor-help text-neutral-500 hover:text-neutral-700" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
This AI-generated summary describes how the agent handled your
task. It&apos;s an experimental feature and may occasionally
be inaccurate.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<p className="text-sm leading-relaxed text-neutral-700">
{run.stats.activity_status}
</p>
{typeof correctnessScore === "number" && (
<div className="flex items-center gap-3 rounded-lg bg-neutral-50 p-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-neutral-600">
Success Estimate:
</span>
<div className="flex items-center gap-2">
<div className="relative h-2 w-16 overflow-hidden rounded-full bg-neutral-200">
<div
className={`h-full transition-all ${
correctnessScore >= 0.8
? "bg-green-500"
: correctnessScore >= 0.6
? "bg-yellow-500"
: correctnessScore >= 0.4
? "bg-orange-500"
: "bg-red-500"
}`}
style={{
width: `${Math.round(correctnessScore * 100)}%`,
}}
/>
</div>
<span className="text-sm font-medium">
{Math.round(correctnessScore * 100)}%
</span>
</div>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<IconCircleAlert className="size-4 cursor-help text-neutral-400 hover:text-neutral-600" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
AI-generated estimate of how well this execution achieved
its intended purpose. This score indicates
{correctnessScore >= 0.8
? " the agent was highly successful."
: correctnessScore >= 0.6
? " the agent was mostly successful with minor issues."
: correctnessScore >= 0.4
? " the agent was partially successful with some gaps."
: " the agent had limited success with significant issues."}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
</RunDetailCard>
);
}

View File

@@ -0,0 +1,118 @@
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,
ArrowBendRightDownIcon,
EyeIcon,
StopIcon,
} from "@phosphor-icons/react";
import { AgentActionsDropdown } from "../../../AgentActionsDropdown";
import { ShareRunButton } from "../../../ShareRunButton/ShareRunButton";
import { useSelectedRunActions } from "./useSelectedRunActions";
type Props = {
agent: LibraryAgent;
run: GraphExecution | undefined;
scheduleRecurrence?: string;
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
};
export function SelectedRunActions(props: Props) {
const {
handleRunAgain,
handleStopRun,
isRunningAgain,
canStop,
isStopping,
openInBuilderHref,
} = useSelectedRunActions({
agentGraphId: props.agent.graph_id,
run: props.run,
onSelectRun: props.onSelectRun,
onClearSelectedRun: props.onClearSelectedRun,
});
const shareExecutionResultsEnabled = useGetFlag(Flag.SHARE_EXECUTION_RESULTS);
const isRunning = props.run?.status === "RUNNING";
if (!props.run || !props.agent) return null;
return (
<div className="my-4 flex flex-col items-center gap-3">
{!isRunning ? (
<Button
variant="icon"
size="icon"
aria-label="Rerun task"
onClick={handleRunAgain}
disabled={isRunningAgain}
>
{isRunningAgain ? (
<LoadingSpinner size="small" />
) : (
<div className="gap- relative flex flex-col items-center justify-center">
<ArrowBendLeftUpIcon
weight="bold"
size={16}
className="relative bottom-[4px] z-0 rotate-90 text-zinc-700"
/>
<ArrowBendRightDownIcon
weight="bold"
size={16}
className="absolute bottom-[-5px] z-10 rotate-90 text-zinc-700"
/>
</div>
)}
</Button>
) : null}
{canStop ? (
<Button
variant="icon"
size="icon"
aria-label="Stop task"
onClick={handleStopRun}
disabled={isStopping}
className="border-red-600 bg-red-600 text-white hover:border-red-800 hover:bg-red-800"
>
<StopIcon weight="bold" size={18} />
</Button>
) : null}
{openInBuilderHref ? (
<Button
variant="icon"
size="icon"
as="NextLink"
href={openInBuilderHref}
target="_blank"
aria-label="View task details"
>
<EyeIcon weight="bold" size={18} className="text-zinc-700" />
</Button>
) : null}
{shareExecutionResultsEnabled && (
<ShareRunButton
graphId={props.agent.graph_id}
executionId={props.run.id}
isShared={props.run.is_shared}
shareToken={props.run.share_token}
/>
)}
<FloatingSafeModeToggle
graph={props.agent}
variant="white"
fullWidth={false}
/>
<AgentActionsDropdown
agent={props.agent}
run={props.run}
agentGraphId={props.agent.graph_id}
onClearSelectedRun={props.onClearSelectedRun}
/>
</div>
);
}

View File

@@ -1,78 +1,50 @@
"use client";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import {
usePostV1StopGraphExecution,
getGetV1ListGraphExecutionsInfiniteQueryOptions,
useDeleteV1DeleteGraphExecution,
usePostV1ExecuteGraphAgent,
usePostV1StopGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import type { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
export function useRunDetailHeader(
agentGraphId: string,
run?: GraphExecution,
onSelectRun?: (id: string) => void,
onClearSelectedRun?: () => void,
) {
interface Args {
agentGraphId: string;
run?: GraphExecution;
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
}
export function useSelectedRunActions(args: Args) {
const queryClient = useQueryClient();
const { toast } = useToast();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const canStop = run?.status === "RUNNING" || run?.status === "QUEUED";
const canStop =
args.run?.status === "RUNNING" || args.run?.status === "QUEUED";
const { mutateAsync: stopRun, isPending: isStopping } =
usePostV1StopGraphExecution();
const { mutateAsync: deleteRun, isPending: isDeleting } =
useDeleteV1DeleteGraphExecution();
const { mutateAsync: executeRun, isPending: isRunningAgain } =
usePostV1ExecuteGraphAgent();
async function handleDeleteRun() {
try {
await deleteRun({ graphExecId: run?.id ?? "" });
toast({ title: "Run deleted" });
await queryClient.refetchQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
});
if (onClearSelectedRun) onClearSelectedRun();
setShowDeleteDialog(false);
} catch (error: unknown) {
toast({
title: "Failed to delete run",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
async function handleStopRun() {
try {
await stopRun({
graphId: run?.graph_id ?? "",
graphExecId: run?.id ?? "",
graphId: args.run?.graph_id ?? "",
graphExecId: args.run?.id ?? "",
});
toast({ title: "Run stopped" });
await queryClient.invalidateQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
args.agentGraphId,
).queryKey,
});
} catch (error: unknown) {
toast({
@@ -87,7 +59,7 @@ export function useRunDetailHeader(
}
async function handleRunAgain() {
if (!run) {
if (!args.run) {
toast({
title: "Run not found",
description: "Run not found",
@@ -100,23 +72,23 @@ export function useRunDetailHeader(
toast({ title: "Run started" });
const res = await executeRun({
graphId: run.graph_id,
graphVersion: run.graph_version,
graphId: args.run.graph_id,
graphVersion: args.run.graph_version,
data: {
inputs: (run as any).inputs || {},
credentials_inputs: (run as any).credential_inputs || {},
inputs: args.run.inputs || {},
credentials_inputs: args.run.credential_inputs || {},
},
});
const newRunId = res?.status === 200 ? (res?.data?.id ?? "") : "";
await queryClient.invalidateQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
args.agentGraphId,
).queryKey,
});
if (newRunId && onSelectRun) onSelectRun(newRunId);
if (newRunId && args.onSelectRun) args.onSelectRun(newRunId);
} catch (error: unknown) {
toast({
title: "Failed to start run",
@@ -134,8 +106,8 @@ export function useRunDetailHeader(
}
// Open in builder URL helper
const openInBuilderHref = run
? `/build?flowID=${run.graph_id}&flowVersion=${run.graph_version}&flowExecutionID=${run.id}`
const openInBuilderHref = args.run
? `/build?flowID=${args.run.graph_id}&flowVersion=${args.run.graph_version}&flowExecutionID=${args.run.id}`
: undefined;
return {
@@ -143,11 +115,8 @@ export function useRunDetailHeader(
showDeleteDialog,
canStop,
isStopping,
isDeleting,
isRunning: run?.status === "RUNNING",
isRunningAgain,
handleShowDeleteDialog,
handleDeleteRun,
handleStopRun,
handleRunAgain,
} as const;

View File

@@ -2,24 +2,23 @@
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import {
TabsLine,
TabsLineContent,
TabsLineList,
TabsLineTrigger,
} from "@/components/molecules/TabsLine/TabsLine";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
import { ScheduleActions } from "./components/ScheduleActions";
import { SelectedViewLayout } from "../SelectedViewLayout";
import { SelectedScheduleActions } from "./components/SelectedScheduleActions";
import { useSelectedScheduleView } from "./useSelectedScheduleView";
const anchorStyles =
"border-b-2 border-transparent pb-1 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 hover:border-slate-900";
interface Props {
agent: LibraryAgent;
scheduleId: string;
@@ -35,12 +34,20 @@ export function SelectedScheduleView({
agent.graph_id,
scheduleId,
);
const { data: userTzRes } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
if (error) {
return (
<ErrorCard
@@ -68,126 +75,129 @@ export function SelectedScheduleView({
}
if (isLoading && !schedule) {
return (
<div className="flex-1 space-y-4 px-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
}
return (
<div className="flex flex-col gap-5">
<div>
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col gap-0">
<RunDetailHeader
agent={agent}
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
: undefined
}
/>
</div>
{schedule ? (
<ScheduleActions
agent={agent}
scheduleId={schedule.id}
onDeleted={onClearSelectedRun}
/>
) : null}
</div>
</div>
<TabsLine defaultValue="input">
<TabsLineList className={AGENT_LIBRARY_SECTION_PADDING_X}>
<TabsLineTrigger value="input">Your input</TabsLineTrigger>
<TabsLineTrigger value="schedule">Schedule</TabsLineTrigger>
</TabsLineList>
<TabsLineContent value="input">
<RunDetailCard>
<div className="relative">
{/* {// TODO: re-enable edit inputs modal once the API supports it */}
{/* {schedule && Object.keys(schedule.input_data).length > 0 && (
<EditInputsModal agent={agent} schedule={schedule} />
)} */}
<AgentInputsReadOnly
agent={agent}
inputs={schedule?.input_data}
credentialInputs={schedule?.input_credentials}
/>
</div>
</RunDetailCard>
</TabsLineContent>
<TabsLineContent value="schedule">
<RunDetailCard>
{isLoading || !schedule ? (
<div className="text-neutral-500">Loading</div>
) : (
<div className="relative flex flex-col gap-8">
{
// TODO: re-enable edit schedule modal once the API supports it
/* <EditScheduleModal
graphId={agent.graph_id}
schedule={schedule}
/> */
}
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Name
</Text>
<p className="text-sm text-zinc-600">{schedule.name}</p>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Recurrence
</Text>
<p className="text-sm text-zinc-600">
{humanizeCronExpression(schedule.cron)}
{" • "}
<span className="text-xs text-zinc-600">
{getTimezoneDisplayName(
schedule.timezone || userTzRes || "UTC",
)}
</span>
</p>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Next run
</Text>
<p className="text-sm text-zinc-600">
{formatInTimezone(
schedule.next_run_time,
userTzRes || "UTC",
{
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
)}{" "}
{" "}
<span className="text-xs text-zinc-600">
{getTimezoneDisplayName(
schedule.timezone || userTzRes || "UTC",
)}
</span>
</p>
<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}>
<div className="flex flex-col gap-4">
<div>
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col gap-0">
<RunDetailHeader
agent={agent}
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
: undefined
}
/>
</div>
</div>
)}
</RunDetailCard>
</TabsLineContent>
</TabsLine>
</div>
{/* Navigation Links */}
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<nav className="flex gap-8 px-3 pb-1">
<button
onClick={() => scrollToSection("schedule")}
className={anchorStyles}
>
Schedule
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
</nav>
</div>
{/* Schedule Section */}
<div id="schedule" className="scroll-mt-4">
<RunDetailCard title="Schedule">
{isLoading || !schedule ? (
<div className="text-neutral-500">
<LoadingSpinner />
</div>
) : (
<div className="relative flex flex-col gap-8">
<div className="flex flex-col gap-1.5">
<Text variant="large-medium">Name</Text>
<Text variant="body">{schedule.name}</Text>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="large-medium">Recurrence</Text>
<Text variant="body" className="flex items-center gap-3">
{humanizeCronExpression(schedule.cron)}{" "}
<span className="text-zinc-500"></span>{" "}
<span className="text-zinc-500">
{getTimezoneDisplayName(
schedule.timezone || userTzRes || "UTC",
)}
</span>
</Text>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="large-medium">Next run</Text>
<Text variant="body" className="flex items-center gap-3">
{formatInTimezone(
schedule.next_run_time,
userTzRes || "UTC",
{
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
)}{" "}
<span className="text-zinc-500"></span>{" "}
<span className="text-zinc-500">
{getTimezoneDisplayName(
schedule.timezone || userTzRes || "UTC",
)}
</span>
</Text>
</div>
</div>
)}
</RunDetailCard>
</div>
{/* Input Section */}
<div id="input" className="scroll-mt-4">
<RunDetailCard title="Your input">
<div className="relative">
{/* {// TODO: re-enable edit inputs modal once the API supports it */}
{/* {schedule && Object.keys(schedule.input_data).length > 0 && (
<EditInputsModal agent={agent} schedule={schedule} />
)} */}
<AgentInputsReadOnly
agent={agent}
inputs={schedule?.input_data}
credentialInputs={schedule?.input_credentials}
/>
</div>
</RunDetailCard>
</div>
</div>
</SelectedViewLayout>
</div>
{schedule ? (
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
<SelectedScheduleActions
agent={agent}
scheduleId={schedule.id}
onDeleted={onClearSelectedRun}
/>
</div>
) : null}
</div>
);
}

View File

@@ -1,119 +0,0 @@
import { useScheduleDetailHeader } from "../../RunDetailHeader/useScheduleDetailHeader";
import { useDeleteV1DeleteExecutionSchedule } from "@/app/api/__generated__/endpoints/schedules/schedules";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useState } from "react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
ArrowSquareOut,
PencilSimpleIcon,
TrashIcon,
} from "@phosphor-icons/react";
type Props = {
agent: LibraryAgent;
scheduleId: string;
onDeleted?: () => void;
};
export function ScheduleActions({ agent, scheduleId, onDeleted }: Props) {
const { toast } = useToast();
const { mutateAsync: deleteSchedule } = useDeleteV1DeleteExecutionSchedule();
const [isDeleting, setIsDeleting] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { openInBuilderHref } = useScheduleDetailHeader(
agent.graph_id,
scheduleId,
agent.graph_version,
);
async function handleDelete() {
setIsDeleting(true);
try {
await deleteSchedule({ scheduleId });
toast({ title: "Schedule deleted" });
setShowDeleteDialog(false);
if (onDeleted) onDeleted();
} catch (error: unknown) {
toast({
title: "Failed to delete schedule",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
} finally {
setIsDeleting(false);
}
}
return (
<>
<div className="flex items-center gap-2">
{openInBuilderHref && (
<Button
variant="secondary"
size="small"
as="NextLink"
href={openInBuilderHref}
>
<ArrowSquareOut size={14} /> Open in builder
</Button>
)}
<Button
variant="secondary"
size="small"
as="NextLink"
href={`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`}
>
<PencilSimpleIcon size={16} /> Edit agent
</Button>
<Button
variant="secondary"
size="small"
onClick={() => setShowDeleteDialog(true)}
>
<TrashIcon size={16} /> Delete
</Button>
</div>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete schedule"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this schedule? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,38 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { EyeIcon } from "@phosphor-icons/react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
import { useScheduleDetailHeader } from "../../RunDetailHeader/useScheduleDetailHeader";
type Props = {
agent: LibraryAgent;
scheduleId: string;
onDeleted?: () => void;
};
export function SelectedScheduleActions({ agent, scheduleId }: Props) {
const { openInBuilderHref } = useScheduleDetailHeader(
agent.graph_id,
scheduleId,
agent.graph_version,
);
return (
<>
<div className="my-4 flex flex-col items-center gap-3">
{openInBuilderHref && (
<Button
variant="icon"
size="icon"
aria-label="Open in builder"
as="NextLink"
href={openInBuilderHref}
>
<EyeIcon weight="bold" size={18} className="text-zinc-700" />
</Button>
)}
<AgentActionsDropdown agent={agent} scheduleId={scheduleId} />
</div>
</>
);
}

View File

@@ -0,0 +1,29 @@
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
import { SectionWrap } from "../other/SectionWrap";
interface Props {
agentName: string;
agentId: string;
children: React.ReactNode;
}
export function SelectedViewLayout(props: Props) {
return (
<SectionWrap className="relative mb-3 flex min-h-0 flex-1 flex-col">
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-4`}
>
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
{ name: props.agentName, link: `/library/agents/${props.agentId}` },
]}
/>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-visible">
{props.children}
</div>
</SectionWrap>
);
}

View File

@@ -1,18 +1,17 @@
"use client";
import React from "react";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
ShareFatIcon,
CopyIcon,
CheckIcon,
CopyIcon,
ShareFatIcon,
WarningIcon,
} from "@phosphor-icons/react";
import { useShareRunButton } from "./useShareRunButton";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
interface Props {
graphId: string;
@@ -49,12 +48,12 @@ export function ShareRunButton({
>
<Dialog.Trigger>
<Button
variant={isShared ? "primary" : "secondary"}
size="small"
variant="icon"
size="icon"
aria-label="Share results"
className={isShared ? "relative" : ""}
>
<ShareFatIcon size={16} />
{isShared ? "Shared" : "Share"}
<ShareFatIcon weight="bold" size={18} className="text-zinc-700" />
</Button>
</Dialog.Trigger>

View File

@@ -62,7 +62,12 @@ export function SidebarRunsList({
if (loading) {
return (
<div className="ml-6 w-[20vw] space-y-4">
<div
className={cn(
"ml-6 mt-8 w-[20vw] space-y-4",
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<Skeleton className="h-12 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-24 w-full" />
@@ -119,7 +124,7 @@ export function SidebarRunsList({
hasMore={!!hasMoreRuns}
isFetchingMore={isFetchingMoreRuns}
onEndReached={fetchMoreRuns}
className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-3 lg:overflow-x-hidden"
className="flex max-h-[76vh] flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden"
itemWrapperClassName="w-auto lg:w-full"
renderItem={(run) => (
<div className="w-[15rem] lg:w-full">
@@ -140,7 +145,7 @@ export function SidebarRunsList({
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-3 lg:overflow-x-hidden">
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden">
{schedules.length > 0 ? (
schedules.map((s: GraphExecutionJobInfo) => (
<div className="w-[15rem] lg:w-full" key={s.id}>
@@ -167,7 +172,7 @@ export function SidebarRunsList({
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-3 lg:overflow-x-hidden">
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden">
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<Text variant="large" className="text-zinc-700">
No templates saved

View File

@@ -15,6 +15,7 @@ function parseTab(value: string | null): "runs" | "scheduled" | "templates" {
export function useNewAgentLibraryView() {
const { id } = useParams();
const agentId = id as string;
const {
data: response,
isSuccess,
@@ -34,12 +35,12 @@ export function useNewAgentLibraryView() {
const activeTab = useMemo(() => parseTab(activeTabRaw), [activeTabRaw]);
useEffect(() => {
if (!activeTabRaw) {
if (!activeTabRaw && !activeItem) {
setQueryStates({
activeTab: "runs",
});
}
}, [activeTabRaw, setQueryStates]);
}, [activeTabRaw, activeItem, setQueryStates]);
const [sidebarCounts, setSidebarCounts] = useState({
runsCount: 0,

View File

@@ -1,11 +1,19 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { Play, Plus } from "lucide-react";
import { TooltipProvider } from "../Tooltip/BaseTooltip";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "Atoms/Button",
tags: ["autodocs"],
component: Button,
decorators: [
(Story) => (
<TooltipProvider>
<Story />
</TooltipProvider>
),
],
parameters: {
layout: "centered",
docs: {

View File

@@ -1,3 +1,8 @@
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { cn } from "@/lib/utils";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import NextLink, { type LinkProps } from "next/link";
@@ -20,6 +25,24 @@ export function Button(props: ButtonProps) {
const disabled = "disabled" in props ? props.disabled : false;
const isDisabled = disabled;
// Extract aria-label for tooltip on icon variant
const ariaLabel =
"aria-label" in restProps ? restProps["aria-label"] : undefined;
const shouldShowTooltip = variant === "icon" && ariaLabel && !loading;
// Helper to wrap button with tooltip if needed
const wrapWithTooltip = (buttonElement: React.ReactElement) => {
if (shouldShowTooltip) {
return (
<Tooltip>
<TooltipTrigger asChild>{buttonElement}</TooltipTrigger>
<TooltipContent>{ariaLabel}</TooltipContent>
</Tooltip>
);
}
return buttonElement;
};
const buttonContent = (
<>
{loading && (
@@ -38,7 +61,7 @@ export function Button(props: ButtonProps) {
delete buttonRest.href;
}
return (
const linkButton = (
<button
className={cn(
extendedButtonVariants({ variant: "link", className }),
@@ -51,6 +74,8 @@ export function Button(props: ButtonProps) {
{buttonContent}
</button>
);
return wrapWithTooltip(linkButton);
}
if (loading) {
@@ -65,25 +90,31 @@ export function Button(props: ButtonProps) {
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
);
return as === "NextLink" ? (
<NextLink
{...(restProps as LinkProps)}
className={loadingClassName}
aria-disabled="true"
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</NextLink>
) : (
if (as === "NextLink") {
return (
<NextLink
{...(restProps as LinkProps)}
className={loadingClassName}
aria-disabled="true"
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</NextLink>
);
}
const loadingButton = (
<button className={loadingClassName} disabled>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</button>
);
return wrapWithTooltip(loadingButton);
}
if (as === "NextLink") {
return (
const nextLinkButton = (
<NextLink
{...(restProps as LinkProps)}
className={cn(
@@ -96,9 +127,11 @@ export function Button(props: ButtonProps) {
{buttonContent}
</NextLink>
);
return wrapWithTooltip(nextLinkButton);
}
return (
const regularButton = (
<button
className={cn(
extendedButtonVariants({ variant, size, className }),
@@ -110,4 +143,6 @@ export function Button(props: ButtonProps) {
{buttonContent}
</button>
);
return wrapWithTooltip(regularButton);
}

View File

@@ -23,7 +23,7 @@ export const extendedButtonVariants = cva(
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
ghost:
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1 !min-w-0",
icon: "bg-transparent text-black border border-zinc-300 hover:bg-zinc-100 hover:border-zinc-600 rounded-[96px] disabled:opacity-1 !min-w-0",
link: cn(
linkBaseClasses,
linkVariantClasses.secondary,

View File

@@ -89,9 +89,11 @@ export function ActivityItem({ execution }: Props) {
}
// Determine the tab based on execution status
const tabParam =
execution.status === AgentExecutionStatus.REVIEW ? "&tab=reviews" : "";
const linkUrl = `/library/agents/${execution.library_agent_id}?executionId=${execution.id}${tabParam}`;
const searchParams = new URLSearchParams();
const isReview = execution.status === AgentExecutionStatus.REVIEW;
searchParams.set("activeTab", isReview ? "reviews" : "runs");
searchParams.set("activeItem", execution.id);
const linkUrl = `/library/agents/${execution.library_agent_id}?${searchParams.toString()}`;
const withExecutionLink = execution.library_agent_id && execution.id;
const content = (

View File

@@ -1,22 +1,22 @@
import { useCallback, useState, useEffect } from "react";
import { ShieldIcon, ShieldCheckIcon } from "@phosphor-icons/react";
import { usePatchV1UpdateGraphSettings } from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2GetLibraryAgentQueryOptions,
useGetV2GetLibraryAgentByGraphId,
} from "@/app/api/__generated__/endpoints/library/library";
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 { 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 { cn } from "@/lib/utils";
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useQueryClient } from "@tanstack/react-query";
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";
function getGraphId(graph: GraphModel | LibraryAgent | Graph): string {
if ("graph_id" in graph) return graph.graph_id || "";
@@ -163,8 +163,8 @@ export function FloatingSafeModeToggle({
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="secondary"
size="small"
variant="icon"
size="icon"
onClick={handleToggle}
disabled={isPending}
loading={isPending}
@@ -177,12 +177,12 @@ export function FloatingSafeModeToggle({
>
{currentSafeMode! ? (
<>
<ShieldCheckIcon className="h-4 w-4" />
<ShieldCheckIcon weight="bold" size={16} />
Safe Mode: ON
</>
) : (
<>
<ShieldIcon className="h-4 w-4" />
<ShieldIcon weight="bold" size={16} />
Safe Mode: OFF
</>
)}