feat(recipe-studio, studio): dataset logic, refine run settings, and improve validation UI

This commit is contained in:
Shine1i 2026-03-09 02:06:12 +01:00
commit 84f005fb25
4 changed files with 171 additions and 51 deletions

View file

@ -1,4 +1,5 @@
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Sheet,
@ -152,6 +153,8 @@ function BlockSheetButton({
draggable = false,
onDragStart,
trailing = "chevron",
disabled = false,
badge,
}: {
icon: typeof Database02Icon;
title: string;
@ -161,24 +164,38 @@ function BlockSheetButton({
draggable?: boolean;
onDragStart?: (event: ReactDragEvent<HTMLButtonElement>) => void;
trailing?: "chevron" | "drag" | "none";
disabled?: boolean;
badge?: string;
}): ReactElement {
return (
<button
type="button"
onClick={onClick}
draggable={draggable}
onDragStart={onDragStart}
className={`flex w-full items-center gap-3 border-l-2 bg-background px-3 py-3 text-left transition hover:bg-muted/35 ${
onClick={disabled ? undefined : onClick}
disabled={disabled}
draggable={disabled ? false : draggable}
onDragStart={disabled ? undefined : onDragStart}
className={`flex w-full items-center gap-3 border-l-2 bg-background px-3 py-3 text-left transition ${
disabled ? "cursor-not-allowed opacity-60" : "hover:bg-muted/35"
} ${
isActive
? "border-emerald-500"
: "border-transparent hover:border-border/60"
: disabled
? "border-transparent"
: "border-transparent hover:border-border/60"
} ${draggable ? "cursor-grab active:cursor-grabbing" : ""}`}
>
<div className="flex size-9 items-center justify-center rounded-xl text-foreground/70">
<HugeiconsIcon icon={icon} className="size-5" />
</div>
<div className="flex-1">
<p className="text-sm font-semibold text-foreground">{title}</p>
<div className="flex items-center gap-2">
<p className="text-sm font-semibold text-foreground">{title}</p>
{badge ? (
<Badge variant="outline" className="rounded-full text-[10px]">
{badge}
</Badge>
) : null}
</div>
<p className="text-[11px] text-muted-foreground">{description}</p>
</div>
{trailing === "chevron" ? (
@ -428,8 +445,12 @@ export function BlockSheet({
trailing={
item.kind === "expression" || item.kind === "note"
? "drag"
: "chevron"
: item.kind === "processor"
? "none"
: "chevron"
}
disabled={item.kind === "processor"}
badge={item.kind === "processor" ? "Work in progress" : undefined}
onClick={() => {
if (item.kind === "processor") {
setSheetOpen(false);

View file

@ -13,7 +13,15 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { CookBookIcon, TestTube01Icon } from "@hugeicons/core-free-icons";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import {
AlertCircleIcon,
CheckmarkCircle02Icon,
CookBookIcon,
SparklesIcon,
TestTube01Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type ReactElement, useEffect, useState } from "react";
import type { RecipeExecutionKind } from "../execution-types";
@ -172,21 +180,43 @@ function ValidationResultPanel({
return (
<div
className={
className={cn(
"space-y-3 rounded-2xl border p-4 shadow-border backdrop-blur-sm",
validateResult.valid
? "space-y-1 rounded-xl border border-emerald-300 bg-emerald-50 p-3"
: "space-y-1 rounded-xl border border-destructive/30 bg-destructive/5 p-3"
}
? "border-emerald-300/70 bg-emerald-50/80 dark:border-emerald-900/60 dark:bg-emerald-950/30"
: "border-destructive/30 bg-destructive/5",
)}
>
<p
className={
validateResult.valid
? "text-xs font-semibold uppercase text-emerald-700"
: "text-xs font-semibold uppercase text-destructive"
}
>
{validateResult.valid ? "Validation passed" : "Validation failed"}
</p>
<div className="flex items-start gap-3">
<div
className={cn(
"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full border",
validateResult.valid
? "border-emerald-300/70 bg-emerald-500/10 text-emerald-700 dark:border-emerald-900/60 dark:text-emerald-300"
: "border-destructive/30 bg-destructive/10 text-destructive",
)}
>
<HugeiconsIcon
icon={validateResult.valid ? CheckmarkCircle02Icon : AlertCircleIcon}
className="size-4"
/>
</div>
<div className="min-w-0 flex-1 space-y-1">
<p
className={cn(
"text-sm font-semibold",
validateResult.valid ? "text-emerald-700 dark:text-emerald-300" : "text-destructive",
)}
>
{validateResult.valid ? "Recipe looks good" : "Recipe needs attention"}
</p>
<p className="text-xs text-muted-foreground">
{validateResult.valid
? "Validation passed. You can start the run when ready."
: "Fix the issues below, then validate again."}
</p>
</div>
</div>
{!validateResult.valid && validateResult.errors.length > 0 && (
<div className="space-y-1">
{validateResult.errors.map((error) => (
@ -253,6 +283,8 @@ export function RunDialog({
const [shutdownRateDraft, setShutdownRateDraft] = useState(
String(settings.shutdownErrorRate),
);
const showBatchingHint =
kind === "full" && rows >= 1000 && !settings.batchEnabled;
useEffect(() => {
if (!open) {
@ -289,17 +321,40 @@ export function RunDialog({
position="absolute"
overlayPosition="absolute"
overlayClassName="bg-transparent"
className="corner-squircle sm:max-w-2xl shadow-border"
className="corner-squircle border-border/70 bg-background/95 sm:max-w-2xl shadow-border backdrop-blur-xl"
>
<DialogHeader>
<DialogHeader className="space-y-2">
<DialogTitle>{kindLabel} settings</DialogTitle>
<p className="text-sm text-muted-foreground">
Configure run size and performance knobs for this execution.
</p>
</DialogHeader>
<div className="flex items-center justify-between text-sm">
<span className="font-medium text-foreground">Preview mode</span>
{showBatchingHint && (
<div className="flex items-start gap-3 rounded-2xl border border-amber-300/70 bg-amber-50/80 p-4 shadow-border dark:border-amber-900/60 dark:bg-amber-950/30">
<div className="flex size-8 shrink-0 items-center justify-center rounded-full border border-amber-300/70 bg-amber-500/10 text-amber-700 dark:border-amber-900/60 dark:text-amber-300">
<HugeiconsIcon icon={SparklesIcon} className="size-4" />
</div>
<div className="space-y-1">
<p className="text-sm font-semibold text-amber-800 dark:text-amber-200">
Bigger runs usually feel smoother with batching on
</p>
<p className="text-xs leading-relaxed text-amber-900/80 dark:text-amber-100/80">
You&apos;re generating {rows.toLocaleString()} records. Turning on batching
usually makes larger runs easier to manage and more resilient if
something goes wrong mid-run.
</p>
</div>
</div>
)}
<div className="flex items-center justify-between rounded-2xl border border-border/70 bg-card/60 px-4 py-3 text-sm shadow-border">
<div className="space-y-0.5">
<span className="font-medium text-foreground">Preview mode</span>
<p className="text-xs text-muted-foreground">
Turn this off for a full dataset run.
</p>
</div>
<Switch
checked={kind === "preview"}
onCheckedChange={(checked) =>
@ -384,9 +439,14 @@ export function RunDialog({
</div>
{kind === "full" && (
<div className="space-y-3">
<div className="space-y-3 rounded-2xl border border-border/70 bg-card/60 p-4 shadow-border">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium">Enable batching</span>
<div className="space-y-0.5">
<span className="font-medium">Enable batching</span>
<p className="text-xs text-muted-foreground">
Split big runs into manageable chunks.
</p>
</div>
<Switch
checked={settings.batchEnabled}
onCheckedChange={(checked) =>
@ -416,9 +476,12 @@ export function RunDialog({
}
/>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium">
Merge batches to one parquet
</span>
<div className="space-y-0.5">
<span className="font-medium">Merge batches to one parquet</span>
<p className="text-xs text-muted-foreground">
Combine chunk outputs into one final file when done.
</p>
</div>
<Switch
checked={settings.mergeBatches}
onCheckedChange={(checked) =>
@ -441,7 +504,7 @@ export function RunDialog({
</button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 rounded-2xl border border-border/70 bg-card/60 p-4 shadow-border md:grid-cols-2">
<DraftInputField
id="run-non-inference-workers"
label="CPU workers"
@ -536,7 +599,13 @@ export function RunDialog({
)
}
/>
<div className="flex items-center gap-3 text-sm text-foreground">
<div className="flex items-center justify-between gap-3 rounded-xl border border-border/60 bg-background/60 px-3 py-2 text-sm text-foreground md:col-span-2">
<div className="space-y-0.5">
<p className="font-medium">Keep running through failures</p>
<p className="text-xs text-muted-foreground">
Recommended for longer runs when you want maximum output.
</p>
</div>
<Switch
checked={settings.disableEarlyShutdown}
onCheckedChange={(checked) =>
@ -545,17 +614,19 @@ export function RunDialog({
})
}
/>
Disable early shutdown
</div>
</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">
Run checks
</p>
<div className="max-h-44 space-y-2 overflow-y-auto rounded-2xl border border-destructive/30 bg-destructive/5 p-4 shadow-border">
<div className="flex items-center gap-2">
<HugeiconsIcon icon={AlertCircleIcon} className="size-4 text-destructive" />
<Badge variant="outline" className="rounded-full text-[10px] text-destructive">
Run checks
</Badge>
</div>
{errors.map((error) => (
<p key={error} className="text-xs text-destructive">
{error}
@ -572,6 +643,7 @@ export function RunDialog({
variant="outline"
onClick={() => onOpenChange(false)}
disabled={loading}
className="corner-squircle border-border/70 bg-card/70"
>
Cancel
</Button>
@ -580,11 +652,12 @@ export function RunDialog({
variant="outline"
onClick={onValidate}
disabled={loading || validateLoading}
className="corner-squircle border-border/70 bg-card/70"
>
<HugeiconsIcon icon={TestTube01Icon} className="size-3.5" />
{validateLoading ? "Validating..." : "Validate recipe"}
</Button>
<Button type="button" onClick={onRun} disabled={loading}>
<Button type="button" onClick={onRun} disabled={loading} className="corner-squircle">
<HugeiconsIcon icon={CookBookIcon} className="size-3.5" />
{loading ? "Starting..." : `Start ${kindLabel.toLowerCase()}`}
</Button>

View file

@ -18,13 +18,13 @@ export type RecipeRunSettings = {
const DEFAULT_RUN_SETTINGS: RecipeRunSettings = {
batchSize: 1000,
batchEnabled: true,
batchEnabled: false,
mergeBatches: false,
llmParallelRequests: null,
nonInferenceWorkers: 4,
maxConversationRestarts: 5,
maxConversationCorrectionSteps: 0,
disableEarlyShutdown: false,
disableEarlyShutdown: true,
shutdownErrorRate: 0.5,
shutdownErrorWindow: 10,
};
@ -60,7 +60,7 @@ const INITIAL_STATE = {
runDialogOpen: false,
runDialogKind: "preview",
previewRows: 5,
fullRows: 1000,
fullRows: 100,
fullRunName: "",
runErrors: [],
runSettings: DEFAULT_RUN_SETTINGS,
@ -86,7 +86,20 @@ const INITIAL_STATE = {
export const useRecipeExecutionsStore = create<RecipeExecutionsState>((set) => ({
...INITIAL_STATE,
setRunDialogOpen: (open) => set({ runDialogOpen: open }),
setRunDialogKind: (kind) => set({ runDialogKind: kind }),
setRunDialogKind: (kind) =>
set((state) => {
if (state.runDialogKind === "preview" && kind === "full") {
return {
runDialogKind: kind,
fullRows: 100,
runSettings: {
...state.runSettings,
batchEnabled: false,
},
};
}
return { runDialogKind: kind };
}),
setPreviewRows: (rows) =>
set({ previewRows: Number.isFinite(rows) && rows > 0 ? Math.floor(rows) : 1 }),
setFullRows: (rows) =>

View file

@ -135,6 +135,7 @@ export function DatasetSection() {
datasetSource === "upload" ? "local" : "huggingface",
);
const [localDatasets, setLocalDatasets] = useState<LocalDatasetInfo[]>([]);
const [hasLoadedLocalDatasets, setHasLoadedLocalDatasets] = useState(false);
const [localLoading, setLocalLoading] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
const openPreview = useDatasetPreviewDialogStore((s) => s.openPreview);
@ -157,6 +158,7 @@ export function DatasetSection() {
error instanceof Error ? error.message : "Failed to load local datasets.",
);
} finally {
setHasLoadedLocalDatasets(true);
setLocalLoading(false);
}
}, []);
@ -247,19 +249,34 @@ export function DatasetSection() {
return new Map(localDatasets.map((item) => [item.id, item.label]));
}, [localDatasets]);
const selectedLocalId = useMemo(() => {
const selectedLocalDataset = useMemo(() => {
if (!uploadedFile) return null;
const item = localDatasets.find((entry) => entry.path === uploadedFile);
return item?.id ?? deriveLocalDatasetName(uploadedFile);
return localDatasets.find((item) => item.path === uploadedFile) ?? null;
}, [localDatasets, uploadedFile]);
const selectedLocalId = selectedLocalDataset?.id ?? null;
const localResultIds = useMemo(() => {
const ids = localFilteredDatasets.map((item) => item.id);
if (selectedLocalId && !ids.includes(selectedLocalId)) {
if (selectedLocalDataset && selectedLocalId && !ids.includes(selectedLocalId)) {
ids.push(selectedLocalId);
}
return ids;
}, [localFilteredDatasets, selectedLocalId]);
}, [localFilteredDatasets, selectedLocalDataset, selectedLocalId]);
useEffect(() => {
if (!hasLoadedLocalDatasets) return;
if (datasetSource !== "upload") return;
if (!uploadedFile) return;
if (selectedLocalDataset) return;
selectLocalDataset(null);
}, [
datasetSource,
hasLoadedLocalDatasets,
uploadedFile,
selectedLocalDataset,
selectLocalDataset,
]);
const activeSourceTab = datasetSource === "upload" ? "local" : "huggingface";
const comboboxItems = pickerTab === "huggingface" ? hfResultIds : localResultIds;
@ -277,10 +294,6 @@ export function DatasetSection() {
!isLikelyLocalDatasetRef(dataset);
const selectedDatasetName = datasetSource === "upload" ? uploadedFile : dataset;
const selectedLocalDataset = useMemo(() => {
if (!uploadedFile) return null;
return localDatasets.find((item) => item.path === uploadedFile) ?? null;
}, [localDatasets, uploadedFile]);
const selectedLocalMetadata = selectedLocalDataset?.metadata ?? null;
const selectedLocalColumns = selectedLocalMetadata?.columns ?? [];
const selectedLocalRows =