diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index f3825ec..662c17f 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -13,6 +13,7 @@ import { import { useEffect, useMemo, useRef, useState } from "react"; import type { ReactNode } from "react"; import { publicConfigClient, type WebPublicConfig } from "../api/publicConfigClient"; +import { toast } from "./toast/toastStore"; import type { ServerConnectionHealth } from "../api/serverConnection"; import { ossAssets } from "../data/ossAssets"; import { canManageCommunityCases, canReviewCommunity } from "../features/community-review/communityPermissions"; diff --git a/src/features/canvas/CanvasPage.tsx b/src/features/canvas/CanvasPage.tsx index 1ca62a2..1d99981 100644 --- a/src/features/canvas/CanvasPage.tsx +++ b/src/features/canvas/CanvasPage.tsx @@ -182,6 +182,7 @@ import { } from "./canvasWorkflowDeserialize"; import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents"; import type { CanvasNodeToolbarAction } from "./canvasComponents"; +import { CanvasMultiGridPanel, CanvasUpscalePanel, CanvasInpaintPanel } from "./canvasToolPanels"; import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing"; const canvasEnterpriseVideoModelOptions: CanvasOption[] = ENTERPRISE_VIDEO_MODEL_OPTIONS.map((option) => ({ @@ -336,6 +337,7 @@ function CanvasPage({ const [imageFocusNodeId, setImageFocusNodeId] = useState(null); const [imageFocusDraft, setImageFocusDraft] = useState(null); const [imageFocusDrag, setImageFocusDrag] = useState(null); + const [canvasToolModal, setCanvasToolModal] = useState<{ tool: "multiGrid" | "upscale" | "inpaint"; imageNode: CanvasImageNode } | null>(null); const [stylePickerImageNodeId, setStylePickerImageNodeId] = useState(null); const [stylePickerCases, setStylePickerCases] = useState([]); const [stylePickerLoading, setStylePickerLoading] = useState(false); @@ -4264,7 +4266,7 @@ function CanvasPage({ setSelectedExistingCategory(""); setSaveAssetOpen(true); } - if (key === "upscale") void handleGenerateImageNode(imageNode.id); + if (key === "upscale") setCanvasToolModal({ tool: "upscale", imageNode }); }} moreActions={[ { key: "copy", label: "复制链接", icon: , disabled: !imageNode.imageUrl }, @@ -4570,16 +4572,42 @@ function CanvasPage({ )} + + @@ -5729,6 +5757,27 @@ function CanvasPage({ + {canvasToolModal && ( +
setCanvasToolModal(null)}> +
e.stopPropagation()} role="dialog" aria-modal="true" aria-label={canvasToolModal.tool === "multiGrid" ? "多宫格" : canvasToolModal.tool === "upscale" ? "超分" : "局部重绘"}> +
+

{canvasToolModal.tool === "multiGrid" ? "多宫格生成" : canvasToolModal.tool === "upscale" ? "图片超分" : "局部重绘"}

+ +
+
+ {canvasToolModal.tool === "multiGrid" && ( + { setImageNodes((nodes) => nodes.map((n) => n.id === canvasToolModal.imageNode.id ? { ...n, imageUrl: url } : n)); setCanvasToolModal(null); }} /> + )} + {canvasToolModal.tool === "upscale" && ( + { setImageNodes((nodes) => nodes.map((n) => n.id === canvasToolModal.imageNode.id ? { ...n, imageUrl: url } : n)); setCanvasToolModal(null); }} /> + )} + {canvasToolModal.tool === "inpaint" && ( + { setImageNodes((nodes) => nodes.map((n) => n.id === canvasToolModal.imageNode.id ? { ...n, imageUrl: url } : n)); setCanvasToolModal(null); }} /> + )} +
+
+
+ )} ); } diff --git a/src/features/canvas/canvasToolPanels.tsx b/src/features/canvas/canvasToolPanels.tsx new file mode 100644 index 0000000..02afbf7 --- /dev/null +++ b/src/features/canvas/canvasToolPanels.tsx @@ -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 ( +
+
+
+ +
+ {([["grid-4", "2×2"], ["grid-9", "3×3"]] as const).map(([value, label]) => ( + + ))} +
+ +