chore: rename Canvas Lab components and utilities

This commit is contained in:
Shine1i 2026-02-12 00:39:08 +01:00
commit 93f45ffd07
76 changed files with 352 additions and 390 deletions

View file

@ -26,4 +26,4 @@ test/
*.njsproj
*.sln
*.sw?
/src/features/canvas-lab/AGENTS.md
/src/features/recipe-studio/AGENTS.md

View file

@ -1,6 +1,6 @@
import { createRouter } from "@tanstack/react-router";
import { Route as rootRoute } from "./routes/__root";
import { Route as canvasLabRoute } from "./routes/canvas-lab";
import { Route as dataRecipesNewRoute } from "./routes/data-recipes-new";
import { Route as chatRoute } from "./routes/chat";
import { Route as exportRoute } from "./routes/export";
import { Route as gridTestRoute } from "./routes/grid-test";
@ -19,7 +19,7 @@ const routeTree = rootRoute.addChildren([
studioRoute,
chatRoute,
exportRoute,
canvasLabRoute,
dataRecipesNewRoute,
]);
export const router = createRouter({ routeTree });

View file

@ -2,14 +2,14 @@ import { createRoute } from "@tanstack/react-router";
import { lazy } from "react";
import { Route as rootRoute } from "./__root";
const CanvasLabPage = lazy(() =>
import("@/features/canvas-lab").then((m) => ({
default: m.CanvasLabPage,
const RecipeStudioPage = lazy(() =>
import("@/features/recipe-studio").then((m) => ({
default: m.RecipeStudioPage,
})),
);
export const Route = createRoute({
getParentRoute: () => rootRoute,
path: "/canvas-lab",
component: CanvasLabPage,
path: "/data-recipes/new",
component: RecipeStudioPage,
});

View file

@ -1 +0,0 @@
export { CanvasLabPage } from "./canvas-lab-page";

View file

@ -1,4 +0,0 @@
export {
applyCanvasConnection,
isValidCanvasConnection,
} from "./graph/canvas-connection";

View file

@ -1,2 +0,0 @@
export { importCanvasPayload } from "./importer";
export type { CanvasSnapshot, ImportResult } from "./types";

View file

@ -1,2 +0,0 @@
export { buildCanvasPayload } from "./build-payload";
export type { CanvasPayload, CanvasPayloadResult } from "./types";

View file

@ -1,6 +1,6 @@
const DEFAULT_BASE = "/preview-api";
const DEFAULT_BASE = "";
export const CANVAS_LAB_API_BASE =
export const DATA_DESIGNER_API_BASE =
import.meta.env.VITE_DATA_DESIGNER_API ?? DEFAULT_BASE;
type PreviewResponse = {
@ -8,10 +8,10 @@ type PreviewResponse = {
processorArtifacts?: Record<string, unknown>;
};
export async function previewCanvas(
export async function previewRecipe(
payload: unknown,
): Promise<PreviewResponse> {
const response = await fetch(`${CANVAS_LAB_API_BASE}/preview`, {
const response = await fetch(`${DATA_DESIGNER_API_BASE}/preview`, {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -18,7 +18,7 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type { ReactElement } from "react";
import { CANVAS_FLOATING_ICON_BUTTON_CLASS } from "./floating-icon-button-class";
import { RECIPE_FLOATING_ICON_BUTTON_CLASS } from "./recipe-floating-icon-button-class";
import type { LlmType, SamplerType } from "../types";
import { BLOCK_GROUPS, getBlocksForKind } from "../blocks/registry";
@ -34,8 +34,8 @@ type RootGroup = {
type BlockSheetProps = {
container: HTMLDivElement | null;
view: SheetView;
onViewChange: (view: SheetView) => void;
sheetView: SheetView;
onViewChange: (sheetView: SheetView) => void;
onAddSampler: (type: SamplerType) => void;
onAddLlm: (type: LlmType) => void;
onAddModelProvider: () => void;
@ -47,17 +47,17 @@ type BlockSheetProps = {
onImport: () => void;
};
function getSheetTitle(view: SheetView): string {
if (view === "root") {
function getSheetTitle(sheetView: SheetView): string {
if (sheetView === "root") {
return "Add a block";
}
if (view === "sampler") {
if (sheetView === "sampler") {
return "Sampler blocks";
}
if (view === "expression") {
if (sheetView === "expression") {
return "Expression blocks";
}
if (view === "processor") {
if (sheetView === "processor") {
return "Processor blocks";
}
return "LLM blocks";
@ -98,7 +98,7 @@ function BlockSheetButton({
<button
type="button"
onClick={onClick}
className={`flex w-full items-center gap-3 bg-white px-3 py-3 text-left transition border-l-2 ${
className={`flex w-full items-center gap-3 bg-white px-3 py-3 text-left transition border-l-2 ${
isActive
? "border-emerald-500"
: "border-transparent hover:border-border/60"
@ -121,7 +121,7 @@ function BlockSheetButton({
export function BlockSheet({
container,
view,
sheetView,
onViewChange,
onAddSampler,
onAddLlm,
@ -133,7 +133,7 @@ export function BlockSheet({
onCopy,
onImport,
}: BlockSheetProps): ReactElement {
const title = getSheetTitle(view);
const sheetTitle = getSheetTitle(sheetView);
return (
<div className="flex flex-col items-end gap-2">
<Sheet
@ -146,7 +146,7 @@ export function BlockSheet({
<SheetTrigger asChild={true}>
<Button
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
variant="ghost"
>
<HugeiconsIcon
@ -165,7 +165,7 @@ export function BlockSheet({
>
<SheetHeader className="border-b border-border/60 px-6 py-5">
<div className="flex items-center gap-2">
{view !== "root" && (
{sheetView !== "root" && (
<Button
type="button"
variant="ghost"
@ -175,12 +175,12 @@ export function BlockSheet({
<HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
</Button>
)}
<SheetTitle>{title}</SheetTitle>
<SheetTitle>{sheetTitle}</SheetTitle>
</div>
</SheetHeader>
<div className=" py-4">
<div className="mt-4 flex flex-col gap-2">
{view === "root" &&
{sheetView === "root" &&
ROOT_GROUPS.map((item, index) => (
<BlockSheetButton
key={item.kind}
@ -191,7 +191,7 @@ export function BlockSheet({
onClick={() => onViewChange(item.kind)}
/>
))}
{view === "processor" && (
{sheetView === "processor" && (
<BlockSheetButton
icon={CodeIcon}
title="Schema Transform"
@ -200,9 +200,9 @@ export function BlockSheet({
onClick={onOpenProcessors}
/>
)}
{view !== "root" &&
view !== "processor" &&
getBlocksForKind(VIEW_KIND[view] ?? "sampler").map(
{sheetView !== "root" &&
sheetView !== "processor" &&
getBlocksForKind(VIEW_KIND[sheetView] ?? "sampler").map(
(item, index) => (
<BlockSheetButton
key={item.type}
@ -236,7 +236,7 @@ export function BlockSheet({
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
onClick={onImport}
>
<HugeiconsIcon
@ -248,7 +248,7 @@ export function BlockSheet({
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
onClick={onCopy}
>
<HugeiconsIcon

View file

@ -8,7 +8,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import type { ReactElement } from "react";
import { useCanvasLabStore } from "../../stores/canvas-lab";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import type { ExpressionConfig, ExpressionDtype } from "../../types";
import { getAvailableVariables } from "../../utils/variables";
import { InlineField } from "./inline-field";
@ -24,7 +24,7 @@ export function InlineExpression({
config,
onUpdate,
}: InlineExpressionProps): ReactElement {
const configs = useCanvasLabStore((state) => state.configs);
const configs = useRecipeStudioStore((state) => state.configs);
const vars = getAvailableVariables(configs, config.id);
return (

View file

@ -14,7 +14,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { type ReactElement, useEffect, useMemo, useRef, useState } from "react";
import { useCanvasLabStore } from "../../stores/canvas-lab";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import type { LlmConfig } from "../../types";
import { InlineField } from "./inline-field";
@ -44,7 +44,7 @@ const CODE_LANG_OPTIONS = [
export function InlineLlm({ config, onUpdate }: InlineLlmProps): ReactElement {
const isCode = config.llm_type === "code";
const configs = useCanvasLabStore((state) => state.configs);
const configs = useRecipeStudioStore((state) => state.configs);
const modelConfigAliases = useMemo(
() =>
Object.values(configs)

View file

@ -1,2 +1,2 @@
export const CANVAS_FLOATING_ICON_BUTTON_CLASS =
export const RECIPE_FLOATING_ICON_BUTTON_CLASS =
"corner-squircle group h-11 w-11 rounded-xl border border-border/60 bg-transparent p-0 text-muted-foreground hover:bg-transparent hover:text-primary hover:border-primary/60";

View file

@ -11,7 +11,7 @@ import {
} 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 { useRecipeStudioStore } from "../stores/recipe-studio";
import type { LayoutDirection, LlmConfig, Score, ScoreOption } from "../types";
import { HANDLE_IDS } from "../utils/handles";
import { getAvailableVariables } from "../utils/variables";
@ -34,8 +34,8 @@ type JudgeScoreNodeData = {
layoutDirection: LayoutDirection;
};
export type CanvasAuxNodeData = PromptInputNodeData | JudgeScoreNodeData;
export type CanvasAuxNodeType = Node<CanvasAuxNodeData, "aux">;
export type RecipeGraphAuxNodeData = PromptInputNodeData | JudgeScoreNodeData;
export type RecipeGraphAuxNodeType = Node<RecipeGraphAuxNodeData, "aux">;
function updateScoreAt(
config: LlmConfig,
@ -59,7 +59,7 @@ function updateOptionAt(
}
function AuxVariableBadges({ llmId }: { llmId: string }): ReactElement | null {
const configs = useCanvasLabStore((state) => state.configs);
const configs = useRecipeStudioStore((state) => state.configs);
const vars = getAvailableVariables(configs, llmId);
if (vars.length === 0) return null;
return (
@ -82,9 +82,9 @@ function AuxVariableBadges({ llmId }: { llmId: string }): ReactElement | null {
function AuxNodeBase({
data,
}: NodeProps<CanvasAuxNodeType>): ReactElement | null {
const config = useCanvasLabStore((state) => state.configs[data.llmId]);
const updateConfig = useCanvasLabStore((state) => state.updateConfig);
}: NodeProps<RecipeGraphAuxNodeType>): ReactElement | null {
const config = useRecipeStudioStore((state) => state.configs[data.llmId]);
const updateConfig = useRecipeStudioStore((state) => state.updateConfig);
if (!(config && config.kind === "llm")) {
return null;
@ -255,4 +255,4 @@ function AuxNodeBase({
);
}
export const CanvasAuxNode = memo(AuxNodeBase);
export const RecipeGraphAuxNode = memo(AuxNodeBase);

View file

@ -27,9 +27,9 @@ import {
} from "@xyflow/react";
import { memo, type ReactElement, useEffect } from "react";
import { MAX_NODE_WIDTH, MIN_NODE_WIDTH } from "../constants";
import { useCanvasLabStore } from "../stores/canvas-lab";
import { useRecipeStudioStore } from "../stores/recipe-studio";
import type {
CanvasNode as CanvasNodeType,
RecipeNode as RecipeGraphNodeType,
LlmType,
NodeConfig,
SamplerType,
@ -90,8 +90,8 @@ const LLM_ICONS: Record<LlmType, IconType> = {
};
function resolveNodeIcon(
kind: CanvasNodeType["data"]["kind"],
blockType: CanvasNodeType["data"]["blockType"],
kind: RecipeGraphNodeType["data"]["kind"],
blockType: RecipeGraphNodeType["data"]["blockType"],
): IconType {
if (kind === "sampler" && blockType in SAMPLER_ICONS) {
return SAMPLER_ICONS[blockType as SamplerType];
@ -274,17 +274,17 @@ function LlmInputHandles({ items, isTopBottom }: LlmInputHandlesProps): ReactEle
);
}
function CanvasNodeBase({
function RecipeGraphNodeBase({
id,
data,
selected,
}: NodeProps<CanvasNodeType>): ReactElement {
}: NodeProps<RecipeGraphNodeType>): ReactElement {
const meta = NODE_META[data.kind];
const icon = resolveNodeIcon(data.kind, data.blockType);
const layoutDirection = data.layoutDirection ?? "LR";
const config = useCanvasLabStore((state) => state.configs[id]);
const openConfig = useCanvasLabStore((state) => state.openConfig);
const updateConfig = useCanvasLabStore((state) => state.updateConfig);
const config = useRecipeStudioStore((state) => state.configs[id]);
const openConfig = useRecipeStudioStore((state) => state.openConfig);
const updateConfig = useRecipeStudioStore((state) => state.updateConfig);
const updateNodeInternals = useUpdateNodeInternals();
useEffect(() => {
@ -409,4 +409,4 @@ function CanvasNodeBase({
);
}
export const CanvasNode = memo(CanvasNodeBase);
export const RecipeNode = memo(RecipeGraphNodeBase);

View file

@ -1,7 +1,7 @@
import { BaseEdge, type EdgeProps, getSmoothStepPath } from "@xyflow/react";
import { memo, type ReactElement } from "react";
export const CanvasSemanticEdge = memo(function CanvasSemanticEdge({
export const RecipeGraphSemanticEdge = memo(function RecipeGraphSemanticEdge({
id,
sourceX,
sourceY,

View file

@ -24,7 +24,7 @@ export function ImportDialog({
}: ImportDialogProps): ReactElement {
const [value, setValue] = useState("");
const [error, setError] = useState<string | null>(null);
const payloadId = "canvas-import-payload";
const payloadId = "recipe-import-payload";
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen) {
setValue("");

View file

@ -186,7 +186,7 @@ export function LlmDialog({
</div>
{scores.length === 0 && (
<p className="text-xs text-muted-foreground">
Add scorer blocks. Each block spawns on canvas and connects to this judge node.
Add scorer blocks. Each block spawns on graph and connects to this judge node.
</p>
)}
{scores.map((score, index) => (

View file

@ -5,14 +5,14 @@ import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { VisuallyHidden } from "radix-ui";
import { type ReactElement, useMemo } from "react";
import type { CanvasProcessorConfig } from "../types";
import type { RecipeProcessorConfig } from "../types";
import { buildDefaultSchemaTransform } from "../utils/processors";
import { AvailableVariables } from "./shared/available-variables";
type ProcessorsDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
processors: CanvasProcessorConfig[];
onProcessorsChange: (processors: CanvasProcessorConfig[]) => void;
processors: RecipeProcessorConfig[];
onProcessorsChange: (processors: RecipeProcessorConfig[]) => void;
container?: HTMLDivElement | null;
};
@ -51,7 +51,7 @@ export function ProcessorsDialog({
);
};
const updateSchema = (patch: Partial<CanvasProcessorConfig>) => {
const updateSchema = (patch: Partial<RecipeProcessorConfig>) => {
if (!schemaProcessor) {
return;
}

View file

@ -1,6 +1,6 @@
import { Badge } from "@/components/ui/badge";
import type { ReactElement } from "react";
import { useCanvasLabStore } from "../../stores/canvas-lab";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import { getAvailableVariables } from "../../utils/variables";
type AvailableVariablesProps = {
@ -10,7 +10,7 @@ type AvailableVariablesProps = {
export function AvailableVariables({
configId,
}: AvailableVariablesProps): ReactElement | null {
const configs = useCanvasLabStore((state) => state.configs);
const configs = useRecipeStudioStore((state) => state.configs);
const vars = getAvailableVariables(configs, configId);
if (vars.length === 0) return null;

View file

@ -0,0 +1 @@
export { RecipeStudioPage } from "./recipe-studio-page";

View file

@ -26,30 +26,30 @@ import { Spinner } from "@/components/ui/spinner";
import { EyeIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Lock, LockOpen, Maximize2, Minus, Plus } from "lucide-react";
import { previewCanvas } from "./api";
import { CanvasAuxNode, type CanvasAuxNodeData } from "./components/canvas-aux-node";
import { previewRecipe } from "./api";
import { RecipeGraphAuxNode, type RecipeGraphAuxNodeData } from "./components/recipe-graph-aux-node";
import { BlockSheet } from "./components/block-sheet";
import { CANVAS_FLOATING_ICON_BUTTON_CLASS } from "./components/floating-icon-button-class";
import { CanvasNode } from "./components/canvas-node";
import { CanvasSemanticEdge } from "./components/canvas-semantic-edge";
import { RECIPE_FLOATING_ICON_BUTTON_CLASS } from "./components/recipe-floating-icon-button-class";
import { RecipeNode } from "./components/recipe-graph-node";
import { RecipeGraphSemanticEdge } from "./components/recipe-graph-semantic-edge";
import { DataEdge } from "./components/rf-ui/data-edge";
import { ConfigDialog } from "./dialogs/config-dialog";
import { ImportDialog } from "./dialogs/import-dialog";
import { ProcessorsDialog } from "./dialogs/processors-dialog";
import { useCanvasLabStore } from "./stores/canvas-lab";
import { useRecipeStudioStore } from "./stores/recipe-studio";
import type {
CanvasNode as CanvasBuilderNode,
CanvasNodeData,
RecipeNode as RecipeBuilderNode,
RecipeNodeData,
SamplerConfig,
} from "./types";
import { isCategoryConfig } from "./utils";
import { deriveDisplayGraph } from "./utils/graph/derive-display-graph";
import { importCanvasPayload } from "./utils/import";
import { buildCanvasPayload } from "./utils/payload";
import { importRecipePayload } from "./utils/import";
import { buildRecipePayload } from "./utils/payload";
import { buildDefaultSchemaTransform } from "./utils/processors";
const NODE_TYPES: NodeTypes = { builder: CanvasNode, aux: CanvasAuxNode };
const EDGE_TYPES: EdgeTypes = { canvas: DataEdge, semantic: CanvasSemanticEdge };
const NODE_TYPES: NodeTypes = { builder: RecipeNode, aux: RecipeGraphAuxNode };
const EDGE_TYPES: EdgeTypes = { canvas: DataEdge, semantic: RecipeGraphSemanticEdge };
type LayoutControlsProps = {
direction: "LR" | "TB";
@ -66,6 +66,17 @@ type InternalsSyncProps = {
nodeIds: string[];
};
type StatusTone = "success" | "error";
type StatusMessage = {
tone: StatusTone;
text: string;
};
const STATUS_MESSAGE_CLASS: Record<StatusTone, string> = {
success: "mt-2 text-xs text-emerald-600",
error: "mt-2 text-xs text-rose-600",
};
function InternalsSync({ nodeIds }: InternalsSyncProps): null {
const updateNodeInternals = useUpdateNodeInternals();
@ -155,7 +166,7 @@ function ViewportControls({
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
onClick={handleZoomIn}
aria-label="Zoom in"
>
@ -165,7 +176,7 @@ function ViewportControls({
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
onClick={handleZoomOut}
aria-label="Zoom out"
>
@ -175,7 +186,7 @@ function ViewportControls({
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
onClick={handleFitView}
aria-label="Fit view"
>
@ -185,7 +196,7 @@ function ViewportControls({
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
onClick={onToggleInteractive}
aria-label={interactive ? "Lock interaction" : "Unlock interaction"}
>
@ -199,7 +210,7 @@ function ViewportControls({
);
}
export function CanvasLabPage(): ReactElement {
export function RecipeStudioPage(): ReactElement {
const {
nodes,
edges,
@ -225,14 +236,14 @@ export function CanvasLabPage(): ReactElement {
setSheetView,
setProcessors,
setDialogOpen,
loadCanvas,
loadRecipe,
setLayoutDirection,
applyLayout,
setAuxNodePosition,
setAuxNodeSize,
syncAuxNodePositions,
syncAuxNodeSizes,
} = useCanvasLabStore(
} = useRecipeStudioStore(
useShallow((state) => ({
nodes: state.nodes,
edges: state.edges,
@ -258,7 +269,7 @@ export function CanvasLabPage(): ReactElement {
setSheetView: state.setSheetView,
setProcessors: state.setProcessors,
setDialogOpen: state.setDialogOpen,
loadCanvas: state.loadCanvas,
loadRecipe: state.loadRecipe,
setLayoutDirection: state.setLayoutDirection,
applyLayout: state.applyLayout,
setAuxNodePosition: state.setAuxNodePosition,
@ -275,10 +286,7 @@ export function CanvasLabPage(): ReactElement {
const [importOpen, setImportOpen] = useState(false);
const [processorsOpen, setProcessorsOpen] = useState(false);
const [interactive, setInteractive] = useState(true);
const [statusMessage, setStatusMessage] = useState<{
tone: "success" | "error";
text: string;
} | null>(null);
const [statusMessage, setStatusMessage] = useState<StatusMessage | null>(null);
const baseNodeIds = useMemo(
() => new Set(nodes.map((node) => node.id)),
@ -311,7 +319,7 @@ export function CanvasLabPage(): ReactElement {
}, [displayGraph.auxNodeIds, syncAuxNodeSizes]);
const handleNodeClick = useCallback(
(_: unknown, node: Node<CanvasNodeData | CanvasAuxNodeData>) => {
(_: unknown, node: Node<RecipeNodeData | RecipeGraphAuxNodeData>) => {
if (node.type !== "builder") {
return;
}
@ -321,12 +329,9 @@ export function CanvasLabPage(): ReactElement {
);
const handleNodesChange = useCallback(
(changes: NodeChange<Node<CanvasNodeData | CanvasAuxNodeData>>[]) => {
(changes: NodeChange<Node<RecipeNodeData | RecipeGraphAuxNodeData>>[]) => {
for (const change of changes) {
if (
!("id" in change) ||
!change.id.startsWith("aux-")
) {
if (!("id" in change) || !change.id.startsWith("aux-")) {
continue;
}
if (change.type === "position" && change.position) {
@ -346,7 +351,7 @@ export function CanvasLabPage(): ReactElement {
}
}
const next = changes.filter(
(change): change is NodeChange<CanvasBuilderNode> =>
(change): change is NodeChange<RecipeBuilderNode> =>
"id" in change && baseNodeIds.has(change.id),
);
if (next.length > 0) {
@ -369,67 +374,77 @@ export function CanvasLabPage(): ReactElement {
[baseEdgeIds, onEdgesChange],
);
const configList = useMemo(() => Object.values(configs), [configs]);
const config = activeConfigId ? configs[activeConfigId] : null;
const categoryOptions = useMemo<SamplerConfig[]>(
() => Object.values(configs).filter(isCategoryConfig),
[configs],
() => configList.filter(isCategoryConfig),
[configList],
);
const modelConfigAliases = useMemo<string[]>(
() =>
Object.values(configs)
.filter((item) => item.kind === "model_config")
.map((item) => item.name),
[configs],
() => configList.filter((item) => item.kind === "model_config").map((item) => item.name),
[configList],
);
const modelProviderOptions = useMemo<string[]>(
() =>
Object.values(configs)
.filter((item) => item.kind === "model_provider")
.map((item) => item.name),
[configs],
() => configList.filter((item) => item.kind === "model_provider").map((item) => item.name),
[configList],
);
const datetimeOptions = useMemo<string[]>(
() =>
Object.values(configs)
configList
.filter(
(item) => item.kind === "sampler" && item.sampler_type === "datetime",
)
.map((item) => item.name),
[configs],
[configList],
);
const handleToggleDirection = useCallback(() => {
setLayoutDirection(layoutDirection === "LR" ? "TB" : "LR");
}, [layoutDirection, setLayoutDirection]);
const toggleInteractive = useCallback(() => {
setInteractive((value) => !value);
}, []);
const setSuccessStatus = useCallback((text: string) => {
setStatusMessage({ tone: "success", text });
}, []);
const setErrorStatus = useCallback((text: string) => {
setStatusMessage({ tone: "error", text });
}, []);
const buildPayload = useCallback(
() => buildRecipePayload(configs, nodes, edges, processors),
[configs, nodes, edges, processors],
);
const readPayload = useCallback(
(fallbackError: string) => {
const { payload, errors } = buildPayload();
if (errors.length === 0) {
return payload;
}
setErrorStatus(errors[0] ?? fallbackError);
return null;
},
[buildPayload, setErrorStatus],
);
const handlePreview = async (): Promise<void> => {
setPreviewLoading(true);
setStatusMessage(null);
const payload = readPayload("Fix config errors before preview.");
if (!payload) {
setPreviewLoading(false);
return;
}
try {
const { payload, errors } = buildCanvasPayload(
configs,
nodes,
edges,
processors,
);
if (errors.length > 0) {
setStatusMessage({
tone: "error",
text: errors[0] ?? "Fix config errors before preview.",
});
return;
}
const result = await previewCanvas(payload);
const result = await previewRecipe(payload);
const rows = Array.isArray(result.dataset) ? result.dataset.length : 0;
setStatusMessage({
tone: "success",
text: `Preview ready (${rows} rows).`,
});
setSuccessStatus(`Preview ready (${rows} rows).`);
} catch (error) {
setStatusMessage({
tone: "error",
text: error instanceof Error ? error.message : "Preview failed.",
});
setErrorStatus(error instanceof Error ? error.message : "Preview failed.");
} finally {
setPreviewLoading(false);
}
@ -438,53 +453,35 @@ export function CanvasLabPage(): ReactElement {
const handleCopyRecipe = async (): Promise<void> => {
setCopied(false);
setStatusMessage(null);
const { payload, errors } = buildCanvasPayload(
configs,
nodes,
edges,
processors,
);
if (errors.length > 0) {
setStatusMessage({
tone: "error",
text: errors[0] ?? "Fix config errors before copy.",
});
const payload = readPayload("Fix config errors before copy.");
if (!payload) {
return;
}
if (!navigator.clipboard) {
setStatusMessage({
tone: "error",
text: "Clipboard not available.",
});
setErrorStatus("Clipboard not available.");
return;
}
try {
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
setStatusMessage({
tone: "success",
text: "Recipe copied to clipboard.",
});
setSuccessStatus("Recipe copied to clipboard.");
} catch (error) {
setStatusMessage({
tone: "error",
text: error instanceof Error ? error.message : "Copy failed.",
});
setErrorStatus(error instanceof Error ? error.message : "Copy failed.");
}
};
const handleImport = (value: string): string | null => {
const result = importCanvasPayload(value);
const result = importRecipePayload(value);
if (result.errors.length > 0 || !result.snapshot) {
return result.errors[0] ?? "Invalid payload.";
}
loadCanvas(result.snapshot);
setStatusMessage({ tone: "success", text: "Recipe imported." });
loadRecipe(result.snapshot);
setSuccessStatus("Recipe imported.");
return null;
};
const handleOpenProcessorsFromSheet = useCallback(() => {
const openProcessorsFromSheet = useCallback(() => {
if (
!processors.some(
(processor) => processor.processor_type === "schema_transform",
@ -501,20 +498,12 @@ export function CanvasLabPage(): ReactElement {
<div className="mb-6 flex flex-col gap-4">
<div className="flex flex-col gap-4 lg:grid lg:grid-cols-[1fr_auto] lg:items-center">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Canvas Lab
</h1>
<h1 className="text-2xl font-semibold tracking-tight">Create Data Recipe</h1>
<p className="text-sm text-muted-foreground">
Minimal React Flow canvas.
Design synthetic-data pipelines with Data Designer.
</p>
{statusMessage && (
<p
className={
statusMessage.tone === "success"
? "mt-2 text-xs text-emerald-600"
: "mt-2 text-xs text-rose-600"
}
>
<p className={STATUS_MESSAGE_CLASS[statusMessage.tone]}>
{statusMessage.text}
</p>
)}
@ -577,14 +566,14 @@ export function CanvasLabPage(): ReactElement {
<Panel position="top-right" className="m-3">
<BlockSheet
container={sheetContainer}
view={sheetView}
sheetView={sheetView}
onViewChange={setSheetView}
onAddSampler={addSamplerNode}
onAddLlm={addLlmNode}
onAddModelProvider={addModelProviderNode}
onAddModelConfig={addModelConfigNode}
onAddExpression={addExpressionNode}
onOpenProcessors={handleOpenProcessorsFromSheet}
onOpenProcessors={openProcessorsFromSheet}
copied={copied}
onCopy={handleCopyRecipe}
onImport={() => setImportOpen(true)}
@ -592,7 +581,7 @@ export function CanvasLabPage(): ReactElement {
</Panel>
<ViewportControls
interactive={interactive}
onToggleInteractive={() => setInteractive((value) => !value)}
onToggleInteractive={toggleInteractive}
/>
</ReactFlow>
</div>

View file

@ -1,7 +1,7 @@
import { type Edge, addEdge } from "@xyflow/react";
import { DEFAULT_NODE_WIDTH } from "../constants";
import type {
CanvasNode,
RecipeNode,
LayoutDirection,
LlmConfig,
ModelConfig,
@ -15,14 +15,14 @@ import { getConfigUiMode } from "../components/inline/inline-policy";
type NodeUpdateState = {
configs: Record<string, NodeConfig>;
nodes: CanvasNode[];
nodes: RecipeNode[];
nextId: number;
nextY: number;
};
type NodeUpdateResult = {
configs: Record<string, NodeConfig>;
nodes: CanvasNode[];
nodes: RecipeNode[];
nextId: number;
nextY: number;
activeConfigId: string;
@ -30,11 +30,11 @@ type NodeUpdateResult = {
};
export function updateNodeData(
nodes: CanvasNode[],
nodes: RecipeNode[],
id: string,
config: NodeConfig,
layoutDirection: LayoutDirection,
): CanvasNode[] {
): RecipeNode[] {
return nodes.map((node) =>
node.id === id
? { ...node, data: nodeDataFromConfig(config, layoutDirection) }
@ -52,12 +52,56 @@ function findNodeIdByName(
return entry ? entry[0] : null;
}
function addRecipeEdge(edges: Edge[], source: string, target: string): Edge[] {
return addEdge(
{
source,
target,
sourceHandle: HANDLE_IDS.dataOut,
targetHandle: HANDLE_IDS.dataIn,
type: "canvas",
},
edges,
);
}
function addSemanticEdge(edges: Edge[], source: string, target: string): Edge[] {
return addEdge(
{
source,
target,
sourceHandle: HANDLE_IDS.semanticOut,
targetHandle: HANDLE_IDS.semanticIn,
type: "semantic",
},
edges,
);
}
function removeTargetEdges(edges: Edge[], targetId: string): Edge[] {
return edges.filter((edge) => edge.target !== targetId);
}
function removeTargetEdgesBySource(
edges: Edge[],
configs: Record<string, NodeConfig>,
targetId: string,
shouldRemove: (source: NodeConfig | undefined) => boolean,
): Edge[] {
return edges.filter((edge) => {
if (edge.target !== targetId) {
return true;
}
return !shouldRemove(configs[edge.source]);
});
}
export function buildNodeUpdate(
state: NodeUpdateState,
config: NodeConfig,
layoutDirection: LayoutDirection,
): NodeUpdateResult {
const node: CanvasNode = {
const node: RecipeNode = {
id: config.id,
type: "builder",
position: { x: 0, y: state.nextY },
@ -77,10 +121,10 @@ export function buildNodeUpdate(
}
export function applyLayoutDirectionToNodes(
nodes: CanvasNode[],
nodes: RecipeNode[],
configs: Record<string, NodeConfig>,
layoutDirection: LayoutDirection,
): CanvasNode[] {
): RecipeNode[] {
return nodes.map((node) => {
const config = configs[node.id];
if (config) {
@ -108,18 +152,9 @@ export function syncEdgesForConfigPatch(
if (isSubcategoryConfig(current) && hasParentPatch) {
const nextParent = (patch as Partial<SamplerConfig>).subcategory_parent ?? "";
const parentId = nextParent ? findNodeIdByName(configs, nextParent) : null;
nextEdges = nextEdges.filter((edge) => edge.target !== current.id);
nextEdges = removeTargetEdges(nextEdges, current.id);
if (parentId) {
nextEdges = addEdge(
{
source: parentId,
target: current.id,
sourceHandle: HANDLE_IDS.dataOut,
targetHandle: HANDLE_IDS.dataIn,
type: "canvas",
},
nextEdges,
);
nextEdges = addRecipeEdge(nextEdges, parentId, current.id);
}
}
@ -129,26 +164,16 @@ export function syncEdgesForConfigPatch(
);
if (current.kind === "model_config" && hasProviderPatch) {
const nextProvider = (patch as Partial<ModelConfig>).provider ?? "";
nextEdges = nextEdges.filter((edge) => {
if (edge.target !== current.id) {
return true;
}
const source = configs[edge.source];
return !(source && source.kind === "model_provider");
});
nextEdges = removeTargetEdgesBySource(
nextEdges,
configs,
current.id,
(source) => Boolean(source && source.kind === "model_provider"),
);
if (nextProvider) {
const providerId = findNodeIdByName(configs, nextProvider);
if (providerId) {
nextEdges = addEdge(
{
source: providerId,
target: current.id,
sourceHandle: HANDLE_IDS.semanticOut,
targetHandle: HANDLE_IDS.semanticIn,
type: "semantic",
},
nextEdges,
);
nextEdges = addSemanticEdge(nextEdges, providerId, current.id);
}
}
}
@ -164,17 +189,17 @@ export function syncEdgesForConfigPatch(
) {
const nextReference =
(patch as Partial<SamplerConfig>).reference_column_name ?? "";
nextEdges = nextEdges.filter((edge) => {
if (edge.target !== current.id) {
return true;
}
const source = configs[edge.source];
return !(
source &&
source.kind === "sampler" &&
source.sampler_type === "datetime"
);
});
nextEdges = removeTargetEdgesBySource(
nextEdges,
configs,
current.id,
(source) =>
Boolean(
source &&
source.kind === "sampler" &&
source.sampler_type === "datetime",
),
);
if (nextReference) {
const referenceId = findNodeIdByName(configs, nextReference);
const source = referenceId ? configs[referenceId] : null;
@ -184,16 +209,7 @@ export function syncEdgesForConfigPatch(
source.kind === "sampler" &&
source.sampler_type === "datetime"
) {
nextEdges = addEdge(
{
source: referenceId,
target: current.id,
sourceHandle: HANDLE_IDS.dataOut,
targetHandle: HANDLE_IDS.dataIn,
type: "canvas",
},
nextEdges,
);
nextEdges = addRecipeEdge(nextEdges, referenceId, current.id);
}
}
}
@ -205,26 +221,16 @@ export function syncEdgesForConfigPatch(
if (current.kind === "llm" && hasModelAliasPatch) {
const nextAlias =
(patch as Partial<NodeConfig> & { model_alias?: string }).model_alias ?? "";
nextEdges = nextEdges.filter((edge) => {
if (edge.target !== current.id) {
return true;
}
const source = configs[edge.source];
return !(source && source.kind === "model_config");
});
nextEdges = removeTargetEdgesBySource(
nextEdges,
configs,
current.id,
(source) => Boolean(source && source.kind === "model_config"),
);
if (nextAlias) {
const modelConfigId = findNodeIdByName(configs, nextAlias);
if (modelConfigId) {
nextEdges = addEdge(
{
source: modelConfigId,
target: current.id,
sourceHandle: HANDLE_IDS.semanticOut,
targetHandle: HANDLE_IDS.semanticIn,
type: "semantic",
},
nextEdges,
);
nextEdges = addSemanticEdge(nextEdges, modelConfigId, current.id);
}
}
}
@ -405,9 +411,18 @@ export function applyRenameToConfigs(
if (!from || from === to) {
return configs;
}
return applyConfigTransform(configs, (config) =>
applyRenameToConfig(config, from, to),
);
}
function applyConfigTransform(
configs: Record<string, NodeConfig>,
transform: (config: NodeConfig) => NodeConfig,
): Record<string, NodeConfig> {
let next = configs;
for (const [id, config] of Object.entries(configs)) {
const updated = applyRenameToConfig(config, from, to);
const updated = transform(config);
if (updated !== config) {
if (next === configs) {
next = { ...configs };
@ -425,15 +440,5 @@ export function applyRemovalToConfigs(
if (!ref) {
return configs;
}
let next = configs;
for (const [id, config] of Object.entries(configs)) {
const updated = applyRemovalToConfig(config, ref);
if (updated !== config) {
if (next === configs) {
next = { ...configs };
}
next[id] = updated;
}
}
return next;
return applyConfigTransform(configs, (config) => applyRemovalToConfig(config, ref));
}

View file

@ -10,17 +10,21 @@ import {
} from "@xyflow/react";
import { create } from "zustand";
import type {
CanvasNode,
CanvasProcessorConfig,
RecipeNode,
RecipeProcessorConfig,
LayoutDirection,
LlmType,
NodeConfig,
SamplerType,
} from "../types";
import { getBlockDefinition } from "../blocks/registry";
import {
getBlockDefinition,
type BlockKind,
type BlockType,
} from "../blocks/registry";
import { isCategoryConfig, isSubcategoryConfig } from "../utils";
import { applyCanvasConnection, isValidCanvasConnection } from "../utils/graph";
import type { CanvasSnapshot } from "../utils/import";
import { applyRecipeConnection, isValidRecipeConnection } from "../utils/graph";
import type { RecipeSnapshot } from "../utils/import";
import { getLayoutedElements } from "../utils/layout";
import {
applyRemovalToConfig,
@ -31,17 +35,17 @@ import {
syncEdgesForConfigPatch,
syncSubcategoryConfigsForCategoryUpdate,
updateNodeData,
} from "./canvas-lab-helpers";
} from "./recipe-studio-helpers";
type SheetView = "root" | "sampler" | "llm" | "expression" | "processor";
type CanvasLabState = {
nodes: CanvasNode[];
type RecipeStudioState = {
nodes: RecipeNode[];
edges: Edge[];
auxNodePositions: Record<string, XYPosition>;
auxNodeSizes: Record<string, { width: number; height: number }>;
configs: Record<string, NodeConfig>;
processors: CanvasProcessorConfig[];
processors: RecipeProcessorConfig[];
sheetView: SheetView;
activeConfigId: string | null;
dialogOpen: boolean;
@ -49,7 +53,7 @@ type CanvasLabState = {
nextId: number;
nextY: number;
setSheetView: (view: SheetView) => void;
setProcessors: (processors: CanvasProcessorConfig[]) => void;
setProcessors: (processors: RecipeProcessorConfig[]) => void;
setDialogOpen: (open: boolean) => void;
selectConfig: (id: string) => void;
openConfig: (id: string) => void;
@ -61,7 +65,7 @@ type CanvasLabState = {
addModelConfigNode: () => void;
addExpressionNode: () => void;
updateConfig: (id: string, patch: Partial<NodeConfig>) => void;
loadCanvas: (snapshot: CanvasSnapshot) => void;
loadRecipe: (snapshot: RecipeSnapshot) => void;
setAuxNodePosition: (id: string, position: XYPosition) => void;
setAuxNodeSize: (
id: string,
@ -72,13 +76,28 @@ type CanvasLabState = {
defaults: Record<string, XYPosition>,
) => void;
syncAuxNodeSizes: (activeIds: string[]) => void;
onNodesChange: (changes: NodeChange<CanvasNode>[]) => void;
onNodesChange: (changes: NodeChange<RecipeNode>[]) => void;
onEdgesChange: (changes: EdgeChange<Edge>[]) => void;
onConnect: (connection: Connection) => void;
isValidConnection: IsValidConnection;
};
export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
function buildAddedNodeState(
state: RecipeStudioState,
kind: BlockKind,
type: BlockType,
): Partial<RecipeStudioState> | RecipeStudioState {
const id = `n${state.nextId}`;
const existing = Object.values(state.configs);
const definition = getBlockDefinition(kind, type);
if (!definition) {
return state;
}
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
}
export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
nodes: [],
edges: [],
auxNodePositions: {},
@ -123,67 +142,16 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
),
};
}),
addSamplerNode: (type) => {
set((state) => {
const id = `n${state.nextId}`;
const existing = Object.values(state.configs);
const definition = getBlockDefinition("sampler", type);
if (!definition) {
return state;
}
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
});
},
addLlmNode: (type) => {
set((state) => {
const id = `n${state.nextId}`;
const existing = Object.values(state.configs);
const definition = getBlockDefinition("llm", type);
if (!definition) {
return state;
}
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
});
},
addModelProviderNode: () => {
set((state) => {
const id = `n${state.nextId}`;
const existing = Object.values(state.configs);
const definition = getBlockDefinition("llm", "model_provider");
if (!definition) {
return state;
}
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
});
},
addModelConfigNode: () => {
set((state) => {
const id = `n${state.nextId}`;
const existing = Object.values(state.configs);
const definition = getBlockDefinition("llm", "model_config");
if (!definition) {
return state;
}
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
});
},
addExpressionNode: () => {
set((state) => {
const id = `n${state.nextId}`;
const existing = Object.values(state.configs);
const definition = getBlockDefinition("expression", "expression");
if (!definition) {
return state;
}
const config = definition.createConfig(id, existing);
return buildNodeUpdate(state, config, state.layoutDirection);
});
},
loadCanvas: (snapshot) =>
addSamplerNode: (type) =>
set((state) => buildAddedNodeState(state, "sampler", type)),
addLlmNode: (type) => set((state) => buildAddedNodeState(state, "llm", type)),
addModelProviderNode: () =>
set((state) => buildAddedNodeState(state, "llm", "model_provider")),
addModelConfigNode: () =>
set((state) => buildAddedNodeState(state, "llm", "model_config")),
addExpressionNode: () =>
set((state) => buildAddedNodeState(state, "expression", "expression")),
loadRecipe: (snapshot) =>
set((state) => ({
configs: snapshot.configs,
nodes: applyLayoutDirectionToNodes(
@ -281,7 +249,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
return state;
}),
updateConfig: (id, patch) => {
const applyUpdate = (state: CanvasLabState) => {
const applyUpdate = (state: RecipeStudioState) => {
const current = state.configs[id];
if (!current) {
return state;
@ -320,7 +288,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
},
onNodesChange: (changes) => {
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: store update
const applyNodesChange = (state: CanvasLabState) => {
const applyNodesChange = (state: RecipeStudioState) => {
const removedIds = changes
.filter((change) => change.type === "remove")
.map((change) => change.id);
@ -367,7 +335,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
}
}
const nodes = applyNodeChanges<CanvasNode>(changes, state.nodes);
const nodes = applyNodeChanges<RecipeNode>(changes, state.nodes);
return { nodes, edges, configs };
};
set(applyNodesChange);
@ -403,7 +371,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
},
onConnect: (connection) => {
set((state) => {
const result = applyCanvasConnection(
const result = applyRecipeConnection(
connection,
state.configs,
state.edges,
@ -414,7 +382,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
});
},
isValidConnection: (connection) =>
isValidCanvasConnection(
isValidRecipeConnection(
{
source: connection.source ?? null,
target: connection.target ?? null,

View file

@ -18,7 +18,7 @@ export type ExpressionDtype = "str" | "int" | "float" | "bool";
export type LayoutDirection = "LR" | "TB";
export type CanvasNodeData = {
export type RecipeNodeData = {
title: string;
name: string;
kind: "sampler" | "llm" | "expression" | "model_provider" | "model_config";
@ -32,7 +32,7 @@ export type CanvasNodeData = {
layoutDirection?: LayoutDirection;
};
export type CanvasNode = Node<CanvasNodeData, "builder">;
export type RecipeNode = Node<RecipeNodeData, "builder">;
export type CategoryConditionalParams = {
// biome-ignore lint/style/useNamingConvention: api schema
@ -171,7 +171,7 @@ export type SchemaTransformProcessorConfig = {
template: string;
};
export type CanvasProcessorConfig = SchemaTransformProcessorConfig;
export type RecipeProcessorConfig = SchemaTransformProcessorConfig;
export type NodeConfig =
| SamplerConfig

View file

@ -0,0 +1,4 @@
export {
applyRecipeConnection,
isValidRecipeConnection,
} from "./graph/recipe-graph-connection";

View file

@ -1,11 +1,11 @@
import type { Edge, Node, XYPosition } from "@xyflow/react";
import type { CanvasAuxNodeData } from "../../components/canvas-aux-node";
import type { RecipeGraphAuxNodeData } from "../../components/recipe-graph-aux-node";
import { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH } from "../../constants";
import type { CanvasNode, LayoutDirection, NodeConfig } from "../../types";
import type { RecipeNode, LayoutDirection, NodeConfig } from "../../types";
import { getLlmJudgeScoreHandleId, HANDLE_IDS } from "../handles";
type DisplayGraphInput = {
nodes: CanvasNode[];
nodes: RecipeNode[];
edges: Edge[];
configs: Record<string, NodeConfig>;
layoutDirection: LayoutDirection;
@ -14,7 +14,7 @@ type DisplayGraphInput = {
};
export type DisplayGraph = {
nodes: Array<Node<CanvasNode["data"] | CanvasAuxNodeData>>;
nodes: Array<Node<RecipeNode["data"] | RecipeGraphAuxNodeData>>;
edges: Edge[];
auxNodeIds: string[];
auxDefaults: Record<string, XYPosition>;
@ -23,7 +23,7 @@ export type DisplayGraph = {
type AuxNodeItem = {
key: string;
targetHandle: string;
data: CanvasAuxNodeData;
data: RecipeGraphAuxNodeData;
};
function getNodeWidth(node: Node): number {
@ -92,7 +92,7 @@ export function deriveDisplayGraph({
style: { ...node.style, width: DEFAULT_NODE_WIDTH },
};
});
const auxNodes: Node<CanvasAuxNodeData>[] = [];
const auxNodes: Node<RecipeGraphAuxNodeData>[] = [];
const auxEdges: Edge[] = [];
const auxDefaults: Record<string, XYPosition> = {};
const auxNodeIds: string[] = [];

View file

@ -126,7 +126,7 @@ function isCompetingIncomingEdge(
return source.kind === "sampler" && source.sampler_type === "datetime";
}
export function isValidCanvasConnection(
export function isValidRecipeConnection(
connection: Connection,
configs: Record<string, NodeConfig>,
): boolean {
@ -151,12 +151,12 @@ export function isValidCanvasConnection(
return isDataLane(connection);
}
export function applyCanvasConnection(
export function applyRecipeConnection(
connection: Connection,
configs: Record<string, NodeConfig>,
edges: Edge[],
): { edges: Edge[]; configs?: Record<string, NodeConfig> } {
if (!isValidCanvasConnection(connection, configs)) {
if (!isValidRecipeConnection(connection, configs)) {
return { edges };
}
const source = connection.source ? configs[connection.source] : null;

View file

@ -11,7 +11,7 @@ export const HANDLE_IDS = {
llmInputOut: "llm-input-out",
} as const;
export type CanvasHandleId = (typeof HANDLE_IDS)[keyof typeof HANDLE_IDS];
export type RecipeHandleId = (typeof HANDLE_IDS)[keyof typeof HANDLE_IDS];
export function getLlmJudgeScoreHandleId(index: number): string {
return `llm-judge-score-in-${index}`;

View file

@ -1,4 +1,4 @@
import type { CanvasProcessorConfig, NodeConfig } from "../../types";
import type { RecipeProcessorConfig, NodeConfig } from "../../types";
import { buildEdges } from "./edges";
import { isRecord, parseJson, readString } from "./helpers";
import {
@ -21,11 +21,11 @@ type UiInput = {
edges?: unknown;
};
function parseProcessors(input: unknown): CanvasProcessorConfig[] {
function parseProcessors(input: unknown): RecipeProcessorConfig[] {
if (!Array.isArray(input)) {
return [];
}
const processors: CanvasProcessorConfig[] = [];
const processors: RecipeProcessorConfig[] = [];
input.forEach((item, index) => {
if (!isRecord(item)) {
return;
@ -55,7 +55,7 @@ function parseProcessors(input: unknown): CanvasProcessorConfig[] {
return processors;
}
export function importCanvasPayload(input: string): ImportResult {
export function importRecipePayload(input: string): ImportResult {
const parsed = parseJson(input);
if (!parsed.data || !isRecord(parsed.data)) {
return {

View file

@ -0,0 +1,2 @@
export { importRecipePayload } from "./importer";
export type { RecipeSnapshot, ImportResult } from "./types";

View file

@ -1,20 +1,20 @@
import type { Edge } from "@xyflow/react";
import type {
CanvasNode,
CanvasProcessorConfig,
RecipeNode,
RecipeProcessorConfig,
NodeConfig,
} from "../../types";
export type CanvasSnapshot = {
export type RecipeSnapshot = {
configs: Record<string, NodeConfig>;
nodes: CanvasNode[];
nodes: RecipeNode[];
edges: Edge[];
processors: CanvasProcessorConfig[];
processors: RecipeProcessorConfig[];
nextId: number;
nextY: number;
};
export type ImportResult = {
errors: string[];
snapshot: CanvasSnapshot | null;
snapshot: RecipeSnapshot | null;
};

View file

@ -1,4 +1,4 @@
import type { CanvasNode, NodeConfig } from "../../types";
import type { RecipeNode, NodeConfig } from "../../types";
import { DEFAULT_NODE_WIDTH } from "../../constants";
import { nodeDataFromConfig } from "../index";
import { isRecord, readString } from "./helpers";
@ -54,7 +54,7 @@ export function parseUi(
export function buildNodes(
configs: NodeConfig[],
layouts: Map<string, { x: number; y: number; width?: number }>,
): CanvasNode[] {
): RecipeNode[] {
return configs.map((config, index) => {
const fallbackLayout: { x: number; y: number; width?: number } = {
x: 0,

View file

@ -1,5 +1,5 @@
import type {
CanvasNodeData,
RecipeNodeData,
ExpressionConfig,
ExpressionDtype,
LayoutDirection,
@ -334,7 +334,7 @@ export function labelForExpression(type: ExpressionDtype): string {
export function nodeDataFromConfig(
config: NodeConfig,
layoutDirection: LayoutDirection = "LR",
): CanvasNodeData {
): RecipeNodeData {
if (config.kind === "sampler") {
return {
title: "Sampler",

View file

@ -1,7 +1,7 @@
import type { Edge } from "@xyflow/react";
import type {
CanvasProcessorConfig,
CanvasNode,
RecipeProcessorConfig,
RecipeNode,
ModelConfig,
ModelProviderConfig,
NodeConfig,
@ -15,7 +15,7 @@ import {
buildProcessors,
buildSamplerColumn,
} from "./builders";
import type { CanvasPayloadResult } from "./types";
import type { RecipePayloadResult } from "./types";
import {
isSemanticRelation,
validateModelAliasLinks,
@ -25,7 +25,7 @@ import {
validateUsedProviders,
} from "./validate";
function getNodeWidth(node: CanvasNode): number | null {
function getNodeWidth(node: RecipeNode): number | null {
if (typeof node.width === "number" && Number.isFinite(node.width)) {
return node.width;
}
@ -42,12 +42,12 @@ function getNodeWidth(node: CanvasNode): number | null {
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: payload build
export function buildCanvasPayload(
export function buildRecipePayload(
configs: Record<string, NodeConfig>,
nodes: CanvasNode[],
nodes: RecipeNode[],
edges: Edge[],
processors: CanvasProcessorConfig[] = [],
): CanvasPayloadResult {
processors: RecipeProcessorConfig[] = [],
): RecipePayloadResult {
const errors: string[] = [];
const columns: Record<string, unknown>[] = [];
const modelAliases = new Set<string>();

View file

@ -1,5 +1,5 @@
import type {
CanvasProcessorConfig,
RecipeProcessorConfig,
CategoryConditionalParams,
ExpressionConfig,
LlmConfig,
@ -349,7 +349,7 @@ export function buildExpressionColumn(
}
export function buildProcessors(
processors: CanvasProcessorConfig[],
processors: RecipeProcessorConfig[],
errors: string[],
): Record<string, unknown>[] {
const output: Record<string, unknown>[] = [];

View file

@ -0,0 +1,2 @@
export { buildRecipePayload } from "./build-payload";
export type { RecipePayload, RecipePayloadResult } from "./types";

View file

@ -1,4 +1,4 @@
export type CanvasPayload = {
export type RecipePayload = {
recipe: {
// biome-ignore lint/style/useNamingConvention: api schema
model_providers: Record<string, unknown>[];
@ -19,7 +19,7 @@ export type CanvasPayload = {
};
};
export type CanvasPayloadResult = {
export type RecipePayloadResult = {
errors: string[];
payload: CanvasPayload;
payload: RecipePayload;
};

View file

@ -1,6 +1,6 @@
import type { CanvasProcessorConfig } from "../types";
import type { RecipeProcessorConfig } from "../types";
export function buildDefaultSchemaTransform(): CanvasProcessorConfig {
export function buildDefaultSchemaTransform(): RecipeProcessorConfig {
return {
id: "schema-transform-1",
// biome-ignore lint/style/useNamingConvention: api schema

View file

@ -17,7 +17,7 @@ export default defineConfig({
target: "http://127.0.0.1:8000",
changeOrigin: true,
},
"/preview-api": {
"/preview": {
target: "http://127.0.0.1:8004",
changeOrigin: true,
},