Debugger Continuity (FE) (#3318)

This commit is contained in:
Jonathan Dobson
2025-08-29 13:30:53 -04:00
committed by GitHub
parent 410343276d
commit 47d51be796
13 changed files with 767 additions and 25 deletions

View File

@@ -6,13 +6,14 @@ import { getElements } from "./workflowEditorUtils";
import { LogoMinimized } from "@/components/LogoMinimized";
import { WorkflowSettings } from "../types/workflowTypes";
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
import { useBlockOutputStore } from "@/store/BlockOutputStore";
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
import { getInitialParameters } from "./utils";
import { Workspace } from "./Workspace";
import { useMountEffect } from "@/hooks/useMountEffect";
function WorkflowEditor() {
const { workflowPermanentId } = useParams();
const { data: workflow, isLoading } = useWorkflowQuery({
workflowPermanentId,
});
@@ -24,6 +25,10 @@ function WorkflowEditor() {
(state) => state.setParameters,
);
const blockOutputStore = useBlockOutputStore();
useMountEffect(() => blockOutputStore.reset());
useEffect(() => {
if (workflow) {
const initialParameters = getInitialParameters(workflow);

View File

@@ -36,6 +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 { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -103,9 +104,8 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
"pointer-events-none bg-slate-950": thisBlockIsPlaying,
"outline outline-2 outline-slate-300": thisBlockIsTargetted,
},
)}
>
@@ -278,6 +278,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
</AccordionContent>
</AccordionItem>
</Accordion>
<NodeFooter blockLabel={label} />
</div>
</div>
<BlockCodeEditor blockLabel={label} blockType={type} script={script} />

View File

@@ -0,0 +1,106 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import { CrossCircledIcon } from "@radix-ui/react-icons";
import { OutputIcon } from "@/components/icons/OutputIcon";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { BlockOutputs } from "@/routes/workflows/components/BlockOutputs";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { useBlockOutputStore } from "@/store/BlockOutputStore";
import { cn } from "@/util/utils";
interface Props {
blockLabel: string;
}
function NodeFooter({ blockLabel }: Props) {
const { blockLabel: urlBlockLabel } = useParams();
const blockOutput = useBlockOutputStore((state) => state.outputs[blockLabel]);
const [isExpanded, setIsExpanded] = useState(false);
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued &&
urlBlockLabel !== undefined &&
urlBlockLabel === blockLabel;
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === blockLabel;
if (thisBlockIsPlaying) {
return null;
}
return (
<>
<div
className={cn(
"pointer-events-none absolute left-0 top-[-1rem] h-full w-full",
{ "opacity-100": isExpanded },
)}
>
<div className="relative h-full w-full overflow-hidden rounded-lg">
<div
className={cn(
"pointer-events-auto flex h-full w-full translate-y-full items-center justify-center bg-slate-elevation3 p-6 transition-all duration-300 ease-in-out",
{ "translate-y-0": isExpanded },
)}
>
<BlockOutputs
blockLabel={blockLabel}
blockOutput={
blockOutput ? JSON.parse(JSON.stringify(blockOutput)) : null
}
/>
</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"
className={cn(
"p-0 opacity-80 hover:translate-y-[-1px] hover:opacity-100 active:translate-y-[0px]",
{ "opacity-100": isExpanded },
)}
onClick={() => {
setIsExpanded(!isExpanded);
}}
>
{isExpanded ? (
<CrossCircledIcon className="scale-[110%]" />
) : (
<OutputIcon className="scale-[80%]" />
)}
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
{isExpanded ? "Close Outputs" : "Open Outputs"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</>
);
}
export { NodeFooter };

View File

@@ -1,15 +1,16 @@
import { AxiosError } from "axios";
import { ReloadIcon, PlayIcon, StopIcon } from "@radix-ui/react-icons";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getClient } from "@/api/AxiosClient";
import { ProxyLocation } from "@/api/types";
import { ProxyLocation, Status } from "@/api/types";
import { Timer } from "@/components/Timer";
import { toast } from "@/components/ui/use-toast";
import { useLogging } from "@/hooks/useLogging";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useOnChange } from "@/hooks/useOnChange";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
@@ -23,6 +24,7 @@ import {
type WorkflowApiResponse,
} from "@/routes/workflows/types/workflowTypes";
import { getInitialValues } from "@/routes/workflows/utils";
import { useBlockOutputStore } from "@/store/BlockOutputStore";
import { useDebugStore } from "@/store/useDebugStore";
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
import { useWorkflowSave } from "@/store/WorkflowHasChangesStore";
@@ -54,6 +56,7 @@ interface Props {
type Payload = Record<string, unknown> & {
block_labels: string[];
block_outputs: Record<string, unknown>;
browser_session_id: string | null;
extra_http_headers: Record<string, string> | null;
max_screenshot_scrolls: number | null;
@@ -67,6 +70,7 @@ type Payload = Record<string, unknown> & {
const getPayload = (opts: {
blockLabel: string;
blockOutputs: Record<string, unknown>;
browserSessionId: string | null;
parameters: Record<string, unknown>;
totpIdentifier: string | null;
@@ -109,6 +113,7 @@ const getPayload = (opts: {
const payload: Payload = {
block_labels: [opts.blockLabel],
block_outputs: opts.blockOutputs,
browser_session_id: opts.browserSessionId,
extra_http_headers: extraHttpHeaders,
max_screenshot_scrolls: opts.workflowSettings.maxScreenshotScrollingTimes,
@@ -138,6 +143,7 @@ function NodeHeader({
workflowPermanentId,
workflowRunId,
} = useParams();
const blockOutputsStore = useBlockOutputStore();
const debugStore = useDebugStore();
const { closeWorkflowPanel } = useWorkflowPanelStore();
const workflowSettingsStore = useWorkflowSettingsStore();
@@ -177,6 +183,26 @@ function NodeHeader({
3500
: null;
const [workflowRunStatus, setWorkflowRunStatus] = useState(
workflowRun?.status,
);
useEffect(() => {
setWorkflowRunStatus(workflowRun?.status);
}, [workflowRun, setWorkflowRunStatus]);
useOnChange(workflowRunStatus, (newValue, oldValue) => {
if (!thisBlockIsTargetted) {
return;
}
if (newValue !== oldValue && oldValue && newValue === Status.Completed) {
queryClient.invalidateQueries({
queryKey: ["block-outputs", workflowPermanentId],
});
}
});
useEffect(() => {
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
return;
@@ -202,6 +228,7 @@ function NodeHeader({
}
}
}, [
queryClient,
urlBlockLabel,
navigate,
workflowPermanentId,
@@ -226,6 +253,10 @@ function NodeHeader({
}
if (!debugSession) {
// TODO: kind of redundant; investigate if this is necessary; either
// Sentry's log should output to the console, or Sentry should just
// gather native console.error output.
console.error("Run block: there is no debug session, yet");
log.error("Run block: there is no debug session, yet");
toast({
variant: "destructive",
@@ -256,6 +287,8 @@ function NodeHeader({
const body = getPayload({
blockLabel,
blockOutputs:
blockOutputsStore.getOutputsWithOverrides(workflowPermanentId),
browserSessionId: debugSession.browser_session_id,
parameters,
totpIdentifier,