refactor(canvas): unify node handles with shared wrapper
This commit is contained in:
@@ -28,6 +28,31 @@ vi.mock("@xyflow/react", () => ({
|
|||||||
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/canvas/canvas-handle", () => ({
|
||||||
|
default: ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
nodeId,
|
||||||
|
nodeType,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
id?: string;
|
||||||
|
type: "source" | "target";
|
||||||
|
nodeId: string;
|
||||||
|
nodeType?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
data-canvas-handle="true"
|
||||||
|
data-handle-id={id ?? ""}
|
||||||
|
data-handle-type={type}
|
||||||
|
data-node-id={nodeId}
|
||||||
|
data-node-type={nodeType ?? ""}
|
||||||
|
data-top={typeof style?.top === "string" ? style.top : ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../nodes/base-node-wrapper", () => ({
|
vi.mock("../nodes/base-node-wrapper", () => ({
|
||||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
}));
|
}));
|
||||||
@@ -261,4 +286,35 @@ describe("CompareNode render preview inputs", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders compare handles through CanvasHandle with preserved ids and positions", () => {
|
||||||
|
const markup = renderCompareNode({
|
||||||
|
id: "compare-1",
|
||||||
|
data: {},
|
||||||
|
selected: false,
|
||||||
|
dragging: false,
|
||||||
|
zIndex: 0,
|
||||||
|
isConnectable: true,
|
||||||
|
type: "compare",
|
||||||
|
xPos: 0,
|
||||||
|
yPos: 0,
|
||||||
|
width: 500,
|
||||||
|
height: 380,
|
||||||
|
sourcePosition: undefined,
|
||||||
|
targetPosition: undefined,
|
||||||
|
positionAbsoluteX: 0,
|
||||||
|
positionAbsoluteY: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(markup).toContain('data-canvas-handle="true"');
|
||||||
|
expect(markup).toContain('data-node-id="compare-1"');
|
||||||
|
expect(markup).toContain('data-node-type="compare"');
|
||||||
|
expect(markup).toContain('data-handle-id="left"');
|
||||||
|
expect(markup).toContain('data-handle-id="right"');
|
||||||
|
expect(markup).toContain('data-handle-id="compare-out"');
|
||||||
|
expect(markup).toContain('data-handle-type="target"');
|
||||||
|
expect(markup).toContain('data-handle-type="source"');
|
||||||
|
expect(markup).toContain('data-top="35%"');
|
||||||
|
expect(markup).toContain('data-top="55%"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,31 @@ vi.mock("@xyflow/react", () => ({
|
|||||||
Position: { Left: "left", Right: "right" },
|
Position: { Left: "left", Right: "right" },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/canvas/canvas-handle", () => ({
|
||||||
|
default: ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
nodeId,
|
||||||
|
nodeType,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
id?: string;
|
||||||
|
type: "source" | "target";
|
||||||
|
nodeId: string;
|
||||||
|
nodeType?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
data-canvas-handle="true"
|
||||||
|
data-handle-id={id ?? ""}
|
||||||
|
data-handle-type={type}
|
||||||
|
data-node-id={nodeId}
|
||||||
|
data-node-type={nodeType ?? ""}
|
||||||
|
data-top={typeof style?.top === "string" ? style.top : ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/components/canvas/canvas-sync-context", () => ({
|
vi.mock("@/components/canvas/canvas-sync-context", () => ({
|
||||||
useCanvasSync: () => ({
|
useCanvasSync: () => ({
|
||||||
queueNodeDataUpdate: mocks.queueNodeDataUpdate,
|
queueNodeDataUpdate: mocks.queueNodeDataUpdate,
|
||||||
@@ -222,8 +247,20 @@ describe("MixerNode", () => {
|
|||||||
it("renders expected mixer handles", async () => {
|
it("renders expected mixer handles", async () => {
|
||||||
await renderNode();
|
await renderNode();
|
||||||
|
|
||||||
expect(container?.querySelector('[data-handle-id="base"][data-handle-type="target"]')).toBeTruthy();
|
expect(
|
||||||
expect(container?.querySelector('[data-handle-id="overlay"][data-handle-type="target"]')).toBeTruthy();
|
container?.querySelector(
|
||||||
expect(container?.querySelector('[data-handle-id="mixer-out"][data-handle-type="source"]')).toBeTruthy();
|
'[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="base"][data-handle-type="target"][data-top="35%"]',
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
container?.querySelector(
|
||||||
|
'[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="overlay"][data-handle-type="target"][data-top="58%"]',
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
container?.querySelector(
|
||||||
|
'[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="mixer-out"][data-handle-type="source"]',
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Bot } from "lucide-react";
|
import { Bot } from "lucide-react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useAction } from "convex/react";
|
import { useAction } from "convex/react";
|
||||||
import type { FunctionReference } from "convex/server";
|
import type { FunctionReference } from "convex/server";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type AgentNodeData = {
|
type AgentNodeData = {
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
@@ -466,13 +467,17 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
|||||||
statusMessage={nodeData._statusMessage}
|
statusMessage={nodeData._statusMessage}
|
||||||
className="min-w-[300px] border-amber-500/30"
|
className="min-w-[300px] border-amber-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="agent"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="agent-in"
|
id="agent-in"
|
||||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="agent"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type AgentOutputNodeData = {
|
type AgentOutputNodeData = {
|
||||||
isSkeleton?: boolean;
|
isSkeleton?: boolean;
|
||||||
@@ -186,7 +187,7 @@ function partitionSections(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutputNodeType>) {
|
export default function AgentOutputNode({ id, data, selected }: NodeProps<AgentOutputNodeType>) {
|
||||||
const t = useTranslations("agentOutputNode");
|
const t = useTranslations("agentOutputNode");
|
||||||
const nodeData = data as AgentOutputNodeData;
|
const nodeData = data as AgentOutputNodeData;
|
||||||
const isSkeleton = nodeData.isSkeleton === true;
|
const isSkeleton = nodeData.isSkeleton === true;
|
||||||
@@ -240,7 +241,9 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
|||||||
statusMessage={nodeData._statusMessage}
|
statusMessage={nodeData._statusMessage}
|
||||||
className={`min-w-[300px] border-amber-500/30 ${isSkeleton ? "opacity-80" : ""}`}
|
className={`min-w-[300px] border-amber-500/30 ${isSkeleton ? "opacity-80" : ""}`}
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="agent-output"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="agent-output-in"
|
id="agent-output-in"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Handle, Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react";
|
import { Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react";
|
||||||
import { useAction } from "convex/react";
|
import { useAction } from "convex/react";
|
||||||
import { api } from "@/convex/_generated/api";
|
import { api } from "@/convex/_generated/api";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type AiImageNodeData = {
|
type AiImageNodeData = {
|
||||||
storageId?: string;
|
storageId?: string;
|
||||||
@@ -194,7 +195,9 @@ export default function AiImageNode({
|
|||||||
]}
|
]}
|
||||||
className="flex h-full w-full min-h-0 min-w-0 flex-col"
|
className="flex h-full w-full min-h-0 min-w-0 flex-col"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="ai-image"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="prompt-in"
|
id="prompt-in"
|
||||||
@@ -331,7 +334,9 @@ export default function AiImageNode({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="ai-image"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="image-out"
|
id="image-out"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAction } from "convex/react";
|
|||||||
import type { FunctionReference } from "convex/server";
|
import type { FunctionReference } from "convex/server";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { AlertCircle, Download, Loader2, RefreshCw, Video } from "lucide-react";
|
import { AlertCircle, Download, Loader2, RefreshCw, Video } from "lucide-react";
|
||||||
import { Handle, Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react";
|
||||||
|
|
||||||
import { api } from "@/convex/_generated/api";
|
import { api } from "@/convex/_generated/api";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
@@ -14,6 +14,7 @@ import { classifyError } from "@/lib/ai-errors";
|
|||||||
import { getVideoModel, type VideoModelDurationSeconds } from "@/lib/ai-video-models";
|
import { getVideoModel, type VideoModelDurationSeconds } from "@/lib/ai-video-models";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type AiVideoNodeData = {
|
type AiVideoNodeData = {
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
@@ -160,7 +161,9 @@ export default function AiVideoNode({ id, data, selected }: NodeProps<AiVideoNod
|
|||||||
statusMessage={nodeData._statusMessage}
|
statusMessage={nodeData._statusMessage}
|
||||||
className="flex h-full w-full min-h-0 min-w-0 flex-col"
|
className="flex h-full w-full min-h-0 min-w-0 flex-col"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="ai-video"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="video-in"
|
id="video-in"
|
||||||
@@ -240,7 +243,9 @@ export default function AiVideoNode({ id, data, selected }: NodeProps<AiVideoNod
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="ai-video"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="video-out"
|
id="video-out"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type MouseEvent,
|
type MouseEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Handle, Position, useStore, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, useStore, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { ExternalLink, ImageIcon } from "lucide-react";
|
import { ExternalLink, ImageIcon } from "lucide-react";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +21,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { resolveMediaAspectRatio } from "@/lib/canvas-utils";
|
import { resolveMediaAspectRatio } from "@/lib/canvas-utils";
|
||||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type AssetNodeData = {
|
type AssetNodeData = {
|
||||||
assetId?: number;
|
assetId?: number;
|
||||||
@@ -152,7 +153,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
|||||||
status={data._status}
|
status={data._status}
|
||||||
statusMessage={data._statusMessage}
|
statusMessage={data._statusMessage}
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="asset"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||||
@@ -273,7 +276,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="asset"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Palette } from "lucide-react";
|
import { Palette } from "lucide-react";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
|||||||
import { COLOR_PRESETS } from "@/lib/image-pipeline/presets";
|
import { COLOR_PRESETS } from "@/lib/image-pipeline/presets";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type ColorAdjustNodeData = ColorAdjustData & {
|
type ColorAdjustNodeData = ColorAdjustData & {
|
||||||
_status?: string;
|
_status?: string;
|
||||||
@@ -191,7 +192,9 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
|
|||||||
statusMessage={data._statusMessage}
|
statusMessage={data._statusMessage}
|
||||||
className="min-w-[300px] border-cyan-500/30"
|
className="min-w-[300px] border-cyan-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="color-adjust"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
|
||||||
@@ -268,7 +271,9 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="color-adjust"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
import { Position, type NodeProps } from "@xyflow/react";
|
||||||
import { ImageIcon } from "lucide-react";
|
import { ImageIcon } from "lucide-react";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
import CompareSurface from "./compare-surface";
|
import CompareSurface from "./compare-surface";
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
resolveMixerPreviewFromGraph,
|
resolveMixerPreviewFromGraph,
|
||||||
type MixerPreviewState,
|
type MixerPreviewState,
|
||||||
} from "@/lib/canvas-mixer-preview";
|
} from "@/lib/canvas-mixer-preview";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
interface CompareNodeData {
|
interface CompareNodeData {
|
||||||
leftUrl?: string;
|
leftUrl?: string;
|
||||||
@@ -242,21 +243,27 @@ export default function CompareNode({ id, data, selected, width }: NodeProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNodeWrapper nodeType="compare" selected={selected} className="p-0">
|
<BaseNodeWrapper nodeType="compare" selected={selected} className="p-0">
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="compare"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="left"
|
id="left"
|
||||||
style={{ top: "35%" }}
|
style={{ top: "35%" }}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-blue-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-blue-500"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="compare"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="right"
|
id="right"
|
||||||
style={{ top: "55%" }}
|
style={{ top: "55%" }}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="compare"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="compare-out"
|
id="compare-out"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useRef, type PointerEvent as ReactPointerEvent } from "react";
|
import { useCallback, useMemo, useRef, type PointerEvent as ReactPointerEvent } from "react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { Crop } from "lucide-react";
|
import { Crop } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type CropNodeViewData = CropNodeData & {
|
type CropNodeViewData = CropNodeData & {
|
||||||
_status?: string;
|
_status?: string;
|
||||||
@@ -400,7 +401,9 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
|
|||||||
statusMessage={data._statusMessage}
|
statusMessage={data._statusMessage}
|
||||||
className="min-w-[320px] border-violet-500/30"
|
className="min-w-[320px] border-violet-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="crop"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
|
||||||
@@ -735,7 +738,9 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
|
|||||||
{error ? <p className="text-[11px] text-destructive">{error}</p> : null}
|
{error ? <p className="text-[11px] text-destructive">{error}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="crop"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { TrendingUp } from "lucide-react";
|
import { TrendingUp } from "lucide-react";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
|||||||
import { CURVE_PRESETS } from "@/lib/image-pipeline/presets";
|
import { CURVE_PRESETS } from "@/lib/image-pipeline/presets";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type CurvesNodeData = CurvesData & {
|
type CurvesNodeData = CurvesData & {
|
||||||
_status?: string;
|
_status?: string;
|
||||||
@@ -163,7 +164,9 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
|
|||||||
statusMessage={data._statusMessage}
|
statusMessage={data._statusMessage}
|
||||||
className="min-w-[300px] border-emerald-500/30"
|
className="min-w-[300px] border-emerald-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="curves"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
||||||
@@ -237,7 +240,9 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="curves"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Focus } from "lucide-react";
|
import { Focus } from "lucide-react";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
|||||||
import { DETAIL_PRESETS } from "@/lib/image-pipeline/presets";
|
import { DETAIL_PRESETS } from "@/lib/image-pipeline/presets";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type DetailAdjustNodeData = DetailAdjustData & {
|
type DetailAdjustNodeData = DetailAdjustData & {
|
||||||
_status?: string;
|
_status?: string;
|
||||||
@@ -202,7 +203,9 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
|
|||||||
statusMessage={data._statusMessage}
|
statusMessage={data._statusMessage}
|
||||||
className="min-w-[300px] border-indigo-500/30"
|
className="min-w-[300px] border-indigo-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="detail-adjust"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
|
||||||
@@ -286,7 +289,9 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="detail-adjust"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
import { Position, type NodeProps } from "@xyflow/react";
|
||||||
import { useAction } from "convex/react";
|
import { useAction } from "convex/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { Download, Loader2 } from "lucide-react";
|
||||||
@@ -11,6 +11,7 @@ import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
|||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
interface FrameNodeData {
|
interface FrameNodeData {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -125,13 +126,17 @@ export default function FrameNode({ id, data, selected, width, height }: NodePro
|
|||||||
|
|
||||||
<div className="nodrag h-full w-full" />
|
<div className="nodrag h-full w-full" />
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="frame"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="frame-in"
|
id="frame-in"
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-orange-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-orange-500"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="frame"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="frame-out"
|
id="frame-out"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
import { Position, type NodeProps, type Node } from "@xyflow/react";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type GroupNodeData = {
|
type GroupNodeData = {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -47,7 +48,9 @@ export default function GroupNode({ id, data, selected }: NodeProps<GroupNode>)
|
|||||||
selected={selected}
|
selected={selected}
|
||||||
className="min-w-[200px] min-h-[150px] p-3 border-dashed"
|
className="min-w-[200px] min-h-[150px] p-3 border-dashed"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="group"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
|
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
|
||||||
@@ -71,7 +74,9 @@ export default function GroupNode({ id, data, selected }: NodeProps<GroupNode>)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="group"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
|
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type DragEvent,
|
type DragEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
import { Position, type NodeProps, type Node } from "@xyflow/react";
|
||||||
import { Maximize2, X } from "lucide-react";
|
import { Maximize2, X } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { api } from "@/convex/_generated/api";
|
import { api } from "@/convex/_generated/api";
|
||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
getImageDimensions,
|
getImageDimensions,
|
||||||
} from "@/components/canvas/canvas-media-utils";
|
} from "@/components/canvas/canvas-media-utils";
|
||||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
const ALLOWED_IMAGE_TYPES = new Set([
|
const ALLOWED_IMAGE_TYPES = new Set([
|
||||||
"image/png",
|
"image/png",
|
||||||
@@ -508,7 +509,9 @@ export default function ImageNode({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="image"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="h-3! w-3! bg-primary! border-2! border-background!"
|
className="h-3! w-3! bg-primary! border-2! border-background!"
|
||||||
@@ -609,7 +612,9 @@ export default function ImageNode({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="image"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="h-3! w-3! bg-primary! border-2! border-background!"
|
className="h-3! w-3! bg-primary! border-2! border-background!"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Sun } from "lucide-react";
|
import { Sun } from "lucide-react";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
|||||||
import { LIGHT_PRESETS } from "@/lib/image-pipeline/presets";
|
import { LIGHT_PRESETS } from "@/lib/image-pipeline/presets";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type LightAdjustNodeData = LightAdjustData & {
|
type LightAdjustNodeData = LightAdjustData & {
|
||||||
_status?: string;
|
_status?: string;
|
||||||
@@ -213,7 +214,9 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
|
|||||||
statusMessage={data._statusMessage}
|
statusMessage={data._statusMessage}
|
||||||
className="min-w-[300px] border-amber-500/30"
|
className="min-w-[300px] border-amber-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="light-adjust"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
|
||||||
@@ -292,7 +295,9 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="light-adjust"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState, type ChangeEvent, type FormEvent } from "react";
|
import { useMemo, useState, type ChangeEvent, type FormEvent } from "react";
|
||||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
import { Position, type NodeProps } from "@xyflow/react";
|
||||||
|
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
import { useCanvasGraph } from "@/components/canvas/canvas-graph-context";
|
import { useCanvasGraph } from "@/components/canvas/canvas-graph-context";
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
type MixerBlendMode,
|
type MixerBlendMode,
|
||||||
} from "@/lib/canvas-mixer-preview";
|
} from "@/lib/canvas-mixer-preview";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
const BLEND_MODE_OPTIONS: MixerBlendMode[] = ["normal", "multiply", "screen", "overlay"];
|
const BLEND_MODE_OPTIONS: MixerBlendMode[] = ["normal", "multiply", "screen", "overlay"];
|
||||||
|
|
||||||
@@ -56,21 +57,27 @@ export default function MixerNode({ id, data, selected }: NodeProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNodeWrapper nodeType="mixer" selected={selected} className="p-0">
|
<BaseNodeWrapper nodeType="mixer" selected={selected} className="p-0">
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="mixer"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="base"
|
id="base"
|
||||||
style={{ top: "35%" }}
|
style={{ top: "35%" }}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="mixer"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="overlay"
|
id="overlay"
|
||||||
style={{ top: "58%" }}
|
style={{ top: "58%" }}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-pink-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-pink-500"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="mixer"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="mixer-out"
|
id="mixer-out"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
import { Position, type NodeProps, type Node } from "@xyflow/react";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type NoteNodeData = {
|
type NoteNodeData = {
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -53,7 +54,9 @@ export default function NoteNode({ id, data, selected }: NodeProps<NoteNode>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNodeWrapper nodeType="note" selected={selected} className="p-3">
|
<BaseNodeWrapper nodeType="note" selected={selected} className="p-3">
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="note"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||||
@@ -85,7 +88,9 @@ export default function NoteNode({ id, data, selected }: NodeProps<NoteNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="note"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Handle,
|
|
||||||
Position,
|
Position,
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
useStore,
|
useStore,
|
||||||
@@ -45,6 +44,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
import { classifyError } from "@/lib/ai-errors";
|
import { classifyError } from "@/lib/ai-errors";
|
||||||
import { normalizePublicTier } from "@/lib/tier-credits";
|
import { normalizePublicTier } from "@/lib/tier-credits";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type PromptNodeData = {
|
type PromptNodeData = {
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
@@ -353,7 +353,9 @@ export default function PromptNode({
|
|||||||
statusMessage={nodeData._statusMessage}
|
statusMessage={nodeData._statusMessage}
|
||||||
className="min-w-[240px] border-violet-500/30"
|
className="min-w-[240px] border-violet-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="prompt"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="image-in"
|
id="image-in"
|
||||||
@@ -489,7 +491,9 @@ export default function PromptNode({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="prompt"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="prompt-out"
|
id="prompt-out"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { AlertCircle, ArrowDown, CheckCircle2, CloudUpload, Loader2, Maximize2, X } from "lucide-react";
|
import { AlertCircle, ArrowDown, CheckCircle2, CloudUpload, Loader2, Maximize2, X } from "lucide-react";
|
||||||
import { useMutation } from "convex/react";
|
import { useMutation } from "convex/react";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type RenderResolutionOption = "original" | "2x" | "custom";
|
type RenderResolutionOption = "original" | "2x" | "custom";
|
||||||
type RenderFormatOption = "png" | "jpeg" | "webp";
|
type RenderFormatOption = "png" | "jpeg" | "webp";
|
||||||
@@ -978,7 +979,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
|||||||
]}
|
]}
|
||||||
className="flex h-full min-w-[280px] flex-col overflow-hidden border-sky-500/30"
|
className="flex h-full min-w-[280px] flex-col overflow-hidden border-sky-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="render"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
||||||
@@ -1273,7 +1276,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="render"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Handle,
|
|
||||||
Position,
|
Position,
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type TextNodeData = {
|
type TextNodeData = {
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -155,7 +155,9 @@ export default function TextNode({ id, data, selected }: NodeProps<TextNode>) {
|
|||||||
]}
|
]}
|
||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="text"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||||
@@ -190,7 +192,9 @@ export default function TextNode({ id, data, selected }: NodeProps<TextNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="text"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Handle, Position, useStore, type NodeProps } from "@xyflow/react";
|
import { Position, useStore, type NodeProps } from "@xyflow/react";
|
||||||
import { useAction } from "convex/react";
|
import { useAction } from "convex/react";
|
||||||
import { Play } from "lucide-react";
|
import { Play } from "lucide-react";
|
||||||
import BaseNodeWrapper from "./base-node-wrapper";
|
import BaseNodeWrapper from "./base-node-wrapper";
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { api } from "@/convex/_generated/api";
|
import { api } from "@/convex/_generated/api";
|
||||||
import type { Id } from "@/convex/_generated/dataModel";
|
import type { Id } from "@/convex/_generated/dataModel";
|
||||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type VideoNodeData = {
|
type VideoNodeData = {
|
||||||
canvasId?: string;
|
canvasId?: string;
|
||||||
@@ -150,7 +151,9 @@ export default function VideoNode({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNodeWrapper nodeType="video" selected={selected}>
|
<BaseNodeWrapper nodeType="video" selected={selected}>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="video"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||||
@@ -245,7 +248,9 @@ export default function VideoNode({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="video"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Handle, Position, useReactFlow, useStore, type Node, type NodeProps } from "@xyflow/react";
|
import { Position, useReactFlow, useStore, type Node, type NodeProps } from "@xyflow/react";
|
||||||
import { useAction } from "convex/react";
|
import { useAction } from "convex/react";
|
||||||
import type { FunctionReference } from "convex/server";
|
import type { FunctionReference } from "convex/server";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||||
|
|
||||||
type VideoPromptNodeData = {
|
type VideoPromptNodeData = {
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
@@ -300,7 +301,9 @@ export default function VideoPromptNode({
|
|||||||
statusMessage={nodeData._statusMessage}
|
statusMessage={nodeData._statusMessage}
|
||||||
className="min-w-[260px] border-violet-500/30"
|
className="min-w-[260px] border-violet-500/30"
|
||||||
>
|
>
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="video-prompt"
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="video-prompt-in"
|
id="video-prompt-in"
|
||||||
@@ -407,7 +410,9 @@ export default function VideoPromptNode({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle
|
<CanvasHandle
|
||||||
|
nodeId={id}
|
||||||
|
nodeType="video-prompt"
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="video-prompt-out"
|
id="video-prompt-out"
|
||||||
|
|||||||
Reference in New Issue
Block a user