refactor: extract reusable runtime utilities and unify execution dialog flows for preview and full runs
This commit is contained in:
parent
17a22fe155
commit
4cda750589
9 changed files with 748 additions and 259 deletions
|
|
@ -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<RecipeRunSettings>) => 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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
|
|
@ -47,57 +89,233 @@ export function PreviewDialog({
|
|||
position="absolute"
|
||||
overlayPosition="absolute"
|
||||
overlayClassName="bg-transparent"
|
||||
className="corner-squircle sm:max-w-md"
|
||||
className="corner-squircle sm:max-w-2xl"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preview data</DialogTitle>
|
||||
<DialogTitle>{kindLabel} settings</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure run size and performance knobs for this execution.
|
||||
</p>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="rounded-xl border border-border/60 p-2">
|
||||
<p className="text-[11px] uppercase text-muted-foreground">Columns</p>
|
||||
<p className="text-sm font-semibold">{summary.totalColumns}</p>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Records"
|
||||
htmlFor="run-rows"
|
||||
hint={rowHint}
|
||||
/>
|
||||
<Input
|
||||
id="run-rows"
|
||||
type="number"
|
||||
min={1}
|
||||
max={200000}
|
||||
value={String(rows)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onRowsChange(clampInt(parsed, 1, 200000));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/60 p-2">
|
||||
<p className="text-[11px] uppercase text-muted-foreground">LLM</p>
|
||||
<p className="text-sm font-semibold">{summary.llmColumns}</p>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Batch size"
|
||||
htmlFor="run-buffer-size"
|
||||
hint="Rows handled per batch. Bigger can be faster; smaller uses less memory."
|
||||
/>
|
||||
<Input
|
||||
id="run-buffer-size"
|
||||
type="number"
|
||||
min={1}
|
||||
max={200000}
|
||||
value={String(settings.bufferSize)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onSettingsChange({
|
||||
bufferSize: clampInt(parsed, 1, 200000),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/60 p-2">
|
||||
<p className="text-[11px] uppercase text-muted-foreground">Samplers</p>
|
||||
<p className="text-sm font-semibold">{summary.samplerColumns}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/60 p-2">
|
||||
<p className="text-[11px] uppercase text-muted-foreground">Expressions</p>
|
||||
<p className="text-sm font-semibold">{summary.expressionColumns}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/60 p-2">
|
||||
<p className="text-[11px] uppercase text-muted-foreground">Tool configs</p>
|
||||
<p className="text-sm font-semibold">{summary.toolConfigs}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/60 p-2">
|
||||
<p className="text-[11px] uppercase text-muted-foreground">MCP servers</p>
|
||||
<p className="text-sm font-semibold">{summary.mcpProviders}</p>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="LLM parallel"
|
||||
htmlFor="run-llm-parallel"
|
||||
hint="How many LLM calls run at once. Leave empty to keep each model's own setting."
|
||||
/>
|
||||
<Input
|
||||
id="run-llm-parallel"
|
||||
type="number"
|
||||
min={1}
|
||||
max={2048}
|
||||
placeholder="Use model config"
|
||||
value={settings.llmParallelRequests ?? ""}
|
||||
onChange={(event) => {
|
||||
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),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Number of records"
|
||||
htmlFor="preview-rows"
|
||||
hint="Target rows for preview run."
|
||||
/>
|
||||
<Input
|
||||
id="preview-rows"
|
||||
type="number"
|
||||
min={1}
|
||||
max={1000}
|
||||
value={String(rows)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (Number.isFinite(parsed) && parsed > 0) {
|
||||
onRowsChange(Math.min(1000, Math.floor(parsed)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{advancedOpen ? "Hide advanced" : "Show advanced"}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-3 space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="CPU workers"
|
||||
htmlFor="run-non-inference-workers"
|
||||
hint="Worker threads for non-LLM steps like samplers and expressions."
|
||||
/>
|
||||
<Input
|
||||
id="run-non-inference-workers"
|
||||
type="number"
|
||||
min={1}
|
||||
max={2048}
|
||||
value={String(settings.nonInferenceWorkers)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onSettingsChange({
|
||||
nonInferenceWorkers: clampInt(parsed, 1, 2048),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Error window"
|
||||
htmlFor="run-shutdown-window"
|
||||
hint="How many attempts to observe before early-stop checks kick in."
|
||||
/>
|
||||
<Input
|
||||
id="run-shutdown-window"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10000}
|
||||
value={String(settings.shutdownErrorWindow)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onSettingsChange({
|
||||
shutdownErrorWindow: clampInt(parsed, 1, 10000),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Conversation restarts"
|
||||
htmlFor="run-max-restarts"
|
||||
hint="How many full retries to do if model output fails validation."
|
||||
/>
|
||||
<Input
|
||||
id="run-max-restarts"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={String(settings.maxConversationRestarts)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onSettingsChange({
|
||||
maxConversationRestarts: clampInt(parsed, 0, 100),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Correction steps"
|
||||
htmlFor="run-correction-steps"
|
||||
hint="Extra in-chat fix attempts before a full retry."
|
||||
/>
|
||||
<Input
|
||||
id="run-correction-steps"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={String(settings.maxConversationCorrectionSteps)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onSettingsChange({
|
||||
maxConversationCorrectionSteps: clampInt(parsed, 0, 100),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Shutdown error rate"
|
||||
htmlFor="run-shutdown-rate"
|
||||
hint="Stop early if failure rate passes this value. Example: 0.5 = 50%."
|
||||
/>
|
||||
<Input
|
||||
id="run-shutdown-rate"
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={String(settings.shutdownErrorRate)}
|
||||
onChange={(event) => {
|
||||
const parsed = Number(event.target.value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return;
|
||||
}
|
||||
onSettingsChange({
|
||||
shutdownErrorRate: clampFloat(parsed, 0, 1),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-3 text-sm text-foreground">
|
||||
<Switch
|
||||
checked={settings.disableEarlyShutdown}
|
||||
onCheckedChange={(checked) =>
|
||||
onSettingsChange({
|
||||
disableEarlyShutdown: Boolean(checked),
|
||||
})
|
||||
}
|
||||
/>
|
||||
Disable early shutdown
|
||||
</label>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{errors.length > 0 && (
|
||||
<div className="max-h-44 space-y-1 overflow-y-auto rounded-xl border border-destructive/30 bg-destructive/5 p-3">
|
||||
<p className="text-xs font-semibold uppercase text-destructive">
|
||||
|
|
@ -110,6 +328,7 @@ export function PreviewDialog({
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -119,8 +338,8 @@ export function PreviewDialog({
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={onPreview} disabled={loading}>
|
||||
{loading ? "Running..." : "Run preview"}
|
||||
<Button type="button" onClick={onRun} disabled={loading}>
|
||||
{loading ? "Starting..." : `Start ${kindLabel.toLowerCase()}`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> {
|
||||
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<string, unknown>) }
|
||||
: {};
|
||||
// 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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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<RecipeRunSettings>) => void;
|
||||
previewLoading: boolean;
|
||||
fullLoading: boolean;
|
||||
executions: RecipeExecutionRecord[];
|
||||
selectedExecutionId: string | null;
|
||||
setSelectedExecutionId: (id: string) => void;
|
||||
openPreviewDialog: () => void;
|
||||
openRunDialog: (kind: RecipeExecutionKind) => void;
|
||||
runFromDialog: () => Promise<boolean>;
|
||||
runPreview: () => Promise<boolean>;
|
||||
runFull: () => Promise<boolean>;
|
||||
cancelExecution: (id: string) => Promise<void>;
|
||||
|
|
@ -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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
return runWithValidation("preview", previewRows);
|
||||
}, [previewRows, runWithValidation]);
|
||||
|
||||
const runFull = useCallback(async (): Promise<boolean> => {
|
||||
const payload = readExecutablePayload();
|
||||
if (!payload) {
|
||||
return false;
|
||||
return runWithValidation("full", fullRows);
|
||||
}, [fullRows, runWithValidation]);
|
||||
|
||||
const runFromDialog = useCallback(async (): Promise<boolean> => {
|
||||
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<void> => {
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<RecipeRunSettings>) => void;
|
||||
previewLoading: boolean;
|
||||
fullLoading: boolean;
|
||||
currentSignature: string;
|
||||
|
|
@ -50,7 +59,8 @@ type UseRecipeStudioActionsResult = {
|
|||
selectedExecutionId: string | null;
|
||||
setSelectedExecutionId: (id: string) => void;
|
||||
persistRecipe: () => Promise<void>;
|
||||
openPreviewDialog: () => void;
|
||||
openRunDialog: (kind: RecipeExecutionKind) => void;
|
||||
runFromDialog: () => Promise<boolean>;
|
||||
runPreview: () => Promise<boolean>;
|
||||
runFull: () => Promise<boolean>;
|
||||
cancelExecution: (id: string) => Promise<void>;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-background">
|
||||
|
|
@ -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}
|
||||
/>
|
||||
<PreviewDialog
|
||||
open={previewDialogOpen}
|
||||
onOpenChange={setPreviewDialogOpen}
|
||||
rows={previewRows}
|
||||
onRowsChange={setPreviewRows}
|
||||
loading={previewLoading}
|
||||
errors={previewErrors}
|
||||
summary={previewSummary}
|
||||
onPreview={() => {
|
||||
void runPreview();
|
||||
<RunDialog
|
||||
open={runDialogOpen}
|
||||
onOpenChange={setRunDialogOpen}
|
||||
kind={runDialogKind}
|
||||
rows={runDialogRows}
|
||||
onRowsChange={(rows) => {
|
||||
if (runDialogKind === "preview") {
|
||||
setPreviewRows(rows);
|
||||
return;
|
||||
}
|
||||
setFullRows(rows);
|
||||
}}
|
||||
settings={runSettings}
|
||||
onSettingsChange={setRunSettings}
|
||||
loading={runDialogLoading}
|
||||
errors={runErrors}
|
||||
onRun={() => {
|
||||
void runFromDialog();
|
||||
}}
|
||||
container={sheetContainer}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<RecipeRunSettings>) => 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<RecipeExecutionsState>((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) =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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<string, unknown>;
|
||||
// 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 }[];
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
const mcpProviderNames = new Set<string>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue