mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-07 22:33:57 -05:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user