feat: add canvas tool panels (multi-grid, upscale, inpaint) and conditional grid mode
Add modal-based tool panels for multi-grid, super-resolution, and inpaint in canvas image-to-image workflow. Grid mode selector only appears for models that support multi-image generation (wan2.7-image, gpt-image-2). Also fixes merge conflict markers in CSS and adds missing toast import. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { publicConfigClient, type WebPublicConfig } from "../api/publicConfigClient";
|
import { publicConfigClient, type WebPublicConfig } from "../api/publicConfigClient";
|
||||||
|
import { toast } from "./toast/toastStore";
|
||||||
import type { ServerConnectionHealth } from "../api/serverConnection";
|
import type { ServerConnectionHealth } from "../api/serverConnection";
|
||||||
import { ossAssets } from "../data/ossAssets";
|
import { ossAssets } from "../data/ossAssets";
|
||||||
import { canManageCommunityCases, canReviewCommunity } from "../features/community-review/communityPermissions";
|
import { canManageCommunityCases, canReviewCommunity } from "../features/community-review/communityPermissions";
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ import {
|
|||||||
} from "./canvasWorkflowDeserialize";
|
} from "./canvasWorkflowDeserialize";
|
||||||
import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents";
|
import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents";
|
||||||
import type { CanvasNodeToolbarAction } from "./canvasComponents";
|
import type { CanvasNodeToolbarAction } from "./canvasComponents";
|
||||||
|
import { CanvasMultiGridPanel, CanvasUpscalePanel, CanvasInpaintPanel } from "./canvasToolPanels";
|
||||||
import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing";
|
import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing";
|
||||||
|
|
||||||
const canvasEnterpriseVideoModelOptions: CanvasOption[] = ENTERPRISE_VIDEO_MODEL_OPTIONS.map((option) => ({
|
const canvasEnterpriseVideoModelOptions: CanvasOption[] = ENTERPRISE_VIDEO_MODEL_OPTIONS.map((option) => ({
|
||||||
@@ -336,6 +337,7 @@ function CanvasPage({
|
|||||||
const [imageFocusNodeId, setImageFocusNodeId] = useState<string | null>(null);
|
const [imageFocusNodeId, setImageFocusNodeId] = useState<string | null>(null);
|
||||||
const [imageFocusDraft, setImageFocusDraft] = useState<CanvasImageFocusSelection | null>(null);
|
const [imageFocusDraft, setImageFocusDraft] = useState<CanvasImageFocusSelection | null>(null);
|
||||||
const [imageFocusDrag, setImageFocusDrag] = useState<CanvasImageFocusDrag | null>(null);
|
const [imageFocusDrag, setImageFocusDrag] = useState<CanvasImageFocusDrag | null>(null);
|
||||||
|
const [canvasToolModal, setCanvasToolModal] = useState<{ tool: "multiGrid" | "upscale" | "inpaint"; imageNode: CanvasImageNode } | null>(null);
|
||||||
const [stylePickerImageNodeId, setStylePickerImageNodeId] = useState<string | null>(null);
|
const [stylePickerImageNodeId, setStylePickerImageNodeId] = useState<string | null>(null);
|
||||||
const [stylePickerCases, setStylePickerCases] = useState<CanvasStyleCase[]>([]);
|
const [stylePickerCases, setStylePickerCases] = useState<CanvasStyleCase[]>([]);
|
||||||
const [stylePickerLoading, setStylePickerLoading] = useState(false);
|
const [stylePickerLoading, setStylePickerLoading] = useState(false);
|
||||||
@@ -4264,7 +4266,7 @@ function CanvasPage({
|
|||||||
setSelectedExistingCategory("");
|
setSelectedExistingCategory("");
|
||||||
setSaveAssetOpen(true);
|
setSaveAssetOpen(true);
|
||||||
}
|
}
|
||||||
if (key === "upscale") void handleGenerateImageNode(imageNode.id);
|
if (key === "upscale") setCanvasToolModal({ tool: "upscale", imageNode });
|
||||||
}}
|
}}
|
||||||
moreActions={[
|
moreActions={[
|
||||||
{ key: "copy", label: "复制链接", icon: <CopyOutlined />, disabled: !imageNode.imageUrl },
|
{ key: "copy", label: "复制链接", icon: <CopyOutlined />, disabled: !imageNode.imageUrl },
|
||||||
@@ -4570,16 +4572,42 @@ function CanvasPage({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={imageNodeFocusActive ? "is-active" : ""}
|
title="多宫格生成"
|
||||||
title="框选聚焦区域"
|
disabled={!imageNode.imageUrl}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
openImageFocusMode(imageNode);
|
setCanvasToolModal({ tool: "multiGrid", imageNode });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BarsOutlined /><span>聚焦</span>
|
<BarsOutlined /><span>多宫格</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="图片超分辨率"
|
||||||
|
disabled={!imageNode.imageUrl}
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setCanvasToolModal({ tool: "upscale", imageNode });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThunderboltOutlined /><span>超分</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="局部重绘"
|
||||||
|
disabled={!imageNode.imageUrl}
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setCanvasToolModal({ tool: "inpaint", imageNode });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditOutlined /><span>局部重绘</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="studio-canvas-image-composer__expand" aria-label="展开">↗</button>
|
<button type="button" className="studio-canvas-image-composer__expand" aria-label="展开">↗</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -5729,6 +5757,27 @@ function CanvasPage({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{canvasToolModal && (
|
||||||
|
<div className="studio-canvas-tool-modal-overlay" onClick={() => setCanvasToolModal(null)}>
|
||||||
|
<div className="studio-canvas-tool-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label={canvasToolModal.tool === "multiGrid" ? "多宫格" : canvasToolModal.tool === "upscale" ? "超分" : "局部重绘"}>
|
||||||
|
<header className="studio-canvas-tool-modal__header">
|
||||||
|
<h3>{canvasToolModal.tool === "multiGrid" ? "多宫格生成" : canvasToolModal.tool === "upscale" ? "图片超分" : "局部重绘"}</h3>
|
||||||
|
<button type="button" aria-label="关闭" onClick={() => setCanvasToolModal(null)}><CloseOutlined /></button>
|
||||||
|
</header>
|
||||||
|
<div className="studio-canvas-tool-modal__body">
|
||||||
|
{canvasToolModal.tool === "multiGrid" && (
|
||||||
|
<CanvasMultiGridPanel imageUrl={canvasToolModal.imageNode.imageUrl || ""} imageNode={canvasToolModal.imageNode} onComplete={(url) => { setImageNodes((nodes) => nodes.map((n) => n.id === canvasToolModal.imageNode.id ? { ...n, imageUrl: url } : n)); setCanvasToolModal(null); }} />
|
||||||
|
)}
|
||||||
|
{canvasToolModal.tool === "upscale" && (
|
||||||
|
<CanvasUpscalePanel imageUrl={canvasToolModal.imageNode.imageUrl || ""} imageNode={canvasToolModal.imageNode} onComplete={(url) => { setImageNodes((nodes) => nodes.map((n) => n.id === canvasToolModal.imageNode.id ? { ...n, imageUrl: url } : n)); setCanvasToolModal(null); }} />
|
||||||
|
)}
|
||||||
|
{canvasToolModal.tool === "inpaint" && (
|
||||||
|
<CanvasInpaintPanel imageUrl={canvasToolModal.imageNode.imageUrl || ""} imageNode={canvasToolModal.imageNode} onComplete={(url) => { setImageNodes((nodes) => nodes.map((n) => n.id === canvasToolModal.imageNode.id ? { ...n, imageUrl: url } : n)); setCanvasToolModal(null); }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</WorkspacePageShell>
|
</WorkspacePageShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
|
import { toast } from "../../components/toast/toastStore";
|
||||||
|
import type { CanvasImageNode } from "./canvasTypes";
|
||||||
|
|
||||||
|
interface CanvasToolPanelProps {
|
||||||
|
imageUrl: string;
|
||||||
|
imageNode: CanvasImageNode;
|
||||||
|
onComplete: (resultUrl: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanvasMultiGridPanel({ imageUrl, onComplete }: CanvasToolPanelProps) {
|
||||||
|
const [gridMode, setGridMode] = useState<"grid-4" | "grid-9">("grid-4");
|
||||||
|
const [prompt, setPrompt] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const cancelRef = useRef(false);
|
||||||
|
|
||||||
|
const handleGenerate = useCallback(async () => {
|
||||||
|
if (!imageUrl) return;
|
||||||
|
setLoading(true);
|
||||||
|
cancelRef.current = false;
|
||||||
|
try {
|
||||||
|
const { taskId } = await aiGenerationClient.createImageTask({
|
||||||
|
model: "gpt-image-2",
|
||||||
|
prompt: prompt || "基于参考图生成多宫格变体",
|
||||||
|
referenceUrls: [imageUrl],
|
||||||
|
gridMode,
|
||||||
|
});
|
||||||
|
const resultUrl = await waitForTask(taskId, { kind: "image", abortRef: cancelRef });
|
||||||
|
if (resultUrl) {
|
||||||
|
onComplete(resultUrl);
|
||||||
|
toast.success("多宫格生成完成");
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (!cancelRef.current) toast.error(err instanceof Error ? err.message : "多宫格生成失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [imageUrl, prompt, gridMode, onComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="studio-canvas-tool-panel">
|
||||||
|
<div className="studio-canvas-tool-panel__preview"><img src={imageUrl} alt="" /></div>
|
||||||
|
<div className="studio-canvas-tool-panel__controls">
|
||||||
|
<label className="studio-canvas-tool-panel__label">{"宫格模式"}</label>
|
||||||
|
<div className="studio-canvas-tool-panel__options">
|
||||||
|
{([["grid-4", "2×2"], ["grid-9", "3×3"]] as const).map(([value, label]) => (
|
||||||
|
<button key={value} type="button" className={gridMode === value ? "is-active" : ""} onClick={() => setGridMode(value)}>{label}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<label className="studio-canvas-tool-panel__label">{"提示词(可选)"}</label>
|
||||||
|
<textarea className="studio-canvas-tool-panel__textarea" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="描述多宫格内容变化" />
|
||||||
|
<button type="button" className="studio-canvas-tool-panel__submit" disabled={loading} onClick={handleGenerate}>
|
||||||
|
{loading ? "生成中..." : "生成多宫格"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanvasUpscalePanel({ imageUrl, onComplete }: CanvasToolPanelProps) {
|
||||||
|
const [scale, setScale] = useState<"2x" | "4x">("2x");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const cancelRef = useRef(false);
|
||||||
|
|
||||||
|
const handleUpscale = useCallback(async () => {
|
||||||
|
if (!imageUrl) return;
|
||||||
|
setLoading(true);
|
||||||
|
cancelRef.current = false;
|
||||||
|
try {
|
||||||
|
const { taskId } = await aiGenerationClient.createImageSuperResolveTask({
|
||||||
|
imageUrl,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
|
const resultUrl = await waitForTask(taskId, { kind: "image", abortRef: cancelRef });
|
||||||
|
if (resultUrl) {
|
||||||
|
onComplete(resultUrl);
|
||||||
|
toast.success("超分完成");
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (!cancelRef.current) toast.error(err instanceof Error ? err.message : "超分失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [imageUrl, scale, onComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="studio-canvas-tool-panel">
|
||||||
|
<div className="studio-canvas-tool-panel__preview"><img src={imageUrl} alt="" /></div>
|
||||||
|
<div className="studio-canvas-tool-panel__controls">
|
||||||
|
<label className="studio-canvas-tool-panel__label">{"放大倍数"}</label>
|
||||||
|
<div className="studio-canvas-tool-panel__options">
|
||||||
|
{(["2x", "4x"] as const).map((s) => (
|
||||||
|
<button key={s} type="button" className={scale === s ? "is-active" : ""} onClick={() => setScale(s)}>{s}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button type="button" className="studio-canvas-tool-panel__submit" disabled={loading} onClick={handleUpscale}>
|
||||||
|
{loading ? "处理中..." : "开始超分"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanvasInpaintPanel({ imageUrl, onComplete }: CanvasToolPanelProps) {
|
||||||
|
const [prompt, setPrompt] = useState("");
|
||||||
|
const [brushSize, setBrushSize] = useState(30);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const isDrawingRef = useRef(false);
|
||||||
|
const cancelRef = useRef(false);
|
||||||
|
|
||||||
|
const initCanvas = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = "anonymous";
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = img.naturalWidth;
|
||||||
|
canvas.height = img.naturalHeight;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
};
|
||||||
|
img.src = imageUrl;
|
||||||
|
}, [imageUrl]);
|
||||||
|
|
||||||
|
const getPos = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
const canvas = canvasRef.current!;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isDrawingRef.current) return;
|
||||||
|
const ctx = canvasRef.current?.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
const { x, y } = getPos(e);
|
||||||
|
ctx.globalCompositeOperation = "source-over";
|
||||||
|
ctx.fillStyle = "rgba(255, 0, 0, 0.4)";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, brushSize, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaskDataUrl = (): string => {
|
||||||
|
const canvas = canvasRef.current!;
|
||||||
|
const maskCanvas = document.createElement("canvas");
|
||||||
|
maskCanvas.width = canvas.width;
|
||||||
|
maskCanvas.height = canvas.height;
|
||||||
|
const srcCtx = canvas.getContext("2d")!;
|
||||||
|
const maskCtx = maskCanvas.getContext("2d")!;
|
||||||
|
const imgData = srcCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const maskData = maskCtx.createImageData(canvas.width, canvas.height);
|
||||||
|
for (let i = 0; i < imgData.data.length; i += 4) {
|
||||||
|
const hasColor = imgData.data[i + 3] > 10;
|
||||||
|
maskData.data[i] = hasColor ? 255 : 0;
|
||||||
|
maskData.data[i + 1] = hasColor ? 255 : 0;
|
||||||
|
maskData.data[i + 2] = hasColor ? 255 : 0;
|
||||||
|
maskData.data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
maskCtx.putImageData(maskData, 0, 0);
|
||||||
|
return maskCanvas.toDataURL("image/png");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInpaint = useCallback(async () => {
|
||||||
|
if (!imageUrl || !prompt) {
|
||||||
|
toast.error("请输入重绘提示词");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
cancelRef.current = false;
|
||||||
|
try {
|
||||||
|
const maskDataUrl = getMaskDataUrl();
|
||||||
|
const { taskId } = await aiGenerationClient.createImageEditTask({
|
||||||
|
imageUrl,
|
||||||
|
function: "inpaint",
|
||||||
|
prompt,
|
||||||
|
});
|
||||||
|
const resultUrl = await waitForTask(taskId, { kind: "image", abortRef: cancelRef });
|
||||||
|
if (resultUrl) {
|
||||||
|
onComplete(resultUrl);
|
||||||
|
toast.success("局部重绘完成");
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (!cancelRef.current) toast.error(err instanceof Error ? err.message : "局部重绘失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [imageUrl, prompt, onComplete]);
|
||||||
|
return (
|
||||||
|
<div className="studio-canvas-tool-panel studio-canvas-tool-panel--inpaint">
|
||||||
|
<div className="studio-canvas-tool-panel__canvas-wrap">
|
||||||
|
<img src={imageUrl} alt="" className="studio-canvas-tool-panel__canvas-bg" />
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="studio-canvas-tool-panel__canvas"
|
||||||
|
onMouseDown={(e) => { isDrawingRef.current = true; draw(e); }}
|
||||||
|
onMouseMove={draw}
|
||||||
|
onMouseUp={() => { isDrawingRef.current = false; }}
|
||||||
|
onMouseLeave={() => { isDrawingRef.current = false; }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="studio-canvas-tool-panel__controls">
|
||||||
|
<label className="studio-canvas-tool-panel__label">{"画笔大小"}</label>
|
||||||
|
<input type="range" min={5} max={80} value={brushSize} onChange={(e) => setBrushSize(Number(e.target.value))} />
|
||||||
|
<label className="studio-canvas-tool-panel__label">{"重绘提示词"}</label>
|
||||||
|
<textarea className="studio-canvas-tool-panel__textarea" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="描述需要重绘区域的内容" />
|
||||||
|
<div className="studio-canvas-tool-panel__actions">
|
||||||
|
<button type="button" className="studio-canvas-tool-panel__reset" onClick={initCanvas}>{"清除蒙版"}</button>
|
||||||
|
<button type="button" className="studio-canvas-tool-panel__submit" disabled={loading} onClick={handleInpaint}>
|
||||||
|
{loading ? "处理中..." : "开始重绘"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ import {
|
|||||||
VIDEO_MODEL_OPTIONS,
|
VIDEO_MODEL_OPTIONS,
|
||||||
RATIO_OPTIONS,
|
RATIO_OPTIONS,
|
||||||
GRID_MODE_OPTIONS,
|
GRID_MODE_OPTIONS,
|
||||||
|
GRID_SUPPORTED_MODELS,
|
||||||
VIDEO_FRAME_OPTIONS,
|
VIDEO_FRAME_OPTIONS,
|
||||||
VIDEO_DURATION_OPTIONS,
|
VIDEO_DURATION_OPTIONS,
|
||||||
MESSAGE_STORAGE_KEY,
|
MESSAGE_STORAGE_KEY,
|
||||||
@@ -2729,7 +2730,7 @@ function WorkbenchPage({
|
|||||||
isOpen={toolbarMenuId === "image-model"}
|
isOpen={toolbarMenuId === "image-model"}
|
||||||
onToggle={() => toggleToolbarMenu("image-model")}
|
onToggle={() => toggleToolbarMenu("image-model")}
|
||||||
onClose={closeToolbarMenus}
|
onClose={closeToolbarMenus}
|
||||||
onChange={setImageModel}
|
onChange={(v) => { setImageModel(v); if (!GRID_SUPPORTED_MODELS.has(v)) setImageGridMode("single"); }}
|
||||||
direction={dropdownDirection}
|
direction={dropdownDirection}
|
||||||
/>
|
/>
|
||||||
<CompoundSelectChip
|
<CompoundSelectChip
|
||||||
@@ -2741,6 +2742,7 @@ function WorkbenchPage({
|
|||||||
onToggle={() => toggleToolbarMenu("image-settings")}
|
onToggle={() => toggleToolbarMenu("image-settings")}
|
||||||
direction={dropdownDirection}
|
direction={dropdownDirection}
|
||||||
/>
|
/>
|
||||||
|
{GRID_SUPPORTED_MODELS.has(imageModel) && (
|
||||||
<SelectChip
|
<SelectChip
|
||||||
chipId="image-grid-mode"
|
chipId="image-grid-mode"
|
||||||
value={imageGridMode}
|
value={imageGridMode}
|
||||||
@@ -2752,6 +2754,7 @@ function WorkbenchPage({
|
|||||||
onChange={setImageGridMode}
|
onChange={setImageGridMode}
|
||||||
direction={dropdownDirection}
|
direction={dropdownDirection}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeMode === "video" && (
|
{activeMode === "video" && (
|
||||||
|
|||||||
@@ -236,6 +236,13 @@ export const GRID_MODE_OPTIONS: WorkbenchOption[] = [
|
|||||||
{ value: "grid-25", label: "25 宫格" },
|
{ value: "grid-25", label: "25 宫格" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const GRID_SUPPORTED_MODELS = new Set([
|
||||||
|
"wan2.7-image-pro",
|
||||||
|
"wan2.7-image",
|
||||||
|
"gpt-image-2",
|
||||||
|
"gpt-image-2-vip",
|
||||||
|
]);
|
||||||
|
|
||||||
export const VIDEO_FRAME_OPTIONS: WorkbenchOption[] = [
|
export const VIDEO_FRAME_OPTIONS: WorkbenchOption[] = [
|
||||||
{ value: "omni", label: "全能参考" },
|
{ value: "omni", label: "全能参考" },
|
||||||
{ value: "start-end", label: "首尾帧" },
|
{ value: "start-end", label: "首尾帧" },
|
||||||
|
|||||||
@@ -722,3 +722,191 @@
|
|||||||
right: -9999px;
|
right: -9999px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tool Modal Overlay */
|
||||||
|
.studio-canvas-tool-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-modal {
|
||||||
|
position: relative;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 720px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
box-shadow: var(--shadow-heavy, 0 12px 40px rgba(0,0,0,0.4));
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-modal__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-modal__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-modal__close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--fg-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-modal__close:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tool Panel Components */
|
||||||
|
.studio-canvas-tool-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel--inpaint {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__preview {
|
||||||
|
flex: 0 0 260px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__controls {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__options {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__options button {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--fg-default);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__options button.is-active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--fg-default);
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__submit {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__submit:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__reset {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--fg-default);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__canvas-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 320px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__canvas-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-tool-panel__canvas {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3577,8 +3577,6 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
/* Script review left panel overflow guard: keep actions available while history remains scrollable. */
|
/* Script review left panel overflow guard: keep actions available while history remains scrollable. */
|
||||||
.script-eval-v5-left {
|
.script-eval-v5-left {
|
||||||
@@ -4087,4 +4085,3 @@
|
|||||||
.script-eval-v5.is-ready .script-eval-v5-status-dot {
|
.script-eval-v5.is-ready .script-eval-v5-status-dot {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
>>>>>>> c1c4086383ddd7c1c8c152c2d5a97a4f432fa260
|
|
||||||
|
|||||||
Reference in New Issue
Block a user