diff --git a/NOTEPAD.md b/NOTEPAD.md index 495d5e3..3bfef05 100644 --- a/NOTEPAD.md +++ b/NOTEPAD.md @@ -16,6 +16,8 @@ Deferring because out of scope is fine, but don't mention deferring than. - FEAT/UI add animation to widgets; i.e. status of bars, gauges etc. should fade from one state to another. Multi-buttons (like "Mode" in the "Home" dashboard of the demo) should transition from one state to another; use inspiration for animations based on the google material guidelines - BUG when clicking "edit" in the "Home" dashboard of the demo on hub.fluksio.com, most of the panels disappear (only a handfull is left for actual edit) - CHORE/UI: the edge popover shows the same value twice — `MessageSparkline` falls through to a collapsed `ValuePreview` for a non-numeric value, and `EdgeInspector` then renders its own `ValuePreview defaultOpen` below it. Cosmetic; one of the two is redundant. +- FEAT/UI: a settings-and-inputs overview page, so what every node of an installation is configured with can be read and searched in one place rather than one panel at a time. +- FEAT/UI: an input endpoint opens the flow panel, which is right for editing but not for reading one value. A panel of its own — the declaration, the current value, its history — is what clicking a label wants to give. - FEAT/UI we should introduce a sync between the header of the python function and the node configuration; i.e. adding an input/output or static paramter would change the header of the python function and changing the python function header and vice versa - BUG/UI on flows like "House history" where the widget sets the range for the "draw the window" node to generate some data, the edges overlap the nodes. We should adjust the flow visualization to account for these cyclic behaviors - BUG/UI when enlarging the code editor of a node, the code editor should enlarge to the left (node settings remain on the right) so that the code editor fills the center of the screen with the node properties available next to it @@ -210,7 +212,7 @@ Open on purpose. Each names what should bring it back. - FEAT/RUNS: stage caching. `run_node.cache_key` is written on every run and the artifact store is content-addressed, so the pieces are in place; what is missing is computing the key from the node's source digest plus its input values and skipping a node whose key already has an `ok` row with its artifacts still present. The two research repos want this more than they want resume — neither persists checkpoints, and both re-run unchanged preprocessing every time. - FEAT/RUNS: per-label requirements overlays (`requirements-gpu.txt`) synced into a remote worker's venv, with drift surfaced against the engine's manifest. Today a worker's environment is whatever `--python` points at, which is fine for one hand-managed GPU box and not for several. `venv_digest` already arrives at attach and is shown on `/workers`, so the reporting half exists. - FEAT/UI: a dashboard shows a run's curve only while it is running. Emissions reach the socket live, but a run's values live in its own state namespace, so reloading the panel afterwards leaves the chart empty — the durable series is on the run (`/runs/{id}/metrics`) and nothing binds a widget to it. A chart variant that reads a run's series, or the existing querying chart pointed at `/runs/series/compare`, is what would close it. This is also what a demo needs to show a finished experiment rather than only a live one. -- FEAT/UI: nothing submits a run with parameters from the UI. Pressing Run on a batch flow submits one with the declared defaults, which is enough to try it; choosing parameters, or launching a sweep, is API-only. A form generated from the flow's `inputs` is the obvious shape. +- FEAT/UI: launching a sweep is API-only. Pressing Run on a batch flow asks for its parameters, but the many-runs-at-once shape has no UI; a run detail screen is what it wants to land next to. - FEAT/RUNS: a run detail screen. The API answers everything — params, per-node status with logs and tracebacks, artifacts, metrics, and `/runs/series/compare` in the chart widget's own `series` shape — but nothing in the dashboard reads it yet, so a run is inspected over HTTP. Comparing curves is a widget binding once someone builds the page around it. - FEAT/RUNS: a thin client CLI (`fluksio run/runs/sweep/worker`) over the same API. The engine being resident is what makes runs cheap; a CLI is ergonomics on top, and `curl` covers it until someone is running sweeps daily. - FEAT/RUNS: the step on a run's series is the count of emissions on that message, so a node yielding every tenth training step records steps 0, 1, 2 rather than 0, 10, 20 — a faithful x-axis of its own emissions, not of the loop inside it. If a real step number ever matters, a `record`-typed streaming port carrying its own `step` is the shape to read it from; the column is already there. diff --git a/frontend/src/components/Flow/EndpointNode.tsx b/frontend/src/components/Flow/EndpointNode.tsx index 81d945e..fa4461c 100644 --- a/frontend/src/components/Flow/EndpointNode.tsx +++ b/frontend/src/components/Flow/EndpointNode.tsx @@ -1,5 +1,5 @@ import { Handle, type NodeProps, Position } from "@xyflow/react" -import { LayoutDashboard, Workflow } from "lucide-react" +import { LayoutDashboard, LogIn, LogOut, Workflow } from "lucide-react" import { memo } from "react" import { useIsMobile } from "@/hooks/useMobile" @@ -9,6 +9,8 @@ import type { EndpointNodeData } from "./endpoints" const KIND_ICONS = { dashboard: LayoutDashboard, flow: Workflow, + input: LogIn, + output: LogOut, } as const /** diff --git a/frontend/src/components/Flow/FlowBoundary.tsx b/frontend/src/components/Flow/FlowBoundary.tsx new file mode 100644 index 0000000..08fb1ea --- /dev/null +++ b/frontend/src/components/Flow/FlowBoundary.tsx @@ -0,0 +1,341 @@ +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 { qualify } from "./deriveEdges" +import { useLiveValue } from "./liveStore" +import { DTYPES } from "./NodePanel" +import { 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 + 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 + } +} + +/** The same value as something a text field can hold. */ +export function asText(value: unknown): string { + if (value === null || value === undefined) return "" + if (typeof value === "object") return JSON.stringify(value) + return String(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 ( +
+ {batch + ? "Runs when a run asks it to, from its inputs to its outputs. Nothing here is subscribed or scheduled." + : "Runs continuously: subscriptions, schedules and webhooks are live."} +
+ ++ Messages that arrive from outside — a dashboard control, a run, the + API — and the value the flow starts from. +
+ ) : null} + + {inputs.map((declared, index) => ( + // Keyed by position: renaming an input must not remount its row. ++ {provided.length === 0 + ? "Nothing is produced here yet, so a run has nothing to report." + : "What a run reports when it finishes. Everything else it computed goes with it."} +
+ {provided.map((name) => ( + + ))} +