ui buttons move, squircle boxes, and resize
This commit is contained in:
parent
ac3e60138f
commit
e0404966d7
4 changed files with 213 additions and 109 deletions
|
|
@ -126,6 +126,7 @@ export function CanvasLabPage(): ReactElement {
|
|||
null,
|
||||
);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [processorsOpen, setProcessorsOpen] = useState(false);
|
||||
const [statusMessage, setStatusMessage] = useState<{
|
||||
|
|
@ -184,6 +185,7 @@ export function CanvasLabPage(): ReactElement {
|
|||
};
|
||||
|
||||
const handleCopyRecipe = async (): Promise<void> => {
|
||||
setCopied(false);
|
||||
setStatusMessage(null);
|
||||
const { payload, errors } = buildCanvasPayload(
|
||||
configs,
|
||||
|
|
@ -207,6 +209,8 @@ export function CanvasLabPage(): ReactElement {
|
|||
}
|
||||
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.",
|
||||
|
|
@ -265,24 +269,6 @@ export function CanvasLabPage(): ReactElement {
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-start gap-2 lg:justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCopyRecipe}
|
||||
className="text-xs"
|
||||
>
|
||||
Copy recipe
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setImportOpen(true)}
|
||||
className="text-xs"
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
|
|
@ -311,7 +297,7 @@ export function CanvasLabPage(): ReactElement {
|
|||
edgeTypes={EDGE_TYPES}
|
||||
defaultEdgeOptions={{
|
||||
type: "canvas",
|
||||
data: { key: "name", path: "smoothstep" },
|
||||
data: { key: "name", path: "auto" },
|
||||
style: { strokeWidth: 1.5, stroke: "var(--border)" },
|
||||
}}
|
||||
onNodesChange={onNodesChange}
|
||||
|
|
@ -344,6 +330,9 @@ export function CanvasLabPage(): ReactElement {
|
|||
onAddModelConfig={addModelConfigNode}
|
||||
onAddExpression={addExpressionNode}
|
||||
onOpenProcessors={handleOpenProcessorsFromSheet}
|
||||
copied={copied}
|
||||
onCopy={handleCopyRecipe}
|
||||
onImport={() => setImportOpen(true)}
|
||||
/>
|
||||
</Panel>
|
||||
<Controls position="bottom-left" />
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ import {
|
|||
ArrowLeft02Icon,
|
||||
ArrowRight01Icon,
|
||||
CodeIcon,
|
||||
Copy02Icon,
|
||||
type Database02Icon,
|
||||
PlusSignIcon,
|
||||
Tick02Icon,
|
||||
Upload01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { ReactElement } from "react";
|
||||
|
|
@ -38,6 +41,9 @@ type BlockSheetProps = {
|
|||
onAddModelConfig: () => void;
|
||||
onAddExpression: () => void;
|
||||
onOpenProcessors: () => void;
|
||||
copied: boolean;
|
||||
onCopy: () => void;
|
||||
onImport: () => void;
|
||||
};
|
||||
|
||||
function getSheetTitle(view: SheetView): string {
|
||||
|
|
@ -112,6 +118,9 @@ function BlockSheetButton({
|
|||
);
|
||||
}
|
||||
|
||||
const 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";
|
||||
|
||||
export function BlockSheet({
|
||||
container,
|
||||
view,
|
||||
|
|
@ -122,97 +131,129 @@ export function BlockSheet({
|
|||
onAddModelConfig,
|
||||
onAddExpression,
|
||||
onOpenProcessors,
|
||||
copied,
|
||||
onCopy,
|
||||
onImport,
|
||||
}: BlockSheetProps): ReactElement {
|
||||
const title = getSheetTitle(view);
|
||||
return (
|
||||
<Sheet
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
onViewChange("root");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetTrigger asChild={true}>
|
||||
<Button size="lg" className={"corner-squircle "} variant="secondary">
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="size-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="right"
|
||||
container={container}
|
||||
position="absolute"
|
||||
overlayPosition="absolute"
|
||||
className="absolute gap-0 p-0 shadow-none"
|
||||
overlayClassName="bg-transparent pointer-events-none backdrop-blur-none supports-backdrop-filter:backdrop-blur-none"
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<Sheet
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
onViewChange("root");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="border-b border-border/60 px-6 py-5">
|
||||
<div className="flex items-center gap-2">
|
||||
{view !== "root" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onViewChange("root")}
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className=" py-4">
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{view === "root" &&
|
||||
ROOT_GROUPS.map((item, index) => (
|
||||
<BlockSheetButton
|
||||
key={item.kind}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
isActive={index === 0}
|
||||
onClick={() => onViewChange(item.kind)}
|
||||
/>
|
||||
))}
|
||||
{view === "processor" && (
|
||||
<BlockSheetButton
|
||||
icon={CodeIcon}
|
||||
title="Schema Transform"
|
||||
description="Transform final dataset schema."
|
||||
isActive={true}
|
||||
onClick={onOpenProcessors}
|
||||
/>
|
||||
)}
|
||||
{view !== "root" &&
|
||||
view !== "processor" &&
|
||||
getBlocksForKind(VIEW_KIND[view] ?? "sampler").map(
|
||||
(item, index) => (
|
||||
<BlockSheetButton
|
||||
key={item.type}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
isActive={index === 0}
|
||||
onClick={() => {
|
||||
if (item.kind === "sampler") {
|
||||
onAddSampler(item.type as SamplerType);
|
||||
} else if (item.kind === "llm") {
|
||||
if (item.type === "model_provider") {
|
||||
onAddModelProvider();
|
||||
} else if (item.type === "model_config") {
|
||||
onAddModelConfig();
|
||||
} else {
|
||||
onAddLlm(item.type as LlmType);
|
||||
}
|
||||
} else {
|
||||
onAddExpression();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
<SheetTrigger asChild={true}>
|
||||
<Button size="icon" className={FLOATING_ICON_BUTTON_CLASS} variant="ghost">
|
||||
<HugeiconsIcon
|
||||
icon={PlusSignIcon}
|
||||
className="size-5 text-muted-foreground group-hover:text-primary"
|
||||
/>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="right"
|
||||
container={container}
|
||||
position="absolute"
|
||||
overlayPosition="absolute"
|
||||
className="absolute gap-0 p-0 shadow-none"
|
||||
overlayClassName="bg-transparent pointer-events-none backdrop-blur-none supports-backdrop-filter:backdrop-blur-none"
|
||||
>
|
||||
<SheetHeader className="border-b border-border/60 px-6 py-5">
|
||||
<div className="flex items-center gap-2">
|
||||
{view !== "root" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onViewChange("root")}
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className=" py-4">
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{view === "root" &&
|
||||
ROOT_GROUPS.map((item, index) => (
|
||||
<BlockSheetButton
|
||||
key={item.kind}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
isActive={index === 0}
|
||||
onClick={() => onViewChange(item.kind)}
|
||||
/>
|
||||
))}
|
||||
{view === "processor" && (
|
||||
<BlockSheetButton
|
||||
icon={CodeIcon}
|
||||
title="Schema Transform"
|
||||
description="Transform final dataset schema."
|
||||
isActive={true}
|
||||
onClick={onOpenProcessors}
|
||||
/>
|
||||
)}
|
||||
{view !== "root" &&
|
||||
view !== "processor" &&
|
||||
getBlocksForKind(VIEW_KIND[view] ?? "sampler").map(
|
||||
(item, index) => (
|
||||
<BlockSheetButton
|
||||
key={item.type}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
isActive={index === 0}
|
||||
onClick={() => {
|
||||
if (item.kind === "sampler") {
|
||||
onAddSampler(item.type as SamplerType);
|
||||
} else if (item.kind === "llm") {
|
||||
if (item.type === "model_provider") {
|
||||
onAddModelProvider();
|
||||
} else if (item.type === "model_config") {
|
||||
onAddModelConfig();
|
||||
} else {
|
||||
onAddLlm(item.type as LlmType);
|
||||
}
|
||||
} else {
|
||||
onAddExpression();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={FLOATING_ICON_BUTTON_CLASS}
|
||||
onClick={onImport}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={Upload01Icon}
|
||||
className="size-5 text-muted-foreground group-hover:text-primary"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={FLOATING_ICON_BUTTON_CLASS}
|
||||
onClick={onCopy}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={copied ? Tick02Icon : Copy02Icon}
|
||||
className="size-5 text-muted-foreground group-hover:text-primary"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ import {
|
|||
UserAccountIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Position, useUpdateNodeInternals, type NodeProps } from "@xyflow/react";
|
||||
import {
|
||||
NodeResizer,
|
||||
Position,
|
||||
useUpdateNodeInternals,
|
||||
type NodeProps,
|
||||
} from "@xyflow/react";
|
||||
import { memo, type ReactElement, useEffect } from "react";
|
||||
import { useCanvasLabStore } from "../stores/canvas-lab";
|
||||
import type {
|
||||
|
|
@ -223,7 +228,11 @@ function renderInlineEditor(
|
|||
return null;
|
||||
}
|
||||
|
||||
function CanvasNodeBase({ id, data }: NodeProps<CanvasNodeType>): ReactElement {
|
||||
function CanvasNodeBase({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<CanvasNodeType>): ReactElement {
|
||||
const meta = NODE_META[data.kind];
|
||||
const icon = resolveNodeIcon(data.kind, data.blockType);
|
||||
const layoutDirection = data.layoutDirection ?? "LR";
|
||||
|
|
@ -251,7 +260,19 @@ function CanvasNodeBase({ id, data }: NodeProps<CanvasNodeType>): ReactElement {
|
|||
const summary = getConfigSummary(config);
|
||||
|
||||
return (
|
||||
<BaseNode className="relative min-w-[260px] overflow-visible rounded-xl border-border/60 shadow-sm">
|
||||
<BaseNode className="corner-squircle relative min-w-[260px] overflow-visible rounded-lg border-border/60 shadow-sm">
|
||||
<NodeResizer
|
||||
isVisible={selected}
|
||||
minWidth={260}
|
||||
minHeight={120}
|
||||
maxWidth={760}
|
||||
maxHeight={520}
|
||||
color="var(--primary)"
|
||||
lineClassName="!border-transparent !shadow-none"
|
||||
lineStyle={{ opacity: 0 }}
|
||||
handleClassName="!h-3 !w-3 !border-transparent !bg-transparent"
|
||||
handleStyle={{ opacity: 0 }}
|
||||
/>
|
||||
<BaseNodeHeader className="border-b border-border/50 px-3 py-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import {
|
|||
|
||||
export type DataEdge<T extends Node = Node> = Edge<{
|
||||
key?: keyof T["data"];
|
||||
path?: "bezier" | "smoothstep" | "step" | "straight";
|
||||
path?: "auto" | "bezier" | "smoothstep" | "step" | "straight";
|
||||
}>;
|
||||
|
||||
export function DataEdge({
|
||||
data = { path: "bezier" },
|
||||
data = { path: "auto" },
|
||||
id,
|
||||
markerEnd,
|
||||
source,
|
||||
|
|
@ -31,8 +31,17 @@ export function DataEdge({
|
|||
targetY,
|
||||
}: EdgeProps<DataEdge>): ReactElement {
|
||||
const nodeData = useStore((state) => state.nodeLookup.get(source)?.data);
|
||||
const resolvedPathType = resolvePathType({
|
||||
type: data.path ?? "auto",
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
});
|
||||
const [edgePath, labelX, labelY] = getPath({
|
||||
type: data.path ?? "bezier",
|
||||
type: resolvedPathType,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
|
|
@ -128,3 +137,47 @@ function getPath({
|
|||
targetY,
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePathType({
|
||||
type,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
}: {
|
||||
type: "auto" | "bezier" | "smoothstep" | "step" | "straight";
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
sourcePosition: Position;
|
||||
targetPosition: Position;
|
||||
}): "bezier" | "smoothstep" | "step" | "straight" {
|
||||
if (type !== "auto") {
|
||||
return type;
|
||||
}
|
||||
|
||||
const isVerticalFlow =
|
||||
(sourcePosition === Position.Bottom && targetPosition === Position.Top) ||
|
||||
(sourcePosition === Position.Top && targetPosition === Position.Bottom);
|
||||
if (isVerticalFlow && Math.abs(sourceX - targetX) <= 18) {
|
||||
return "straight";
|
||||
}
|
||||
|
||||
const isHorizontalFlow =
|
||||
(sourcePosition === Position.Right && targetPosition === Position.Left) ||
|
||||
(sourcePosition === Position.Left && targetPosition === Position.Right);
|
||||
if (isHorizontalFlow && Math.abs(sourceY - targetY) <= 18) {
|
||||
return "straight";
|
||||
}
|
||||
|
||||
const deltaX = Math.abs(sourceX - targetX);
|
||||
const deltaY = Math.abs(sourceY - targetY);
|
||||
if (deltaX < 40 || deltaY < 40) {
|
||||
return "smoothstep";
|
||||
}
|
||||
|
||||
return "bezier";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue