import { useEffect, useState } from "react" import type { DType, FlowDef_Input, FlowInput_Input } from "@/client" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { parseByDtype } from "./FlowBoundary" import { asText } from "./NodePanel" /** * A field per declared input, the way its type is entered. * * Held as text rather than as values: half-typed input is normal ("-", "1.", * a JSON object with one brace), and a list of them is a sweep — both of * which a parsed value would have thrown away. The caller parses on submit. */ export function ParamFields({ inputs, values, placeholderFor, onChange, }: { inputs: FlowInput_Input[] values: Record /** What an empty field says it takes, when the default is not enough. */ placeholderFor?: (dtype: DType | undefined) => string onChange: (values: Record) => void }) { return (
{inputs.map((declared) => { const name = declared.spec?.name ?? "" const dtype = declared.spec?.dtype return (
{dtype === "bool" ? ( ) : ( . or sha256:…" : `${dtype ?? "float"} or @run:.`) } className="text-sm" onChange={(event) => onChange({ ...values, [name]: event.target.value }) } /> )}
) })}
) } /** What the fields start from: the declared value of each, as text. */ export function initialText(inputs: FlowInput_Input[]): Record { return Object.fromEntries( inputs.map((one) => [one.spec?.name ?? "", asText(one.initial)]), ) } /** * The declared inputs worth drawing a field for. * * A panel input is a control's to write, so a run keeps its starting value * and the form does not offer it as a parameter. */ export function declaredInputs(definition: FlowDef_Input): FlowInput_Input[] { return (definition.inputs ?? []).filter( (one) => one.spec?.name && one.origin !== "panel", ) } /** * The parameters of one run, taken from the flow's inputs. * * A batch flow's inputs *are* its parameters — a run supplies values for them * and keeps whatever it does not name — so this is a form over the same * declarations the flow panel edits, prefilled with what they start from. */ export function RunDialog({ open, definition, pending, onOpenChange, onRun, }: { open: boolean definition: FlowDef_Input pending: boolean onOpenChange: (open: boolean) => void onRun: (params: Record) => void }) { const inputs = declaredInputs(definition) const [values, setValues] = useState>({}) // Opening is what fills the form: an edit to the flow between two runs // should show up, and the last run's values should not linger. The inputs // are read then rather than watched, so typing over one is not undone. // biome-ignore lint/correctness/useExhaustiveDependencies: opening is the dependency. useEffect(() => { if (!open) return setValues(initialText(inputs)) }, [open]) return ( Run {definition.title || definition.name} {inputs.length === 0 ? "This flow declares no parameters, so there is nothing to choose." : "The run starts from these. Everything it computes is dropped afterwards except its result."} ) } /** * The fields as the engine takes them. * * A field nobody filled in is left out, which is what keeps its declared * value — an empty parameter is not the same as a zero. */ export function parseParams( inputs: FlowInput_Input[], values: Record, ): Record { const params: Record = {} for (const declared of inputs) { const name = declared.spec?.name ?? "" const parsed = parseByDtype(declared.spec?.dtype, values[name] ?? "") if (parsed !== null) params[name] = parsed } return params }