import { useMutation } from "@tanstack/react-query" import { ArrowUpFromLine, Plus, X } from "lucide-react" import type { DType, FlowDef_Input, FlowInput_Input } from "@/client" import { MessagesService } from "@/client" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import { qualify } from "./deriveEdges" import { useLiveValue } from "./liveStore" import { asText, DTYPES } from "./NodePanel" import { InfoTip, PANEL_SECTION } from "./SidePanel" import { ValuePreview } from "./ValuePreview" /** * A typed literal, read back from what was typed. * * Half-finished input is normal while editing — "-", "1.", a JSON object with * one brace — so anything that does not parse yet is kept as text rather than * replaced with a zero the person did not type. */ export function parseByDtype(dtype: DType | undefined, raw: string): unknown { if (raw === "") return null // A run's output is named rather than typed out: "@run:.", or a // digest naming bytes. The engine resolves either into the value itself, // whatever its type, so both spellings are kept as text on the way out. if (raw.startsWith("@run:") || raw.startsWith("sha256:")) return raw if (dtype === "int" || dtype === "float") { const parsed = Number(raw) return Number.isNaN(parsed) ? raw : parsed } if (dtype === "bool") return raw === "true" if (dtype === "str") return raw try { return JSON.parse(raw) } catch { return raw } } /** * One literal, entered the way its type is entered. * * A flag has two values and gets a choice of them; everything else is typed * and read back with {@link parseByDtype}. Deliberately not `type="number"` * for the numbers — see that function on why half-typed input must survive. * * `className` carries no height: the two controls need different ones. */ export function DtypeValue({ dtype, value, label, id, placeholder, className, onChange, }: { dtype: DType | undefined value: unknown /** What the field is called, for anyone not looking at it. */ label: string id?: string placeholder?: string className?: string onChange: (next: unknown) => void }) { if (dtype === "bool") { return ( ) } return ( onChange(parseByDtype(dtype, event.target.value))} /> ) } /** Putting a declared value into the running graph, credited to the input. */ function usePublishInput() { return useMutation({ mutationFn: ({ message, value }: { message: string; value: unknown }) => MessagesService.publishMessage({ name: message, requestBody: { value, source_kind: "flow", // Matches the endpoint id the canvas builds, so that label pulses. source_id: `input:${message}`, source_label: message, source_detail: "input", }, }), }) } function InputRow({ flow, declared, live, onChange, onRemove, }: { flow: string declared: FlowInput_Input /** Whether the engine is running this flow, so a value can be put into it. */ live: boolean onChange: (next: FlowInput_Input) => void onRemove: () => void }) { const spec = declared.spec ?? {} const name = spec.name ?? "" const message = qualify(flow, name) const current = useLiveValue(message) const publish = usePublishInput() return (
onChange({ ...declared, spec: { ...spec, name: event.target.value.trim() }, }) } /> onChange({ ...declared, initial })} />
{live && name ? (
now {current === undefined ? ( ) : ( )} Put this value into the flow now
) : null}
) } /** * What the flow takes from outside, and what a run of it reports. * * An input is a message no node here computes — a control writes it, a run * passes it in, an agent publishes it — so the flow declares it and says what * it starts from. Without that the node reading it waits for something nothing * provides, which is what the canvas reports. */ export function BoundarySections({ flow, definition, running, onChange, }: { flow: string definition: FlowDef_Input /** Whether the engine is running this flow, so it holds values worth showing. */ running: boolean onChange: (next: FlowDef_Input) => void }) { const inputs = definition.inputs ?? [] const batch = definition.mode === "batch" const outputs = definition.outputs ?? [] // A run reads its parameters into a namespace of its own, so what the engine // holds for a batch flow is not what any run of it saw. const live = running && !batch // Everything the flow computes, which is what a result can be made of. const provided = [ ...new Set( (definition.nodes ?? []).flatMap((node) => (node.provides ?? []).map((spec) => spec.name ?? "").filter(Boolean), ), ), ].sort() const setInputs = (next: FlowInput_Input[]) => onChange({ ...definition, inputs: next }) return ( <>
Mode A live flow runs continuously, with its subscriptions, schedules and webhooks active. A batch flow runs only when a run asks it to, from its inputs to its outputs.

{batch ? "Batch" : "Live"}

Inputs Messages that arrive from outside, such as a dashboard control, a run or the API, and the value the flow starts from.
{inputs.map((declared, index) => ( // Keyed by position: renaming an input must not remount its row. setInputs(inputs.map((one, at) => (at === index ? next : one))) } onRemove={() => setInputs(inputs.filter((_one, at) => at !== index)) } /> ))}
{batch ? (
Result What a run reports when it finishes. Everything else it computed goes with it.
{provided.length === 0 ? (

Nothing is produced here yet, so a run has nothing to report.

) : null} {provided.map((name) => ( ))}
) : null} ) }