Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observer Timeline UI Updates #1480

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions skyvern-frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export type WorkflowRunStatusApiResponse = {
downloaded_file_urls: Array<string> | null;
total_steps: number | null;
total_cost: number | null;
observer_cruise: ObserverCruise | null;
};

export type TaskGenerationApiResponse = {
Expand Down
6 changes: 4 additions & 2 deletions skyvern-frontend/src/components/SwitchBarNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cn } from "@/util/utils";
import { NavLink } from "react-router-dom";
import { NavLink, useSearchParams } from "react-router-dom";

type Option = {
label: string;
Expand All @@ -11,12 +11,14 @@ type Props = {
};

function SwitchBarNavigation({ options }: Props) {
const [searchParams] = useSearchParams();

return (
<div className="flex w-fit gap-2 rounded-sm border border-slate-700 p-2">
{options.map((option) => {
return (
<NavLink
to={option.to}
to={`${option.to}?${searchParams.toString()}`}
replace
key={option.to}
className={({ isActive }) => {
Expand Down
26 changes: 26 additions & 0 deletions skyvern-frontend/src/components/icons/BrainIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
type Props = {
className?: string;
};

function BrainIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
>
<path
d="M16.9979 7.127C17.3193 7.04234 17.6533 7 17.9999 7C18.5686 7.00059 19.1306 7.12242 19.6485 7.35737C20.1663 7.59232 20.6281 7.93499 21.0031 8.36253C21.3781 8.79006 21.6576 9.29263 21.8229 9.83672C21.9883 10.3808 22.0358 10.9539 21.9622 11.5178C21.8886 12.0817 21.6956 12.6234 21.396 13.1068C21.0965 13.5902 20.6974 14.0042 20.2252 14.3211C19.7531 14.638 19.2188 14.8507 18.658 14.9448C18.0971 15.039 17.5227 15.0124 16.9729 14.867M16.9979 7.127L16.9999 7C17.0015 6.01205 16.6374 5.05848 15.9777 4.323C15.3181 3.58752 14.4096 3.12218 13.4273 3.01662C12.445 2.91106 11.4584 3.17276 10.6576 3.7513C9.85673 4.32984 9.29833 5.18428 9.08994 6.15M16.9979 7.127C16.9773 7.78571 16.7942 8.42911 16.4649 9M16.9729 14.867C16.9909 14.747 16.9999 14.6247 16.9999 14.5C17.0001 13.9237 16.801 13.365 16.4366 12.9186C16.0721 12.4721 15.5646 12.1653 14.9999 12.05M16.9729 14.867C16.8849 15.46 16.5868 16.0016 16.1329 16.3931C15.6789 16.7846 15.0994 17 14.4999 17H13.9999C12.9391 17 11.9217 17.4214 11.1715 18.1716C10.4214 18.9217 9.99994 19.9391 9.99994 21M9.08994 6.15C8.40228 5.95474 7.67487 5.94733 6.98338 6.12853C6.29188 6.30973 5.66158 6.67293 5.15806 7.18033C4.65453 7.68774 4.29619 8.3208 4.1203 9.01367C3.94441 9.70653 3.9574 10.4339 4.15794 11.12M9.08994 6.15C10.0923 6.43386 10.9444 7.09759 11.4649 8M4.15794 11.12C3.46598 11.3236 2.87149 11.7695 2.48147 12.3763C2.09145 12.983 1.933 13.7099 2.03512 14.4239C2.13725 15.1379 2.49311 15.7913 3.03757 16.2643C3.58203 16.7374 4.27866 16.9986 4.99994 17C5.62059 17.0003 6.22607 16.8081 6.73292 16.4499C7.23977 16.0917 7.62305 15.5852 7.82994 15M4.15794 11.12C4.24861 11.4313 4.37494 11.7247 4.53494 12M11.8359 11.744C11.3259 12.235 10.4529 12.32 9.70694 11.901C8.96094 11.481 8.57994 10.691 8.73494 10"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

export { BrainIcon };
45 changes: 42 additions & 3 deletions skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ import {
} from "@radix-ui/react-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import fetchToCurl from "fetch-to-curl";
import { Link, Outlet, useParams } from "react-router-dom";
import { Link, Outlet, useParams, useSearchParams } from "react-router-dom";
import { statusIsFinalized, statusIsRunningOrQueued } from "../tasks/types";
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
import { useWorkflowRunQuery } from "./hooks/useWorkflowRunQuery";
import { WorkflowRunTimeline } from "./workflowRun/WorkflowRunTimeline";
import { useWorkflowRunTimelineQuery } from "./hooks/useWorkflowRunTimelineQuery";
import { findActiveItem } from "./workflowRun/workflowTimelineUtils";

function WorkflowRun() {
const [searchParams, setSearchParams] = useSearchParams();
const active = searchParams.get("active");
const { workflowRunId, workflowPermanentId } = useParams();
const credentialGetter = useCredentialGetter();
const apiCredential = useApiCredential();
Expand All @@ -45,6 +50,8 @@ function WorkflowRun() {
const { data: workflowRun, isLoading: workflowRunIsLoading } =
useWorkflowRunQuery();

const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery();

const cancelWorkflowMutation = useMutation({
mutationFn: async () => {
const client = await getClient(credentialGetter);
Expand Down Expand Up @@ -78,7 +85,11 @@ function WorkflowRun() {
workflowRun && statusIsRunningOrQueued(workflowRun);

const workflowRunIsFinalized = workflowRun && statusIsFinalized(workflowRun);

const selection = findActiveItem(
workflowRunTimeline ?? [],
active,
!!workflowRunIsFinalized,
);
const parameters = workflowRun?.parameters ?? {};
const proxyLocation =
workflowRun?.proxy_location ?? ProxyLocation.Residential;
Expand Down Expand Up @@ -108,6 +119,13 @@ function WorkflowRun() {
</div>
) : null;

function handleSetActiveItem(id: string) {
searchParams.set("active", id);
setSearchParams(searchParams, {
replace: true,
});
}

return (
<div className="space-y-8">
<header className="flex justify-between">
Expand Down Expand Up @@ -230,7 +248,28 @@ function WorkflowRun() {
},
]}
/>
<Outlet />
<div className="flex h-[42rem] gap-6">
<div className="w-2/3">
<Outlet />
</div>
<div className="w-1/3">
<WorkflowRunTimeline
activeItem={selection}
onActionItemSelected={(item) => {
handleSetActiveItem(item.action.action_id);
}}
onBlockItemSelected={(item) => {
handleSetActiveItem(item.workflow_run_block_id);
}}
onLiveStreamSelected={() => {
handleSetActiveItem("stream");
}}
onObserverThoughtCardSelected={(item) => {
handleSetActiveItem(item.observer_thought_id);
}}
/>
</div>
</div>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export type WorkflowRunBlock = {
wait_sec?: number | null;
created_at: string;
modified_at: string;

// for loop block itself
loop_values: Array<unknown> | null;

// for blocks in loop
current_value: string | null;
current_index: number | null;
};

export type WorkflowRunTimelineBlockItem = {
Expand Down
17 changes: 15 additions & 2 deletions skyvern-frontend/src/routes/workflows/workflowRun/ActionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,41 @@ import { Separator } from "@/components/ui/separator";
import { ActionTypePill } from "@/routes/tasks/detail/ActionTypePill";
import { cn } from "@/util/utils";
import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons";
import { useCallback } from "react";

type Props = {
action: ActionsApiResponse;
index: number;
active: boolean;
onClick: () => void;
onClick: React.DOMAttributes<HTMLDivElement>["onClick"];
};

function ActionCard({ action, onClick, active, index }: Props) {
const success = action.status === Status.Completed;

const refCallback = useCallback((element: HTMLDivElement | null) => {
if (element && active) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
// this should only run once at mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div
className={cn(
"flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 hover:border-slate-50",
"flex cursor-pointer rounded-lg border-2 border-transparent bg-slate-elevation3 hover:border-slate-50",
{
"border-l-destructive": !success,
"border-l-success": success,
"border-slate-50": active,
},
)}
onClick={onClick}
ref={refCallback}
>
<div className="flex-1 space-y-2 p-4 pl-5">
<div className="flex justify-between">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { Input } from "@/components/ui/input";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { WorkflowRunBlock } from "../types/workflowRunTypes";
import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes";

type Props = {
block: WorkflowRunBlock;
};

function TaskBlockParameters({ block }: Props) {
const isTaskVariant = isTaskVariantBlock(block);
if (!isTaskVariant) {
return null;
}

const showNavigationParameters =
block.block_type === WorkflowBlockTypes.Task ||
block.block_type === WorkflowBlockTypes.Action ||
block.block_type === WorkflowBlockTypes.Login ||
block.block_type === WorkflowBlockTypes.Navigation;

const showDataExtractionParameters =
block.block_type === WorkflowBlockTypes.Task ||
block.block_type === WorkflowBlockTypes.Extraction;

const showValidationParameters =
block.block_type === WorkflowBlockTypes.Validation;

return (
<>
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">URL</h1>
<h2 className="text-base text-slate-400">
The starting URL for the block
</h2>
</div>
<Input value={block.url ?? ""} readOnly />
</div>

{showNavigationParameters ? (
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">Navigation Goal</h1>
<h2 className="text-base text-slate-400">
Where should Skyvern go and what should Skyvern do?
</h2>
</div>
<AutoResizingTextarea value={block.navigation_goal ?? ""} readOnly />
</div>
) : null}

{showNavigationParameters ? (
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">Navigation Payload</h1>
<h2 className="text-base text-slate-400">
Specify important parameters, routes, or states
</h2>
</div>
<CodeEditor
className="w-full"
language="json"
value={JSON.stringify(block.navigation_payload, null, 2)}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
) : null}

{showDataExtractionParameters ? (
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">Data Extraction Goal</h1>
<h2 className="text-base text-slate-400">
What outputs are you looking to get?
</h2>
</div>
<AutoResizingTextarea
value={block.data_extraction_goal ?? ""}
readOnly
/>
</div>
) : null}

{showDataExtractionParameters ? (
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">Data Schema</h1>
<h2 className="text-base text-slate-400">
Specify the output format in JSON
</h2>
</div>
<CodeEditor
className="w-full"
language="json"
value={JSON.stringify(block.data_schema, null, 2)}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
) : null}

{showValidationParameters ? (
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">Completion Criteria</h1>
<h2 className="text-base text-slate-400">Complete if...</h2>
</div>
<AutoResizingTextarea
value={block.complete_criterion ?? ""}
readOnly
/>
</div>
) : null}

{showValidationParameters ? (
<div className="flex gap-16">
<div className="w-80">
<h1 className="text-lg">Termination Criteria</h1>
<h2 className="text-base text-slate-400">Terminate if...</h2>
</div>
<AutoResizingTextarea
value={block.terminate_criterion ?? ""}
readOnly
/>
</div>
) : null}
</>
);
}

export { TaskBlockParameters };
25 changes: 21 additions & 4 deletions skyvern-frontend/src/routes/workflows/workflowRun/ThoughtCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PersonIcon } from "@radix-ui/react-icons";
import { QuestionMarkIcon } from "@radix-ui/react-icons";
import { ObserverThought } from "../types/workflowRunTypes";
import { cn } from "@/util/utils";
import { BrainIcon } from "@/components/icons/BrainIcon";
import { useCallback } from "react";

type Props = {
active: boolean;
Expand All @@ -9,6 +11,17 @@ type Props = {
};

function ThoughtCard({ thought, onClick, active }: Props) {
const refCallback = useCallback((element: HTMLDivElement | null) => {
if (element && active) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
// this should only run once at mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div
className={cn(
Expand All @@ -20,11 +33,15 @@ function ThoughtCard({ thought, onClick, active }: Props) {
onClick={() => {
onClick(thought);
}}
ref={refCallback}
>
<div className="flex justify-between">
<span>Thought</span>
<div className="flex items-center gap-1 bg-slate-elevation5">
<PersonIcon className="size-4" />
<div className="flex gap-3">
<BrainIcon className="size-6" />
<span>Thinking</span>
</div>
<div className="flex items-center gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
<QuestionMarkIcon className="size-4" />
<span className="text-xs">Decision</span>
</div>
</div>
Expand Down
Loading
Loading