diff --git a/frontend/src/domain/design.ts b/frontend/src/domain/design.ts index 52ec859..d4cb1c4 100644 --- a/frontend/src/domain/design.ts +++ b/frontend/src/domain/design.ts @@ -148,7 +148,9 @@ export type GeneratorTaskArtifact = { content: string; metadata: { artifact_id?: string; + bbox?: number[]; format?: string; + generator_name?: string; height?: number; label?: string; node_id?: string; @@ -177,6 +179,14 @@ export type GeneratorTaskResponse = { }; }; +export type GeneratorTaskSubmitResponse = { + code: number; + message: string; + data: { + generator_task_id: string; + }; +}; + export type DeleteProjectResponse = { id: string; deleted: boolean; diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index 0249e62..f4fba6f 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -66,6 +66,7 @@ "recentProjects": "Recent Projects", "viewAll": "View all", "collapse": "Collapse", + "expandLayer": "Expand layer", "newProject": "New Project", "continueEditing": "Continue", "inspiration": "Inspiration", @@ -126,6 +127,8 @@ "renameCanvasTitle": "Rename canvas", "renameImageNode": "Rename image", "imageTitlePlaceholder": "Image name", + "renameLayer": "Rename layer", + "layerTitlePlaceholder": "Layer name", "projectTitlePlaceholder": "Name this canvas", "select": "Select", "pan": "Move", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index e5dbfde..e1f7b04 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -66,6 +66,7 @@ "recentProjects": "最近项目", "viewAll": "查看全部", "collapse": "收起", + "expandLayer": "展开图层", "newProject": "新建项目", "continueEditing": "继续编辑", "inspiration": "灵感发现", @@ -126,6 +127,8 @@ "renameCanvasTitle": "重命名画布", "renameImageNode": "重命名图片", "imageTitlePlaceholder": "输入图片名称", + "renameLayer": "重命名图层", + "layerTitlePlaceholder": "输入图层名称", "projectTitlePlaceholder": "输入画布名称", "select": "选择", "pan": "移动", diff --git a/frontend/src/infrastructure/designGateway.ts b/frontend/src/infrastructure/designGateway.ts index 42bb2f7..70ae6b6 100644 --- a/frontend/src/infrastructure/designGateway.ts +++ b/frontend/src/infrastructure/designGateway.ts @@ -11,6 +11,7 @@ import type { DeleteProjectsResponse, ExtractNodeTextResponse, GeneratorTaskResponse, + GeneratorTaskSubmitResponse, GenerateResult, MergeLayersRequest, NodeActionRequest, @@ -358,6 +359,18 @@ export const designGateway = { return request(`/api/generator/tasks?${params.toString()}`); }, + submitGeneratorTask(projectId: string, generatorName: string, inputArgs: Record) { + return request("/api/generator/tasks", { + method: "POST", + body: JSON.stringify({ + cid: createClientId(), + project_id: projectId, + generator_name: generatorName, + input_args: inputArgs + }) + }); + }, + recognizeObject(imageUrl: string, boundingBox: NormalizedBox, lang = "zh") { return request("/api/context/vision/recognize_object", { method: "POST", diff --git a/frontend/src/ui/components/ArtboardPreview/index.tsx b/frontend/src/ui/components/ArtboardPreview/index.tsx index f114b73..9885fc8 100644 --- a/frontend/src/ui/components/ArtboardPreview/index.tsx +++ b/frontend/src/ui/components/ArtboardPreview/index.tsx @@ -26,6 +26,10 @@ export function ArtboardPreview({ node, viewportScale = 1, imageGeneratorTargetS ); } + if (node.status === "generating" && node.type === "frame" && node.layerRole === "manual-frame" && isImageUrl(node.content)) { + return ; + } + if (node.status === "generating" && node.type === "image" && isImageUrl(node.content)) { return ; } diff --git a/frontend/src/ui/components/CanvasPanels/index.tsx b/frontend/src/ui/components/CanvasPanels/index.tsx index 769252c..d5b5a9b 100644 --- a/frontend/src/ui/components/CanvasPanels/index.tsx +++ b/frontend/src/ui/components/CanvasPanels/index.tsx @@ -1,10 +1,11 @@ "use client"; -import { ChevronDown, ChevronUp, Download, Eye, EyeOff, Frame, Image as ImageIcon, Layers, Lock, Type, Unlock, X } from "lucide-react"; -import { Fragment, useEffect, useMemo, useRef, useState } from "react"; +import { ChevronDown, ChevronRight, ChevronUp, Download, Eye, EyeOff, Frame, GripVertical, Image as ImageIcon, Layers, Lock, Type, Unlock, X } from "lucide-react"; +import { Fragment, useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent } from "react"; import type { AgentMessage, CanvasNode, CanvasViewport } from "@/domain/design"; import { useI18n } from "@/i18n/i18n"; import { thumbnailImageUrl } from "@/ui/lib/imageDelivery"; +import { isFrameContainerNode, type LayerReorderItem, type LayerReorderPlacement, type LayerReorderRequest } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps"; import { ShapeNodePreview } from "@/ui/pages/CanvasWorkspace/canvas/ShapeNodePreview"; import { isBooleanShapeGroupNode, isBooleanShapeOperandNode, isShapeNode } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode"; @@ -18,6 +19,11 @@ type LayerPanelItem = node: CanvasNode; layers: CanvasNode[]; } + | { + type: "frame"; + node: CanvasNode; + layers: CanvasNode[]; + } | { type: "group"; id: string; @@ -125,6 +131,7 @@ export function CanvasLayerPanel({ onToggleLocked, onToggleHiddenMany, onToggleLockedMany, + onReorderLayer, onClose }: { nodes: CanvasNode[]; @@ -138,10 +145,65 @@ export function CanvasLayerPanel({ onToggleLocked: (nodeId: string) => void; onToggleHiddenMany: (nodeIds: string[]) => void; onToggleLockedMany: (nodeIds: string[]) => void; + onReorderLayer: (request: LayerReorderRequest) => void; onClose: () => void; }) { const { t } = useI18n(); const layers = useMemo(() => buildLayerPanelItems(nodes), [nodes]); + const [collapsedLayerIds, setCollapsedLayerIds] = useState>(() => new Set()); + const [draggingItem, setDraggingItem] = useState(null); + const [dropTarget, setDropTarget] = useState<{ item: LayerReorderItem; placement: LayerReorderPlacement } | null>(null); + const draggingKey = draggingItem ? layerDragKey(draggingItem) : ""; + const dropTargetKey = dropTarget ? layerDragKey(dropTarget.item) : ""; + const toggleCollapsedLayer = (layerId: string) => { + setCollapsedLayerIds((current) => { + const next = new Set(current); + if (next.has(layerId)) { + next.delete(layerId); + } else { + next.add(layerId); + } + return next; + }); + }; + const clearLayerDrag = () => { + setDraggingItem(null); + setDropTarget(null); + }; + const startLayerDrag = (event: ReactDragEvent, item: LayerReorderItem) => { + const target = event.target as HTMLElement; + if (target.closest("input") || target.closest(".layer-control") || target.closest(".layer-disclosure")) { + event.preventDefault(); + return; + } + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("application/x-canvas-layer", JSON.stringify(item)); + setDraggingItem(item); + }; + const updateLayerDropTarget = (event: ReactDragEvent, item: LayerReorderItem) => { + if (!draggingItem || layerDragKey(draggingItem) === layerDragKey(item)) return; + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + const rect = event.currentTarget.getBoundingClientRect(); + const placement: LayerReorderPlacement = event.clientY < rect.top + rect.height / 2 ? "before" : "after"; + setDropTarget({ item, placement }); + }; + const dropLayer = (event: ReactDragEvent, over: LayerReorderItem) => { + if (!draggingItem) return; + event.preventDefault(); + const placement = dropTargetKey === layerDragKey(over) ? dropTarget?.placement ?? "before" : "before"; + onReorderLayer({ active: draggingItem, over, placement }); + clearLayerDrag(); + }; + const layerRowDragState = (item: LayerReorderItem) => ({ + dragItem: item, + dragging: draggingKey === layerDragKey(item), + dropPlacement: dropTargetKey === layerDragKey(item) ? dropTarget?.placement ?? null : null, + onDragStart: (event: ReactDragEvent) => startLayerDrag(event, item), + onDragOver: (event: ReactDragEvent) => updateLayerDropTarget(event, item), + onDrop: (event: ReactDragEvent) => dropLayer(event, item), + onDragEnd: clearLayerDrag + }); return (