Jon/sky 6375 alter how show all code works (#3445)

This commit is contained in:
Jonathan Dobson
2025-09-16 16:32:15 -04:00
committed by GitHub
parent d57fccfaaf
commit f2146080ce
6 changed files with 273 additions and 172 deletions

View File

@@ -341,30 +341,21 @@ function Splitter({
const [isDragging, setIsDragging] = useState(false);
useMountEffect(() => {
// small delay here, to allow for arbitrary layout thrashing to settle;
// otherwise we have to rely on an observer for the container size, and
// resetting whenever the container resizes it likely incorrect behaviour
setTimeout(() => {
if (containerRef.current) {
const newPosition = normalizeUnitsToPercent(
containerRef,
direction,
firstSizingTarget,
firstSizing,
storageKey,
);
if (containerRef.current) {
const newPosition = normalizeUnitsToPercent(
containerRef,
direction,
firstSizingTarget,
firstSizing,
storageKey,
);
setSplitPosition(newPosition);
setSplitPosition(newPosition);
if (storageKey) {
setStoredSizing(
firstSizingTarget,
storageKey,
newPosition.toString(),
);
}
if (storageKey) {
setStoredSizing(firstSizingTarget, storageKey, newPosition.toString());
}
}, 100);
}
});
useOnChange(isDragging, (newValue, oldValue) => {

View File

@@ -1,6 +1,7 @@
import {
ChevronDownIcon,
ChevronUpIcon,
CodeIcon,
CopyIcon,
PlayIcon,
ReloadIcon,
@@ -41,12 +42,14 @@ type Props = {
cacheKeyValuesPanelOpen: boolean;
parametersPanelOpen: boolean;
saving: boolean;
showAllCode: boolean;
workflow: WorkflowApiResponse;
onCacheKeyValueAccept: (cacheKeyValue: string | null) => void;
onCacheKeyValuesBlurred: (cacheKeyValue: string | null) => void;
onCacheKeyValuesFilter: (cacheKeyValue: string) => void;
onCacheKeyValuesKeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onParametersClick: () => void;
onShowAllCodeClick?: () => void;
onCacheKeyValuesClick: () => void;
onSave: () => void;
onRun?: () => void;
@@ -58,12 +61,14 @@ function WorkflowHeader({
cacheKeyValuesPanelOpen,
parametersPanelOpen,
saving,
showAllCode,
workflow,
onCacheKeyValueAccept,
onCacheKeyValuesBlurred,
onCacheKeyValuesFilter,
onCacheKeyValuesKeydown,
onParametersClick,
onShowAllCodeClick,
onCacheKeyValuesClick,
onSave,
onRun,
@@ -87,6 +92,10 @@ function WorkflowHeader({
input: useRef<HTMLInputElement>(null),
};
const handleShowAllCode = () => {
onShowAllCodeClick?.();
};
useEffect(() => {
if (cacheKeyValue === chosenCacheKeyValue) {
return;
@@ -125,62 +134,75 @@ function WorkflowHeader({
<div className="flex h-full items-center justify-end gap-4">
{user && workflow.generate_script && (
// (cacheKeyValues?.total_count ?? 0) > 0 && (
<div
tabIndex={1}
className="flex max-w-[15rem] items-center justify-center gap-1 rounded-md border border-input pr-1 focus-within:ring-1 focus-within:ring-ring"
>
<Input
ref={dom.input}
className="focus-visible:transparent focus-visible:none h-[2.75rem] text-ellipsis whitespace-nowrap border-none focus-visible:outline-none focus-visible:ring-0"
onChange={(e) => {
setChosenCacheKeyValue(e.target.value);
onCacheKeyValuesFilter(e.target.value);
}}
onMouseDown={() => {
if (!cacheKeyValuesPanelOpen) {
onCacheKeyValuesClick();
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const numFiltered = cacheKeyValues?.values?.length ?? 0;
if (numFiltered === 1) {
const first = cacheKeyValues?.values?.[0];
if (first) {
setChosenCacheKeyValue(first);
onCacheKeyValueAccept(first);
}
return;
<>
{debugStore.isDebugMode && (
<Button
className="pl-2 pr-3"
size="lg"
variant={!showAllCode ? "tertiary" : "default"}
onClick={handleShowAllCode}
>
<CodeIcon className="mr-2 h-6 w-6" />
Show Code
</Button>
)}
<div
tabIndex={1}
className="flex max-w-[10rem] items-center justify-center gap-1 rounded-md border border-input pr-1 focus-within:ring-1 focus-within:ring-ring"
>
<Input
ref={dom.input}
className="focus-visible:transparent focus-visible:none h-[2.75rem] text-ellipsis whitespace-nowrap border-none focus-visible:outline-none focus-visible:ring-0"
onChange={(e) => {
setChosenCacheKeyValue(e.target.value);
onCacheKeyValuesFilter(e.target.value);
}}
onMouseDown={() => {
if (!cacheKeyValuesPanelOpen) {
onCacheKeyValuesClick();
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const numFiltered = cacheKeyValues?.values?.length ?? 0;
setChosenCacheKeyValue(chosenCacheKeyValue);
onCacheKeyValueAccept(chosenCacheKeyValue);
}
onCacheKeyValuesKeydown(e);
}}
placeholder="Code Key Value"
value={chosenCacheKeyValue ?? undefined}
onBlur={(e) => {
onCacheKeyValuesBlurred(e.target.value);
setChosenCacheKeyValue(e.target.value);
}}
/>
{cacheKeyValuesPanelOpen ? (
<ChevronUpIcon
className="h-6 w-6 cursor-pointer"
onClick={onCacheKeyValuesClick}
/>
) : (
<ChevronDownIcon
className="h-6 w-6 cursor-pointer"
onClick={() => {
dom.input.current?.focus();
onCacheKeyValuesClick();
if (numFiltered === 1) {
const first = cacheKeyValues?.values?.[0];
if (first) {
setChosenCacheKeyValue(first);
onCacheKeyValueAccept(first);
}
return;
}
setChosenCacheKeyValue(chosenCacheKeyValue);
onCacheKeyValueAccept(chosenCacheKeyValue);
}
onCacheKeyValuesKeydown(e);
}}
placeholder="Code Key Value"
value={chosenCacheKeyValue ?? undefined}
onBlur={(e) => {
onCacheKeyValuesBlurred(e.target.value);
setChosenCacheKeyValue(e.target.value);
}}
/>
)}
</div>
{cacheKeyValuesPanelOpen ? (
<ChevronUpIcon
className="h-6 w-6 cursor-pointer"
onClick={onCacheKeyValuesClick}
/>
) : (
<ChevronDownIcon
className="h-6 w-6 cursor-pointer"
onClick={() => {
dom.input.current?.focus();
onCacheKeyValuesClick();
}}
/>
)}
</div>
</>
)}
{isGlobalWorkflow ? (
<Button

View File

@@ -1,11 +1,13 @@
import { AxiosError } from "axios";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, MutableRefObject } from "react";
import { nanoid } from "nanoid";
import {
ChevronRightIcon,
ChevronLeftIcon,
GlobeIcon,
ReloadIcon,
CheckIcon,
CopyIcon,
} from "@radix-ui/react-icons";
import { useParams, useSearchParams } from "react-router-dom";
import { useEdgesState, useNodesState, Edge } from "@xyflow/react";
@@ -42,6 +44,7 @@ import {
import { toast } from "@/components/ui/use-toast";
import { BrowserStream } from "@/components/BrowserStream";
import { statusIsFinalized } from "@/routes/tasks/types.ts";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { DebuggerRun } from "@/routes/workflows/debugger/DebuggerRun";
import { DebuggerRunMinimal } from "@/routes/workflows/debugger/DebuggerRunMinimal";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -50,6 +53,7 @@ import {
useWorkflowHasChangesStore,
useWorkflowSave,
} from "@/store/WorkflowHasChangesStore";
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
import { cn } from "@/util/utils";
@@ -69,7 +73,6 @@ import {
startNode,
} from "./workflowEditorUtils";
import { constructCacheKeyValue } from "./utils";
import "./workspace-styles.css";
const Constants = {
@@ -90,6 +93,26 @@ export type AddNodeProps = {
connectingEdgeType: string;
};
interface Dom {
splitLeft: MutableRefObject<HTMLInputElement | null>;
}
function CopyText({ text }: { text: string }) {
const [wasCopied, setWasCopied] = useState(false);
function handleCopy(code: string) {
navigator.clipboard.writeText(code);
setWasCopied(true);
setTimeout(() => setWasCopied(false), 2000);
}
return (
<Button size="icon" variant="link" onClick={() => handleCopy(text)}>
{wasCopied ? <CheckIcon /> : <CopyIcon />}
</Button>
);
}
function Workspace({
initialNodes,
initialEdges,
@@ -146,6 +169,15 @@ function Workspace({
: constructCacheKeyValue({ codeKey: cacheKey, workflow }),
);
const [showAllCode, setShowAllCode] = useState(false);
const [leftSideLayoutMode, setLeftSideLayoutMode] = useState<
"single" | "side-by-side"
>("single");
const dom: Dom = {
splitLeft: useRef<HTMLInputElement>(null),
};
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
@@ -400,6 +432,32 @@ function Workspace({
};
}, [debugSession, shouldFetchDebugSession, workflowPermanentId, queryClient]);
useEffect(() => {
const splitLeft = dom.splitLeft.current;
if (!splitLeft) {
return;
}
const parent = splitLeft.parentElement;
if (!parent) {
return;
}
const observer = new ResizeObserver(() => {
setLeftSideLayoutMode(
parent.offsetWidth < 1100 ? "single" : "side-by-side",
);
});
observer.observe(parent);
return () => {
observer.disconnect();
};
}, [dom.splitLeft]);
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
const layoutedElements = layout(nodes, edges);
setNodes(layoutedElements.nodes);
@@ -508,6 +566,9 @@ function Workspace({
}
}
const orderedBlockLabels = getOrderedBlockLabels(workflow);
const code = getCode(orderedBlockLabels, blockScripts).join("");
return (
<div className="relative h-full w-full">
{/* cycle browser dialog */}
@@ -631,6 +692,7 @@ function Workspace({
workflowPanelState.active &&
workflowPanelState.content === "parameters"
}
showAllCode={showAllCode}
workflow={workflow}
onCacheKeyValueAccept={(v) => {
setCacheKeyValue(v ?? "");
@@ -696,6 +758,9 @@ function Workspace({
onRun={() => {
closeWorkflowPanel();
}}
onShowAllCodeClick={() => {
setShowAllCode(!showAllCode);
}}
/>
</div>
@@ -806,7 +871,7 @@ function Workspace({
</div>
)}
{/* infinite canvas, browser, and timeline when in debug mode */}
{/* code, infinite canvas, browser, and timeline when in debug mode */}
{showBrowser && (
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
<Splitter
@@ -816,21 +881,64 @@ function Workspace({
split={{ left: workflowWidth }}
onResize={() => setContainerResizeTrigger((prev) => prev + 1)}
>
{/* infinite canvas */}
<div className="skyvern-split-left h-full w-full">
<FlowRenderer
hideBackground={true}
hideControls={true}
nodes={nodes}
edges={edges}
setNodes={setNodes}
setEdges={setEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
initialTitle={initialTitle}
workflow={workflow}
onContainerResize={containerResizeTrigger}
/>
{/* code and infinite canvas */}
<div className="relative h-full w-full">
<div
className={cn(
"skyvern-split-left flex h-full w-[200%] translate-x-[-50%] transition-none duration-300",
{
"w-[100%] translate-x-0":
leftSideLayoutMode === "side-by-side",
},
{
"translate-x-0": showAllCode,
},
)}
ref={dom.splitLeft}
>
{/* code */}
<div
className={cn("h-full w-[50%]", {
"w-[0%]":
leftSideLayoutMode === "side-by-side" && !showAllCode,
})}
>
<div className="relative mt-[8.5rem] w-full p-6 pr-5 pt-0">
<div className="absolute right-[1.25rem] top-0 z-20">
<CopyText text={code} />
</div>
<CodeEditor
className="w-full overflow-y-scroll"
language="python"
value={code}
lineWrap={false}
readOnly
fontSize={10}
/>
</div>
</div>
{/* infinite canvas */}
<div
className={cn("h-full w-[50%]", {
"w-[100%]":
leftSideLayoutMode === "side-by-side" && !showAllCode,
})}
>
<FlowRenderer
hideBackground={true}
hideControls={true}
nodes={nodes}
edges={edges}
setNodes={setNodes}
setEdges={setEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
initialTitle={initialTitle}
workflow={workflow}
onContainerResize={containerResizeTrigger}
/>
</div>
</div>
</div>
{/* browser & timeline */}

View File

@@ -1,13 +1,6 @@
import { getClient } from "@/api/AxiosClient";
import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react";
import type { StartNode } from "./types";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import {
Accordion,
AccordionContent,
@@ -42,7 +35,6 @@ import { useRerender } from "@/hooks/useRerender";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { cn } from "@/util/utils";
import { LightningBoltIcon } from "@radix-ui/react-icons";
function StartNode({ id, data }: NodeProps<StartNode>) {
const workflowSettingsStore = useWorkflowSettingsStore();
@@ -115,19 +107,20 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
);
}
function showAllScripts() {
for (const node of reactFlowInstance.getNodes()) {
const label = node.data.label;
// NOTE(jdo): keeping for reference; we seem to revert stuff all the time
// function showAllScripts() {
// for (const node of reactFlowInstance.getNodes()) {
// const label = node.data.label;
label &&
nodeIsFlippable(node) &&
typeof label === "string" &&
toggleScriptForNodeCallback({
label,
show: true,
});
}
}
// label &&
// nodeIsFlippable(node) &&
// typeof label === "string" &&
// toggleScriptForNodeCallback({
// label,
// show: true,
// });
// }
// }
function hideAllScripts() {
for (const node of reactFlowInstance.getNodes()) {
@@ -160,20 +153,6 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
)}
>
<div className="relative">
<div className="absolute right-[-0.5rem] top-[-0.25rem]">
<div>
<Button variant="link" size="icon" onClick={showAllScripts}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<LightningBoltIcon className="h-4 w-4 text-[gold]" />
</TooltipTrigger>
<TooltipContent>Show all code</TooltipContent>
</Tooltip>
</TooltipProvider>
</Button>
</div>
</div>
<header className="mb-6 mt-2">Start</header>
<Separator />
<Accordion

View File

@@ -1,5 +1,6 @@
import { useLocation } from "react-router-dom";
import type { WorkflowParameter } from "./types/workflowTypes";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
type Location = ReturnType<typeof useLocation>;
@@ -77,3 +78,49 @@ export const formatDuration = (duration: Duration): string => {
return `${duration.second}s`;
}
};
export const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
if (!workflow) {
return [];
}
const blockLabels = workflow.workflow_definition.blocks.map(
(block) => block.label,
);
return blockLabels;
};
const getCommentForBlockWithoutCode = (blockLabel: string) => {
return `
# block '${blockLabel}' code goes here
`;
};
export 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;
};

View File

@@ -18,58 +18,12 @@ import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValu
import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { constructCacheKeyValue } from "@/routes/workflows/editor/utils";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
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();