From 1d5d1b625b58607b2f5207fc0e05fd3b5900a408 Mon Sep 17 00:00:00 2001 From: shine1i Date: Wed, 4 Feb 2026 14:06:38 +0100 Subject: [PATCH] canvaslab v1 --- studio/frontend/.gitignore | 1 + studio/frontend/src/components/ui/sheet.tsx | 26 +++- .../features/canvas-lab/canvas-lab-page.tsx | 3 + .../canvas-lab/components/block-sheet.tsx | 64 ++++++++- .../canvas-lab/components/canvas-node.tsx | 10 +- .../canvas-lab/dialogs/config-dialog.tsx | 7 + .../dialogs/expression/expression-dialog.tsx | 83 ++++++++++++ .../canvas-lab/dialogs/llm/llm-dialog.tsx | 3 + .../features/canvas-lab/stores/canvas-lab.ts | 127 +++++------------- .../src/features/canvas-lab/types/index.ts | 14 +- .../src/features/canvas-lab/utils/graph.ts | 107 +++++++++++++++ .../src/features/canvas-lab/utils/index.ts | 69 ++++++++-- .../src/features/canvas-lab/utils/payload.ts | 33 ++++- 13 files changed, 429 insertions(+), 118 deletions(-) create mode 100644 studio/frontend/src/features/canvas-lab/dialogs/expression/expression-dialog.tsx create mode 100644 studio/frontend/src/features/canvas-lab/utils/graph.ts diff --git a/studio/frontend/.gitignore b/studio/frontend/.gitignore index 585a80197d..ec9ec8d674 100644 --- a/studio/frontend/.gitignore +++ b/studio/frontend/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +test/ *.local .env .env.* diff --git a/studio/frontend/src/components/ui/sheet.tsx b/studio/frontend/src/components/ui/sheet.tsx index af7132e63e..b086790203 100644 --- a/studio/frontend/src/components/ui/sheet.tsx +++ b/studio/frontend/src/components/ui/sheet.tsx @@ -30,13 +30,17 @@ function SheetPortal({ function SheetOverlay({ className, + position = "fixed", ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + position?: "fixed" | "absolute"; +}) { return ( & { side?: "top" | "right" | "bottom" | "left"; showCloseButton?: boolean; + container?: HTMLElement | null; + position?: "fixed" | "absolute"; + overlayClassName?: string; + overlayPosition?: "fixed" | "absolute"; }) { return ( - - + + diff --git a/studio/frontend/src/features/canvas-lab/components/block-sheet.tsx b/studio/frontend/src/features/canvas-lab/components/block-sheet.tsx index 3e1c969353..b2858b6e21 100644 --- a/studio/frontend/src/features/canvas-lab/components/block-sheet.tsx +++ b/studio/frontend/src/features/canvas-lab/components/block-sheet.tsx @@ -19,7 +19,7 @@ import { HugeiconsIcon } from "@hugeicons/react"; import type { ReactElement } from "react"; import type { LlmType, SamplerType } from "../types"; -type SheetView = "root" | "sampler" | "llm"; +type SheetView = "root" | "sampler" | "llm" | "expression"; type BlockSheetProps = { container: HTMLDivElement | null; @@ -27,6 +27,7 @@ type BlockSheetProps = { onViewChange: (view: SheetView) => void; onAddSampler: (type: SamplerType) => void; onAddLlm: (type: LlmType) => void; + onAddExpression: () => void; }; function getSheetTitle(view: SheetView): string { @@ -36,6 +37,9 @@ function getSheetTitle(view: SheetView): string { if (view === "sampler") { return "Sampler blocks"; } + if (view === "expression") { + return "Expression blocks"; + } return "LLM blocks"; } @@ -52,6 +56,12 @@ const MAIN_SHEET_ITEMS = [ description: "Text + structured blocks.", icon: SparklesIcon, }, + { + kind: "expression" as const, + title: "Expression", + description: "Derived columns with Jinja.", + icon: CodeIcon, + }, ]; const SAMPLER_ITEMS = [ @@ -120,12 +130,31 @@ const LLM_ITEMS = [ }, ]; +const EXPRESSION_ITEMS = [ + { + title: "Expression", + description: "Transform columns with Jinja.", + icon: CodeIcon, + }, +]; + +function nextViewForKind(kind: "sampler" | "llm" | "expression"): SheetView { + if (kind === "sampler") { + return "sampler"; + } + if (kind === "expression") { + return "expression"; + } + return "llm"; +} + export function BlockSheet({ container, view, onViewChange, onAddSampler, onAddLlm, + onAddExpression, }: BlockSheetProps): ReactElement { const title = getSheetTitle(view); return ( @@ -138,8 +167,10 @@ export function BlockSheet({
@@ -163,9 +194,7 @@ export function BlockSheet({ ))} + {view === "expression" && + EXPRESSION_ITEMS.map((item) => ( + + ))}
diff --git a/studio/frontend/src/features/canvas-lab/components/canvas-node.tsx b/studio/frontend/src/features/canvas-lab/components/canvas-node.tsx index 70d101397d..cea2911db9 100644 --- a/studio/frontend/src/features/canvas-lab/components/canvas-node.tsx +++ b/studio/frontend/src/features/canvas-lab/components/canvas-node.tsx @@ -1,5 +1,9 @@ import { cn } from "@/lib/utils"; -import { Database02Icon, SparklesIcon } from "@hugeicons/core-free-icons"; +import { + CodeIcon, + Database02Icon, + SparklesIcon, +} from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import type { NodeProps } from "@xyflow/react"; import { Handle, Position } from "@xyflow/react"; @@ -15,6 +19,10 @@ const NODE_META = { icon: SparklesIcon, tone: "bg-purple-50 text-purple-600 border-purple-100", }, + expression: { + icon: CodeIcon, + tone: "bg-sky-50 text-sky-600 border-sky-100", + }, } as const; function CanvasNodeBase({ diff --git a/studio/frontend/src/features/canvas-lab/dialogs/config-dialog.tsx b/studio/frontend/src/features/canvas-lab/dialogs/config-dialog.tsx index ca1a7f19c5..f047f804a0 100644 --- a/studio/frontend/src/features/canvas-lab/dialogs/config-dialog.tsx +++ b/studio/frontend/src/features/canvas-lab/dialogs/config-dialog.tsx @@ -12,6 +12,7 @@ import { UniformDialog } from "./samplers/uniform-dialog"; import { UuidDialog } from "./samplers/uuid-dialog"; import { DialogShell } from "./shared/dialog-shell"; import { ValidationBanner } from "./shared/validation-banner"; +import { ExpressionDialog } from "./expression/expression-dialog"; type ConfigDialogProps = { open: boolean; @@ -93,6 +94,12 @@ export function ConfigDialog({ onUpdate={(patch) => onUpdate(config.id, patch)} /> )} + {config.kind === "expression" && ( + onUpdate(config.id, patch)} + /> + )} )} diff --git a/studio/frontend/src/features/canvas-lab/dialogs/expression/expression-dialog.tsx b/studio/frontend/src/features/canvas-lab/dialogs/expression/expression-dialog.tsx new file mode 100644 index 0000000000..55756a3cc4 --- /dev/null +++ b/studio/frontend/src/features/canvas-lab/dialogs/expression/expression-dialog.tsx @@ -0,0 +1,83 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import type { ReactElement } from "react"; +import type { ExpressionConfig, ExpressionDtype } from "../../types"; +import { NameField } from "../shared/name-field"; + +const DTYPE_OPTIONS: ExpressionDtype[] = ["str", "int", "float", "bool"]; + +type ExpressionDialogProps = { + config: ExpressionConfig; + onUpdate: (patch: Partial) => void; +}; + +export function ExpressionDialog({ + config, + onUpdate, +}: ExpressionDialogProps): ReactElement { + const dtypeId = `${config.id}-dtype`; + const exprId = `${config.id}-expr`; + const updateField = ( + key: K, + value: ExpressionConfig[K], + ) => { + onUpdate({ [key]: value } as Partial); + }; + return ( +
+ onUpdate({ name: value })} + /> +
+ + +
+
+ +