feat(recipe-studio, studio): dataset logic, refine run settings, and improve validation UI
This commit is contained in:
parent
e00d7c6745
commit
84f005fb25
4 changed files with 171 additions and 51 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue