diff --git a/frontend/src/components/Flow/EdgeInspector.tsx b/frontend/src/components/Flow/EdgeInspector.tsx index 51b5105..d45f118 100644 --- a/frontend/src/components/Flow/EdgeInspector.tsx +++ b/frontend/src/components/Flow/EdgeInspector.tsx @@ -1,8 +1,11 @@ -import { ArrowRight } from "lucide-react" +import { ArrowRight, Trash2 } from "lucide-react" +import { motion } from "motion/react" +import { useEffect, useRef, useState } from "react" import { Button } from "@/components/ui/button" import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover" import { ScrollArea } from "@/components/ui/scroll-area" +import { cn } from "@/lib/utils" import { displayName } from "./deriveEdges" import { useLiveValue } from "./liveStore" @@ -15,6 +18,60 @@ function relativeTime(ts: number | null | undefined): string { return `${Math.round(seconds / 3600)}h ago` } +/** + * A value short enough to share the summary row, or `null` for the objects and + * arrays that need the payload view instead. + */ +function formatScalar(value: unknown): string | null { + // Three decimals, trailing zeros dropped: enough precision to be useful, never + // wide enough to wrap the row. + if (typeof value === "number") return String(Number(value.toFixed(3))) + if (typeof value === "string") return value + if (typeof value === "boolean" || value === null) return String(value) + return null +} + +/** + * Text that scrolls its own overflow into view and back, so a long name stays + * readable without widening the popover. Still text at rest when it fits. + */ +function Marquee({ text, className }: { text: string; className?: string }) { + const ref = useRef(null) + const [overflow, setOverflow] = useState(0) + + // biome-ignore lint/correctness/useExhaustiveDependencies: a new string is what changes the measurement. + useEffect(() => { + const el = ref.current + if (el) setOverflow(Math.max(0, el.scrollWidth - el.clientWidth)) + }, [text]) + + return ( + + + {text} + + + ) +} + export type InspectedEdge = { message: string /** Node titles, so the popover names the two ends in the user's own words. */ @@ -41,6 +98,8 @@ export function EdgeInspector({ const live = useLiveValue(edge?.message) if (!edge) return null + const scalar = live === undefined ? null : formatScalar(live.value) + return ( !open && onClose()}> -

- - {edge.from} - +

+ - - {edge.to} + + + + +
+ + {scalar === null ? null : ( + + {scalar} + + )} + + {relativeTime(live?.ts)} -

-

- {displayName(flow, edge.message)} -

+ {live === undefined ? (

Nothing has come through yet. Run the flow to see a value here.

- ) : ( - <> - -
-                {JSON.stringify(live.value, null, 2)}
-              
-
-

- {relativeTime(live.ts)} -

- - )} - - + ) : scalar === null ? ( + +
+              {JSON.stringify(live.value, null, 2)}
+            
+
+ ) : null} ) diff --git a/frontend/src/components/Flow/FlowDock.tsx b/frontend/src/components/Flow/FlowDock.tsx index 2d0bd6c..acbe2af 100644 --- a/frontend/src/components/Flow/FlowDock.tsx +++ b/frontend/src/components/Flow/FlowDock.tsx @@ -3,7 +3,6 @@ import { AlertCircle, Loader2, Maximize2, - Pencil, Play, Plus, ZoomIn, @@ -34,14 +33,12 @@ export function FlowDock({ issues, running, onAddNode, - onEditFlow, onRun, onFocusNode, }: { issues: ValidationIssue[] running: boolean onAddNode: () => void - onEditFlow: () => void onRun: () => void onFocusNode: (nodeId: string) => void }) { @@ -71,22 +68,6 @@ export function FlowDock({ Add a node (⌘K) - - - - - Flow settings - - + + + + ) } @@ -634,14 +840,7 @@ function mergeDragged( /** Seed xyflow's own node state once; it owns positions while you drag. */ function useUnpositionedNodes(definitions: NodeDef_Input[]) { - return useNodesState( - definitions.map((node) => ({ - id: node.id, - type: "flow", - position: { x: node.position?.x ?? 0, y: node.position?.y ?? 0 }, - data: {}, - })), - ) + return useNodesState(toCanvasNodes(definitions)) } export function FlowEditor({ flowName }: { flowName: string }) { diff --git a/frontend/src/components/Flow/FlowTabs.tsx b/frontend/src/components/Flow/FlowTabs.tsx index 046c981..baf39e8 100644 --- a/frontend/src/components/Flow/FlowTabs.tsx +++ b/frontend/src/components/Flow/FlowTabs.tsx @@ -1,7 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useMutation, useQueryClient } from "@tanstack/react-query" import { Link, useNavigate } from "@tanstack/react-router" -import { Check, Loader2, Plus, WifiOff } from "lucide-react" +import { Check, Loader2, Pencil, Plus, WifiOff } from "lucide-react" import { motion } from "motion/react" import { useState } from "react" import { useForm } from "react-hook-form" @@ -114,10 +114,12 @@ export function FlowTabs({ flows, active, saving, + onEditFlow, }: { flows: FlowSummary[] active: string saving: boolean + onEditFlow: () => void }) { const [dialogOpen, setDialogOpen] = useState(false) const connected = useLiveConnection() @@ -129,7 +131,7 @@ export function FlowTabs({ initial="hidden" animate="visible" transition={transitions.emphasized} - className="pointer-events-auto absolute left-1/2 top-4 z-10 flex max-w-[calc(100vw-2rem)] -translate-x-1/2 items-center gap-1 rounded-full border border-border bg-card/80 px-1.5 py-1 shadow-e2 backdrop-blur-md" + className="pointer-events-auto absolute left-1/2 top-4 z-10 flex max-w-[calc(100%-2rem)] -translate-x-1/2 items-center gap-1 rounded-full border border-border bg-card/80 px-1.5 py-1 shadow-e2 backdrop-blur-md" > @@ -166,6 +168,22 @@ export function FlowTabs({ New flow + + + + + Flow settings + + @@ -182,7 +200,7 @@ export function FlowTabs({ {!connected ? "Reconnecting to the engine" : saving - ? "Saving" + ? "Saving" : "All changes saved"} diff --git a/frontend/src/components/Flow/LiveEdge.tsx b/frontend/src/components/Flow/LiveEdge.tsx index 45d3bf3..a236c9f 100644 --- a/frontend/src/components/Flow/LiveEdge.tsx +++ b/frontend/src/components/Flow/LiveEdge.tsx @@ -72,7 +72,7 @@ function LiveEdgeComponent({ style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`, }} - className="pointer-events-none absolute max-w-[140px] truncate rounded-full border border-border bg-card/80 px-2 py-0.5 font-mono text-xs text-muted-foreground backdrop-blur-md" + className="pointer-events-none absolute max-w-[140px] truncate rounded-full border border-border bg-card px-2 py-0.5 font-mono text-xs text-foreground" > {formatValue(live.value)} diff --git a/frontend/src/components/Flow/NodePanel.tsx b/frontend/src/components/Flow/NodePanel.tsx index 38c2896..5901a3f 100644 --- a/frontend/src/components/Flow/NodePanel.tsx +++ b/frontend/src/components/Flow/NodePanel.tsx @@ -44,14 +44,20 @@ function MessageNameInput({ placeholder, autoFocus, onChange, + onRenamed, }: { value: string suggestions: string[] placeholder: string autoFocus: boolean onChange: (next: string) => void + /** The name as it was before this edit, once the field is done with. */ + onRenamed?: (previous: string, next: string) => void }) { const [open, setOpen] = useState(false) + // Every keystroke commits, so a rename is only a rename once the user is + // finished with the field. + const before = useRef(value) const matches = suggestions.filter( (name) => name !== value && name.toLowerCase().includes(value.toLowerCase()), @@ -68,8 +74,15 @@ function MessageNameInput({ // A port added by hand is meant to be named right away. autoFocus={autoFocus} className="h-8 flex-1 font-mono text-sm" - onFocus={() => setOpen(true)} - onBlur={() => setOpen(false)} + onFocus={() => { + before.current = value + setOpen(true) + }} + onBlur={() => { + setOpen(false) + if (before.current !== value) onRenamed?.(before.current, value) + before.current = value + }} onChange={(event) => { onChange(event.target.value) setOpen(true) @@ -122,6 +135,7 @@ function PortList({ emptyHint, suggestions, onChange, + onRenamed, }: { title: string specs: MessageSpec[] @@ -129,6 +143,7 @@ function PortList({ emptyHint: string suggestions: string[] onChange: (next: MessageSpec[]) => void + onRenamed?: (previous: string, next: string) => void }) { // The port just added, so its name field can take focus. const [freshIndex, setFreshIndex] = useState(null) @@ -169,6 +184,7 @@ function PortList({ placeholder={`name in ${flow}`} autoFocus={index === freshIndex} onChange={(name) => update(index, { name, port: "" })} + onRenamed={onRenamed} />