From bffda3a479f9fedc1481028ceaf7d2db53ce7a96 Mon Sep 17 00:00:00 2001
From: Shine1i
Date: Thu, 5 Mar 2026 11:56:40 +0100
Subject: [PATCH] feat(recipe-studio): persist advanced collapsible states
across components and sessions
---
.../recipe-studio/dialogs/llm/general-tab.tsx | 9 ++--
.../dialogs/samplers/category-dialog.tsx | 7 ++-
.../dialogs/seed/seed-dialog.tsx | 7 ++-
.../dialogs/validators/validator-dialog.tsx | 9 ++--
.../src/features/recipe-studio/types/index.ts | 8 ++++
.../recipe-studio/utils/import/importer.ts | 47 ++++++++++++++++++-
.../utils/payload/build-payload.ts | 28 +++++++++++
.../recipe-studio/utils/payload/types.ts | 2 +
8 files changed, 106 insertions(+), 11 deletions(-)
diff --git a/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx b/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx
index 90ba25f822..eb342c26de 100644
--- a/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx
+++ b/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx
@@ -20,7 +20,7 @@ import {
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
-import { type ReactElement, type RefObject, useMemo, useState } from "react";
+import { type ReactElement, type RefObject, useMemo } from "react";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import type { LlmConfig } from "../../types";
import { isLikelyImageValue } from "../../utils/image-preview";
@@ -151,7 +151,7 @@ export function LlmGeneralTab({
}, [imageColumnOptions, imageContext.column_name, seedColumns]);
const traceModeId = `${config.id}-trace-mode`;
const reasoningToggleId = `${config.id}-reasoning-content`;
- const [advancedOpen, setAdvancedOpen] = useState(false);
+ const advancedOpen = config.advancedOpen === true;
return (
@@ -358,7 +358,10 @@ export function LlmGeneralTab({
)}
-
+ onUpdate({ advancedOpen: open })}
+ >
-
+ onUpdate({ advancedOpen: open })}
+ >
)}
-
+ onUpdate({ advancedOpen: open })}
+ >
{
+ if (!isRecord(input)) {
+ return {};
+ }
+ const out: Record = {};
+ for (const [nameRaw, value] of Object.entries(input)) {
+ const name = nameRaw.trim();
+ if (!name || typeof value !== "boolean") {
+ continue;
+ }
+ out[name] = value;
+ }
+ return out;
+}
+
+type AdvancedOpenConfig = LlmConfig | SamplerConfig | SeedConfig | ValidatorConfig;
+
+function isAdvancedOpenConfig(config: NodeConfig): config is AdvancedOpenConfig {
+ return (
+ config.kind === "llm" ||
+ config.kind === "sampler" ||
+ config.kind === "seed" ||
+ config.kind === "validator"
+ );
+}
+
+function applyAdvancedOpen(
+ config: NodeConfig,
+ advancedOpenByNode: Record,
+): void {
+ if (!isAdvancedOpenConfig(config)) {
+ return;
+ }
+ config.advancedOpen = advancedOpenByNode[config.name] === true;
+}
+
function attachLlmTooling(
config: LlmConfig,
toolConfigsByAlias: Map,
@@ -336,6 +376,7 @@ export function importRecipePayload(input: string): ImportResult {
const uiUnstructuredChunkOverlap = readStringNumber(
ui?.unstructured_chunk_overlap,
);
+ const uiAdvancedOpenByNode = parseAdvancedOpenByNode(ui?.advanced_open_by_node);
const uiMarkdownNotes = parseUiMarkdownNoteNodes(ui?.nodes);
for (const note of uiMarkdownNotes) {
@@ -364,7 +405,9 @@ export function importRecipePayload(input: string): ImportResult {
preferredSourceType: uiSeedSourceType,
seed_columns: uiSeedColumns,
seed_drop_columns:
- uiSeedDropColumns ?? payloadSeedDropColumns,
+ uiSeedDropColumns && uiSeedDropColumns.length > 0
+ ? uiSeedDropColumns
+ : payloadSeedDropColumns,
seed_preview_rows: uiSeedPreviewRows,
local_file_name: uiLocalFileName,
unstructured_file_name: uiUnstructuredFileName,
@@ -372,6 +415,7 @@ export function importRecipePayload(input: string): ImportResult {
unstructured_chunk_overlap: uiUnstructuredChunkOverlap,
});
if (seedConfig) {
+ applyAdvancedOpen(seedConfig, uiAdvancedOpenByNode);
if (nameToId.has(seedConfig.name)) {
errors.push(`Duplicate column name: ${seedConfig.name}.`);
} else {
@@ -441,6 +485,7 @@ export function importRecipePayload(input: string): ImportResult {
if (config.kind === "llm") {
attachLlmTooling(config, toolConfigsByAlias, mcpProvidersByName);
}
+ applyAdvancedOpen(config, uiAdvancedOpenByNode);
if (nameToId.has(config.name)) {
errors.push(`Duplicate column name: ${config.name}.`);
return;
diff --git a/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts b/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts
index 7f97547549..0d8945a443 100644
--- a/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts
+++ b/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts
@@ -66,6 +66,29 @@ function pushUniqueJson(
}
}
+function collectAdvancedOpenByNode(
+ configs: Record,
+): Record {
+ const out: Record = {};
+ for (const config of Object.values(configs)) {
+ if (
+ !(
+ config.kind === "sampler" ||
+ config.kind === "llm" ||
+ config.kind === "validator" ||
+ config.kind === "seed"
+ )
+ ) {
+ continue;
+ }
+ if (config.advancedOpen !== true) {
+ continue;
+ }
+ out[config.name] = true;
+ }
+ return out;
+}
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: payload build
export function buildRecipePayload(
configs: Record,
@@ -333,6 +356,7 @@ export function buildRecipePayload(
if (seedDropProcessor) {
recipeProcessors.push(seedDropProcessor);
}
+ const uiAdvancedOpenByNode = collectAdvancedOpenByNode(configs);
return {
errors,
@@ -386,6 +410,10 @@ export function buildRecipePayload(
firstSeed.unstructured_chunk_overlap !== undefined && {
unstructured_chunk_overlap: firstSeed.unstructured_chunk_overlap,
}),
+ ...(Object.keys(uiAdvancedOpenByNode).length > 0 && {
+ // biome-ignore lint/style/useNamingConvention: ui schema
+ advanced_open_by_node: uiAdvancedOpenByNode,
+ }),
},
},
};
diff --git a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts
index ceaec9c9b0..af048107ce 100644
--- a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts
+++ b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts
@@ -67,6 +67,8 @@ export type RecipePayload = {
unstructured_file_name?: string;
unstructured_chunk_size?: string;
unstructured_chunk_overlap?: string;
+ // ui-only: per-node advanced accordion state
+ advanced_open_by_node?: Record;
};
};