feat(recipe-studio): persist advanced collapsible states across components and sessions
This commit is contained in:
parent
9062755e8f
commit
bffda3a479
8 changed files with 106 additions and 11 deletions
|
|
@ -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 (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -358,7 +358,10 @@ export function LlmGeneralTab({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<Collapsible
|
||||
open={advancedOpen}
|
||||
onOpenChange={(open) => onUpdate({ advancedOpen: open })}
|
||||
>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function CategoryDialog({
|
|||
onUpdate,
|
||||
}: CategoryDialogProps): ReactElement {
|
||||
const [conditionDraft, setConditionDraft] = useState("");
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const advancedOpen = config.advancedOpen === true;
|
||||
const conditionInputId = `${config.id}-conditional-rule`;
|
||||
const conditional = config.conditional_params ?? {};
|
||||
const conditionalCount = Object.keys(conditional).length;
|
||||
|
|
@ -112,7 +112,10 @@ export function CategoryDialog({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<Collapsible
|
||||
open={advancedOpen}
|
||||
onOpenChange={(open) => onUpdate({ advancedOpen: open })}
|
||||
>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ async function toUnstructuredUploadFile(file: File): Promise<File> {
|
|||
export function SeedDialog({ config, onUpdate, open }: SeedDialogProps): ReactElement {
|
||||
const [inspectError, setInspectError] = useState<string | null>(null);
|
||||
const [isInspecting, setIsInspecting] = useState(false);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const advancedOpen = config.advancedOpen === true;
|
||||
const [previewRows, setPreviewRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [expandedPreviewRows, setExpandedPreviewRows] = useState<Record<number, boolean>>({});
|
||||
const [localFile, setLocalFile] = useState<File | null>(null);
|
||||
|
|
@ -618,7 +618,10 @@ export function SeedDialog({ config, onUpdate, open }: SeedDialogProps): ReactEl
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<Collapsible
|
||||
open={advancedOpen}
|
||||
onOpenChange={(openState) => onUpdate({ advancedOpen: openState })}
|
||||
>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { type ReactElement, useMemo, useState } from "react";
|
||||
import { type ReactElement, useMemo } from "react";
|
||||
import { useRecipeStudioStore } from "../../stores/recipe-studio";
|
||||
import type { ValidatorConfig } from "../../types";
|
||||
import { isValidatorCodeLang } from "../../utils/validators/code-lang";
|
||||
|
|
@ -32,7 +32,7 @@ export function ValidatorDialog({
|
|||
const configs = useRecipeStudioStore((state) => state.configs);
|
||||
const targetColumnId = `${config.id}-target-column`;
|
||||
const batchSizeId = `${config.id}-batch-size`;
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const advancedOpen = config.advancedOpen === true;
|
||||
const codeOptions = useMemo(
|
||||
() =>
|
||||
Object.values(configs)
|
||||
|
|
@ -106,7 +106,10 @@ export function ValidatorDialog({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<Collapsible
|
||||
open={advancedOpen}
|
||||
onOpenChange={(open) => onUpdate({ advancedOpen: open })}
|
||||
>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export type CategoryConditionalParams = {
|
|||
export type SamplerConfig = {
|
||||
id: string;
|
||||
kind: "sampler";
|
||||
// ui-only
|
||||
advancedOpen?: boolean;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
sampler_type: SamplerType;
|
||||
name: string;
|
||||
|
|
@ -174,6 +176,8 @@ export type LlmTraceType = "none" | "last_message" | "all_messages";
|
|||
export type LlmConfig = {
|
||||
id: string;
|
||||
kind: "llm";
|
||||
// ui-only
|
||||
advancedOpen?: boolean;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
llm_type: LlmType;
|
||||
name: string;
|
||||
|
|
@ -248,6 +252,8 @@ export type ExpressionConfig = {
|
|||
export type ValidatorConfig = {
|
||||
id: string;
|
||||
kind: "validator";
|
||||
// ui-only
|
||||
advancedOpen?: boolean;
|
||||
name: string;
|
||||
drop?: boolean;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
|
|
@ -272,6 +278,8 @@ export type MarkdownNoteConfig = {
|
|||
export type SeedConfig = {
|
||||
id: string;
|
||||
kind: "seed";
|
||||
// ui-only
|
||||
advancedOpen?: boolean;
|
||||
name: string;
|
||||
drop?: boolean;
|
||||
// ui-only: explicit per-column drop for structured seed sources (hf/local)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import type {
|
|||
MarkdownNoteConfig,
|
||||
NodeConfig,
|
||||
RecipeProcessorConfig,
|
||||
SeedConfig,
|
||||
SamplerConfig,
|
||||
SeedSourceType,
|
||||
ValidatorConfig,
|
||||
} from "../../types";
|
||||
import { buildEdges } from "./edges";
|
||||
import { isRecord, parseJson, readString } from "./helpers";
|
||||
|
|
@ -39,6 +42,7 @@ type UiInput = {
|
|||
unstructured_file_name?: unknown;
|
||||
unstructured_chunk_size?: unknown;
|
||||
unstructured_chunk_overlap?: unknown;
|
||||
advanced_open_by_node?: unknown;
|
||||
};
|
||||
|
||||
type UiMarkdownNoteNode = {
|
||||
|
|
@ -256,6 +260,42 @@ function parseUiMarkdownNoteNodes(input: unknown): UiMarkdownNoteNode[] {
|
|||
return noteNodes;
|
||||
}
|
||||
|
||||
function parseAdvancedOpenByNode(input: unknown): Record<string, boolean> {
|
||||
if (!isRecord(input)) {
|
||||
return {};
|
||||
}
|
||||
const out: Record<string, boolean> = {};
|
||||
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<string, boolean>,
|
||||
): void {
|
||||
if (!isAdvancedOpenConfig(config)) {
|
||||
return;
|
||||
}
|
||||
config.advancedOpen = advancedOpenByNode[config.name] === true;
|
||||
}
|
||||
|
||||
function attachLlmTooling(
|
||||
config: LlmConfig,
|
||||
toolConfigsByAlias: Map<string, LlmToolConfig>,
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,29 @@ function pushUniqueJson(
|
|||
}
|
||||
}
|
||||
|
||||
function collectAdvancedOpenByNode(
|
||||
configs: Record<string, NodeConfig>,
|
||||
): Record<string, boolean> {
|
||||
const out: Record<string, boolean> = {};
|
||||
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<string, NodeConfig>,
|
||||
|
|
@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, boolean>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue