diff --git a/studio/frontend/src/features/recipe-studio/dialogs/preview-dialog.tsx b/studio/frontend/src/features/recipe-studio/dialogs/preview-dialog.tsx index a4fbe6f953..33c1fcd078 100644 --- a/studio/frontend/src/features/recipe-studio/dialogs/preview-dialog.tsx +++ b/studio/frontend/src/features/recipe-studio/dialogs/preview-dialog.tsx @@ -1,4 +1,10 @@ +import { type ReactElement, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { Dialog, DialogContent, @@ -7,39 +13,75 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { type ReactElement } from "react"; +import { Switch } from "@/components/ui/switch"; +import type { RecipeExecutionKind } from "../execution-types"; +import type { RecipeRunSettings } from "../stores/recipe-executions"; import { FieldLabel } from "./shared/field-label"; -type PreviewDialogProps = { +type RunDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; + kind: RecipeExecutionKind; rows: number; onRowsChange: (rows: number) => void; + settings: RecipeRunSettings; + onSettingsChange: (patch: Partial) => void; loading: boolean; errors: string[]; - summary: { - totalColumns: number; - llmColumns: number; - samplerColumns: number; - expressionColumns: number; - toolConfigs: number; - mcpProviders: number; - }; - onPreview: () => void; + onRun: () => void; container?: HTMLDivElement | null; }; -export function PreviewDialog({ +function clampInt(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) { + return min; + } + const next = Math.floor(value); + if (next < min) { + return min; + } + if (next > max) { + return max; + } + return next; +} + +function clampFloat(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) { + return min; + } + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} + +export function RunDialog({ open, onOpenChange, + kind, rows, onRowsChange, + settings, + onSettingsChange, loading, errors, - summary, - onPreview, + onRun, container, -}: PreviewDialogProps): ReactElement { +}: RunDialogProps): ReactElement { + const [advancedOpen, setAdvancedOpen] = useState(false); + const kindLabel = kind === "preview" ? "Preview" : "Full run"; + const rowHint = useMemo( + () => + kind === "preview" + ? "How many sample rows to generate for a quick check." + : "How many rows to generate in total.", + [kind], + ); + return ( - Preview data + {kindLabel} settings +

+ Configure run size and performance knobs for this execution. +

-
-
-

Columns

-

{summary.totalColumns}

+ +
+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onRowsChange(clampInt(parsed, 1, 200000)); + }} + />
-
-

LLM

-

{summary.llmColumns}

+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + bufferSize: clampInt(parsed, 1, 200000), + }); + }} + />
-
-

Samplers

-

{summary.samplerColumns}

-
-
-

Expressions

-

{summary.expressionColumns}

-
-
-

Tool configs

-

{summary.toolConfigs}

-
-
-

MCP servers

-

{summary.mcpProviders}

+
+ + { + const value = event.target.value.trim(); + if (!value) { + onSettingsChange({ llmParallelRequests: null }); + return; + } + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + llmParallelRequests: clampInt(parsed, 1, 2048), + }); + }} + />
-
- - { - const parsed = Number(event.target.value); - if (Number.isFinite(parsed) && parsed > 0) { - onRowsChange(Math.min(1000, Math.floor(parsed))); - } - }} - /> -
+ + + + + + +
+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + nonInferenceWorkers: clampInt(parsed, 1, 2048), + }); + }} + /> +
+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + shutdownErrorWindow: clampInt(parsed, 1, 10000), + }); + }} + /> +
+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + maxConversationRestarts: clampInt(parsed, 0, 100), + }); + }} + /> +
+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + maxConversationCorrectionSteps: clampInt(parsed, 0, 100), + }); + }} + /> +
+
+ + { + const parsed = Number(event.target.value); + if (!Number.isFinite(parsed)) { + return; + } + onSettingsChange({ + shutdownErrorRate: clampFloat(parsed, 0, 1), + }); + }} + /> +
+ +
+
+
+ {errors.length > 0 && (

@@ -110,6 +328,7 @@ export function PreviewDialog({ ))}

)} + - diff --git a/studio/frontend/src/features/recipe-studio/executions/run-settings.ts b/studio/frontend/src/features/recipe-studio/executions/run-settings.ts new file mode 100644 index 0000000000..a427037588 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/executions/run-settings.ts @@ -0,0 +1,169 @@ +import type { RecipeExecutionKind } from "../execution-types"; +import type { RecipeRunSettings } from "../stores/recipe-executions"; +import type { RecipePayload } from "../utils/payload/types"; + +function toPositiveInt( + value: number, + fallback: number, + min = 1, + max = Number.MAX_SAFE_INTEGER, +): number { + if (!Number.isFinite(value)) { + return fallback; + } + const next = Math.floor(value); + if (next < min) { + return min; + } + if (next > max) { + return max; + } + return next; +} + +function toNonNegativeInt( + value: number, + fallback: number, + max = Number.MAX_SAFE_INTEGER, +): number { + if (!Number.isFinite(value)) { + return fallback; + } + const next = Math.floor(value); + if (next < 0) { + return 0; + } + if (next > max) { + return max; + } + return next; +} + +function toRatio(value: number, fallback: number): number { + if (!Number.isFinite(value)) { + return fallback; + } + if (value < 0) { + return 0; + } + if (value > 1) { + return 1; + } + return value; +} + +export function sanitizeExecutionRows( + rows: number, + kind: RecipeExecutionKind, +): number { + return toPositiveInt(rows, kind === "preview" ? 5 : 1000); +} + +export function normalizeRunSettings(settings: RecipeRunSettings): RecipeRunSettings { + return { + bufferSize: toPositiveInt(settings.bufferSize, 1000, 1, 200_000), + llmParallelRequests: + typeof settings.llmParallelRequests === "number" + ? toPositiveInt(settings.llmParallelRequests, 4, 1, 2048) + : null, + nonInferenceWorkers: toPositiveInt( + settings.nonInferenceWorkers, + 4, + 1, + 2048, + ), + maxConversationRestarts: toNonNegativeInt( + settings.maxConversationRestarts, + 5, + 100, + ), + maxConversationCorrectionSteps: toNonNegativeInt( + settings.maxConversationCorrectionSteps, + 0, + 100, + ), + disableEarlyShutdown: Boolean(settings.disableEarlyShutdown), + shutdownErrorRate: toRatio(settings.shutdownErrorRate, 0.5), + shutdownErrorWindow: toPositiveInt(settings.shutdownErrorWindow, 10, 1, 10_000), + }; +} + +function buildRunConfigPayload( + settings: RecipeRunSettings, +): Record { + return { + // biome-ignore lint/style/useNamingConvention: backend schema + buffer_size: settings.bufferSize, + // biome-ignore lint/style/useNamingConvention: backend schema + non_inference_max_parallel_workers: settings.nonInferenceWorkers, + // biome-ignore lint/style/useNamingConvention: backend schema + max_conversation_restarts: settings.maxConversationRestarts, + // biome-ignore lint/style/useNamingConvention: backend schema + max_conversation_correction_steps: settings.maxConversationCorrectionSteps, + // biome-ignore lint/style/useNamingConvention: backend schema + disable_early_shutdown: settings.disableEarlyShutdown, + // biome-ignore lint/style/useNamingConvention: backend schema + shutdown_error_rate: settings.shutdownErrorRate, + // biome-ignore lint/style/useNamingConvention: backend schema + shutdown_error_window: settings.shutdownErrorWindow, + }; +} + +function applyGlobalParallelismOverride( + payload: RecipePayload, + llmParallelRequests: number | null, +): RecipePayload { + if (typeof llmParallelRequests !== "number") { + return payload; + } + + const modelConfigs = payload.recipe.model_configs.map((modelConfig) => { + const nextModelConfig = { ...modelConfig }; + const inferenceRaw = modelConfig.inference_parameters; + const inference = + inferenceRaw && + typeof inferenceRaw === "object" && + !Array.isArray(inferenceRaw) + ? { ...(inferenceRaw as Record) } + : {}; + // biome-ignore lint/style/useNamingConvention: backend schema + inference.max_parallel_requests = llmParallelRequests; + // biome-ignore lint/style/useNamingConvention: backend schema + nextModelConfig.inference_parameters = inference; + return nextModelConfig; + }); + + return { + ...payload, + recipe: { + ...payload.recipe, + // biome-ignore lint/style/useNamingConvention: backend schema + model_configs: modelConfigs, + }, + }; +} + +export function buildExecutionPayload(input: { + payload: RecipePayload; + kind: RecipeExecutionKind; + rows: number; + settings: RecipeRunSettings; +}): RecipePayload { + const normalizedSettings = normalizeRunSettings(input.settings); + const payloadWithParallelism = applyGlobalParallelismOverride( + input.payload, + normalizedSettings.llmParallelRequests, + ); + + return { + ...payloadWithParallelism, + run: { + ...payloadWithParallelism.run, + rows: input.rows, + // biome-ignore lint/style/useNamingConvention: backend schema + execution_type: input.kind, + // biome-ignore lint/style/useNamingConvention: backend schema + run_config: buildRunConfigPayload(normalizedSettings), + }, + }; +} diff --git a/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts b/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts index d00c9955e5..98690824f2 100644 --- a/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts +++ b/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts @@ -8,7 +8,10 @@ import { validateRecipe, } from "../api"; import { saveRecipeExecution } from "../data/executions-db"; -import type { RecipeExecutionRecord } from "../execution-types"; +import type { + RecipeExecutionKind, + RecipeExecutionRecord, +} from "../execution-types"; import { DATASET_PAGE_SIZE, executionLabel, @@ -21,8 +24,15 @@ import { loadSortedRecipeExecutions, } from "../executions/hydration"; import { createBaseExecutionRecord } from "../executions/runtime"; +import { + buildExecutionPayload, + sanitizeExecutionRows, +} from "../executions/run-settings"; import { trackRecipeExecution } from "../executions/tracker"; -import { useRecipeExecutionsStore } from "../stores/recipe-executions"; +import { + type RecipeRunSettings, + useRecipeExecutionsStore, +} from "../stores/recipe-executions"; import type { RecipePayload, RecipePayloadResult } from "../utils/payload/types"; type UseRecipeExecutionsParams = { @@ -34,17 +44,23 @@ type UseRecipeExecutionsParams = { }; type UseRecipeExecutionsResult = { - previewDialogOpen: boolean; - setPreviewDialogOpen: (open: boolean) => void; + runDialogOpen: boolean; + runDialogKind: RecipeExecutionKind; + setRunDialogOpen: (open: boolean) => void; previewRows: number; + fullRows: number; setPreviewRows: (rows: number) => void; - previewErrors: string[]; + setFullRows: (rows: number) => void; + runErrors: string[]; + runSettings: RecipeRunSettings; + setRunSettings: (patch: Partial) => void; previewLoading: boolean; fullLoading: boolean; executions: RecipeExecutionRecord[]; selectedExecutionId: string | null; setSelectedExecutionId: (id: string) => void; - openPreviewDialog: () => void; + openRunDialog: (kind: RecipeExecutionKind) => void; + runFromDialog: () => Promise; runPreview: () => Promise; runFull: () => Promise; cancelExecution: (id: string) => Promise; @@ -59,16 +75,22 @@ export function useRecipeExecutions({ onPreviewSuccess, }: UseRecipeExecutionsParams): UseRecipeExecutionsResult { const { - previewDialogOpen, + runDialogOpen, + runDialogKind, previewRows, - previewErrors, + fullRows, + runErrors, + runSettings, previewLoading, fullLoading, executions, selectedExecutionId, - setPreviewDialogOpen, + setRunDialogOpen, + setRunDialogKind, setPreviewRows, - setPreviewErrors, + setFullRows, + setRunErrors, + setRunSettings, setPreviewLoading, setFullLoading, setExecutions, @@ -77,16 +99,22 @@ export function useRecipeExecutions({ resetForRecipe, } = useRecipeExecutionsStore( useShallow((state) => ({ - previewDialogOpen: state.previewDialogOpen, + runDialogOpen: state.runDialogOpen, + runDialogKind: state.runDialogKind, previewRows: state.previewRows, - previewErrors: state.previewErrors, + fullRows: state.fullRows, + runErrors: state.runErrors, + runSettings: state.runSettings, previewLoading: state.previewLoading, fullLoading: state.fullLoading, executions: state.executions, selectedExecutionId: state.selectedExecutionId, - setPreviewDialogOpen: state.setPreviewDialogOpen, + setRunDialogOpen: state.setRunDialogOpen, + setRunDialogKind: state.setRunDialogKind, setPreviewRows: state.setPreviewRows, - setPreviewErrors: state.setPreviewErrors, + setFullRows: state.setFullRows, + setRunErrors: state.setRunErrors, + setRunSettings: state.setRunSettings, setPreviewLoading: state.setPreviewLoading, setFullLoading: state.setFullLoading, setExecutions: state.setExecutions, @@ -134,7 +162,7 @@ export function useRecipeExecutions({ initialExecution: resumable, notify: false, onUpsert: upsertAndPersist, - onSetPreviewErrors: setPreviewErrors, + onSetPreviewErrors: setRunErrors, onPreviewSuccess, }); } catch (error) { @@ -152,7 +180,7 @@ export function useRecipeExecutions({ recipeId, resetForRecipe, setExecutions, - setPreviewErrors, + setRunErrors, upsertAndPersist, ]); @@ -162,29 +190,26 @@ export function useRecipeExecutions({ } return null; }, [payloadResult.errors.length, payloadResult.payload]); + const readExecutablePayload = useCallback((): RecipePayload | null => { const payload = readPayload(); if (payload) { return payload; } - setPreviewErrors(payloadResult.errors); + setRunErrors(payloadResult.errors); toastError("Invalid recipe payload", payloadErrorMessage); return null; - }, [payloadErrorMessage, payloadResult.errors, readPayload, setPreviewErrors]); - - const openPreviewDialog = useCallback((): void => { - setPreviewErrors([]); - setPreviewDialogOpen(true); - }, [setPreviewDialogOpen, setPreviewErrors]); + }, [payloadErrorMessage, payloadResult.errors, readPayload, setRunErrors]); const runExecution = useCallback( async (input: { - kind: "preview" | "full"; + kind: RecipeExecutionKind; payload: RecipePayload; rows: number; + settings: RecipeRunSettings; }): Promise => { - const { kind, payload, rows } = input; + const { kind, payload, rows, settings } = input; const setLoading = kind === "preview" ? setPreviewLoading : setFullLoading; const label = executionLabel(kind); @@ -198,20 +223,15 @@ export function useRecipeExecutions({ upsertAndPersist(baseExecution); onExecutionStart?.(); - if (kind === "preview") { - setPreviewDialogOpen(false); - } + setRunDialogOpen(false); try { - const jobPayload = { - ...payload, - run: { - ...payload.run, - rows, - // biome-ignore lint/style/useNamingConvention: backend schema - execution_type: kind, - }, - }; + const jobPayload = buildExecutionPayload({ + payload, + kind, + rows, + settings, + }); const createdJob = await createRecipeJob(jobPayload); const executionWithJob = { ...baseExecution, @@ -227,7 +247,7 @@ export function useRecipeExecutions({ initialExecution: executionWithJob, notify: true, onUpsert: upsertAndPersist, - onSetPreviewErrors: setPreviewErrors, + onSetPreviewErrors: setRunErrors, onPreviewSuccess, }); } catch (error) { @@ -238,9 +258,7 @@ export function useRecipeExecutions({ error: message, finishedAt: Date.now(), }); - if (kind === "preview") { - setPreviewErrors([message]); - } + setRunErrors([message]); toastError(`${label} failed`, message); return false; } finally { @@ -253,68 +271,91 @@ export function useRecipeExecutions({ onPreviewSuccess, recipeId, setFullLoading, - setPreviewDialogOpen, - setPreviewErrors, setPreviewLoading, + setRunDialogOpen, + setRunErrors, upsertAndPersist, ], ); - const runPreview = useCallback(async (): Promise => { - const payload = readExecutablePayload(); - if (!payload) { - return false; - } - - const previewPayload = { - ...payload, - run: { - ...payload.run, - rows: previewRows, - }, - }; - try { - const validation = await validateRecipe(previewPayload); - if (!validation.valid) { - const errors = validation.errors.map((item) => item.message); - const fallback = validation.raw_detail ?? "Validation failed."; - const nextErrors = errors.length > 0 ? errors : [fallback]; - setPreviewErrors(nextErrors); - toastError("Validation failed", nextErrors[0]); + const runWithValidation = useCallback( + async (kind: RecipeExecutionKind, rows: number): Promise => { + const payload = readExecutablePayload(); + if (!payload) { return false; } - } catch (error) { - const message = toErrorMessage(error, "Validation failed."); - setPreviewErrors([message]); - toastError("Validation failed", message); - return false; - } - return runExecution({ - kind: "preview", - payload, - rows: previewRows, - }); - }, [previewRows, readExecutablePayload, runExecution, setPreviewErrors]); + const normalizedRows = sanitizeExecutionRows(rows, kind); + const executionPayload = buildExecutionPayload({ + payload, + kind, + rows: normalizedRows, + settings: runSettings, + }); + + try { + const validation = await validateRecipe(executionPayload); + if (!validation.valid) { + const errors = validation.errors.map((item) => item.message); + const fallback = validation.raw_detail ?? "Validation failed."; + const nextErrors = errors.length > 0 ? errors : [fallback]; + setRunErrors(nextErrors); + toastError("Validation failed", nextErrors[0]); + return false; + } + } catch (error) { + const message = toErrorMessage(error, "Validation failed."); + setRunErrors([message]); + toastError("Validation failed", message); + return false; + } + + return runExecution({ + kind, + payload, + rows: normalizedRows, + settings: runSettings, + }); + }, + [readExecutablePayload, runExecution, runSettings, setRunErrors], + ); + + const runPreview = useCallback(async (): Promise => { + return runWithValidation("preview", previewRows); + }, [previewRows, runWithValidation]); const runFull = useCallback(async (): Promise => { - const payload = readExecutablePayload(); - if (!payload) { - return false; + return runWithValidation("full", fullRows); + }, [fullRows, runWithValidation]); + + const runFromDialog = useCallback(async (): Promise => { + if (runDialogKind === "preview") { + return runPreview(); } + return runFull(); + }, [runDialogKind, runFull, runPreview]); - const requestedRows = Number(payload.run?.rows); - const rows = - Number.isFinite(requestedRows) && requestedRows > 0 - ? Math.floor(requestedRows) - : 1000; - - return runExecution({ - kind: "full", - payload, - rows, - }); - }, [readExecutablePayload, runExecution]); + const openRunDialog = useCallback( + (kind: RecipeExecutionKind): void => { + setRunErrors([]); + setRunDialogKind(kind); + if (kind === "full") { + const payload = readPayload(); + const payloadRows = Number(payload?.run?.rows); + if (Number.isFinite(payloadRows) && payloadRows > 0) { + setFullRows(Math.floor(payloadRows)); + } + } + setRunDialogOpen(true); + }, + [ + readPayload, + setFullRows, + setRunDialogKind, + setRunDialogOpen, + setRunErrors, + ], + ); const cancelExecution = useCallback( async (id: string): Promise => { @@ -375,17 +416,23 @@ export function useRecipeExecutions({ ); return { - previewDialogOpen, - setPreviewDialogOpen, + runDialogOpen, + runDialogKind, + setRunDialogOpen, previewRows, + fullRows, setPreviewRows, - previewErrors, + setFullRows, + runErrors, + runSettings, + setRunSettings, previewLoading, fullLoading, executions, selectedExecutionId, setSelectedExecutionId, - openPreviewDialog, + openRunDialog, + runFromDialog, runPreview, runFull, cancelExecution, diff --git a/studio/frontend/src/features/recipe-studio/hooks/use-recipe-studio-actions.ts b/studio/frontend/src/features/recipe-studio/hooks/use-recipe-studio-actions.ts index 6978b5cc77..119c5f2355 100644 --- a/studio/frontend/src/features/recipe-studio/hooks/use-recipe-studio-actions.ts +++ b/studio/frontend/src/features/recipe-studio/hooks/use-recipe-studio-actions.ts @@ -1,6 +1,10 @@ import { useRecipeExecutions } from "./use-recipe-executions"; import { useRecipePersistence } from "./use-recipe-persistence"; -import type { RecipeExecutionRecord } from "../execution-types"; +import type { + RecipeExecutionKind, + RecipeExecutionRecord, +} from "../execution-types"; +import type { RecipeRunSettings } from "../stores/recipe-executions"; import type { RecipeSnapshot } from "../utils/import"; import type { RecipePayload, RecipePayloadResult } from "../utils/payload/types"; @@ -38,11 +42,16 @@ type UseRecipeStudioActionsResult = { copied: boolean; importOpen: boolean; setImportOpen: (open: boolean) => void; - previewDialogOpen: boolean; - setPreviewDialogOpen: (open: boolean) => void; + runDialogOpen: boolean; + runDialogKind: RecipeExecutionKind; + setRunDialogOpen: (open: boolean) => void; previewRows: number; + fullRows: number; setPreviewRows: (rows: number) => void; - previewErrors: string[]; + setFullRows: (rows: number) => void; + runErrors: string[]; + runSettings: RecipeRunSettings; + setRunSettings: (patch: Partial) => void; previewLoading: boolean; fullLoading: boolean; currentSignature: string; @@ -50,7 +59,8 @@ type UseRecipeStudioActionsResult = { selectedExecutionId: string | null; setSelectedExecutionId: (id: string) => void; persistRecipe: () => Promise; - openPreviewDialog: () => void; + openRunDialog: (kind: RecipeExecutionKind) => void; + runFromDialog: () => Promise; runPreview: () => Promise; runFull: () => Promise; cancelExecution: (id: string) => Promise; @@ -101,11 +111,16 @@ export function useRecipeStudioActions({ copied: persistence.copied, importOpen: persistence.importOpen, setImportOpen: persistence.setImportOpen, - previewDialogOpen: executions.previewDialogOpen, - setPreviewDialogOpen: executions.setPreviewDialogOpen, + runDialogOpen: executions.runDialogOpen, + runDialogKind: executions.runDialogKind, + setRunDialogOpen: executions.setRunDialogOpen, previewRows: executions.previewRows, + fullRows: executions.fullRows, setPreviewRows: executions.setPreviewRows, - previewErrors: executions.previewErrors, + setFullRows: executions.setFullRows, + runErrors: executions.runErrors, + runSettings: executions.runSettings, + setRunSettings: executions.setRunSettings, previewLoading: executions.previewLoading, fullLoading: executions.fullLoading, currentSignature: persistence.currentSignature, @@ -113,7 +128,8 @@ export function useRecipeStudioActions({ selectedExecutionId: executions.selectedExecutionId, setSelectedExecutionId: executions.setSelectedExecutionId, persistRecipe: persistence.persistRecipe, - openPreviewDialog: executions.openPreviewDialog, + openRunDialog: executions.openRunDialog, + runFromDialog: executions.runFromDialog, runPreview: executions.runPreview, runFull: executions.runFull, cancelExecution: executions.cancelExecution, diff --git a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx index d583ece1ea..1308428916 100644 --- a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx +++ b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx @@ -33,7 +33,7 @@ import { RecipeGraphSemanticEdge } from "./components/recipe-graph-semantic-edge import { DataEdge } from "./components/rf-ui/data-edge"; import { ConfigDialog } from "./dialogs/config-dialog"; import { ImportDialog } from "./dialogs/import-dialog"; -import { PreviewDialog } from "./dialogs/preview-dialog"; +import { RunDialog } from "./dialogs/preview-dialog"; import { ProcessorsDialog } from "./dialogs/processors-dialog"; import { useRecipeStudioActions } from "./hooks/use-recipe-studio-actions"; import { useRecipeStudioStore } from "./stores/recipe-studio"; @@ -52,7 +52,6 @@ import { } from "./utils/reactflow-changes"; import { buildDialogOptions, - buildPreviewSummary, } from "./utils/recipe-studio-view"; import type { RecipeStudioView } from "./execution-types"; @@ -251,10 +250,6 @@ export function RecipeStudioPage({ () => buildDialogOptions(configList), [configList], ); - const previewSummary = useMemo( - () => buildPreviewSummary(configList), - [configList], - ); const handleToggleDirection = useCallback(() => { setLayoutDirection(layoutDirection === "LR" ? "TB" : "LR"); @@ -286,11 +281,16 @@ export function RecipeStudioPage({ copied, importOpen, setImportOpen, - previewDialogOpen, - setPreviewDialogOpen, + runDialogOpen, + runDialogKind, + setRunDialogOpen, previewRows, + fullRows, setPreviewRows, - previewErrors, + setFullRows, + runErrors, + runSettings, + setRunSettings, previewLoading, fullLoading, currentSignature, @@ -298,9 +298,8 @@ export function RecipeStudioPage({ selectedExecutionId, setSelectedExecutionId, persistRecipe, - openPreviewDialog, - runPreview, - runFull, + openRunDialog, + runFromDialog, cancelExecution, loadExecutionDatasetPage, copyRecipe, @@ -334,6 +333,9 @@ export function RecipeStudioPage({ setSheetView("root"); setBlockSheetOpen(true); }, [setSheetView]); + const runDialogRows = runDialogKind === "preview" ? previewRows : fullRows; + const runDialogLoading = + runDialogKind === "preview" ? previewLoading : fullLoading; return (
@@ -352,10 +354,8 @@ export function RecipeStudioPage({ workflowName={workflowName} onWorkflowNameChange={setWorkflowName} onViewChange={setActiveView} - onPreview={openPreviewDialog} - onRunFull={() => { - void runFull(); - }} + onPreview={() => openRunDialog("preview")} + onRunFull={() => openRunDialog("full")} onSaveRecipe={() => { void persistRecipe(); }} @@ -483,16 +483,24 @@ export function RecipeStudioPage({ onProcessorsChange={setProcessors} container={sheetContainer} /> - { - void runPreview(); + { + if (runDialogKind === "preview") { + setPreviewRows(rows); + return; + } + setFullRows(rows); + }} + settings={runSettings} + onSettingsChange={setRunSettings} + loading={runDialogLoading} + errors={runErrors} + onRun={() => { + void runFromDialog(); }} container={sheetContainer} /> diff --git a/studio/frontend/src/features/recipe-studio/stores/recipe-executions.ts b/studio/frontend/src/features/recipe-studio/stores/recipe-executions.ts index 699304d15f..33db25bce9 100644 --- a/studio/frontend/src/features/recipe-studio/stores/recipe-executions.ts +++ b/studio/frontend/src/features/recipe-studio/stores/recipe-executions.ts @@ -1,18 +1,47 @@ import { create } from "zustand"; +import type { RecipeExecutionKind } from "../execution-types"; import type { RecipeExecutionRecord } from "../execution-types"; import { sortExecutions, withExecutionDefaults } from "../executions/execution-helpers"; +export type RecipeRunSettings = { + bufferSize: number; + llmParallelRequests: number | null; + nonInferenceWorkers: number; + maxConversationRestarts: number; + maxConversationCorrectionSteps: number; + disableEarlyShutdown: boolean; + shutdownErrorRate: number; + shutdownErrorWindow: number; +}; + +const DEFAULT_RUN_SETTINGS: RecipeRunSettings = { + bufferSize: 1000, + llmParallelRequests: null, + nonInferenceWorkers: 4, + maxConversationRestarts: 5, + maxConversationCorrectionSteps: 0, + disableEarlyShutdown: false, + shutdownErrorRate: 0.5, + shutdownErrorWindow: 10, +}; + type RecipeExecutionsState = { - previewDialogOpen: boolean; + runDialogOpen: boolean; + runDialogKind: RecipeExecutionKind; previewRows: number; - previewErrors: string[]; + fullRows: number; + runErrors: string[]; + runSettings: RecipeRunSettings; previewLoading: boolean; fullLoading: boolean; executions: RecipeExecutionRecord[]; selectedExecutionId: string | null; - setPreviewDialogOpen: (open: boolean) => void; + setRunDialogOpen: (open: boolean) => void; + setRunDialogKind: (kind: RecipeExecutionKind) => void; setPreviewRows: (rows: number) => void; - setPreviewErrors: (errors: string[]) => void; + setFullRows: (rows: number) => void; + setRunErrors: (errors: string[]) => void; + setRunSettings: (patch: Partial) => void; setPreviewLoading: (loading: boolean) => void; setFullLoading: (loading: boolean) => void; setExecutions: (records: RecipeExecutionRecord[]) => void; @@ -22,18 +51,24 @@ type RecipeExecutionsState = { }; const INITIAL_STATE = { - previewDialogOpen: false, + runDialogOpen: false, + runDialogKind: "preview", previewRows: 5, - previewErrors: [], + fullRows: 1000, + runErrors: [], + runSettings: DEFAULT_RUN_SETTINGS, previewLoading: false, fullLoading: false, executions: [], selectedExecutionId: null, } satisfies Pick< RecipeExecutionsState, - | "previewDialogOpen" + | "runDialogOpen" + | "runDialogKind" | "previewRows" - | "previewErrors" + | "fullRows" + | "runErrors" + | "runSettings" | "previewLoading" | "fullLoading" | "executions" @@ -42,10 +77,20 @@ const INITIAL_STATE = { export const useRecipeExecutionsStore = create((set) => ({ ...INITIAL_STATE, - setPreviewDialogOpen: (open) => set({ previewDialogOpen: open }), + setRunDialogOpen: (open) => set({ runDialogOpen: open }), + setRunDialogKind: (kind) => set({ runDialogKind: kind }), setPreviewRows: (rows) => set({ previewRows: Number.isFinite(rows) && rows > 0 ? Math.floor(rows) : 1 }), - setPreviewErrors: (errors) => set({ previewErrors: errors }), + setFullRows: (rows) => + set({ fullRows: Number.isFinite(rows) && rows > 0 ? Math.floor(rows) : 1 }), + setRunErrors: (errors) => set({ runErrors: errors }), + setRunSettings: (patch) => + set((state) => ({ + runSettings: { + ...state.runSettings, + ...patch, + }, + })), setPreviewLoading: (loading) => set({ previewLoading: loading }), setFullLoading: (loading) => set({ fullLoading: loading }), setExecutions: (records) => diff --git a/studio/frontend/src/features/recipe-studio/utils/handle-layout.ts b/studio/frontend/src/features/recipe-studio/utils/handle-layout.ts new file mode 100644 index 0000000000..9f4af72ce9 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/utils/handle-layout.ts @@ -0,0 +1,36 @@ +import { Position } from "@xyflow/react"; +import type { LayoutDirection } from "../types"; + +export const NODE_HANDLE_CLASS = + "pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"; + +export const AUX_HANDLE_CLASS = + "!size-2 !border-border/80 !bg-muted/80 shadow-sm"; + +export type NodeHandleLayout = { + isTopBottom: boolean; + dataInPosition: Position; + dataOutPosition: Position; + semanticInPosition: Position; + semanticOutPosition: Position; +}; + +export function getNodeHandleLayout( + direction: LayoutDirection, +): NodeHandleLayout { + const isTopBottom = direction === "TB"; + return { + isTopBottom, + dataInPosition: isTopBottom ? Position.Top : Position.Left, + dataOutPosition: isTopBottom ? Position.Bottom : Position.Right, + semanticInPosition: isTopBottom ? Position.Left : Position.Top, + semanticOutPosition: isTopBottom ? Position.Right : Position.Bottom, + }; +} + +export function getAuxSourceHandlePosition( + direction: LayoutDirection, +): Position { + return direction === "TB" ? Position.Bottom : Position.Right; +} + 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 884f5c1f03..349183aaa6 100644 --- a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts +++ b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts @@ -18,6 +18,14 @@ export type RecipePayload = { preview: boolean; // biome-ignore lint/style/useNamingConvention: api schema output_formats: string[]; + // biome-ignore lint/style/useNamingConvention: backend schema + execution_type?: "preview" | "full"; + // biome-ignore lint/style/useNamingConvention: backend schema + run_config?: Record; + // biome-ignore lint/style/useNamingConvention: backend schema + dataset_name?: string; + // biome-ignore lint/style/useNamingConvention: backend schema + artifact_path?: string; }; ui: { nodes: { id: string; x: number; y: number }[]; diff --git a/studio/frontend/src/features/recipe-studio/utils/recipe-studio-view.ts b/studio/frontend/src/features/recipe-studio/utils/recipe-studio-view.ts index d5f74ec2b7..b75dc454ba 100644 --- a/studio/frontend/src/features/recipe-studio/utils/recipe-studio-view.ts +++ b/studio/frontend/src/features/recipe-studio/utils/recipe-studio-view.ts @@ -1,14 +1,5 @@ import type { NodeConfig, SamplerConfig } from "../types"; -export type PreviewSummary = { - totalColumns: number; - llmColumns: number; - samplerColumns: number; - expressionColumns: number; - toolConfigs: number; - mcpProviders: number; -}; - export type DialogOptions = { categoryOptions: SamplerConfig[]; modelConfigAliases: string[]; @@ -48,53 +39,3 @@ export function buildDialogOptions(configList: NodeConfig[]): DialogOptions { datetimeOptions, }; } - -export function buildPreviewSummary(configList: NodeConfig[]): PreviewSummary { - const toolConfigAliases = new Set(); - const mcpProviderNames = new Set(); - let totalColumns = 0; - let llmColumns = 0; - let samplerColumns = 0; - let expressionColumns = 0; - - for (const config of configList) { - if (config.kind === "sampler") { - totalColumns += 1; - samplerColumns += 1; - continue; - } - if (config.kind === "expression") { - totalColumns += 1; - expressionColumns += 1; - continue; - } - if (config.kind !== "llm") { - continue; - } - - totalColumns += 1; - llmColumns += 1; - for (const toolConfig of config.tool_configs ?? []) { - const toolAlias = toolConfig.tool_alias.trim(); - if (toolAlias) { - toolConfigAliases.add(toolAlias); - } - } - - for (const provider of config.mcp_providers ?? []) { - const providerName = provider.name.trim(); - if (providerName) { - mcpProviderNames.add(providerName); - } - } - } - - return { - totalColumns, - llmColumns, - samplerColumns, - expressionColumns, - toolConfigs: toolConfigAliases.size, - mcpProviders: mcpProviderNames.size, - }; -}