diff --git a/next.config.ts b/next.config.ts index 969b7994..2160d996 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,7 @@ const nextConfig: NextConfig = { reactStrictMode: true, output: "standalone", experimental: { + proxyClientMaxBodySize: "32mb", serverActions: { bodySizeLimit: "100mb", // Increased for large media files }, diff --git a/src/app/api/popi/media/upload/route.ts b/src/app/api/popi/media/upload/route.ts index fe6f30c4..fe92ca14 100644 --- a/src/app/api/popi/media/upload/route.ts +++ b/src/app/api/popi/media/upload/route.ts @@ -189,7 +189,6 @@ export async function POST(request: NextRequest) { ? body.fileName.trim() : `canvas-media-${Date.now()}.${extension}`; const fileName = rawFileName.includes(".") ? rawFileName : `${rawFileName}.${extension}`; - const upstream = await fetch(getPopiBaseUrl(request) + POPI_MEDIA_UPLOAD_BASE64_PATH, { method: "POST", headers: { diff --git a/src/components/GroupsOverlay.tsx b/src/components/GroupsOverlay.tsx index 43cee3e2..bd31db96 100644 --- a/src/components/GroupsOverlay.tsx +++ b/src/components/GroupsOverlay.tsx @@ -30,8 +30,58 @@ interface GroupBackgroundProps { // Renders just the group background - displayed below nodes (z-index 1) function GroupBackground({ groupId }: GroupBackgroundProps) { - const { groups } = useWorkflowStore(); + const { groups, updateGroup, moveGroupNodes } = useWorkflowStore(); const group = groups[groupId]; + const { zoom } = useViewport(); + const [isDragging, setIsDragging] = useState(false); + const dragStartRef = useRef<{ x: number; y: number } | null>(null); + + const handleBackgroundMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button !== 0) return; + + e.stopPropagation(); + e.preventDefault(); + setIsDragging(true); + dragStartRef.current = { x: e.clientX, y: e.clientY }; + }, + [] + ); + + useEffect(() => { + if (!isDragging || !group) return; + + const handleMouseMove = (e: MouseEvent) => { + if (!dragStartRef.current) return; + + const deltaX = (e.clientX - dragStartRef.current.x) / zoom; + const deltaY = (e.clientY - dragStartRef.current.y) / zoom; + + if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) { + updateGroup(groupId, { + position: { + x: group.position.x + deltaX, + y: group.position.y + deltaY, + }, + }); + moveGroupNodes(groupId, { x: deltaX, y: deltaY }); + dragStartRef.current = { x: e.clientX, y: e.clientY }; + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + dragStartRef.current = null; + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isDragging, group, groupId, moveGroupNodes, updateGroup, zoom]); if (!group) return null; @@ -39,7 +89,8 @@ function GroupBackground({ groupId }: GroupBackgroundProps) { return (
); @@ -531,7 +582,7 @@ export function GroupBackgroundsPortal() { return ( -
+
{groupIds.map((groupId) => ( ))} diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 5a5cd896..dd0357ed 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -2586,29 +2586,14 @@ export function WorkflowCanvas() { fitViewOptions={FIT_VIEW_OPTIONS} deleteKeyCode={["Backspace", "Delete"]} multiSelectionKeyCode="Shift" - selectionOnDrag={ - canvasNavigationSettings.selectionMode === "altDrag" || canvasNavigationSettings.selectionMode === "shiftDrag" - ? false - : canvasNavigationSettings.panMode === "always" - ? false - : isMacOSClient && !isModalOpen - } - selectionKeyCode={ - isModalOpen ? null - : canvasNavigationSettings.selectionMode === "altDrag" ? "Alt" - : canvasNavigationSettings.selectionMode === "shiftDrag" ? "Shift" - : "Shift" - } + selectionOnDrag={tutorialActive ? false : !isModalOpen} + selectionKeyCode={null} panOnDrag={ tutorialActive ? false : isModalOpen ? false - : canvasNavigationSettings.panMode === "always" - ? true - : canvasNavigationSettings.panMode === "middleMouse" - ? [2] - : !isMacOSClient + : false } selectNodesOnDrag={false} nodeDragThreshold={5} @@ -2623,9 +2608,7 @@ export function WorkflowCanvas() { ? null : isModalOpen ? null - : canvasNavigationSettings.panMode === "space" - ? "Space" - : null + : "Space" } nodesDraggable={!isModalOpen} nodesConnectable={!isModalOpen} diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 44691cc7..089fffc2 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -381,7 +381,7 @@ describe("GenerationComposer", () => { expect(screen.getByPlaceholderText(/Describe what you want to generate/)).toHaveClass("h-36"); }); - it("places the selected node editor above the node when below would leave the viewport", () => { + it("keeps the selected node editor below the node when below would leave the viewport", () => { const originalInnerHeight = window.innerHeight; const originalInnerWidth = window.innerWidth; Object.defineProperty(window, "innerHeight", { configurable: true, value: 500 }); @@ -400,7 +400,7 @@ describe("GenerationComposer", () => { expect(screen.getByTestId("node-inline-composer")).toHaveStyle({ left: "16px", - top: "104px", + top: "536px", }); Object.defineProperty(window, "innerHeight", { configurable: true, value: originalInnerHeight }); Object.defineProperty(window, "innerWidth", { configurable: true, value: originalInnerWidth }); @@ -1187,7 +1187,6 @@ describe("GenerationComposer", () => { }); render(); - expect(screen.getByText("Video Stitch")).toBeInTheDocument(); expect(screen.getByText("2 video clips")).toBeInTheDocument(); expect(screen.getByText("Video inputs")).toBeInTheDocument(); expect(screen.getByText("Clip 1")).toBeInTheDocument(); @@ -1224,7 +1223,6 @@ describe("GenerationComposer", () => { }); render(); - expect(screen.getByText("Ease Curve")).toBeInTheDocument(); expect(screen.getByText("Video connected")).toBeInTheDocument(); expect(screen.getByText("Curve settings")).toBeInTheDocument(); expect(screen.getByText("easeInOutSine")).toBeInTheDocument(); diff --git a/src/components/__tests__/GroupsOverlay.test.tsx b/src/components/__tests__/GroupsOverlay.test.tsx index 84cf1888..7573cbbf 100644 --- a/src/components/__tests__/GroupsOverlay.test.tsx +++ b/src/components/__tests__/GroupsOverlay.test.tsx @@ -147,7 +147,7 @@ describe("GroupBackgroundsPortal", () => { expect(style).toContain("border"); }); - it("should set pointerEvents to none on backgrounds", () => { + it("should make backgrounds interactive for group dragging", () => { mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ groups: { "group-1": createMockGroup() }, @@ -156,7 +156,32 @@ describe("GroupBackgroundsPortal", () => { const { container } = render(); const background = container.querySelector(".rounded-xl"); - expect(background).toHaveStyle({ pointerEvents: "none" }); + expect(background).toHaveStyle({ pointerEvents: "auto" }); + }); + + it("should drag a group from its background", () => { + mockUseWorkflowStore.mockImplementation((selector) => { + return selector(createDefaultState({ + groups: { "group-1": createMockGroup() }, + })); + }); + + const { container } = render(); + const background = container.querySelector(".rounded-xl") as HTMLElement; + + fireEvent.mouseDown(background, { button: 0, clientX: 100, clientY: 100 }); + act(() => { + fireEvent.mouseMove(window, { clientX: 130, clientY: 120 }); + }); + + expect(mockUpdateGroup).toHaveBeenCalledWith("group-1", { + position: { x: 130, y: 120 }, + }); + expect(mockMoveGroupNodes).toHaveBeenCalledWith("group-1", { x: 30, y: 20 }); + + act(() => { + fireEvent.mouseUp(window); + }); }); }); }); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 81a3d661..14e5a247 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -124,8 +124,6 @@ const MAX_REFERENCE_IMAGE_BYTES = 10 * 1024 * 1024; const COMPOSER_VIEWPORT_MARGIN = 16; const COMPOSER_EXPANDED_WIDTH = 640; const COMPOSER_COLLAPSED_WIDTH = 560; -const COMPOSER_EXPANDED_HEIGHT_ESTIMATE = 300; -const COMPOSER_COLLAPSED_HEIGHT_ESTIMATE = 80; const DEFAULT_IMAGE_COUNT_OPTIONS: ImageGenerationCount[] = [1, 2, 4]; interface SchemaSelectOption { @@ -1408,16 +1406,10 @@ export function GenerationComposer() { ); const safeZoom = zoom > 0 ? zoom : 1; const viewportWidth = typeof window === "undefined" ? Number.POSITIVE_INFINITY : window.innerWidth; - const viewportHeight = typeof window === "undefined" ? Number.POSITIVE_INFINITY : window.innerHeight; const visibleLeft = -viewportX / safeZoom; const visibleRight = (viewportWidth - viewportX) / safeZoom; - const visibleTop = -viewportY / safeZoom; - const visibleBottom = (viewportHeight - viewportY) / safeZoom; const composerWidth = isComposerCollapsed ? COMPOSER_COLLAPSED_WIDTH : COMPOSER_EXPANDED_WIDTH; const composerFlowWidth = composerWidth / safeZoom; - const composerHeightEstimate = isComposerCollapsed - ? COMPOSER_COLLAPSED_HEIGHT_ESTIMATE - : COMPOSER_EXPANDED_HEIGHT_ESTIMATE; const viewportMargin = COMPOSER_VIEWPORT_MARGIN / safeZoom; const inlineComposerPosition = context.node && selectedNodeDimensions ? (() => { @@ -1428,13 +1420,10 @@ export function GenerationComposer() { ? Math.min(Math.max(centeredLeft, minLeft), maxLeft) : minLeft; const belowTop = context.node.position.y + selectedNodeDimensions.height + viewportMargin; - const aboveTop = context.node.position.y - composerHeightEstimate / safeZoom - viewportMargin; - const belowFits = belowTop + composerHeightEstimate / safeZoom <= visibleBottom - viewportMargin; - const aboveFits = aboveTop >= visibleTop + viewportMargin; return { left, - top: belowFits || !aboveFits ? belowTop : aboveTop, + top: belowTop, }; })() : null; @@ -1996,11 +1985,6 @@ export function GenerationComposer() { ) : ( <>
-
- {t("composer.console")} - {modeLabel} -
- {isProcessNode ? ( isVideoStitchNode ? (
diff --git a/src/components/nodes/AnnotationNode.tsx b/src/components/nodes/AnnotationNode.tsx index ecd25c98..ad8b9efa 100644 --- a/src/components/nodes/AnnotationNode.tsx +++ b/src/components/nodes/AnnotationNode.tsx @@ -32,10 +32,10 @@ export function AnnotationNode({ id, data, selected }: NodeProps 10 * 1024 * 1024) { - alert(t("upload.imageTooLarge")); - return; - } + // if (file.size > 10 * 1024 * 1024) { + // alert(t("upload.imageTooLarge")); + // return; + // } const reader = new FileReader(); reader.onload = (event) => { diff --git a/src/components/nodes/ImageInputNode.tsx b/src/components/nodes/ImageInputNode.tsx index d57a2a59..7e489f8b 100644 --- a/src/components/nodes/ImageInputNode.tsx +++ b/src/components/nodes/ImageInputNode.tsx @@ -135,10 +135,10 @@ export function ImageInputNode({ id, data, selected }: NodeProps 10 * 1024 * 1024) { - alert(t("upload.imageTooLarge")); - return; - } + // if (file.size > 10 * 1024 * 1024) { + // alert(t("upload.imageTooLarge")); + // return; + // } const reader = new FileReader(); reader.onload = (event) => { diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index d293b5b8..d19751d0 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -592,7 +592,6 @@ const en = { "composer.uploadReferenceImage": "Upload reference image", "composer.expand": "Expand composer", "composer.collapse": "Collapse composer", - "composer.console": "Console", "composer.style": "Style", "composer.mark": "Mark", "composer.focus": "Focus", @@ -1422,7 +1421,6 @@ const zhCN: Dictionary = { "composer.uploadReferenceImage": "上传参考图片", "composer.expand": "展开底部栏", "composer.collapse": "收起底部栏", - "composer.console": "控制台", "composer.style": "风格", "composer.mark": "标记", "composer.focus": "聚焦",