From 8a996afbfba9df84ddd0679455a3b04feb3b35be Mon Sep 17 00:00:00 2001 From: Shine1i Date: Thu, 26 Feb 2026 15:27:46 +0100 Subject: [PATCH] feat(recipe-studio): add live execution graph state (active flows, node status, editor lock) p1 --- .gitignore | 1 + .../backend/core/data_recipe/jobs/manager.py | 1 + studio/backend/core/data_recipe/jobs/parse.py | 9 + studio/backend/core/data_recipe/jobs/types.py | 1 + .../src/features/recipe-studio/api/index.ts | 2 + .../components/controls/viewport-controls.tsx | 3 + .../components/recipe-graph-aux-node.tsx | 27 ++- .../components/recipe-graph-node.tsx | 42 +++- .../components/recipe-graph-semantic-edge.tsx | 12 +- .../components/rf-ui/data-edge.tsx | 18 +- .../recipe-studio/dialogs/config-dialog.tsx | 54 +++-- .../features/recipe-studio/execution-types.ts | 2 + .../executions/execution-helpers.ts | 5 + .../recipe-studio/executions/runtime.ts | 6 + .../recipe-studio/recipe-studio-page.tsx | 195 +++++++++++++++--- .../recipe-studio/stores/recipe-studio.ts | 113 +++++++--- .../src/features/recipe-studio/types/index.ts | 2 + .../utils/graph/derive-display-graph.ts | 49 ++++- .../utils/graph/runtime-visual-state.ts | 157 ++++++++++++++ studio/frontend/src/index.css | 1 + 20 files changed, 584 insertions(+), 116 deletions(-) create mode 100644 studio/frontend/src/features/recipe-studio/utils/graph/runtime-visual-state.ts diff --git a/.gitignore b/.gitignore index 044775e846..8b54712a6c 100755 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ unsloth_compiled_cache/ outputs/ exports/ /datasets/ +studio/backend/assets/datasets/ unsloth_training_checkpoints/ *.gguf *.safetensors diff --git a/studio/backend/core/data_recipe/jobs/manager.py b/studio/backend/core/data_recipe/jobs/manager.py index dbaa620004..2a8b1fe44d 100644 --- a/studio/backend/core/data_recipe/jobs/manager.py +++ b/studio/backend/core/data_recipe/jobs/manager.py @@ -138,6 +138,7 @@ class JobManager: "status": job.status, "stage": job.stage, "current_column": job.current_column, + "completed_columns": list(job.completed_columns), "batch": {"idx": job.batch.idx, "total": job.batch.total}, "progress": { "done": job.progress.done, diff --git a/studio/backend/core/data_recipe/jobs/parse.py b/studio/backend/core/data_recipe/jobs/parse.py index 6e2142adf2..1be9fbd34a 100644 --- a/studio/backend/core/data_recipe/jobs/parse.py +++ b/studio/backend/core/data_recipe/jobs/parse.py @@ -151,6 +151,15 @@ def apply_update(job: Job, update: ParsedUpdate) -> None: job.cols = update.cols if update.progress is not None: job.column_progress = update.progress + if ( + job.current_column + and update.progress.done is not None + and update.progress.total is not None + and update.progress.total > 0 + and update.progress.done >= update.progress.total + and job.current_column not in job.completed_columns + ): + job.completed_columns.append(job.current_column) job.progress = _compute_overall_progress(job, update.progress) if update.batch_idx is not None: job.batch.idx = update.batch_idx diff --git a/studio/backend/core/data_recipe/jobs/types.py b/studio/backend/core/data_recipe/jobs/types.py index 24a63d062c..80bd03cf77 100644 --- a/studio/backend/core/data_recipe/jobs/types.py +++ b/studio/backend/core/data_recipe/jobs/types.py @@ -66,6 +66,7 @@ class Job: processor_artifacts: dict[str, Any] | None = None model_usage: dict[str, ModelUsage] = field(default_factory=dict) progress_columns_total: int | None = None + completed_columns: list[str] = field(default_factory=list) _current_usage_model: str | None = None _in_usage_summary: bool = False _seen_generation_columns: list[str] = field(default_factory=list) diff --git a/studio/frontend/src/features/recipe-studio/api/index.ts b/studio/frontend/src/features/recipe-studio/api/index.ts index a654580ef1..88a6c91d21 100644 --- a/studio/frontend/src/features/recipe-studio/api/index.ts +++ b/studio/frontend/src/features/recipe-studio/api/index.ts @@ -15,6 +15,8 @@ export type JobStatusResponse = { stage?: string | null; // biome-ignore lint/style/useNamingConvention: api schema current_column?: string | null; + // biome-ignore lint/style/useNamingConvention: api schema + completed_columns?: string[] | null; batch?: { idx?: number | null; total?: number | null; diff --git a/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx b/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx index e104841100..2ba13bfb33 100644 --- a/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx +++ b/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx @@ -7,11 +7,13 @@ import { RECIPE_FLOATING_ICON_BUTTON_CLASS } from "../recipe-floating-icon-butto type ViewportControlsProps = { interactive: boolean; + lockDisabled?: boolean; onToggleInteractive: () => void; }; export function ViewportControls({ interactive, + lockDisabled = false, onToggleInteractive, }: ViewportControlsProps): ReactElement { const { zoomIn, zoomOut, fitView, getNodes } = useReactFlow(); @@ -68,6 +70,7 @@ export function ViewportControls({ variant="ghost" size="icon" className={RECIPE_FLOATING_ICON_BUTTON_CLASS} + disabled={lockDisabled} onClick={onToggleInteractive} aria-label={interactive ? "Lock interaction" : "Unlock interaction"} > diff --git a/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx b/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx index bbb5dead41..8559f3e05c 100644 --- a/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx +++ b/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx @@ -25,12 +25,14 @@ type PromptInputNodeData = { llmId: string; field: PromptField; title: string; + executionLocked?: boolean; }; type JudgeScoreNodeData = { kind: "llm-judge-score"; llmId: string; scoreIndex: number; + executionLocked?: boolean; }; export type RecipeGraphAuxNodeData = PromptInputNodeData | JudgeScoreNodeData; @@ -81,6 +83,7 @@ function AuxNodeBase({ if (!(config && config.kind === "llm")) { return null; } + const executionLocked = Boolean(data.executionLocked); const sourceHandles = ( <> @@ -135,6 +138,7 @@ function AuxNodeBase({ className="corner-squircle nodrag nowheel max-h-40 min-h-[88px] w-full resize-none overflow-y-auto text-xs" aria-invalid={hasInvalidRefs} value={value} + disabled={executionLocked} onChange={(event) => updateConfig(data.llmId, { [data.field]: event.target.value, @@ -193,7 +197,14 @@ function AuxNodeBase({ {score.name.trim() || `Scorer ${data.scoreIndex + 1}`} - @@ -202,12 +213,14 @@ function AuxNodeBase({ className="nodrag h-7 w-full text-xs" placeholder="Score name" value={score.name} + disabled={executionLocked} onChange={(event) => updateScore({ name: event.target.value })} />