Jon/UI updates 09 15 1 (#3441)

This commit is contained in:
Jonathan Dobson
2025-09-15 18:54:03 -04:00
committed by GitHub
parent 6ee329866b
commit b6c1e16c96
11 changed files with 476 additions and 204 deletions

View File

@@ -304,6 +304,7 @@ export type WorkflowRunApiResponse = {
failure_reason: string | null;
modified_at: string;
proxy_location: ProxyLocation | null;
script_run: boolean | null;
status: Status;
title?: string;
webhook_callback_url: string;

View File

@@ -24,6 +24,7 @@ import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/Workfl
import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview";
import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording";
import { WorkflowRunCode } from "@/routes/workflows/workflowRun/WorkflowRunCode";
import { DebugStoreProvider } from "@/store/DebugStoreContext";
const router = createBrowserRouter([
@@ -158,6 +159,12 @@ const router = createBrowserRouter([
path: "recording",
element: <WorkflowRunRecording />,
},
{
path: "code",
element: (
<WorkflowRunCode showCacheKeyValueSelector={true} />
),
},
],
},
],

View File

@@ -1,3 +1,6 @@
import { LightningBoltIcon } from "@radix-ui/react-icons";
import { Tip } from "@/components/Tip";
import { Status, Task, WorkflowRunApiResponse } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
@@ -162,6 +165,19 @@ function RunHistory() {
</TableRow>
);
}
const workflowTitle =
run.script_run === true ? (
<div className="flex items-center gap-2">
<Tip content="Ran with code">
<LightningBoltIcon className="text-[gold]" />
</Tip>
<span>{run.workflow_title ?? ""}</span>
</div>
) : (
run.workflow_title ?? ""
);
return (
<TableRow
key={run.workflow_run_id}
@@ -183,7 +199,7 @@ function RunHistory() {
className="max-w-0 truncate"
title={run.workflow_title ?? undefined}
>
{run.workflow_title ?? ""}
{workflowTitle}
</TableCell>
<TableCell>
<StatusBadge status={run.status} />

View File

@@ -2,6 +2,12 @@ import { getClient } from "@/api/AxiosClient";
import { ProxyLocation } from "@/api/types";
import { ProxySelector } from "@/components/ProxySelector";
import { Button } from "@/components/ui/button";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Form,
FormControl,
@@ -311,7 +317,7 @@ function RunWorkflowForm({
<div className="space-y-8 rounded-lg bg-slate-elevation3 px-6 py-5">
<header>
<h1 className="text-lg">Advanced Settings</h1>
<h1 className="text-lg">Settings</h1>
</header>
<FormField
key="webhookCallbackUrl"
@@ -433,118 +439,136 @@ function RunWorkflowForm({
);
}}
/>
<FormField
key="cdpAddress"
control={form.control}
name="cdpAddress"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Browser Address
</div>
<h2 className="text-sm text-slate-400">
The address of the Browser server to use for the
workflow run.
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<Input
{...field}
placeholder="http://127.0.0.1:9222"
value={
field.value === null ? "" : (field.value as string)
}
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
<FormField
key="extraHttpHeaders"
control={form.control}
name="extraHttpHeaders"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Extra HTTP Headers
</div>
<h2 className="text-sm text-slate-400">
Specify some self defined HTTP requests headers in
Dict format
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<KeyValueInput
value={field.value ?? ""}
onChange={(val) => field.onChange(val)}
addButtonText="Add Header"
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
<FormField
key="maxScreenshotScrolls"
control={form.control}
name="maxScreenshotScrolls"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Max Screenshot Scrolls
</div>
<h2 className="text-sm text-slate-400">
{`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<Input
{...field}
type="number"
min={0}
value={field.value ?? ""}
placeholder={`Default: ${MAX_SCREENSHOT_SCROLLS_DEFAULT}`}
onChange={(event) => {
const value =
event.target.value === ""
? null
: Number(event.target.value);
field.onChange(value);
}}
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
</div>
<div className="space-y-8 rounded-lg bg-slate-elevation3 px-6 py-5">
<Accordion type="single" collapsible>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger className="py-0">
<header>
<h1 className="text-lg">Advanced Settings</h1>
</header>
</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-1">
<div className="space-y-8 pt-5">
<FormField
key="cdpAddress"
control={form.control}
name="cdpAddress"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Browser Address
</div>
<h2 className="text-sm text-slate-400">
The address of the Browser server to use for
the workflow run.
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<Input
{...field}
placeholder="http://127.0.0.1:9222"
value={
field.value === null
? ""
: (field.value as string)
}
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
<FormField
key="extraHttpHeaders"
control={form.control}
name="extraHttpHeaders"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Extra HTTP Headers
</div>
<h2 className="text-sm text-slate-400">
Specify some self defined HTTP requests
headers in Dict format
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<KeyValueInput
value={field.value ?? ""}
onChange={(val) => field.onChange(val)}
addButtonText="Add Header"
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
<FormField
key="maxScreenshotScrolls"
control={form.control}
name="maxScreenshotScrolls"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Max Screenshot Scrolls
</div>
<h2 className="text-sm text-slate-400">
{`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<Input
{...field}
type="number"
min={0}
value={field.value ?? ""}
placeholder={`Default: ${MAX_SCREENSHOT_SCROLLS_DEFAULT}`}
onChange={(event) => {
const value =
event.target.value === ""
? null
: Number(event.target.value);
field.onChange(value);
}}
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="flex justify-end gap-2">
<CopyApiCommandDropdown

View File

@@ -1,3 +1,6 @@
import { LightningBoltIcon } from "@radix-ui/react-icons";
import { Tip } from "@/components/Tip";
import { Status } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
@@ -129,34 +132,50 @@ function WorkflowPage() {
<TableCell colSpan={3}>No workflow runs found</TableCell>
</TableRow>
) : (
workflowRuns?.map((workflowRun) => (
<TableRow
key={workflowRun.workflow_run_id}
onClick={(event) => {
if (event.ctrlKey || event.metaKey) {
window.open(
window.location.origin +
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
"_blank",
"noopener,noreferrer",
workflowRuns?.map((workflowRun) => {
const workflowRunId =
workflowRun.script_run === true ? (
<div className="flex items-center gap-2">
<Tip content="Ran with code">
<LightningBoltIcon className="text-[gold]" />
</Tip>
<span>{workflowRun.workflow_run_id ?? ""}</span>
</div>
) : (
workflowRun.workflow_run_id ?? ""
);
return (
<TableRow
key={workflowRun.workflow_run_id}
onClick={(event) => {
if (event.ctrlKey || event.metaKey) {
window.open(
window.location.origin +
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
"_blank",
"noopener,noreferrer",
);
return;
}
navigate(
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
);
return;
}
navigate(
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
);
}}
className="cursor-pointer"
>
<TableCell>{workflowRun.workflow_run_id}</TableCell>
<TableCell>
<StatusBadge status={workflowRun.status} />
</TableCell>
<TableCell title={basicTimeFormat(workflowRun.created_at)}>
{basicLocalTimeFormat(workflowRun.created_at)}
</TableCell>
</TableRow>
))
}}
className="cursor-pointer"
>
<TableCell>{workflowRunId}</TableCell>
<TableCell>
<StatusBadge status={workflowRun.status} />
</TableCell>
<TableCell
title={basicTimeFormat(workflowRun.created_at)}
>
{basicLocalTimeFormat(workflowRun.created_at)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>

View File

@@ -53,6 +53,8 @@ function WorkflowRun() {
workflowPermanentId,
});
const hasScript = false;
const {
data: workflowRun,
isLoading: workflowRunIsLoading,
@@ -206,6 +208,32 @@ function WorkflowRun() {
webhookFailureReasonData) &&
workflowRun.status === Status.Completed;
const switchBarOptions = [
{
label: "Overview",
to: "overview",
},
{
label: "Output",
to: "output",
},
{
label: "Parameters",
to: "parameters",
},
{
label: "Recording",
to: "recording",
},
];
if (!hasScript) {
switchBarOptions.push({
label: "Code",
to: "code",
});
}
return (
<div className="space-y-8">
{!isEmbedded && (
@@ -352,28 +380,7 @@ function WorkflowRun() {
</div>
)}
{workflowFailureReason}
{!isEmbedded && (
<SwitchBarNavigation
options={[
{
label: "Overview",
to: "overview",
},
{
label: "Output",
to: "output",
},
{
label: "Parameters",
to: "parameters",
},
{
label: "Recording",
to: "recording",
},
]}
/>
)}
{!isEmbedded && <SwitchBarNavigation options={switchBarOptions} />}
<div className="flex h-[42rem] gap-6">
<div className="w-2/3">
<Outlet />

View File

@@ -36,7 +36,7 @@ import { ModelSelector } from "@/components/ModelSelector";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { NodeFooter } from "../components/NodeFooter";
import { NodeTabs } from "../components/NodeTabs";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -278,7 +278,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
</AccordionContent>
</AccordionItem>
</Accordion>
<NodeFooter blockLabel={label} />
<NodeTabs blockLabel={label} />
</div>
</div>
<BlockCodeEditor blockLabel={label} blockType={type} script={script} />

View File

@@ -17,7 +17,7 @@ import { MAX_STEPS_DEFAULT, type Taskv2Node } from "./types";
import { ModelSelector } from "@/components/ModelSelector";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { NodeFooter } from "../components/NodeFooter";
import { NodeTabs } from "../components/NodeTabs";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -196,7 +196,7 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
</AccordionContent>
</AccordionItem>
</Accordion>
<NodeFooter blockLabel={label} />
<NodeTabs blockLabel={label} />
</div>
</div>
);

View File

@@ -19,7 +19,7 @@ interface Props {
blockLabel: string;
}
function NodeFooter({ blockLabel }: Props) {
function NodeTabs({ blockLabel }: Props) {
const { blockLabel: urlBlockLabel } = useParams();
const blockOutput = useBlockOutputStore((state) => state.outputs[blockLabel]);
const [isExpanded, setIsExpanded] = useState(false);
@@ -61,46 +61,61 @@ function NodeFooter({ blockLabel }: Props) {
</div>
</div>
</div>
<div className="relative flex w-full overflow-visible bg-[pink]">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"absolute bottom-[-2.25rem] right-[-0.75rem] flex h-[2.5rem] w-[2.5rem] items-center justify-center gap-2 rounded-[50%] bg-slate-elevation3 p-2",
{
"opacity-100 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>
<Button
variant="link"
size="sm"
<div
className={cn(
"absolute right-[-1rem] top-0 h-[6rem] w-[2rem] overflow-visible",
{ "top-[2.5rem]": thisBlockIsTargetted },
)}
>
<div className="relative flex h-full w-full items-start justify-center gap-1 overflow-visible">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"p-0 opacity-80 hover:translate-y-[-1px] hover:opacity-100 active:translate-y-[0px]",
{ "opacity-100": isExpanded },
"flex h-[2.5rem] w-[2.5rem] min-w-[2.5rem] rotate-[-90deg] items-center justify-center gap-2 rounded-[50%] bg-slate-elevation3 p-2",
{
"opacity-100 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
{
"hover:translate-x-[1px] active:translate-x-[0px]":
blockOutput,
},
)}
onClick={() => {
setIsExpanded(!isExpanded);
}}
>
{isExpanded ? (
<CrossCircledIcon className="scale-[110%]" />
) : (
<OutputIcon className="scale-[80%]" />
)}
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
{isExpanded ? "Close Outputs" : "Open Outputs"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
variant="link"
size="sm"
disabled={!blockOutput}
className={cn("p-0 opacity-80 hover:opacity-100", {
"opacity-100": isExpanded,
})}
onClick={() => {
setIsExpanded(!isExpanded);
}}
>
{isExpanded ? (
<CrossCircledIcon className="scale-[110%]" />
) : (
<OutputIcon className="scale-[80%]" />
)}
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
{!blockOutput
? "No outputs. Run block first."
: isExpanded
? "Close Outputs"
: "Open Outputs"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</>
);
}
export { NodeFooter };
export { NodeTabs };

View File

@@ -115,8 +115,12 @@ const getInitialParameters = (workflow: WorkflowApiResponse) => {
*/
const constructCacheKeyValue = (
codeKey: string,
workflow: WorkflowApiResponse,
workflow?: WorkflowApiResponse,
) => {
if (!workflow) {
return "";
}
const workflowParameters = getInitialParameters(workflow)
.filter((p) => p.parameterType === "workflow")
.reduce(

View File

@@ -0,0 +1,179 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { HelpTooltip } from "@/components/HelpTooltip";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValuesQuery";
import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
import { constructCacheKeyValue } from "@/routes/workflows/editor/utils";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
interface Props {
showCacheKeyValueSelector?: boolean;
}
const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
if (!workflow) {
return [];
}
const blockLabels = workflow.workflow_definition.blocks.map(
(block) => block.label,
);
return blockLabels;
};
const getCommentForBlockWithoutCode = (blockLabel: string) => {
return `
# If the "Generate Code" option is turned on for this workflow when it runs, AI will execute block '${blockLabel}', and generate code for it.
`;
};
const getCode = (
orderedBlockLabels: string[],
blockScripts?: {
[blockName: string]: string;
},
): string[] => {
const blockCode: string[] = [];
const startBlockCode = blockScripts?.__start_block__;
if (startBlockCode) {
blockCode.push(startBlockCode);
}
for (const blockLabel of orderedBlockLabels) {
const code = blockScripts?.[blockLabel];
if (!code) {
blockCode.push(getCommentForBlockWithoutCode(blockLabel));
continue;
}
blockCode.push(`${code}
`);
}
return blockCode;
};
function WorkflowRunCode(props?: Props) {
const showCacheKeyValueSelector = props?.showCacheKeyValueSelector ?? false;
const queryClient = useQueryClient();
const { workflowPermanentId } = useParams();
const { data: workflow } = useWorkflowQuery({
workflowPermanentId,
});
const cacheKey = workflow?.cache_key ?? "";
const [cacheKeyValue, setCacheKeyValue] = useState(
cacheKey === "" ? "" : constructCacheKeyValue(cacheKey, workflow),
);
const { data: cacheKeyValues } = useCacheKeyValuesQuery({
cacheKey,
debounceMs: 100,
page: 1,
workflowPermanentId,
});
useEffect(() => {
setCacheKeyValue(
cacheKeyValues?.values[0] ?? constructCacheKeyValue(cacheKey, workflow),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cacheKeyValues, setCacheKeyValue, workflow]);
useEffect(() => {
queryClient.invalidateQueries({
queryKey: [
"cache-key-values",
workflowPermanentId,
cacheKey,
1,
undefined,
],
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workflow]);
const { data: blockScripts } = useBlockScriptsQuery({
cacheKey,
cacheKeyValue,
workflowPermanentId,
});
const orderedBlockLabels = getOrderedBlockLabels(workflow);
const code = getCode(orderedBlockLabels, blockScripts).join("");
if (code.length === 0) {
return (
<div className="flex items-center justify-center bg-slate-elevation3 p-8">
No code has been generated yet.
</div>
);
}
if (
!showCacheKeyValueSelector ||
(cacheKeyValues?.values ?? []).length <= 1
) {
return (
<CodeEditor
className="h-full overflow-y-scroll"
language="python"
value={code}
lineWrap={false}
readOnly
fontSize={10}
/>
);
}
return (
<div className="flex h-full w-full flex-col items-end justify-center gap-2">
<div className="flex w-[20rem] gap-4">
<div className="flex items-center justify-around gap-2">
<Label className="w-[7rem]">Code Cache Key</Label>
<HelpTooltip content="Which generated (& cached) code to view." />
</div>
<Select
value={cacheKeyValue}
onValueChange={(v: string) => setCacheKeyValue(v)}
>
<SelectTrigger>
<SelectValue placeholder="Code Key Value" />
</SelectTrigger>
<SelectContent>
{(cacheKeyValues?.values ?? []).map((value) => {
return (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<CodeEditor
className="h-full w-full overflow-y-scroll"
language="python"
value={code}
lineWrap={false}
readOnly
fontSize={10}
/>
</div>
);
}
export { WorkflowRunCode };