10 KiB
Canvas Lab Architecture (Current)
Root:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab
This doc explains current architecture, how nodes map to payload/import, and how to add new blocks safely.
1) High-level flow
- UI renders canvas + dialogs in:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/canvas-lab-page.tsx - Add-block sheet uses registry metadata to create config objects:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/components/block-sheet.tsx/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/blocks/registry.tsx - Zustand store owns nodes/edges/configs and all mutation logic:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/stores/canvas-lab.ts - Graph connection logic updates references + semantic edges:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/graph.ts - Export (preview/copy) converts in-memory graph/config to API payload:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/payload.ts - Import reconstructs configs, nodes, edges from JSON:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/import/importer.ts
2) Core types (single source of truth)
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/types/index.ts
NodeConfig is the main union the whole feature uses:
export type NodeConfig =
| SamplerConfig
| LlmConfig
| ExpressionConfig
| ModelProviderConfig
| ModelConfig;
Canvas node UI data (CanvasNodeData) is derived from config via nodeDataFromConfig.
3) Entrypoint wiring
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/canvas-lab-page.tsx
Key wiring:
const NODE_TYPES: NodeTypes = { builder: CanvasNode };
const EDGE_TYPES: EdgeTypes = { canvas: CanvasEdge, semantic: CanvasEdge };
CanvasLabPage pulls actions/state from store and passes add handlers into BlockSheet:
<BlockSheet
onAddSampler={addSamplerNode}
onAddLlm={addLlmNode}
onAddModelProvider={addModelProviderNode}
onAddModelConfig={addModelConfigNode}
onAddExpression={addExpressionNode}
/>
Preview/copy route through buildCanvasPayload, import route through importCanvasPayload.
4) Registry-driven block system
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/blocks/registry.tsx
Registry defines each block in one place:
- sheet title/icon/description
- config factory (
createConfig) - config dialog (
renderDialog)
Example (model blocks):
{
kind: "llm",
type: "model_provider",
createConfig: (id, existing) => makeModelProviderConfig(id, existing),
renderDialog: ({ config, onUpdate }) =>
config.kind === "model_provider" ? (
<ModelProviderDialog config={config} onUpdate={(patch) => onUpdate(config.id, patch)} />
) : null,
}
Important: getBlockDefinitionForConfig must map every new config.kind, else dialog won't render.
5) Config factories + node label mapping
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/index.ts
Responsibilities:
- create default config objects (
makeSamplerConfig,makeLlmConfig,makeModelProviderConfig,makeModelConfig,makeExpressionConfig) - map
NodeConfig -> CanvasNodeDatavianodeDataFromConfig - sampler set includes
category,subcategory,uniform,gaussian,bernoulli,datetime,timedelta,uuid,person,person_from_faker
Example mapping:
if (config.kind === "model_provider") {
return {
title: "Model Provider",
kind: "model_provider",
subtype: config.provider_type || "Provider",
blockType: "model_provider",
name: config.name,
layoutDirection,
};
}
This is what controls visible node title/subtitle in the canvas.
6) Store responsibilities
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/stores/canvas-lab.ts
Store owns:
- graph state (
nodes,edges) - config map (
configs[id]) - add/update/remove/connect operations
- layout direction + apply layout
Add-node pattern (all block types follow same shape):
const definition = getBlockDefinition("llm", "model_config");
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
When model config provider field changes, store auto-syncs semantic edge to matching provider name.
7) Edge semantics + connection behavior
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/graph.ts
Semantic edge classifier:
function isSemanticEdge(source: NodeConfig, target: NodeConfig): boolean {
if (source.kind === "model_provider" && target.kind === "model_config") return true;
return source.kind === "model_config" && target.kind === "llm";
}
Handle lanes:
- data edges use
data-out -> data-in(right -> left) - semantic edges use
semantic-out -> semantic-in(bottom -> top) - semantic lane only used for
model_provider -> model_config -> llm
Connection side effects:
model_provider -> model_config: setmodel_config.provider = source.namemodel_config -> llm: setllm.model_alias = source.namedatetime -> timedelta: settimedelta.reference_column_name = source.name- regular data edges into LLM/expression append
{{ source_name }}refs
Edge rendering (dotted semantic edges):
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/components/canvas-edge.tsx
const nextStyle = type === "semantic"
? { ...style, strokeDasharray: "4 4" }
: style;
8) Rename/remove propagation
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/stores/canvas-lab-helpers.ts
Centralized consistency updates:
- rename updates:
- Jinja refs in
llm.prompt/system_prompt/output_format - expression
expr - subcategory parent
model_config.providerllm.model_alias
- Jinja refs in
- removal clears same references
This keeps graph fields stable when upstream nodes renamed/deleted.
9) Payload building (node graph -> API)
File:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/payload.ts
buildCanvasPayload(configs, nodes, edges) outputs:
{
recipe: {
model_providers: [...],
model_configs: [...],
columns: [...],
processors: [],
},
run: { rows: 5, preview: true, output_formats: ["jsonl"] },
ui: { nodes: [...], edges: [...] }
}
How relation is enforced:
- collect
model_aliasvalues used by LLM columns - ensure each alias exists in
recipe.model_configs - validate
model_config.providerpoints to existing provider - validate
timedelta.reference_column_namepoints to a datetime sampler - require endpoint/provider_type only for providers that are actually referenced
- category sampler supports typed
conditional_paramsin payload output
This is why unused provider/config blocks can exist without blocking preview.
10) Import pipeline (API -> node graph)
Entry file:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/import/importer.ts
Order of reconstruction:
- parse
recipe.model_providers->ModelProviderConfig - parse
recipe.model_configs->ModelConfig - parse
recipe.columns-> sampler/llm/expression - build nodes with positions
- build edges
Edge inference file:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/import/edges.ts
If UI edges missing, infer edges from fields:
subcategory_parent(canvas edge)model_config.providerllm.model_alias- infer data edge from
timedelta.reference_column_name
11) Dialog routing and edit UIs
Config dialog shell:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/dialogs/config-dialog.tsx
It calls:
renderBlockDialog(config, categoryOptions, onUpdate)
Model dialogs:
- provider:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/dialogs/models/model-provider-dialog.tsx - model config:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/dialogs/models/model-config-dialog.tsx
ModelConfigDialog and LlmDialog use shadcn Combobox fed from store configs:
- model config
providersuggests model-provider node names - llm
model_aliassuggests model-config aliases - timedelta dialog suggests datetime columns for
reference_column_name
12) How to add a new block (checklist)
Minimal path for a new block type:
- Add/extend type in:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/types/index.ts - Add default factory + node label mapping in:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/index.ts - Add block definition in:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/blocks/registry.tsx - Add dialog component and route it via
renderDialogin registry. - Add store add-action if block should be special-cased from sheet:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/stores/canvas-lab.ts - Add payload serialization/validation in:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/payload.ts - Add import parsing + inferred edges in:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/import/parsers.ts/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/import/importer.ts/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/import/edges.ts - If connection has semantic meaning, extend:
/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/utils/graph.ts
13) Practical mental model
NodeConfigis source-of-truth business state.CanvasNodeDatais derived display state.- registry = block metadata + factories + dialog routing.
- store = mutation orchestration.
- graph utils = connection semantics.
- payload/import utils = external contract boundary.
If one piece changes, keep all six in sync.