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; failure_reason: string | null;
modified_at: string; modified_at: string;
proxy_location: ProxyLocation | null; proxy_location: ProxyLocation | null;
script_run: boolean | null;
status: Status; status: Status;
title?: string; title?: string;
webhook_callback_url: 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 { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview"; import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview";
import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording"; import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording";
import { WorkflowRunCode } from "@/routes/workflows/workflowRun/WorkflowRunCode";
import { DebugStoreProvider } from "@/store/DebugStoreContext"; import { DebugStoreProvider } from "@/store/DebugStoreContext";
const router = createBrowserRouter([ const router = createBrowserRouter([
@@ -158,6 +159,12 @@ const router = createBrowserRouter([
path: "recording", path: "recording",
element: <WorkflowRunRecording />, 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 { Status, Task, WorkflowRunApiResponse } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge"; import { StatusBadge } from "@/components/StatusBadge";
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown"; import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
@@ -162,6 +165,19 @@ function RunHistory() {
</TableRow> </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 ( return (
<TableRow <TableRow
key={run.workflow_run_id} key={run.workflow_run_id}
@@ -183,7 +199,7 @@ function RunHistory() {
className="max-w-0 truncate" className="max-w-0 truncate"
title={run.workflow_title ?? undefined} title={run.workflow_title ?? undefined}
> >
{run.workflow_title ?? ""} {workflowTitle}
</TableCell> </TableCell>
<TableCell> <TableCell>
<StatusBadge status={run.status} /> <StatusBadge status={run.status} />

View File

@@ -2,6 +2,12 @@ import { getClient } from "@/api/AxiosClient";
import { ProxyLocation } from "@/api/types"; import { ProxyLocation } from "@/api/types";
import { ProxySelector } from "@/components/ProxySelector"; import { ProxySelector } from "@/components/ProxySelector";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { import {
Form, Form,
FormControl, FormControl,
@@ -311,7 +317,7 @@ function RunWorkflowForm({
<div className="space-y-8 rounded-lg bg-slate-elevation3 px-6 py-5"> <div className="space-y-8 rounded-lg bg-slate-elevation3 px-6 py-5">
<header> <header>
<h1 className="text-lg">Advanced Settings</h1> <h1 className="text-lg">Settings</h1>
</header> </header>
<FormField <FormField
key="webhookCallbackUrl" key="webhookCallbackUrl"
@@ -433,6 +439,18 @@ function RunWorkflowForm({
); );
}} }}
/> />
</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 <FormField
key="cdpAddress" key="cdpAddress"
control={form.control} control={form.control}
@@ -447,8 +465,8 @@ function RunWorkflowForm({
Browser Address Browser Address
</div> </div>
<h2 className="text-sm text-slate-400"> <h2 className="text-sm text-slate-400">
The address of the Browser server to use for the The address of the Browser server to use for
workflow run. the workflow run.
</h2> </h2>
</div> </div>
</FormLabel> </FormLabel>
@@ -458,7 +476,9 @@ function RunWorkflowForm({
{...field} {...field}
placeholder="http://127.0.0.1:9222" placeholder="http://127.0.0.1:9222"
value={ value={
field.value === null ? "" : (field.value as string) field.value === null
? ""
: (field.value as string)
} }
/> />
</FormControl> </FormControl>
@@ -483,8 +503,8 @@ function RunWorkflowForm({
Extra HTTP Headers Extra HTTP Headers
</div> </div>
<h2 className="text-sm text-slate-400"> <h2 className="text-sm text-slate-400">
Specify some self defined HTTP requests headers in Specify some self defined HTTP requests
Dict format headers in Dict format
</h2> </h2>
</div> </div>
</FormLabel> </FormLabel>
@@ -546,6 +566,10 @@ function RunWorkflowForm({
}} }}
/> />
</div> </div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<CopyApiCommandDropdown <CopyApiCommandDropdown
getOptions={() => { getOptions={() => {

View File

@@ -1,3 +1,6 @@
import { LightningBoltIcon } from "@radix-ui/react-icons";
import { Tip } from "@/components/Tip";
import { Status } from "@/api/types"; import { Status } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge"; import { StatusBadge } from "@/components/StatusBadge";
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown"; import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
@@ -129,7 +132,20 @@ function WorkflowPage() {
<TableCell colSpan={3}>No workflow runs found</TableCell> <TableCell colSpan={3}>No workflow runs found</TableCell>
</TableRow> </TableRow>
) : ( ) : (
workflowRuns?.map((workflowRun) => ( 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 <TableRow
key={workflowRun.workflow_run_id} key={workflowRun.workflow_run_id}
onClick={(event) => { onClick={(event) => {
@@ -148,15 +164,18 @@ function WorkflowPage() {
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
<TableCell>{workflowRun.workflow_run_id}</TableCell> <TableCell>{workflowRunId}</TableCell>
<TableCell> <TableCell>
<StatusBadge status={workflowRun.status} /> <StatusBadge status={workflowRun.status} />
</TableCell> </TableCell>
<TableCell title={basicTimeFormat(workflowRun.created_at)}> <TableCell
title={basicTimeFormat(workflowRun.created_at)}
>
{basicLocalTimeFormat(workflowRun.created_at)} {basicLocalTimeFormat(workflowRun.created_at)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) );
})
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ interface Props {
blockLabel: string; blockLabel: string;
} }
function NodeFooter({ blockLabel }: Props) { function NodeTabs({ blockLabel }: Props) {
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
const blockOutput = useBlockOutputStore((state) => state.outputs[blockLabel]); const blockOutput = useBlockOutputStore((state) => state.outputs[blockLabel]);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
@@ -61,26 +61,36 @@ function NodeFooter({ blockLabel }: Props) {
</div> </div>
</div> </div>
</div> </div>
<div className="relative flex w-full overflow-visible bg-[pink]"> <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> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <div
className={cn( 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", "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": "opacity-100 outline outline-2 outline-slate-300":
thisBlockIsTargetted, thisBlockIsTargetted,
}, },
{
"hover:translate-x-[1px] active:translate-x-[0px]":
blockOutput,
},
)} )}
> >
<Button <Button
variant="link" variant="link"
size="sm" size="sm"
className={cn( disabled={!blockOutput}
"p-0 opacity-80 hover:translate-y-[-1px] hover:opacity-100 active:translate-y-[0px]", className={cn("p-0 opacity-80 hover:opacity-100", {
{ "opacity-100": isExpanded }, "opacity-100": isExpanded,
)} })}
onClick={() => { onClick={() => {
setIsExpanded(!isExpanded); setIsExpanded(!isExpanded);
}} }}
@@ -94,13 +104,18 @@ function NodeFooter({ blockLabel }: Props) {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{isExpanded ? "Close Outputs" : "Open Outputs"} {!blockOutput
? "No outputs. Run block first."
: isExpanded
? "Close Outputs"
: "Open Outputs"}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
</div>
</> </>
); );
} }
export { NodeFooter }; export { NodeTabs };

View File

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