fix: refine canvas toolbar layout
This commit is contained in:
parent
d2199975a5
commit
3d091a665b
@ -11217,12 +11217,13 @@ export function CanvasChatPanel({
|
||||
{/* Bottom Toolbar */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-2 bottom-2 right-2 flex items-center gap-2",
|
||||
"absolute left-2 bottom-2 right-2 flex items-center justify-between gap-3",
|
||||
isVideoWorkspace &&
|
||||
"rounded-[24px] border border-border/60 bg-background/88 px-2.5 py-2 shadow-sm backdrop-blur-xl supports-[backdrop-filter]:bg-background/80"
|
||||
"rounded-[24px] border border-border/60 bg-background/88 px-3 py-2 shadow-sm backdrop-blur-xl supports-[backdrop-filter]:bg-background/80",
|
||||
!isVideoWorkspace && "px-1"
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5 pr-0.5">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@ -11404,7 +11405,7 @@ export function CanvasChatPanel({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full text-muted-foreground transition-colors hover:text-foreground",
|
||||
"h-8 w-8 rounded-full text-muted-foreground transition-colors hover:text-foreground mr-1",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background hover:bg-background/90"
|
||||
: "hover:bg-muted"
|
||||
@ -11418,7 +11419,7 @@ export function CanvasChatPanel({
|
||||
|
||||
{isVideoModel ? (
|
||||
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
||||
<div className="w-max pr-1">
|
||||
<div className="w-max pr-1 pl-1">
|
||||
<VideoParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={videoParams || undefined}
|
||||
@ -11430,7 +11431,7 @@ export function CanvasChatPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-0 shrink">
|
||||
<div className="min-w-0 shrink pl-1 border-l border-border/40 dark:border-border/60 ml-1">
|
||||
<ImageParamsBar
|
||||
modelName={activeComposerMode === "agent" ? agentDisplayName : currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
@ -11445,7 +11446,7 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center gap-2.5">
|
||||
<div className="flex shrink-0 items-center gap-2 pl-2 border-l border-border/40 dark:border-border/60">
|
||||
{!isWorkspacePresentation && activeComposerMode === "image" ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -11535,7 +11536,7 @@ export function CanvasChatPanel({
|
||||
{estimatedPointsCost ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 items-center gap-1.5 rounded-full px-2.5 text-xs",
|
||||
"flex h-8 items-center gap-1.5 rounded-full px-2.5 text-xs ml-2",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background text-muted-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
|
||||
@ -164,6 +164,15 @@ type NodeContextState = {
|
||||
screenY: number;
|
||||
} | null;
|
||||
|
||||
type ConnectionSide = "source" | "target";
|
||||
|
||||
type ConnectionMenuState = {
|
||||
nodeId: string;
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
side: ConnectionSide;
|
||||
} | null;
|
||||
|
||||
type PresetControlAnchor = {
|
||||
bottomY: number;
|
||||
centerX: number;
|
||||
@ -217,6 +226,18 @@ const ADD_NODE_MODULES: Array<{
|
||||
{ kind: "script", label: "脚本", description: "创意脚本、生成故事板", icon: Layers, badge: "Beta" },
|
||||
];
|
||||
|
||||
const CONNECTION_ADD_MODULES: Array<{
|
||||
icon: LucideIcon;
|
||||
kind: Exclude<AddModuleKind, "audio">;
|
||||
label: string;
|
||||
}> = [
|
||||
{ kind: "text", label: "文本生成", icon: FileText },
|
||||
{ kind: "image", label: "图片生成", icon: ImageIcon },
|
||||
{ kind: "video", label: "视频生成", icon: Video },
|
||||
{ kind: "video-clip", label: "视频合成", icon: Scissors },
|
||||
{ kind: "script", label: "脚本生成", icon: Layers },
|
||||
];
|
||||
|
||||
const TARGET_MODULE_CENTER_OFFSETS: Record<AddModuleKind, { x: number; y: number }> = {
|
||||
audio: { x: 705, y: -257 },
|
||||
image: { x: 53, y: -257 },
|
||||
@ -1077,6 +1098,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const [publishPublic, setPublishPublic] = useState(false);
|
||||
const [canvasCommand, setCanvasCommand] = useState<CanvasCommandState>(null);
|
||||
const [nodeContextMenu, setNodeContextMenu] = useState<NodeContextState>(null);
|
||||
const [connectionMenu, setConnectionMenu] = useState<ConnectionMenuState>(null);
|
||||
|
||||
const selectedNode = useMemo(
|
||||
() => nodes.find((node) => node.id === selectedNodeId) || null,
|
||||
@ -1088,6 +1110,11 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
[nodeContextMenu?.nodeId, nodes]
|
||||
);
|
||||
|
||||
const connectionNode = useMemo(
|
||||
() => nodes.find((node) => node.id === connectionMenu?.nodeId) || null,
|
||||
[connectionMenu?.nodeId, nodes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
@ -1199,6 +1226,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
addNodes([node], []);
|
||||
setActivePanel(null);
|
||||
setCanvasCommand(null);
|
||||
setConnectionMenu(null);
|
||||
toast.success(`已添加 ${node.title}`);
|
||||
},
|
||||
[addNodes, nodes.length, toCanvasPoint]
|
||||
@ -1218,6 +1246,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
addNodes(newNodes, []);
|
||||
setActivePanel(null);
|
||||
setCanvasCommand(null);
|
||||
setConnectionMenu(null);
|
||||
toast.success(files.length === 1 ? `已添加 ${newNodes[0]?.title || "素材"}` : `已添加 ${newNodes.length} 个素材`);
|
||||
},
|
||||
[addNodes, nodes.length]
|
||||
@ -1245,6 +1274,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const point = toCanvasPoint(event.clientX, event.clientY);
|
||||
setSelectedNodeId(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setActivePanel(null);
|
||||
setCanvasCommand({
|
||||
canvasX: point.x,
|
||||
@ -1261,6 +1291,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
event.stopPropagation();
|
||||
setSelectedNodeId(node.id);
|
||||
setCanvasCommand(null);
|
||||
setConnectionMenu(null);
|
||||
setNodeContextMenu({
|
||||
nodeId: node.id,
|
||||
screenX: event.clientX,
|
||||
@ -1268,6 +1299,53 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openConnectionMenu = useCallback((event: ReactMouseEvent<HTMLButtonElement>, node: CloneNode, side: ConnectionSide) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setSelectedNodeId(node.id);
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setActivePanel(null);
|
||||
setConnectionMenu({
|
||||
nodeId: node.id,
|
||||
screenX: event.clientX,
|
||||
screenY: event.clientY,
|
||||
side,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addConnectedModule = useCallback(
|
||||
(kind: Exclude<AddModuleKind, "audio">) => {
|
||||
if (!connectionMenu) return;
|
||||
const anchor = nodes.find((node) => node.id === connectionMenu.nodeId);
|
||||
if (!anchor) return;
|
||||
const created = createTargetModuleNode(kind, 0, 0, nodes.length + 1);
|
||||
const gap = 120;
|
||||
const node =
|
||||
connectionMenu.side === "source"
|
||||
? {
|
||||
...created,
|
||||
x: Math.round(anchor.x + anchor.width + gap),
|
||||
y: Math.round(anchor.y + anchor.height / 2 - created.height / 2),
|
||||
}
|
||||
: {
|
||||
...created,
|
||||
x: Math.round(anchor.x - created.width - gap),
|
||||
y: Math.round(anchor.y + anchor.height / 2 - created.height / 2),
|
||||
};
|
||||
const edge =
|
||||
connectionMenu.side === "source"
|
||||
? { id: makeId("edge"), source: anchor.id, target: node.id }
|
||||
: { id: makeId("edge"), source: node.id, target: anchor.id };
|
||||
|
||||
addNodes([node], [edge]);
|
||||
setConnectionMenu(null);
|
||||
toast.success(`已添加并连接 ${node.title}`);
|
||||
},
|
||||
[addNodes, connectionMenu, nodes]
|
||||
);
|
||||
|
||||
const createFreeNode = useCallback(
|
||||
(kind: CanvasCreateKind) => {
|
||||
if (!canvasCommand) return;
|
||||
@ -1279,6 +1357,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
};
|
||||
addNodes([node], []);
|
||||
setCanvasCommand(null);
|
||||
setConnectionMenu(null);
|
||||
toast.success(`已添加 ${node.title}`);
|
||||
},
|
||||
[addNodes, canvasCommand, nodes.length]
|
||||
@ -1287,6 +1366,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const configureNode = useCallback((nodeId: string) => {
|
||||
setSelectedNodeId(nodeId);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
}, []);
|
||||
|
||||
const copyNode = useCallback(async (nodeId: string) => {
|
||||
@ -1294,6 +1374,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
if (!node) return;
|
||||
await navigator.clipboard?.writeText(node.title).catch(() => undefined);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
toast.success("节点名称已复制");
|
||||
}, [nodes]);
|
||||
|
||||
@ -1309,6 +1390,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
setNodes((current) => [...current, duplicate]);
|
||||
setSelectedNodeId(duplicate.id);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
toast.success("已复制节点");
|
||||
}, [nodes]);
|
||||
|
||||
@ -1317,6 +1399,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
if (!node) return;
|
||||
setSelectedNodeId(nodeId);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
toast.message(`${node.title} 生成接口待接入。`);
|
||||
}, [nodes]);
|
||||
|
||||
@ -1325,6 +1408,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
setEdges((current) => current.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
|
||||
setSelectedNodeId((current) => (current === nodeId ? null : current));
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
toast.success("节点已删除");
|
||||
}, []);
|
||||
|
||||
@ -1337,6 +1421,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
setViewport(PRESET_VIEWPORT);
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setActivePanel(null);
|
||||
toast.success(`已载入 ${starter.title} 工作流`);
|
||||
return;
|
||||
@ -1397,6 +1482,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
setSelectedNodeId(null);
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
dragRef.current = {
|
||||
kind: "canvas",
|
||||
startX: event.clientX,
|
||||
@ -1417,6 +1503,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
if (event.key === "Escape") {
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setSelectedNodeId(null);
|
||||
setActivePanel(null);
|
||||
}
|
||||
@ -1508,6 +1595,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const toggleTopPanel = useCallback((panel: Exclude<ActivePanel, null>) => {
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setActivePanel((current) => (current === panel ? null : panel));
|
||||
}, []);
|
||||
|
||||
@ -1515,6 +1603,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const selectedPresetControlNode =
|
||||
selectedNode?.kind === "media" && selectedNode.inputType !== "audio" ? selectedNode : null;
|
||||
const selectedPresetMode = getPresetMode(selectedPresetControlNode);
|
||||
const selectedTargetModuleKind = selectedNode ? getTargetModuleKind(selectedNode) : null;
|
||||
const selectedPresetAnchor = useMemo<PresetControlAnchor | null>(() => {
|
||||
if (!selectedPresetControlNode) return null;
|
||||
return {
|
||||
@ -1548,6 +1637,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
node={node}
|
||||
selected={node.id === selectedNodeId}
|
||||
onContextMenu={(event) => openNodeContext(event, node)}
|
||||
onOpenConnectionMenu={(event, side) => openConnectionMenu(event, node, side)}
|
||||
onPointerDown={(event) => startNodeDrag(event, node)}
|
||||
/>
|
||||
))}
|
||||
@ -1622,6 +1712,15 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{connectionMenu && connectionNode ? (
|
||||
<ConnectionAddMenu
|
||||
node={connectionNode}
|
||||
onClose={() => setConnectionMenu(null)}
|
||||
onCreate={addConnectedModule}
|
||||
state={connectionMenu}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{selectedPresetMode && selectedPresetAnchor ? (
|
||||
<PresetWorkflowControls anchor={selectedPresetAnchor} mode={selectedPresetMode} nodes={nodes} />
|
||||
) : null}
|
||||
@ -1670,7 +1769,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{selectedNode && !selectedPresetMode ? (
|
||||
{selectedNode && !selectedPresetMode && !selectedTargetModuleKind ? (
|
||||
<InspectorPanel
|
||||
node={selectedNode}
|
||||
onChangeProperty={updateNodeDraft}
|
||||
@ -3401,18 +3500,20 @@ function CloneEdges({ edges, nodes }: { edges: CloneEdge[]; nodes: CloneNode[] }
|
||||
function CloneCanvasNode({
|
||||
node,
|
||||
onContextMenu,
|
||||
onOpenConnectionMenu,
|
||||
onPointerDown,
|
||||
selected,
|
||||
}: {
|
||||
node: CloneNode;
|
||||
onContextMenu: (event: ReactMouseEvent<HTMLDivElement>) => void;
|
||||
onOpenConnectionMenu: (event: ReactMouseEvent<HTMLButtonElement>, side: ConnectionSide) => void;
|
||||
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const targetModuleKind = getTargetModuleKind(node);
|
||||
if (targetModuleKind) {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onOpenConnectionMenu={onOpenConnectionMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<TargetModuleCanvasNode kind={targetModuleKind} node={node} selected={selected} />
|
||||
</CanvasNodeShell>
|
||||
);
|
||||
@ -3420,7 +3521,7 @@ function CloneCanvasNode({
|
||||
|
||||
if (node.kind === "media") {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onOpenConnectionMenu={onOpenConnectionMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<PresetMediaCanvasNode node={node} selected={selected} />
|
||||
</CanvasNodeShell>
|
||||
);
|
||||
@ -3428,7 +3529,7 @@ function CloneCanvasNode({
|
||||
|
||||
if (node.kind === "script") {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onOpenConnectionMenu={onOpenConnectionMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<PresetScriptCanvasNode node={node} selected={selected} />
|
||||
</CanvasNodeShell>
|
||||
);
|
||||
@ -3440,7 +3541,7 @@ function CloneCanvasNode({
|
||||
const isWarm = node.kind === "text" || node.kind === "audio";
|
||||
const previewUrl = getDraftString(node, "thumbnailUrl") || getDraftString(node, "resultImage");
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onOpenConnectionMenu={onOpenConnectionMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
{isInput ? (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user