From 58b40a90157d5c12751b1343d9ef70bf77209646 Mon Sep 17 00:00:00 2001 From: shine1i Date: Fri, 6 Feb 2026 13:18:18 +0100 Subject: [PATCH] inline sizes, aux nodes resize, and auto layout keep nodes close logic --- docs/canvas-lab-architecture.md | 22 ++- .../features/canvas-lab/canvas-lab-page.tsx | 34 +++- .../canvas-lab/components/canvas-aux-node.tsx | 47 ++++- .../canvas-lab/components/canvas-node.tsx | 13 +- .../components/inline/inline-expression.tsx | 53 ++--- .../components/inline/inline-field.tsx | 23 +++ .../components/inline/inline-llm.tsx | 63 +++--- .../components/inline/inline-model.tsx | 97 +++++---- .../components/inline/inline-sampler.tsx | 157 ++++++++------- .../src/features/canvas-lab/constants.ts | 4 + .../canvas-lab/stores/canvas-lab-helpers.ts | 2 + .../features/canvas-lab/stores/canvas-lab.ts | 48 +++++ .../utils/graph/derive-display-graph.ts | 184 +++++++++++++++--- .../canvas-lab/utils/import/importer.ts | 4 +- .../features/canvas-lab/utils/import/ui.ts | 27 ++- .../canvas-lab/utils/payload/build-payload.ts | 18 ++ 16 files changed, 569 insertions(+), 227 deletions(-) create mode 100644 studio/frontend/src/features/canvas-lab/components/inline/inline-field.tsx create mode 100644 studio/frontend/src/features/canvas-lab/constants.ts diff --git a/docs/canvas-lab-architecture.md b/docs/canvas-lab-architecture.md index 75749d6b1f..f5acbd1ab5 100644 --- a/docs/canvas-lab-architecture.md +++ b/docs/canvas-lab-architecture.md @@ -34,7 +34,8 @@ Owns: - source-of-truth state (`configs`, `nodes`, `edges`, `processors`) - mutation entrypoints (`updateConfig`, `onConnect`, `onNodesChange`, etc) - selection/dialog state (`selectConfig`, `openConfig`) -- aux node position persistence +- aux node position persistence (`auxNodePositions`) +- aux node size persistence (`auxNodeSizes`) Helper module: `/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/stores/canvas-lab-helpers.ts` @@ -77,7 +78,7 @@ Owns: - dialog router per block type Notes: -- registry passes `modelConfigAliases` into `LlmDialog` +- registry receives dialog option lists from page and forwards to block dialogs - avoids dialog -> store dependency cycle Do not place here: @@ -93,6 +94,7 @@ Files: Owns: - contract mapping between UI state and backend payload - edge inference fallback when import payload has no `ui.edges` +- node width persistence via `ui.nodes[].width` Do not place here: - ReactFlow render logic @@ -135,6 +137,13 @@ Dialog flow: - node click -> `selectConfig` (no forced modal) - node `Details` button -> `openConfig` +Node sizing: +- default builder/aux node width is `400px` +- users can resize builder + aux nodes +- resized width is kept in canvas state and round-tripped through import/export +- sizing constants live in: +`/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/constants.ts` + ## 4) UI Mode Policy (Inline vs Dialog) File: @@ -185,6 +194,9 @@ Rules: Aux nodes: - are UI projections, not new payload schema entities - have independent drag positions persisted in Zustand `auxNodePositions` +- have independent sizes persisted in Zustand `auxNodeSizes` +- are resizable (same hidden-control resize UX as builder nodes) +- are re-anchored near parent nodes after auto-layout/direction change ## 7) Connect Rules (single source of truth) @@ -211,9 +223,9 @@ Connect side-effects: Current safe flow: - store state -> page (`configs`) -- page derives `modelConfigAliases` -- page passes aliases -> `ConfigDialog` -- dialog passes aliases -> registry -> `LlmDialog` +- page derives dialog option lists (`modelConfigAliases`, `modelProviderOptions`, `datetimeOptions`) +- page passes these options -> `ConfigDialog` +- dialog passes options -> registry -> block dialogs (`LlmDialog`, `ModelConfigDialog`, `TimedeltaDialog`) No dialog component should import store directly. diff --git a/studio/frontend/src/features/canvas-lab/canvas-lab-page.tsx b/studio/frontend/src/features/canvas-lab/canvas-lab-page.tsx index cd2b5dc2dd..2449f4831a 100644 --- a/studio/frontend/src/features/canvas-lab/canvas-lab-page.tsx +++ b/studio/frontend/src/features/canvas-lab/canvas-lab-page.tsx @@ -130,6 +130,7 @@ export function CanvasLabPage(): ReactElement { nodes, edges, auxNodePositions, + auxNodeSizes, configs, processors, sheetView, @@ -154,12 +155,15 @@ export function CanvasLabPage(): ReactElement { setLayoutDirection, applyLayout, setAuxNodePosition, + setAuxNodeSize, syncAuxNodePositions, + syncAuxNodeSizes, } = useCanvasLabStore( useShallow((state) => ({ nodes: state.nodes, edges: state.edges, auxNodePositions: state.auxNodePositions, + auxNodeSizes: state.auxNodeSizes, configs: state.configs, processors: state.processors, sheetView: state.sheetView, @@ -184,7 +188,9 @@ export function CanvasLabPage(): ReactElement { setLayoutDirection: state.setLayoutDirection, applyLayout: state.applyLayout, setAuxNodePosition: state.setAuxNodePosition, + setAuxNodeSize: state.setAuxNodeSize, syncAuxNodePositions: state.syncAuxNodePositions, + syncAuxNodeSizes: state.syncAuxNodeSizes, })), ); const [sheetContainer, setSheetContainer] = useState( @@ -215,8 +221,9 @@ export function CanvasLabPage(): ReactElement { configs, layoutDirection, auxNodePositions, + auxNodeSizes, }); - }, [auxNodePositions, configs, edges, layoutDirection, nodes]); + }, [auxNodePositions, auxNodeSizes, configs, edges, layoutDirection, nodes]); const displayNodeIds = useMemo( () => displayGraph.nodes.map((node) => node.id), [displayGraph.nodes], @@ -224,6 +231,9 @@ export function CanvasLabPage(): ReactElement { useEffect(() => { syncAuxNodePositions(displayGraph.auxNodeIds, displayGraph.auxDefaults); }, [displayGraph.auxDefaults, displayGraph.auxNodeIds, syncAuxNodePositions]); + useEffect(() => { + syncAuxNodeSizes(displayGraph.auxNodeIds); + }, [displayGraph.auxNodeIds, syncAuxNodeSizes]); const handleNodeClick = useCallback( (_: unknown, node: Node) => { @@ -240,13 +250,25 @@ export function CanvasLabPage(): ReactElement { for (const change of changes) { if ( !("id" in change) || - change.type !== "position" || - !change.id.startsWith("aux-") || - !change.position + !change.id.startsWith("aux-") ) { continue; } - setAuxNodePosition(change.id, change.position); + if (change.type === "position" && change.position) { + setAuxNodePosition(change.id, change.position); + continue; + } + if ( + change.type === "dimensions" && + change.dimensions && + change.dimensions.width > 0 && + change.dimensions.height > 0 + ) { + setAuxNodeSize(change.id, { + width: change.dimensions.width, + height: change.dimensions.height, + }); + } } const next = changes.filter( (change): change is NodeChange => @@ -256,7 +278,7 @@ export function CanvasLabPage(): ReactElement { onNodesChange(next); } }, - [baseNodeIds, onNodesChange, setAuxNodePosition], + [baseNodeIds, onNodesChange, setAuxNodePosition, setAuxNodeSize], ); const handleEdgesChange = useCallback( diff --git a/studio/frontend/src/features/canvas-lab/components/canvas-aux-node.tsx b/studio/frontend/src/features/canvas-lab/components/canvas-aux-node.tsx index cee539b989..2c531a432c 100644 --- a/studio/frontend/src/features/canvas-lab/components/canvas-aux-node.tsx +++ b/studio/frontend/src/features/canvas-lab/components/canvas-aux-node.tsx @@ -1,8 +1,15 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; +import { + Handle, + NodeResizer, + Position, + type Node, + type NodeProps, +} from "@xyflow/react"; import { memo, type ReactElement } from "react"; +import { MAX_NODE_WIDTH, MIN_NODE_WIDTH } from "../constants"; import { useCanvasLabStore } from "../stores/canvas-lab"; import type { LayoutDirection, LlmConfig, Score, ScoreOption } from "../types"; import { HANDLE_IDS } from "../utils/handles"; @@ -49,7 +56,9 @@ function updateOptionAt( ); } -function AuxNodeBase({ data }: NodeProps): ReactElement | null { +function AuxNodeBase({ + data, +}: NodeProps): ReactElement | null { const config = useCanvasLabStore((state) => state.configs[data.llmId]); const updateConfig = useCanvasLabStore((state) => state.updateConfig); @@ -63,13 +72,25 @@ function AuxNodeBase({ data }: NodeProps): ReactElement | nul if (data.kind === "llm-prompt-input") { const value = data.field === "prompt" ? config.prompt : config.system_prompt; return ( - + + {data.title}