mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -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";
|
"use client";
|
||||||
|
|
||||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||||
@@ -12,8 +11,10 @@ import { EmptySchedules } from "./components/other/EmptySchedules";
|
|||||||
import { EmptyTasks } from "./components/other/EmptyTasks";
|
import { EmptyTasks } from "./components/other/EmptyTasks";
|
||||||
import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
||||||
import { SectionWrap } from "./components/other/SectionWrap";
|
import { SectionWrap } from "./components/other/SectionWrap";
|
||||||
|
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
|
||||||
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
|
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
|
||||||
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
|
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
|
||||||
|
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
||||||
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
||||||
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
||||||
@@ -101,49 +102,36 @@ export function NewAgentLibraryView() {
|
|||||||
/>
|
/>
|
||||||
</SectionWrap>
|
</SectionWrap>
|
||||||
|
|
||||||
<SectionWrap className="mb-3">
|
{activeItem ? (
|
||||||
<div
|
activeTab === "scheduled" ? (
|
||||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} border-b border-zinc-100 pb-4`}
|
<SelectedScheduleView
|
||||||
>
|
agent={agent}
|
||||||
<Breadcrumbs
|
scheduleId={activeItem}
|
||||||
items={[
|
onClearSelectedRun={handleClearSelectedRun}
|
||||||
{ name: "My Library", link: "/library" },
|
|
||||||
{ name: agent.name, link: `/library/agents/${agentId}` },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<SelectedRunView
|
||||||
{activeItem ? (
|
agent={agent}
|
||||||
activeTab === "scheduled" ? (
|
runId={activeItem}
|
||||||
<SelectedScheduleView
|
onSelectRun={handleSelectRun}
|
||||||
agent={agent}
|
onClearSelectedRun={handleClearSelectedRun}
|
||||||
scheduleId={activeItem}
|
/>
|
||||||
onClearSelectedRun={handleClearSelectedRun}
|
)
|
||||||
/>
|
) : sidebarLoading ? (
|
||||||
) : (
|
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
|
||||||
<SelectedRunView
|
) : activeTab === "scheduled" ? (
|
||||||
agent={agent}
|
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||||
runId={activeItem}
|
<EmptySchedules />
|
||||||
onSelectRun={handleSelectRun}
|
</SelectedViewLayout>
|
||||||
onClearSelectedRun={handleClearSelectedRun}
|
) : activeTab === "templates" ? (
|
||||||
/>
|
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||||
)
|
<EmptyTemplates />
|
||||||
) : sidebarLoading ? (
|
</SelectedViewLayout>
|
||||||
<div className="flex flex-col gap-4">
|
) : (
|
||||||
<Skeleton className="h-8 w-full bg-slate-100" />
|
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||||
<Skeleton className="h-12 w-full bg-slate-100" />
|
<EmptyTasks agent={agent} />
|
||||||
<Skeleton className="h-64 w-full bg-slate-100" />
|
</SelectedViewLayout>
|
||||||
<Skeleton className="h-32 w-full bg-slate-100" />
|
)}
|
||||||
</div>
|
|
||||||
) : activeTab === "scheduled" ? (
|
|
||||||
<EmptySchedules />
|
|
||||||
) : activeTab === "templates" ? (
|
|
||||||
<EmptyTemplates />
|
|
||||||
) : (
|
|
||||||
<EmptyTasks agent={agent} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SectionWrap>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,83 @@
|
|||||||
"use client";
|
"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 { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/molecules/DropdownMenu/DropdownMenu";
|
} 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 { 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 { useRouter } from "next/navigation";
|
||||||
import { useDeleteV2DeleteLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
|
import { useState } from "react";
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agent: LibraryAgent;
|
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 { 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;
|
if (!agent.id) return;
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeletingAgent(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteAgent({ libraryAgentId: agent.id });
|
await deleteAgent({ libraryAgentId: agent.id });
|
||||||
|
|
||||||
|
await queryClient.refetchQueries({
|
||||||
|
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||||
|
});
|
||||||
|
|
||||||
toast({ title: "Agent deleted" });
|
toast({ title: "Agent deleted" });
|
||||||
setShowDeleteDialog(false);
|
setShowDeleteDialog(false);
|
||||||
router.push("/library");
|
router.push("/library");
|
||||||
@@ -54,7 +91,7 @@ export function AgentActionsDropdown({ agent }: Props) {
|
|||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
} finally {
|
} 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
{run ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => setShowDeleteRunDialog(true)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
Delete this task
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
href={`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`}
|
href={`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<PencilSimpleIcon size={16} /> Edit agent
|
Edit agent
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<FileArrowDownIcon size={16} /> Export agent
|
Export agent to file
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setShowDeleteDialog(true)}
|
onClick={() => setShowDeleteDialog(true)}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<TrashIcon size={16} /> Delete agent
|
Delete agent
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</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
|
<Dialog
|
||||||
controlled={{
|
controlled={{
|
||||||
isOpen: showDeleteDialog,
|
isOpen: showDeleteDialog,
|
||||||
@@ -131,17 +274,51 @@ export function AgentActionsDropdown({ agent }: Props) {
|
|||||||
<Dialog.Footer>
|
<Dialog.Footer>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={isDeleting}
|
disabled={isDeletingAgent}
|
||||||
onClick={() => setShowDeleteDialog(false)}
|
onClick={() => setShowDeleteDialog(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleDelete}
|
onClick={handleDeleteAgent}
|
||||||
loading={isDeleting}
|
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>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
import { OutputRenderer, OutputMetadata } from "../types";
|
import { OutputMetadata, OutputRenderer } from "../types";
|
||||||
|
|
||||||
interface OutputItemProps {
|
interface OutputItemProps {
|
||||||
value: any;
|
value: any;
|
||||||
@@ -19,7 +19,9 @@ export function OutputItem({
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{label && (
|
{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>
|
<div className="relative">{renderer.render(value, metadata)}</div>
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RunDetailCard({ children, className }: Props) {
|
export function RunDetailCard({ children, className, title }: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{title && <Text variant="lead-semibold">{title}</Text>}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,9 @@
|
|||||||
import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
|
import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
|
||||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
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 moment from "moment";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||||
import { AgentActionsDropdown } from "../AgentActionsDropdown";
|
|
||||||
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
|
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
|
||||||
import { ShareRunButton } from "../ShareRunButton/ShareRunButton";
|
|
||||||
import { useRunDetailHeader } from "./useRunDetailHeader";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
agent: LibraryAgent;
|
agent: LibraryAgent;
|
||||||
@@ -26,29 +13,7 @@ type Props = {
|
|||||||
onClearSelectedRun?: () => void;
|
onClearSelectedRun?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RunDetailHeader({
|
export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
@@ -60,62 +25,6 @@ export function RunDetailHeader({
|
|||||||
{agent.name}
|
{agent.name}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{run ? (
|
{run ? (
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2 gap-y-1 text-zinc-400">
|
<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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,26 @@
|
|||||||
|
|
||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
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 { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||||
import {
|
|
||||||
TabsLine,
|
|
||||||
TabsLineContent,
|
|
||||||
TabsLineList,
|
|
||||||
TabsLineTrigger,
|
|
||||||
} from "@/components/molecules/TabsLine/TabsLine";
|
|
||||||
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
|
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
|
||||||
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
|
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
|
||||||
import { parseAsString, useQueryState } from "nuqs";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||||
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
|
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
|
||||||
|
import { LoadingSelectedContent } from "../LoadingSelectedContent";
|
||||||
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
|
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
|
||||||
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
|
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
|
||||||
|
import { SelectedViewLayout } from "../SelectedViewLayout";
|
||||||
import { RunOutputs } from "./components/RunOutputs";
|
import { RunOutputs } from "./components/RunOutputs";
|
||||||
|
import { RunSummary } from "./components/RunSummary";
|
||||||
|
import { SelectedRunActions } from "./components/SelectedRunActions/SelectedRunActions";
|
||||||
import { useSelectedRunView } from "./useSelectedRunView";
|
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 {
|
interface Props {
|
||||||
agent: LibraryAgent;
|
agent: LibraryAgent;
|
||||||
runId: string;
|
runId: string;
|
||||||
@@ -45,18 +46,22 @@ export function SelectedRunView({
|
|||||||
refetch: refetchReviews,
|
refetch: refetchReviews,
|
||||||
} = usePendingReviewsForExecution(runId);
|
} = usePendingReviewsForExecution(runId);
|
||||||
|
|
||||||
// Tab state management
|
|
||||||
const [activeTab, setActiveTab] = useQueryState(
|
|
||||||
"tab",
|
|
||||||
parseAsString.withDefault("output"),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (run?.status === AgentExecutionStatus.REVIEW && runId) {
|
if (run?.status === AgentExecutionStatus.REVIEW && runId) {
|
||||||
refetchReviews();
|
refetchReviews();
|
||||||
}
|
}
|
||||||
}, [run?.status, 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) {
|
if (responseError || httpError) {
|
||||||
return (
|
return (
|
||||||
<ErrorCard
|
<ErrorCard
|
||||||
@@ -68,79 +73,118 @@ export function SelectedRunView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading && !run) {
|
if (isLoading && !run) {
|
||||||
return (
|
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
|
||||||
<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 (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex h-full w-full gap-4">
|
||||||
<RunDetailHeader
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
agent={agent}
|
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||||
run={run}
|
<div className="flex flex-col gap-4">
|
||||||
onSelectRun={onSelectRun}
|
<RunDetailHeader agent={agent} run={run} />
|
||||||
onClearSelectedRun={onClearSelectedRun}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Navigation Links */}
|
||||||
<TabsLine value={activeTab} onValueChange={setActiveTab}>
|
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
||||||
<TabsLineList className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
<nav className="flex gap-8 px-3 pb-1">
|
||||||
<TabsLineTrigger value="output">Output</TabsLineTrigger>
|
{withSummary && (
|
||||||
<TabsLineTrigger value="input">Your input</TabsLineTrigger>
|
<button
|
||||||
{run?.status === AgentExecutionStatus.REVIEW && (
|
onClick={() => scrollToSection("summary")}
|
||||||
<TabsLineTrigger value="reviews">
|
className={anchorStyles}
|
||||||
Reviews ({pendingReviews.length})
|
>
|
||||||
</TabsLineTrigger>
|
Summary
|
||||||
)}
|
</button>
|
||||||
</TabsLineList>
|
)}
|
||||||
|
<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">
|
{/* Summary Section */}
|
||||||
<RunDetailCard>
|
{withSummary && (
|
||||||
{isLoading ? (
|
<div id="summary" className="scroll-mt-4">
|
||||||
<div className="text-neutral-500">Loading…</div>
|
<RunDetailCard title="Summary">
|
||||||
) : run && "outputs" in run ? (
|
<RunSummary run={run} />
|
||||||
<RunOutputs outputs={run.outputs as any} />
|
</RunDetailCard>
|
||||||
) : (
|
</div>
|
||||||
<div className="text-neutral-600">No output from this run.</div>
|
|
||||||
)}
|
)}
|
||||||
</RunDetailCard>
|
|
||||||
</TabsLineContent>
|
|
||||||
|
|
||||||
<TabsLineContent value="input">
|
{/* Output Section */}
|
||||||
<RunDetailCard>
|
<div id="output" className="scroll-mt-4">
|
||||||
<AgentInputsReadOnly
|
<RunDetailCard title="Output">
|
||||||
agent={agent}
|
{isLoading ? (
|
||||||
inputs={(run as any)?.inputs}
|
<div className="text-neutral-500">
|
||||||
credentialInputs={(run as any)?.credential_inputs}
|
<LoadingSpinner />
|
||||||
/>
|
</div>
|
||||||
</RunDetailCard>
|
) : run && "outputs" in run ? (
|
||||||
</TabsLineContent>
|
<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 && (
|
{/* Input Section */}
|
||||||
<TabsLineContent value="reviews">
|
<div id="input" className="scroll-mt-4">
|
||||||
<RunDetailCard>
|
<RunDetailCard title="Your input">
|
||||||
{reviewsLoading ? (
|
<AgentInputsReadOnly
|
||||||
<div className="text-neutral-500">Loading reviews…</div>
|
agent={agent}
|
||||||
) : pendingReviews.length > 0 ? (
|
inputs={(run as any)?.inputs}
|
||||||
<PendingReviewsList
|
credentialInputs={(run as any)?.credential_inputs}
|
||||||
reviews={pendingReviews}
|
|
||||||
onReviewComplete={refetchReviews}
|
|
||||||
emptyMessage="No pending reviews for this execution"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</RunDetailCard>
|
||||||
<div className="text-neutral-600">
|
</div>
|
||||||
No pending reviews for this execution
|
|
||||||
</div>
|
{/* Reviews Section */}
|
||||||
)}
|
{withReviews && (
|
||||||
</RunDetailCard>
|
<div id="reviews" className="scroll-mt-4">
|
||||||
</TabsLineContent>
|
<RunDetailCard>
|
||||||
)}
|
{reviewsLoading ? (
|
||||||
</TabsLine>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ export function RunOutputs({ outputs }: RunOutputsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="absolute -top-3 right-0 z-10">
|
<div className="absolute right-3 top-3 z-10">
|
||||||
<OutputActions
|
<OutputActions
|
||||||
items={items.map((item) => ({
|
items={items.map((item) => ({
|
||||||
value: item.value,
|
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";
|
"use client";
|
||||||
|
|
||||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
import {
|
||||||
usePostV1StopGraphExecution,
|
|
||||||
getGetV1ListGraphExecutionsInfiniteQueryOptions,
|
getGetV1ListGraphExecutionsInfiniteQueryOptions,
|
||||||
useDeleteV1DeleteGraphExecution,
|
|
||||||
usePostV1ExecuteGraphAgent,
|
usePostV1ExecuteGraphAgent,
|
||||||
|
usePostV1StopGraphExecution,
|
||||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||||
import type { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
|
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";
|
import { useState } from "react";
|
||||||
|
|
||||||
export function useRunDetailHeader(
|
interface Args {
|
||||||
agentGraphId: string,
|
agentGraphId: string;
|
||||||
run?: GraphExecution,
|
run?: GraphExecution;
|
||||||
onSelectRun?: (id: string) => void,
|
onSelectRun?: (id: string) => void;
|
||||||
onClearSelectedRun?: () => void,
|
onClearSelectedRun?: () => void;
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
export function useSelectedRunActions(args: Args) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
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 } =
|
const { mutateAsync: stopRun, isPending: isStopping } =
|
||||||
usePostV1StopGraphExecution();
|
usePostV1StopGraphExecution();
|
||||||
|
|
||||||
const { mutateAsync: deleteRun, isPending: isDeleting } =
|
|
||||||
useDeleteV1DeleteGraphExecution();
|
|
||||||
|
|
||||||
const { mutateAsync: executeRun, isPending: isRunningAgain } =
|
const { mutateAsync: executeRun, isPending: isRunningAgain } =
|
||||||
usePostV1ExecuteGraphAgent();
|
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() {
|
async function handleStopRun() {
|
||||||
try {
|
try {
|
||||||
await stopRun({
|
await stopRun({
|
||||||
graphId: run?.graph_id ?? "",
|
graphId: args.run?.graph_id ?? "",
|
||||||
graphExecId: run?.id ?? "",
|
graphExecId: args.run?.id ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({ title: "Run stopped" });
|
toast({ title: "Run stopped" });
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey:
|
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
|
||||||
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
|
args.agentGraphId,
|
||||||
.queryKey,
|
).queryKey,
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
toast({
|
toast({
|
||||||
@@ -87,7 +59,7 @@ export function useRunDetailHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRunAgain() {
|
async function handleRunAgain() {
|
||||||
if (!run) {
|
if (!args.run) {
|
||||||
toast({
|
toast({
|
||||||
title: "Run not found",
|
title: "Run not found",
|
||||||
description: "Run not found",
|
description: "Run not found",
|
||||||
@@ -100,23 +72,23 @@ export function useRunDetailHeader(
|
|||||||
toast({ title: "Run started" });
|
toast({ title: "Run started" });
|
||||||
|
|
||||||
const res = await executeRun({
|
const res = await executeRun({
|
||||||
graphId: run.graph_id,
|
graphId: args.run.graph_id,
|
||||||
graphVersion: run.graph_version,
|
graphVersion: args.run.graph_version,
|
||||||
data: {
|
data: {
|
||||||
inputs: (run as any).inputs || {},
|
inputs: args.run.inputs || {},
|
||||||
credentials_inputs: (run as any).credential_inputs || {},
|
credentials_inputs: args.run.credential_inputs || {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const newRunId = res?.status === 200 ? (res?.data?.id ?? "") : "";
|
const newRunId = res?.status === 200 ? (res?.data?.id ?? "") : "";
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey:
|
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
|
||||||
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
|
args.agentGraphId,
|
||||||
.queryKey,
|
).queryKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newRunId && onSelectRun) onSelectRun(newRunId);
|
if (newRunId && args.onSelectRun) args.onSelectRun(newRunId);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
toast({
|
toast({
|
||||||
title: "Failed to start run",
|
title: "Failed to start run",
|
||||||
@@ -134,8 +106,8 @@ export function useRunDetailHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open in builder URL helper
|
// Open in builder URL helper
|
||||||
const openInBuilderHref = run
|
const openInBuilderHref = args.run
|
||||||
? `/build?flowID=${run.graph_id}&flowVersion=${run.graph_version}&flowExecutionID=${run.id}`
|
? `/build?flowID=${args.run.graph_id}&flowVersion=${args.run.graph_version}&flowExecutionID=${args.run.id}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -143,11 +115,8 @@ export function useRunDetailHeader(
|
|||||||
showDeleteDialog,
|
showDeleteDialog,
|
||||||
canStop,
|
canStop,
|
||||||
isStopping,
|
isStopping,
|
||||||
isDeleting,
|
|
||||||
isRunning: run?.status === "RUNNING",
|
|
||||||
isRunningAgain,
|
isRunningAgain,
|
||||||
handleShowDeleteDialog,
|
handleShowDeleteDialog,
|
||||||
handleDeleteRun,
|
|
||||||
handleStopRun,
|
handleStopRun,
|
||||||
handleRunAgain,
|
handleRunAgain,
|
||||||
} as const;
|
} as const;
|
||||||
@@ -2,24 +2,23 @@
|
|||||||
|
|
||||||
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
|
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
|
||||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
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 { Text } from "@/components/atoms/Text/Text";
|
||||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
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 { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||||
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
|
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||||
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
|
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
|
||||||
|
import { LoadingSelectedContent } from "../LoadingSelectedContent";
|
||||||
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
|
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
|
||||||
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
|
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
|
||||||
import { ScheduleActions } from "./components/ScheduleActions";
|
import { SelectedViewLayout } from "../SelectedViewLayout";
|
||||||
|
import { SelectedScheduleActions } from "./components/SelectedScheduleActions";
|
||||||
import { useSelectedScheduleView } from "./useSelectedScheduleView";
|
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 {
|
interface Props {
|
||||||
agent: LibraryAgent;
|
agent: LibraryAgent;
|
||||||
scheduleId: string;
|
scheduleId: string;
|
||||||
@@ -35,12 +34,20 @@ export function SelectedScheduleView({
|
|||||||
agent.graph_id,
|
agent.graph_id,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: userTzRes } = useGetV1GetUserTimezone({
|
const { data: userTzRes } = useGetV1GetUserTimezone({
|
||||||
query: {
|
query: {
|
||||||
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
|
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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorCard
|
<ErrorCard
|
||||||
@@ -68,126 +75,129 @@ export function SelectedScheduleView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading && !schedule) {
|
if (isLoading && !schedule) {
|
||||||
return (
|
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
|
||||||
<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 (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex h-full w-full gap-4">
|
||||||
<div>
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
<div className="flex w-full items-center justify-between">
|
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||||
<div className="flex w-full flex-col gap-0">
|
<div className="flex flex-col gap-4">
|
||||||
<RunDetailHeader
|
<div>
|
||||||
agent={agent}
|
<div className="flex w-full items-center justify-between">
|
||||||
run={undefined}
|
<div className="flex w-full flex-col gap-0">
|
||||||
scheduleRecurrence={
|
<RunDetailHeader
|
||||||
schedule
|
agent={agent}
|
||||||
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
|
run={undefined}
|
||||||
: undefined
|
scheduleRecurrence={
|
||||||
}
|
schedule
|
||||||
/>
|
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
|
||||||
</div>
|
: undefined
|
||||||
{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</RunDetailCard>
|
|
||||||
</TabsLineContent>
|
{/* Navigation Links */}
|
||||||
</TabsLine>
|
<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>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
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 { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
|
||||||
|
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||||
import {
|
import {
|
||||||
ShareFatIcon,
|
|
||||||
CopyIcon,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
CopyIcon,
|
||||||
|
ShareFatIcon,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useShareRunButton } from "./useShareRunButton";
|
import { useShareRunButton } from "./useShareRunButton";
|
||||||
import { Input } from "@/components/atoms/Input/Input";
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
graphId: string;
|
graphId: string;
|
||||||
@@ -49,12 +48,12 @@ export function ShareRunButton({
|
|||||||
>
|
>
|
||||||
<Dialog.Trigger>
|
<Dialog.Trigger>
|
||||||
<Button
|
<Button
|
||||||
variant={isShared ? "primary" : "secondary"}
|
variant="icon"
|
||||||
size="small"
|
size="icon"
|
||||||
|
aria-label="Share results"
|
||||||
className={isShared ? "relative" : ""}
|
className={isShared ? "relative" : ""}
|
||||||
>
|
>
|
||||||
<ShareFatIcon size={16} />
|
<ShareFatIcon weight="bold" size={18} className="text-zinc-700" />
|
||||||
{isShared ? "Shared" : "Share"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ export function SidebarRunsList({
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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-12 w-full" />
|
||||||
<Skeleton className="h-32 w-full" />
|
<Skeleton className="h-32 w-full" />
|
||||||
<Skeleton className="h-24 w-full" />
|
<Skeleton className="h-24 w-full" />
|
||||||
@@ -119,7 +124,7 @@ export function SidebarRunsList({
|
|||||||
hasMore={!!hasMoreRuns}
|
hasMore={!!hasMoreRuns}
|
||||||
isFetchingMore={isFetchingMoreRuns}
|
isFetchingMore={isFetchingMoreRuns}
|
||||||
onEndReached={fetchMoreRuns}
|
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"
|
itemWrapperClassName="w-auto lg:w-full"
|
||||||
renderItem={(run) => (
|
renderItem={(run) => (
|
||||||
<div className="w-[15rem] lg:w-full">
|
<div className="w-[15rem] lg:w-full">
|
||||||
@@ -140,7 +145,7 @@ export function SidebarRunsList({
|
|||||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
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.length > 0 ? (
|
||||||
schedules.map((s: GraphExecutionJobInfo) => (
|
schedules.map((s: GraphExecutionJobInfo) => (
|
||||||
<div className="w-[15rem] lg:w-full" key={s.id}>
|
<div className="w-[15rem] lg:w-full" key={s.id}>
|
||||||
@@ -167,7 +172,7 @@ export function SidebarRunsList({
|
|||||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
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">
|
<div className="flex min-h-[50vh] flex-col items-center justify-center">
|
||||||
<Text variant="large" className="text-zinc-700">
|
<Text variant="large" className="text-zinc-700">
|
||||||
No templates saved
|
No templates saved
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function parseTab(value: string | null): "runs" | "scheduled" | "templates" {
|
|||||||
export function useNewAgentLibraryView() {
|
export function useNewAgentLibraryView() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const agentId = id as string;
|
const agentId = id as string;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: response,
|
data: response,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
@@ -34,12 +35,12 @@ export function useNewAgentLibraryView() {
|
|||||||
const activeTab = useMemo(() => parseTab(activeTabRaw), [activeTabRaw]);
|
const activeTab = useMemo(() => parseTab(activeTabRaw), [activeTabRaw]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeTabRaw) {
|
if (!activeTabRaw && !activeItem) {
|
||||||
setQueryStates({
|
setQueryStates({
|
||||||
activeTab: "runs",
|
activeTab: "runs",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeTabRaw, setQueryStates]);
|
}, [activeTabRaw, activeItem, setQueryStates]);
|
||||||
|
|
||||||
const [sidebarCounts, setSidebarCounts] = useState({
|
const [sidebarCounts, setSidebarCounts] = useState({
|
||||||
runsCount: 0,
|
runsCount: 0,
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||||
import { Play, Plus } from "lucide-react";
|
import { Play, Plus } from "lucide-react";
|
||||||
|
import { TooltipProvider } from "../Tooltip/BaseTooltip";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
|
||||||
const meta: Meta<typeof Button> = {
|
const meta: Meta<typeof Button> = {
|
||||||
title: "Atoms/Button",
|
title: "Atoms/Button",
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
component: Button,
|
component: Button,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Story />
|
||||||
|
</TooltipProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "centered",
|
layout: "centered",
|
||||||
docs: {
|
docs: {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
|
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
|
||||||
import NextLink, { type LinkProps } from "next/link";
|
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 disabled = "disabled" in props ? props.disabled : false;
|
||||||
const isDisabled = disabled;
|
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 = (
|
const buttonContent = (
|
||||||
<>
|
<>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -38,7 +61,7 @@ export function Button(props: ButtonProps) {
|
|||||||
delete buttonRest.href;
|
delete buttonRest.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const linkButton = (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
extendedButtonVariants({ variant: "link", className }),
|
extendedButtonVariants({ variant: "link", className }),
|
||||||
@@ -51,6 +74,8 @@ export function Button(props: ButtonProps) {
|
|||||||
{buttonContent}
|
{buttonContent}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return wrapWithTooltip(linkButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -65,25 +90,31 @@ export function Button(props: ButtonProps) {
|
|||||||
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
|
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
|
||||||
);
|
);
|
||||||
|
|
||||||
return as === "NextLink" ? (
|
if (as === "NextLink") {
|
||||||
<NextLink
|
return (
|
||||||
{...(restProps as LinkProps)}
|
<NextLink
|
||||||
className={loadingClassName}
|
{...(restProps as LinkProps)}
|
||||||
aria-disabled="true"
|
className={loadingClassName}
|
||||||
>
|
aria-disabled="true"
|
||||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
>
|
||||||
{children}
|
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||||
</NextLink>
|
{children}
|
||||||
) : (
|
</NextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingButton = (
|
||||||
<button className={loadingClassName} disabled>
|
<button className={loadingClassName} disabled>
|
||||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return wrapWithTooltip(loadingButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (as === "NextLink") {
|
if (as === "NextLink") {
|
||||||
return (
|
const nextLinkButton = (
|
||||||
<NextLink
|
<NextLink
|
||||||
{...(restProps as LinkProps)}
|
{...(restProps as LinkProps)}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -96,9 +127,11 @@ export function Button(props: ButtonProps) {
|
|||||||
{buttonContent}
|
{buttonContent}
|
||||||
</NextLink>
|
</NextLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return wrapWithTooltip(nextLinkButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const regularButton = (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
extendedButtonVariants({ variant, size, className }),
|
extendedButtonVariants({ variant, size, className }),
|
||||||
@@ -110,4 +143,6 @@ export function Button(props: ButtonProps) {
|
|||||||
{buttonContent}
|
{buttonContent}
|
||||||
</button>
|
</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",
|
"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:
|
ghost:
|
||||||
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
|
"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(
|
link: cn(
|
||||||
linkBaseClasses,
|
linkBaseClasses,
|
||||||
linkVariantClasses.secondary,
|
linkVariantClasses.secondary,
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ export function ActivityItem({ execution }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine the tab based on execution status
|
// Determine the tab based on execution status
|
||||||
const tabParam =
|
const searchParams = new URLSearchParams();
|
||||||
execution.status === AgentExecutionStatus.REVIEW ? "&tab=reviews" : "";
|
const isReview = execution.status === AgentExecutionStatus.REVIEW;
|
||||||
const linkUrl = `/library/agents/${execution.library_agent_id}?executionId=${execution.id}${tabParam}`;
|
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 withExecutionLink = execution.library_agent_id && execution.id;
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { useCallback, useState, useEffect } from "react";
|
import { usePatchV1UpdateGraphSettings } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||||
import { ShieldIcon, ShieldCheckIcon } from "@phosphor-icons/react";
|
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 { Button } from "@/components/atoms/Button/Button";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
} 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 { 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 { 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 {
|
function getGraphId(graph: GraphModel | LibraryAgent | Graph): string {
|
||||||
if ("graph_id" in graph) return graph.graph_id || "";
|
if ("graph_id" in graph) return graph.graph_id || "";
|
||||||
@@ -163,8 +163,8 @@ export function FloatingSafeModeToggle({
|
|||||||
<Tooltip delayDuration={100}>
|
<Tooltip delayDuration={100}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="icon"
|
||||||
size="small"
|
size="icon"
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
@@ -177,12 +177,12 @@ export function FloatingSafeModeToggle({
|
|||||||
>
|
>
|
||||||
{currentSafeMode! ? (
|
{currentSafeMode! ? (
|
||||||
<>
|
<>
|
||||||
<ShieldCheckIcon className="h-4 w-4" />
|
<ShieldCheckIcon weight="bold" size={16} />
|
||||||
Safe Mode: ON
|
Safe Mode: ON
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ShieldIcon className="h-4 w-4" />
|
<ShieldIcon weight="bold" size={16} />
|
||||||
Safe Mode: OFF
|
Safe Mode: OFF
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user