Workflow Run Timeline UI (#1433)

This commit is contained in:
Shuchang Zheng
2024-12-23 23:44:47 -08:00
committed by GitHub
parent 682bc717f9
commit 517de67811
42 changed files with 1517 additions and 301 deletions

View File

@@ -19,7 +19,6 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { observerFeatureEnabled } from "@/util/env";
import {
FileTextIcon,
GearIcon,
@@ -233,35 +232,33 @@ function PromptBox() {
placeholder="Enter your prompt..."
rows={1}
/>
{observerFeatureEnabled && (
<Select value={selectValue} onValueChange={setSelectValue}>
<SelectTrigger className="w-48 focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="border-slate-500 bg-slate-elevation3">
<CustomSelectItem value="v1">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 1.0 (Tasks)</SelectItemText>
</div>
<div className="text-xs text-slate-400">
best for simple tasks
</div>
<Select value={selectValue} onValueChange={setSelectValue}>
<SelectTrigger className="w-48 focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="border-slate-500 bg-slate-elevation3">
<CustomSelectItem value="v1">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 1.0 (Tasks)</SelectItemText>
</div>
</CustomSelectItem>
<CustomSelectItem value="v2" className="hover:bg-slate-800">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 2.0 (Observer)</SelectItemText>
</div>
<div className="text-xs text-slate-400">
best for complex tasks
</div>
<div className="text-xs text-slate-400">
best for simple tasks
</div>
</CustomSelectItem>
</SelectContent>
</Select>
)}
</div>
</CustomSelectItem>
<CustomSelectItem value="v2" className="hover:bg-slate-800">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 2.0 (Observer)</SelectItemText>
</div>
<div className="text-xs text-slate-400">
best for complex tasks
</div>
</div>
</CustomSelectItem>
</SelectContent>
</Select>
<div className="flex items-center">
{startObserverCruiseMutation.isPending ||
getTaskFromPromptMutation.isPending ||
@@ -271,7 +268,7 @@ function PromptBox() {
<PaperPlaneIcon
className="h-6 w-6 cursor-pointer"
onClick={async () => {
if (observerFeatureEnabled && selectValue === "v2") {
if (selectValue === "v2") {
startObserverCruiseMutation.mutate(prompt);
return;
}

View File

@@ -3,7 +3,6 @@ import { ArtifactApiResponse, ArtifactType, Status } from "@/api/types";
import { ZoomableImage } from "@/components/ZoomableImage";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { getImageURL } from "./artifactUtils";
import { ReloadIcon } from "@radix-ui/react-icons";
import { statusIsNotFinalized } from "../types";
@@ -15,15 +14,18 @@ type Props = {
};
function ActionScreenshot({ stepId, index, taskStatus }: Props) {
const { taskId } = useParams();
const credentialGetter = useCredentialGetter();
const { data: artifacts, isLoading } = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["task", taskId, "steps", stepId, "artifacts"],
const {
data: artifacts,
isLoading,
isFetching,
} = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["step", stepId, "artifacts"],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
.get(`/tasks/${taskId}/steps/${stepId}/artifacts`)
.get(`/step/${stepId}/artifacts`)
.then((response) => response.data);
},
refetchInterval: (query) => {
@@ -46,7 +48,7 @@ function ActionScreenshot({ stepId, index, taskStatus }: Props) {
if (isLoading) {
return (
<div className="mx-auto flex max-h-[400px] flex-col items-center gap-2 overflow-hidden">
<div className="flex h-full items-center justify-center gap-2 bg-slate-elevation1">
<ReloadIcon className="h-6 w-6 animate-spin" />
<div>Loading screenshot...</div>
</div>
@@ -59,14 +61,25 @@ function ActionScreenshot({ stepId, index, taskStatus }: Props) {
statusIsNotFinalized({ status: taskStatus })
) {
return <div>The screenshot for this action is not available yet.</div>;
} else if (isFetching) {
return (
<div className="flex h-full items-center justify-center gap-2 bg-slate-elevation1">
<ReloadIcon className="h-6 w-6 animate-spin" />
<div>Loading screenshot...</div>
</div>
);
}
if (!screenshot) {
return <div>No screenshot found for this action.</div>;
return (
<div className="flex h-full items-center justify-center bg-slate-elevation1">
No screenshot found for this action.
</div>
);
}
return (
<figure className="mx-auto flex max-w-full flex-col items-center gap-2 overflow-hidden">
<figure className="mx-auto flex max-w-full flex-col items-center gap-2 overflow-hidden rounded">
<ZoomableImage src={getImageURL(screenshot)} alt="llm-screenshot" />
</figure>
);

View File

@@ -4,6 +4,8 @@ import {
TaskApiResponse,
WorkflowRunStatusApiResponse,
} from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { SwitchBarNavigation } from "@/components/SwitchBarNavigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -18,20 +20,18 @@ import {
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "@/components/ui/use-toast";
import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { copyText } from "@/util/copyText";
import { apiBaseUrl } from "@/util/env";
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, NavLink, Outlet, useParams } from "react-router-dom";
import { useTaskQuery } from "./hooks/useTaskQuery";
import fetchToCurl from "fetch-to-curl";
import { apiBaseUrl } from "@/util/env";
import { useApiCredential } from "@/hooks/useApiCredential";
import { copyText } from "@/util/copyText";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { StatusBadge } from "@/components/StatusBadge";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { Link, Outlet, useParams } from "react-router-dom";
import { statusIsFinalized } from "../types";
import { useTaskQuery } from "./hooks/useTaskQuery";
function createTaskRequestObject(values: TaskApiResponse) {
return {
@@ -257,7 +257,7 @@ function TaskDetails() {
workflow &&
workflowRun && (
<Link
to={`/workflows/${workflow.workflow_permanent_id}/${workflowRun.workflow_run_id}/blocks`}
to={`/workflows/${workflow.workflow_permanent_id}/${workflowRun.workflow_run_id}/overview`}
>
{workflow.title}
</Link>
@@ -274,64 +274,26 @@ function TaskDetails() {
{failureReason}
</>
)}
<div className="flex w-fit gap-2 rounded-sm border border-slate-700 p-2">
<NavLink
to="actions"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Actions
</NavLink>
<NavLink
to="recording"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Recording
</NavLink>
<NavLink
to="parameters"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Parameters
</NavLink>
<NavLink
to="diagnostics"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Diagnostics
</NavLink>
</div>
<SwitchBarNavigation
options={[
{
label: "Actions",
to: "actions",
},
{
label: "Recording",
to: "recording",
},
{
label: "Parameters",
to: "parameters",
},
{
label: "Diagnostics",
to: "diagnostics",
},
]}
/>
<Outlet />
</div>
);

View File

@@ -35,6 +35,14 @@ export function statusIsFinalized({ status }: { status: Status }): boolean {
);
}
export function statusIsAFailureType({ status }: { status: Status }): boolean {
return (
status === Status.Failed ||
status === Status.Terminated ||
status === Status.TimedOut
);
}
export function statusIsRunningOrQueued({
status,
}: {