"use client"; import { useEffect, useLayoutEffect, useState, useCallback, use, useRef, useMemo } from "react"; import { useRouter, useParams, useSearchParams } from "next/navigation"; import useSWR from "swr"; import { AlertCircle, ArrowLeft, Save, Undo2, Redo2, ZoomIn, ZoomOut, MousePointer, Hand, Plus, ImagePlus, Type, Loader2, MessageCircle, Download, Upload, Wand2, Grid3X3, Settings, PenTool, Box, Layers, Crosshair, Scan, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { ScrollArea } from "@/components/ui/scroll-area"; import { toast } from "sonner"; import { useCanvasStore, canvasSelectors } from "@/store/canvas"; import { canvasService, imageGenerationService } from "@/services/canvas"; import { AgentWorkflowPanelNew, type AgentWorkflowInputs, } from "@/components/agents/AgentWorkflowPanelNew"; import { AgentGenerationWorkspaceNew } from "@/components/agents/AgentGenerationWorkspaceNew"; import { AgentWorkflowBrandModelPicker } from "@/components/agents/AgentWorkflowBrandModelPicker"; import { AgentWorkflowMaterialSelectionPanel } from "@/components/agents/AgentWorkflowMaterialSelectionPanel"; import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew"; // 深色主题组件 V2 (新版布局) import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2"; import { AgentWorkflowPanelDarkV2 } from "@/components/agents/AgentWorkflowPanelDarkV2"; import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2"; import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet"; import { CanvasWorkspace } from "@/components/canvas"; import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel"; import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel"; import { ObjectPickerOverlay } from "@/components/canvas/ObjectPickerOverlay"; import { SceneFusionDialog } from "@/components/scene-fusion-dialog"; import { ClientErrorBoundary } from "@/components/ui/client-error-boundary"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn, fetcher } from "@/lib/utils"; import { defaultAgentWorkflowUiConfig, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; import { createEmptyAgentWorkflowMaterialSelections, normalizeAgentWorkflowMaterialSelections, } from "@/lib/agents/workflow-material-config"; import { buildAgentWorkflowComposerContext, getAgentWorkflowProgress, } from "@/lib/agents/workflow-composer"; import { createEmptyAgentModeWorkspaceState, normalizeAgentModeWorkspaceState, } from "@/lib/agents/mode-workspace-state"; import { createEmptyAgentGenerationProjectPersistedState, createEmptyAgentGenerationProjectWorkspaceState, normalizeAgentGenerationProjectPersistedState, } from "@/lib/agents/generation-project-state"; import { agentProjectService } from "@/services/agent-projects"; import type { AgentGenerationProjectWorkspaceState, AgentModeWorkspaceState, AgentWorkflowComposerContext, AgentWorkflowMaterialSelections, AgentWorkflowPersistedState, AgentWorkflowSelectedBrandModel, AgentWorkflowShowcaseConfig, AgentWorkflowStepKey, } from "@/types/agent"; interface PageProps { params: Promise<{ id: string }>; } // 图片尺寸预设 const SIZE_PRESETS = [ { label: "1:1", value: "1:1", width: 1024, height: 1024 }, { label: "3:2", value: "3:2", width: 1536, height: 1024 }, { label: "2:3", value: "2:3", width: 1024, height: 1536 }, { label: "4:3", value: "4:3", width: 1365, height: 1024 }, { label: "3:4", value: "3:4", width: 1024, height: 1365 }, { label: "16:9", value: "16:9", width: 1820, height: 1024 }, { label: "9:16", value: "9:16", width: 1024, height: 1820 }, ]; const FLOATING_PANEL_WIDTH = 460; const FLOATING_PANEL_INSET = 16; const DOCKED_PANEL_INSET = 0; const FLOATING_UI_GAP = 16; const FLOATING_BOTTOM_INSET = 20; function buildScopedCanvasChatId(projectId: string, scope: "agent-mode") { const normalized = String(projectId || "").trim().toLowerCase().replace(/-/g, ""); if (!/^[0-9a-f]{32}$/.test(normalized)) { return projectId; } const scopeSuffix = scope === "agent-mode" ? "a93e17a60001" : "a93e17a60000"; const scoped = `${normalized.slice(0, 20)}${scopeSuffix}`; return [ scoped.slice(0, 8), scoped.slice(8, 12), scoped.slice(12, 16), scoped.slice(16, 20), scoped.slice(20, 32), ].join("-"); } function buildDefaultAgentProjectTitle() { const now = new Date(); const month = `${now.getMonth() + 1}`.padStart(2, "0"); const day = `${now.getDate()}`.padStart(2, "0"); const hour = `${now.getHours()}`.padStart(2, "0"); const minute = `${now.getMinutes()}`.padStart(2, "0"); return `生图项目 ${month}-${day} ${hour}:${minute}`; } function areStringArraysEqual(left: string[], right: string[]) { if (left === right) return true; if (left.length !== right.length) return false; for (let index = 0; index < left.length; index += 1) { if (left[index] !== right[index]) { return false; } } return true; } export default function CanvasEditorPage({ params }: PageProps) { // 使用 useParams 确保客户端导航时能正确获取更新的 id const routeParams = useParams(); const searchParams = useSearchParams(); const id = (routeParams?.id as string) || use(params).id; const router = useRouter(); const mode = searchParams.get("mode"); const shouldOpenBatchProcessing = searchParams.get("batch") === "1"; const goHome = useCallback(() => { if (typeof window !== "undefined") { window.location.assign("/"); } }, []); // Store state const projectTitle = useCanvasStore((state) => state.projectTitle); const viewport = useCanvasStore(canvasSelectors.viewport); const tool = useCanvasStore(canvasSelectors.tool); const canUndo = useCanvasStore(canvasSelectors.canUndo); const canRedo = useCanvasStore(canvasSelectors.canRedo); const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds); const nodes = useCanvasStore(canvasSelectors.nodes); // Store actions const setProject = useCanvasStore((state) => state.setProject); const setProjectTitle = useCanvasStore((state) => state.setProjectTitle); const setTool = useCanvasStore((state) => state.setTool); const zoomIn = useCanvasStore((state) => state.zoomIn); const zoomOut = useCanvasStore((state) => state.zoomOut); const undo = useCanvasStore((state) => state.undo); const redo = useCanvasStore((state) => state.redo); const loadCanvas = useCanvasStore((state) => state.loadCanvas); const clearCanvas = useCanvasStore((state) => state.clearCanvas); const getCanvasState = useCanvasStore((state) => state.getCanvasState); const addNode = useCanvasStore((state) => state.addNode); const selectNodes = useCanvasStore((state) => state.selectNodes); const updateNode = useCanvasStore((state) => state.updateNode); const setGenerating = useCanvasStore((state) => state.setGenerating); const setPickerMode = useCanvasStore((state) => state.setPickerMode); const requestImageNodeAction = useCanvasStore((state) => state.requestImageNodeAction); // Local state const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false); const [editTitle, setEditTitle] = useState(""); const [isPanelOpen, setIsPanelOpen] = useState(true); const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false); // 面板模式: 'chat' 聊天面板, 'fileList' 文件列表 const [panelMode, setPanelMode] = useState<'chat' | 'fileList'>('chat'); const [viewportWidth, setViewportWidth] = useState(0); const [workflowInputs, setWorkflowInputs] = useState({ primarySku: "", secondarySku: "", brand: "", store: "", }); const [workflowMaterials, setWorkflowMaterials] = useState( () => createEmptyAgentWorkflowMaterialSelections() ); const [workflowSelections, setWorkflowSelections] = useState< Partial> >({}); const [selectedBrandModel, setSelectedBrandModel] = useState(null); const [selectedAgentPresetId, setSelectedAgentPresetId] = useState(null); const [currentAgentStage, setCurrentAgentStage] = useState<"workflow" | "mode">("workflow"); const [agentModeState, setAgentModeState] = useState( () => createEmptyAgentModeWorkspaceState() ); const [agentWorkspaceState, setAgentWorkspaceState] = useState( () => createEmptyAgentGenerationProjectWorkspaceState() ); const [agentProjectTitle, setAgentProjectTitle] = useState("未命名生图项目"); const [agentProjectThumbnail, setAgentProjectThumbnail] = useState(null); const [agentProjectResultUrls, setAgentProjectResultUrls] = useState([]); const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false); const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false); const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false); const [workflowHydrationSource, setWorkflowHydrationSource] = useState< "pending" | "project" | "local" | "empty" >("pending"); const requestedAgentProjectId = searchParams.get("agentProjectId"); const agentProjectId = requestedAgentProjectId ? requestedAgentProjectId.trim() : ""; const hasActiveAgentProject = Boolean(agentProjectId); const agentModeChatId = useMemo( () => buildScopedCanvasChatId(agentProjectId || id, "agent-mode"), [agentProjectId, id] ); const { data: siteSettingsData } = useSWR<{ siteName?: string; siteLogo?: string | null; siteLogoDark?: string | null; agentEnabled?: boolean; realtimeCanvasEnabled?: boolean; realtimeCanvasServiceEnabled?: boolean; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; agentWorkflowUiConfig?: { steps?: Array<{ key: AgentWorkflowStepKey; title: string; description?: string | null; enabled?: boolean; sortOrder?: number; options?: Array<{ key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null; enabled?: boolean; sortOrder?: number; promptFragment?: string | null; }>; }>; }; }>("/api/site-settings", fetcher); // 生成配置 const [prompt, setPrompt] = useState(""); const [negativePrompt, setNegativePrompt] = useState(""); const [aspectRatio, setAspectRatio] = useState("auto"); const [imageCount, setImageCount] = useState(1); const previousNodeIdsRef = useRef>(new Set()); const requestedAgentMode = mode === "agent"; const isAgentFeatureEnabled = siteSettingsData?.agentEnabled !== false; const isAgentMode = isAgentFeatureEnabled && requestedAgentMode; // 获取选中的图片节点 const selectedImageNode = selectedNodeIds.length === 1 ? nodes.find(n => n.id === selectedNodeIds[0] && n.type === 'image') : null; const resolvedPanelWidth = viewportWidth > 0 ? Math.min(FLOATING_PANEL_WIDTH, Math.max(viewportWidth - DOCKED_PANEL_INSET, 0)) : FLOATING_PANEL_WIDTH; const panelOcclusionRight = isPanelOpen ? resolvedPanelWidth + DOCKED_PANEL_INSET : 0; const floatingToolbarOffset = isPanelOpen ? -(panelOcclusionRight / 2) : 0; const compactControlsLeft = FLOATING_UI_GAP; const updateCanvasSearchParams = useCallback( (updates: Record) => { const nextParams = new URLSearchParams(searchParams.toString()); Object.entries(updates).forEach(([key, value]) => { if (value === null || value === "") { nextParams.delete(key); } else { nextParams.set(key, value); } }); const nextQuery = nextParams.toString(); router.replace(nextQuery ? `/canvas/${id}?${nextQuery}` : `/canvas/${id}`); }, [id, router, searchParams] ); const handleToggleAgentMode = useCallback(() => { if (isAgentMode) { updateCanvasSearchParams({ mode: null, agentPresetId: null, agentProjectId: null, }); return; } updateCanvasSearchParams({ mode: "agent", agentPresetId: null, agentProjectId: agentProjectId || null, }); setIsPanelOpen(true); setPanelMode("chat"); }, [agentProjectId, isAgentMode, updateCanvasSearchParams]); // 加载画布数据 useLayoutEffect(() => { console.log('[Canvas] Loading project:', id); const loadProject = async () => { setIsLoading(true); setWorkflowHydrationSource("pending"); setWorkflowInputs({ primarySku: "", secondarySku: "", brand: "", store: "", }); setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections()); setWorkflowSelections({}); setSelectedBrandModel(null); setSelectedAgentPresetId(null); setAgentModeState(createEmptyAgentModeWorkspaceState()); setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState()); setAgentProjectTitle("未命名生图项目"); setAgentProjectThumbnail(null); setAgentProjectResultUrls([]); // 🆕 先清空当前画布状态,确保切换时不会显示旧数据 useCanvasStore.getState().clearCanvas(); const result = await canvasService.getProject(id); if (result.success && result.data) { useCanvasStore.getState().setProject(id, result.data.title); if (result.data.canvasData) { useCanvasStore.getState().loadCanvas(result.data.canvasData); const persistedWorkflow = result.data.canvasData.workflowState; if (!requestedAgentMode && persistedWorkflow) { setWorkflowInputs({ primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), brand: String(persistedWorkflow.inputs?.brand || "").trim(), store: String(persistedWorkflow.inputs?.store || "").trim(), }); setWorkflowMaterials( normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) ); setWorkflowSelections(persistedWorkflow.selections || {}); setSelectedBrandModel( persistedWorkflow.selectedBrandModel ? { ...persistedWorkflow.selectedBrandModel, brandName: String(persistedWorkflow.selectedBrandModel.brandName || "").trim(), id: String(persistedWorkflow.selectedBrandModel.id || "").trim(), imageUrl: String(persistedWorkflow.selectedBrandModel.imageUrl || "").trim(), assetId: String(persistedWorkflow.selectedBrandModel.assetId || "").trim() || null, displayName: String(persistedWorkflow.selectedBrandModel.displayName || "").trim() || null, mimeType: String(persistedWorkflow.selectedBrandModel.mimeType || "").trim() || null, sortOrder: Number.isFinite( Number(persistedWorkflow.selectedBrandModel.sortOrder) ) ? Math.max( 0, Math.min( 999, Math.round(Number(persistedWorkflow.selectedBrandModel.sortOrder)) ) ) : 0, } : null ); setSelectedAgentPresetId( String(persistedWorkflow.selectedAgentPresetId || "").trim() || null ); setCurrentAgentStage( persistedWorkflow.currentStage === "mode" ? "mode" : "workflow" ); setAgentModeState( normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState) ); setWorkflowHydrationSource("project"); } else { setWorkflowHydrationSource("empty"); } } else { setWorkflowHydrationSource("empty"); } } else { // 画布不存在时,创建一个新画布 console.log('[Canvas] 画布不存在,创建新画布:', id); useCanvasStore.getState().setProject(id, requestedAgentMode ? '智能体工作台' : '未命名画布'); setWorkflowHydrationSource("empty"); if (!requestedAgentMode) { // 保存新画布到数据库 try { await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' }); } catch (error) { console.error('[Canvas] 创建新画布失败:', error); } } } setIsLoading(false); }; loadProject(); }, [id, requestedAgentMode]); useEffect(() => { const updateViewportWidth = () => setViewportWidth(window.innerWidth); updateViewportWidth(); window.addEventListener("resize", updateViewportWidth); return () => window.removeEventListener("resize", updateViewportWidth); }, []); useEffect(() => { if (isAgentMode) { setIsPanelOpen(true); setPanelMode("chat"); } }, [isAgentMode]); useEffect(() => { if (shouldOpenBatchProcessing) { setSceneFusionDialogOpen(true); } }, [shouldOpenBatchProcessing]); const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]); const buildPersistedCanvasData = useCallback(() => { return { ...getCanvasState(), }; }, [ getCanvasState, ]); const buildPersistedAgentProjectState = useCallback(() => { const workflowState: AgentWorkflowPersistedState = { inputs: { primarySku: workflowInputs.primarySku.trim(), secondarySku: workflowInputs.secondarySku.trim(), brand: workflowInputs.brand.trim(), store: workflowInputs.store.trim(), }, materials: workflowMaterials, selections: workflowSelections, selectedBrandModel, selectedAgentPresetId: selectedAgentPresetId || null, currentStage: currentAgentStage, agentModeState, }; return { workflowState, workspaceState: agentWorkspaceState, resultImageUrls: agentProjectResultUrls, }; }, [ agentModeState, agentProjectResultUrls, agentWorkspaceState, currentAgentStage, selectedAgentPresetId, selectedBrandModel, workflowInputs, workflowMaterials, workflowSelections, ]); useEffect(() => { if (isAgentMode) return; if (typeof window === "undefined") return; if (workflowHydrationSource !== "empty") return; try { const raw = window.localStorage.getItem(workflowStorageKey); if (!raw) { setWorkflowHydrationSource("local"); return; } const parsed = JSON.parse(raw) as AgentWorkflowPersistedState; setWorkflowInputs({ primarySku: String(parsed.inputs?.primarySku || "").trim(), secondarySku: String(parsed.inputs?.secondarySku || "").trim(), brand: String(parsed.inputs?.brand || "").trim(), store: String(parsed.inputs?.store || "").trim(), }); setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials)); setWorkflowSelections(parsed.selections || {}); setSelectedBrandModel( parsed.selectedBrandModel ? { ...parsed.selectedBrandModel, brandName: String(parsed.selectedBrandModel.brandName || "").trim(), id: String(parsed.selectedBrandModel.id || "").trim(), imageUrl: String(parsed.selectedBrandModel.imageUrl || "").trim(), assetId: String(parsed.selectedBrandModel.assetId || "").trim() || null, displayName: String(parsed.selectedBrandModel.displayName || "").trim() || null, mimeType: String(parsed.selectedBrandModel.mimeType || "").trim() || null, sortOrder: Number.isFinite(Number(parsed.selectedBrandModel.sortOrder)) ? Math.max(0, Math.min(999, Math.round(Number(parsed.selectedBrandModel.sortOrder)))) : 0, } : null ); setSelectedAgentPresetId(String(parsed.selectedAgentPresetId || "").trim() || null); setCurrentAgentStage(parsed.currentStage === "mode" ? "mode" : "workflow"); setAgentModeState(normalizeAgentModeWorkspaceState(parsed.agentModeState)); } catch (error) { console.warn("[Canvas] Failed to hydrate workflow state from localStorage:", error); } finally { setWorkflowHydrationSource("local"); } }, [isAgentMode, workflowHydrationSource, workflowStorageKey]); useEffect(() => { if (isAgentMode) return; if (typeof window === "undefined" || workflowHydrationSource === "pending") return; try { window.localStorage.setItem( workflowStorageKey, JSON.stringify({ inputs: workflowInputs, materials: workflowMaterials, selections: workflowSelections, selectedBrandModel, selectedAgentPresetId, currentAgentStage, agentModeState, }) ); } catch (error) { console.warn("[Canvas] Failed to persist workflow state:", error); } }, [ isAgentMode, workflowHydrationSource, workflowInputs, workflowMaterials, workflowSelections, workflowStorageKey, selectedBrandModel, selectedAgentPresetId, currentAgentStage, agentModeState, ]); const persistAgentProject = useCallback( async ( reason: "autosave" | "manual" | "switch", overrides?: { title?: string | null } ) => { if (!isAgentMode || !agentProjectId) return false; const nextTitle = String(overrides?.title ?? agentProjectTitle ?? "").trim() || buildDefaultAgentProjectTitle(); const result = await agentProjectService.updateProject(agentProjectId, { title: nextTitle, thumbnail: agentProjectThumbnail, projectState: buildPersistedAgentProjectState(), }); if (!result.success) { console.error(`[Agent Project] ${reason} save failed:`, result.error); return false; } if (result.data) { setAgentProjectTitle(result.data.title || buildDefaultAgentProjectTitle()); setAgentProjectThumbnail(result.data.thumbnail || null); } return true; }, [ agentProjectId, agentProjectThumbnail, agentProjectTitle, buildPersistedAgentProjectState, isAgentMode, ] ); useEffect(() => { if (!requestedAgentMode || !isAgentFeatureEnabled || agentProjectId) return; let cancelled = false; const createAgentProject = async () => { setIsAgentProjectLoading(true); try { const result = await agentProjectService.createProject({ title: buildDefaultAgentProjectTitle(), projectState: createEmptyAgentGenerationProjectPersistedState(), }); if (!result.success || !result.data) { throw new Error(result.error || "创建生图项目失败"); } if (cancelled) return; updateCanvasSearchParams({ mode: "agent", agentProjectId: result.data.id, }); } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : "创建生图项目失败"); } } finally { if (!cancelled) { setIsAgentProjectLoading(false); } } }; void createAgentProject(); return () => { cancelled = true; }; }, [agentProjectId, isAgentFeatureEnabled, requestedAgentMode, updateCanvasSearchParams]); useEffect(() => { if (!requestedAgentMode || !agentProjectId) return; let cancelled = false; const loadAgentProject = async () => { setIsAgentProjectLoading(true); try { const result = await agentProjectService.getProject(agentProjectId); if (!result.success || !result.data) { throw new Error(result.error || "加载生图项目失败"); } if (cancelled) return; const persistedState = normalizeAgentGenerationProjectPersistedState( result.data.projectState ); const persistedWorkflow = persistedState.workflowState; setWorkflowInputs({ primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), brand: String(persistedWorkflow.inputs?.brand || "").trim(), store: String(persistedWorkflow.inputs?.store || "").trim(), }); setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)); setWorkflowSelections(persistedWorkflow.selections || {}); setSelectedBrandModel(persistedWorkflow.selectedBrandModel || null); setSelectedAgentPresetId(String(persistedWorkflow.selectedAgentPresetId || "").trim() || null); setCurrentAgentStage(persistedWorkflow.currentStage === "mode" ? "mode" : "workflow"); setAgentModeState(normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState)); setAgentWorkspaceState( persistedState.workspaceState || createEmptyAgentGenerationProjectWorkspaceState() ); setAgentProjectTitle(result.data.title || "未命名生图项目"); setAgentProjectThumbnail(result.data.thumbnail || null); setAgentProjectResultUrls(persistedState.resultImageUrls || []); } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : "加载生图项目失败"); } } finally { if (!cancelled) { setIsAgentProjectLoading(false); } } }; void loadAgentProject(); return () => { cancelled = true; }; }, [agentProjectId, requestedAgentMode]); useEffect(() => { if (!isAgentMode || !agentProjectId || isAgentProjectLoading) return; const timer = window.setTimeout(async () => { setIsAgentProjectSaving(true); try { await persistAgentProject("autosave"); } finally { setIsAgentProjectSaving(false); } }, 2500); return () => window.clearTimeout(timer); }, [ agentModeState, agentProjectId, agentProjectResultUrls, agentProjectThumbnail, agentProjectTitle, agentWorkspaceState, currentAgentStage, isAgentMode, isAgentProjectLoading, persistAgentProject, selectedAgentPresetId, selectedBrandModel, workflowInputs, workflowMaterials, workflowSelections, ]); const handleManualSaveAgentProject = useCallback(async () => { if (!agentProjectId) { toast.error("当前还没有可保存的生图项目"); return; } const suggestedTitle = String(agentProjectTitle || "").trim() || buildDefaultAgentProjectTitle(); const promptedTitle = window.prompt("请输入要保存的项目名称", suggestedTitle); if (promptedTitle === null) { return; } const nextTitle = promptedTitle.trim(); if (!nextTitle) { toast.error("项目名称不能为空"); return; } setIsAgentProjectSaving(true); try { const success = await persistAgentProject("manual", { title: nextTitle }); if (success) { toast.success("生图项目已保存"); } else { toast.error("保存生图项目失败"); } } finally { setIsAgentProjectSaving(false); } }, [agentProjectId, persistAgentProject]); const handleRenameAgentProject = useCallback( async (targetProjectId: string, currentTitle?: string | null) => { const suggestedTitle = String(currentTitle || "").trim() || "未命名生图项目"; const promptedTitle = window.prompt("请输入新的项目名称", suggestedTitle); if (promptedTitle === null) { return; } const nextTitle = promptedTitle.trim(); if (!nextTitle) { toast.error("项目名称不能为空"); return; } const result = await agentProjectService.updateProject(targetProjectId, { title: nextTitle, }); if (!result.success || !result.data) { toast.error(result.error || "更新生图项目名称失败"); return; } if (targetProjectId === agentProjectId) { setAgentProjectTitle(result.data.title || nextTitle); } toast.success("项目名称已更新"); }, [agentProjectId] ); const handleCreateAgentProject = useCallback(async () => { if (agentProjectId) { await persistAgentProject("switch"); } const result = await agentProjectService.createProject({ title: buildDefaultAgentProjectTitle(), projectState: createEmptyAgentGenerationProjectPersistedState(), }); if (!result.success || !result.data) { toast.error(result.error || "创建生图项目失败"); return; } updateCanvasSearchParams({ mode: "agent", agentProjectId: result.data.id, }); }, [agentProjectId, persistAgentProject, updateCanvasSearchParams]); const handleSelectAgentProject = useCallback( async (nextProjectId: string) => { if (!nextProjectId || nextProjectId === agentProjectId) return; if (agentProjectId) { await persistAgentProject("switch"); } updateCanvasSearchParams({ mode: "agent", agentProjectId: nextProjectId, }); }, [agentProjectId, persistAgentProject, updateCanvasSearchParams] ); const handleDeleteAgentProject = useCallback( async (targetProjectId: string) => { const result = await agentProjectService.deleteProject(targetProjectId); if (!result.success) { toast.error(result.error || "删除生图项目失败"); return; } if (targetProjectId === agentProjectId) { const listResult = await agentProjectService.getProjects(); const nextProject = listResult.success ? listResult.data?.find((project) => project.id !== targetProjectId) || null : null; if (nextProject) { updateCanvasSearchParams({ mode: "agent", agentProjectId: nextProject.id, }); } else { const createResult = await agentProjectService.createProject({ title: buildDefaultAgentProjectTitle(), projectState: createEmptyAgentGenerationProjectPersistedState(), }); if (createResult.success && createResult.data) { updateCanvasSearchParams({ mode: "agent", agentProjectId: createResult.data.id, }); } } } toast.success("生图项目已删除"); }, [agentProjectId, updateCanvasSearchParams] ); const handleAgentResultPreviewUrlsChange = useCallback((urls: string[]) => { setAgentProjectResultUrls((current) => areStringArraysEqual(current, urls) ? current : urls ); setAgentProjectThumbnail((current) => { const nextThumbnail = urls[0] || null; return current === nextThumbnail ? current : nextThumbnail; }); }, []); useEffect(() => { if (!selectedBrandModel) return; const normalizedBrand = workflowInputs.brand.trim().toLowerCase(); if (!normalizedBrand) { setSelectedBrandModel(null); return; } if (selectedBrandModel.brandName.toLowerCase() !== normalizedBrand) { setSelectedBrandModel(null); } }, [selectedBrandModel, workflowInputs.brand]); const persistCanvasState = useCallback( async (reason: "autosave" | "delete" | "beforeunload") => { const canvasData = buildPersistedCanvasData(); const result = await canvasService.saveCanvasState(id, canvasData); if (!result.success) { console.error(`[Canvas] ${reason} save failed:`, result.error); } return result.success; }, [buildPersistedCanvasData, id] ); // 自动保存逻辑 useEffect(() => { if (isLoading || isAgentMode) return; const saveTimer = setTimeout(async () => { setIsSaving(true); try { await persistCanvasState("autosave"); } catch (error) { console.error("Auto save failed:", error); } setIsSaving(false); }, 3000); // 3秒自动保存 return () => clearTimeout(saveTimer); }, [isAgentMode, nodes, isLoading, persistCanvasState]); // 删除节点时立即保存(按节点ID差集检测),避免删除后快速刷新被回滚 useEffect(() => { if (isLoading || isAgentMode) { previousNodeIdsRef.current = new Set(nodes.map((node) => node.id)); return; } const previousIds = previousNodeIdsRef.current; const currentIds = new Set(nodes.map((node) => node.id)); const removedIds: string[] = []; previousIds.forEach((nodeId) => { if (!currentIds.has(nodeId)) { removedIds.push(nodeId); } }); previousNodeIdsRef.current = currentIds; if (removedIds.length > 0) { const persistAfterDelete = async () => { setIsSaving(true); try { await persistCanvasState("delete"); } catch (error) { console.error("Delete immediate save failed:", error); } setIsSaving(false); }; void persistAfterDelete(); } }, [isAgentMode, nodes, isLoading, persistCanvasState]); // 页面关闭/刷新前尝试做一次 keepalive 保存,降低“刚删就刷新”回滚概率 useEffect(() => { if (isLoading || isAgentMode) return; const handleBeforeUnload = () => { try { const canvasData = buildPersistedCanvasData(); void fetch(`/api/canvas/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ canvasData }), keepalive: true, }); } catch (error) { console.error("[Canvas] beforeunload save failed:", error); } }; window.addEventListener("beforeunload", handleBeforeUnload); window.addEventListener("pagehide", handleBeforeUnload); return () => { window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("pagehide", handleBeforeUnload); }; }, [buildPersistedCanvasData, id, isAgentMode, isLoading]); // 智能命名与缩略图逻辑 useEffect(() => { if (isAgentMode) return; const autoTitleAndThumbnail = async () => { // 如果标题是默认值,且有图片节点 if ((projectTitle === "未命名画布" || projectTitle === "新画布") && nodes.length > 0) { const imageNode = nodes.find(n => n.type === 'image' && (n.data as any)?.src); const imageSrc = (imageNode?.data as any)?.src; if (imageSrc) { try { // 1. 设置缩略图 await canvasService.updateProject(id, { thumbnail: imageSrc }); // 2. 智能命名 const res = await fetch('/api/canvas/auto-title', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageUrl: imageSrc }) }); const data = await res.json(); if (data.title) { setProjectTitle(data.title); await canvasService.updateProject(id, { title: data.title }); toast.success(`画布已自动命名为:${data.title}`); } } catch (error) { console.error("Auto title failed:", error); } } } }; // 使用防抖,避免频繁触发 const timer = setTimeout(autoTitleAndThumbnail, 5000); // 5秒后触发,给用户一点时间操作 return () => clearTimeout(timer); }, [id, isAgentMode, nodes, projectTitle, setProjectTitle]); // 保存画布 (手动触发保留作为备用,但UI上移除) const handleSave = useCallback(async () => { setIsSaving(true); const canvasData = buildPersistedCanvasData(); const result = await canvasService.saveCanvasState(id, canvasData); if (result.success) { toast.success("保存成功"); } else { toast.error(result.error || "保存失败"); } setIsSaving(false); }, [buildPersistedCanvasData, id]); // 更新标题 const handleUpdateTitle = async () => { if (!editTitle.trim() || editTitle === projectTitle) { setIsEditingTitle(false); return; } const result = await canvasService.updateProject(id, { title: editTitle.trim() }); if (result.success) { setProjectTitle(editTitle.trim()); } else { toast.error(result.error || "更新标题失败"); } setIsEditingTitle(false); }; // 添加文本节点 const handleAddTextNode = () => { addNode({ type: 'text', position: { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 }, size: { width: 200, height: 100 }, data: { content: '双击编辑文本' }, connections: [], }); }; // 添加图片节点 const handleAddImageNode = () => { addNode({ type: 'image', position: { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 }, size: { width: 300, height: 300 }, data: { src: '', status: 'pending', title: '新图片', prompt: '', }, connections: [], }); }; const handleBatchAddToCanvas = useCallback((imageUrls: string[]) => { if (!imageUrls || imageUrls.length === 0) { toast.error("没有可添加到画布的图片"); return; } const columns = Math.min(4, imageUrls.length); const startX = 120; const startY = 120; const createdNodeIds: string[] = []; for (const [index, imageUrl] of imageUrls.entries()) { const x = startX + (index % columns) * 320; const y = startY + Math.floor(index / columns) * 320; const nodeId = addNode({ type: "image", position: { x, y }, size: { width: 300, height: 300 }, data: { src: imageUrl, status: "completed", title: `批量结果-${index + 1}`, prompt: "批量处理结果", }, connections: [], }); createdNodeIds.push(nodeId); } if (createdNodeIds.length > 0) { selectNodes(createdNodeIds); } }, [addNode, selectNodes]); // 处理图片操作(通过事件分发到选中图片节点执行) const handleAction = (action: string) => { if (!selectedImageNode) { toast.error("请先选择一张图片"); return; } if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("canvas:image-action", { detail: { nodeId: selectedImageNode.id, action, }, }), ); } }; // AI 生成图片 const handleGenerate = async () => { if (!prompt.trim()) { toast.error("请输入提示词"); return; } setGenerating(true); // 先创建占位节点(显示生成中状态) const placeholderNodeIds: string[] = []; for (let i = 0; i < imageCount; i++) { const nodeId = addNode({ type: 'image', position: { x: 100 + (i % 3) * 320, y: 100 + Math.floor(i / 3) * 320 }, size: { width: 300, height: 300 }, data: { src: '', status: 'generating', title: prompt.slice(0, 24) || '生成中', prompt: prompt, }, connections: [], }); placeholderNodeIds.push(nodeId); } try { // 调用 AI 图片生成 API const result = await imageGenerationService.generateImage({ prompt, negativePrompt, aspectRatio, imageCount, }); if (result.success && result.data?.images) { // 更新节点为生成的图片 result.data.images.forEach((image, index) => { if (placeholderNodeIds[index]) { updateNode(placeholderNodeIds[index], { data: { src: image.originalUrl || image.thumbnailUrl, status: 'completed', title: `${prompt.slice(0, 20) || '生成图片'}-${index + 1}`, prompt: prompt, }, }); } }); toast.success(`成功生成 ${result.data.images.length} 张图片`); setPrompt(""); // 清空提示词 } else { // 生成失败,更新节点状态 placeholderNodeIds.forEach(nodeId => { updateNode(nodeId, { data: { src: '', status: 'failed', title: '生成失败', prompt: prompt, }, }); }); toast.error(result.error || "图片生成失败"); } } catch (error) { console.error("AI 图片生成失败:", error); // 生成失败,更新节点状态 placeholderNodeIds.forEach(nodeId => { updateNode(nodeId, { data: { src: '', status: 'failed', title: '生成失败', prompt: prompt, }, }); }); toast.error("图片生成失败,请重试"); } finally { setGenerating(false); } }; // 键盘快捷键 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 如果正在输入则不触发快捷键(包括 input、textarea 和 contentEditable 元素) const target = e.target as HTMLElement; if (target instanceof HTMLInputElement) { const inputType = String(target.type || "").toLowerCase(); const isTextInput = inputType === "text" || inputType === "search" || inputType === "url" || inputType === "email" || inputType === "password" || inputType === "number"; if (isTextInput) { return; } } if ( target instanceof HTMLTextAreaElement || target.isContentEditable || target.closest('[contenteditable="true"]') ) { return; } // Ctrl/Cmd + S 保存 if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); handleSave(); } // Ctrl/Cmd + A:当已选中图片时,选中画布内全部图片 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') { const { nodes: currentNodes, selectedNodeIds: currentSelectedNodeIds } = useCanvasStore.getState(); const selectedHasImage = currentSelectedNodeIds.some((selectedId) => currentNodes.some((node) => node.id === selectedId && node.type === "image") ); if (selectedHasImage) { const allImageNodeIds = currentNodes .filter((node) => node.type === "image" && Boolean((node.data as any)?.src)) .map((node) => node.id); if (allImageNodeIds.length > 0) { e.preventDefault(); selectNodes(allImageNodeIds); } } } // Ctrl/Cmd + Z 撤销 if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); } // Ctrl/Cmd + Shift + Z 重做 if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) { e.preventDefault(); redo(); } // V 选择工具 if (e.key === 'v' && !e.ctrlKey && !e.metaKey) { setTool('select'); } // H 平移工具 if (e.key === 'h' && !e.ctrlKey && !e.metaKey) { setTool('pan'); } // Delete 删除选中 if (e.key === 'Delete' || e.key === 'Backspace') { useCanvasStore.getState().deleteSelectedNodes(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleSave, nodes, redo, selectNodes, selectedNodeIds, setTool, undo]); useEffect(() => { const html = document.documentElement; const body = document.body; const prevHtmlOverscrollX = html.style.overscrollBehaviorX; const prevBodyOverscrollX = body.style.overscrollBehaviorX; html.style.overscrollBehaviorX = "none"; body.style.overscrollBehaviorX = "none"; const preventHistorySwipe = (event: WheelEvent) => { if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return; if (event.cancelable) { event.preventDefault(); } }; window.addEventListener("wheel", preventHistorySwipe, { passive: false }); return () => { window.removeEventListener("wheel", preventHistorySwipe); html.style.overscrollBehaviorX = prevHtmlOverscrollX; body.style.overscrollBehaviorX = prevBodyOverscrollX; }; }, []); const resolvedWorkflowUiConfig = useMemo( () => normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig), [siteSettingsData?.agentWorkflowUiConfig] ); const workflowContext = useMemo(() => { return buildAgentWorkflowComposerContext({ config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, inputs: workflowInputs, materials: workflowMaterials, selectedBrandModel, selections: workflowSelections, }); }, [ resolvedWorkflowUiConfig, selectedBrandModel, workflowInputs, workflowMaterials, workflowSelections, ]); const workflowProgress = useMemo( () => getAgentWorkflowProgress({ config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, selections: workflowSelections, }), [resolvedWorkflowUiConfig, workflowSelections] ); if (isLoading) { return (
加载中...
); } const brandEntry = (
); const agentDisabledNotice = requestedAgentMode && !isAgentFeatureEnabled ? (
智能体功能未开启,请联系管理员在系统设置中开启。
) : null; const agentProjectNavSlot = isAgentMode ? (
{ event.preventDefault(); void handleManualSaveAgentProject(); }} > 保存项目 { event.preventDefault(); setIsAgentProjectSheetOpen(true); }} > 项目列表
) : null; if (isAgentMode) { // 深色主题智能体模式 V2 - 完整功能版本 // 第一步:工作流配置(品牌/店铺/步骤选择) if (currentAgentStage === "workflow") { return ( setCurrentAgentStage("mode")} onSkipStep={() => setCurrentAgentStage("mode")} onReset={() => { setWorkflowInputs({ primarySku: "", secondarySku: "", brand: "", store: "" }); setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections()); setWorkflowSelections({}); setSelectedBrandModel(null); setSelectedAgentPresetId(null); setAgentModeState(createEmptyAgentModeWorkspaceState()); setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState()); setCurrentAgentStage("workflow"); }} /> ); } // 第二步:智能体选择 + 生图工作台 return ( setCurrentAgentStage("workflow")} onOpenCanvasEditor={() => { setCurrentAgentStage("mode"); updateCanvasSearchParams({ mode: null, }); }} onClearPreset={() => setSelectedAgentPresetId(null)} // 嵌入的智能体选择面板 presetPanel={ } brandModelPanel={ } materialSelectionPanel={ } /> ); } return (
{/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */}
{brandEntry} {agentDisabledNotice} {/* 2. Floating Actions (Top Right) */} {!isPanelOpen && (
)} {/* 4. Floating Sidebar (Right) */}
{panelMode === 'chat' ? ( // AI 对话内容 - 使用 key 强制在 id 变化时重新挂载
setIsPanelOpen(false)} onOpenFileList={() => setPanelMode('fileList')} selectedAgentPresetId={null} chatScope="canvas" agentEnabled={isAgentFeatureEnabled} />
) : ( // 文件列表 setPanelMode('chat')} /> )}
{/* 5. Floating Toolbar (Bottom) */}
选择 (V) 平移 (H) {isAgentFeatureEnabled ? ( 智能体模式 ) : null} 批量处理 添加图片 添加文本 {selectedImageNode ? "识物" : "先选中图片后识物"} {selectedImageNode ? "框选抠图" : "先选中图片后框选抠图"} {siteSettingsData?.realtimeCanvasEnabled === true && siteSettingsData?.realtimeCanvasServiceEnabled !== false ? ( 实时画布 ) : null} 3D生成 新建画布
{/* 6. Bottom Left Compact Controls */}
撤销 重做
{Math.round(viewport.zoom * 100)}%
{/* 7. 物品拾取模式覆盖层 */}
); }