diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 35edf26e6..e4022481f 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -32,6 +32,14 @@ import { Scan, } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; @@ -116,6 +124,7 @@ import type { AgentPsdTemplateCategoryTagIndex } from "@/lib/agents/psd-template import { agentProjectService } from "@/services/agent-projects"; import type { AgentPreset, + AgentGenerationProjectPersistedState, AgentGenerationProjectResultCollections, AgentGenerationResultModeType, AgentGenerationProjectWorkspaceState, @@ -233,6 +242,146 @@ function normalizeResultCollectionUrls(urls: string[] | undefined | null) { ); } +const AGENT_PROJECT_DRAFT_STORAGE_KEY_PREFIX = "agent-project-draft:"; + +function getAgentProjectDraftStorageKey(projectId: string) { + return `${AGENT_PROJECT_DRAFT_STORAGE_KEY_PREFIX}${String(projectId || "").trim()}`; +} + +interface AgentProjectDraftSnapshot { + savedAt: number; + title?: string | null; + thumbnail?: string | null; + projectState?: AgentGenerationProjectPersistedState; +} + +function readAgentProjectDraftSnapshot(projectId: string): AgentProjectDraftSnapshot | null { + if (typeof window === "undefined") { + return null; + } + + const storageKey = getAgentProjectDraftStorageKey(projectId); + if (!storageKey) { + return null; + } + + try { + const raw = window.localStorage.getItem(storageKey); + if (!raw) { + return null; + } + + const parsed = JSON.parse(raw) as AgentProjectDraftSnapshot | null; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return null; + } + + return { + savedAt: Number.isFinite(Number(parsed.savedAt)) ? Number(parsed.savedAt) : 0, + title: String(parsed.title || "").trim() || null, + thumbnail: String(parsed.thumbnail || "").trim() || null, + projectState: + parsed.projectState && typeof parsed.projectState === "object" + ? parsed.projectState + : undefined, + }; + } catch (error) { + console.warn("[Agent Project] Failed to read local draft:", error); + return null; + } +} + +function removeAgentProjectDraftSnapshot(projectId: string) { + if (typeof window === "undefined") { + return; + } + + const storageKey = getAgentProjectDraftStorageKey(projectId); + if (!storageKey) { + return; + } + + try { + window.localStorage.removeItem(storageKey); + } catch (error) { + console.warn("[Agent Project] Failed to remove local draft:", error); + } +} + +function isUnauthorizedApiError(error?: string | null) { + return /未登录|unauthorized/i.test(String(error || "").trim()); +} + +function clearAppearanceSummaryFromResultCollection( + result: AgentWorkflowMaterialSelections["primary"]["result"] | null | undefined +) { + if (!result) { + return result; + } + + let changed = false; + const nextImages = (result.images || []).map((image) => { + if (!String(image.appearanceSummary || "").trim()) { + return image; + } + changed = true; + return { + ...image, + appearanceSummary: null, + }; + }); + const nextGoodsInfo = + result.goodsInfo && String(result.goodsInfo.appearanceSummary || "").trim() + ? { + ...result.goodsInfo, + appearanceSummary: null, + } + : result.goodsInfo; + + if (nextGoodsInfo !== result.goodsInfo) { + changed = true; + } + + if (!changed) { + return result; + } + + return { + ...result, + images: nextImages, + goodsInfo: nextGoodsInfo, + }; +} + +function clearAppearanceSummaryFromMaterials( + materials: AgentWorkflowMaterialSelections +): AgentWorkflowMaterialSelections { + const nextPrimary = clearAppearanceSummaryFromResultCollection( + materials.primary.result || null + ); + const nextSecondary = clearAppearanceSummaryFromResultCollection( + materials.secondary.result || null + ); + + if ( + nextPrimary === materials.primary.result && + nextSecondary === materials.secondary.result + ) { + return materials; + } + + return { + primary: { + ...materials.primary, + result: nextPrimary || null, + }, + secondary: { + ...materials.secondary, + result: nextSecondary || null, + }, + }; +} + function areResultCollectionsEqual( left: AgentGenerationProjectResultCollections, right: AgentGenerationProjectResultCollections @@ -364,6 +513,10 @@ export default function CanvasEditorPage({ params }: PageProps) { const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false); const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false); const [isAgentProjectMenuOpen, setIsAgentProjectMenuOpen] = useState(false); + const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false); + const [loginDialogMessage, setLoginDialogMessage] = useState("当前登录状态已失效,请重新登录后继续操作。"); + const [authSessionChecked, setAuthSessionChecked] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(true); const [workflowHydrationSource, setWorkflowHydrationSource] = useState< "pending" | "project" | "local" | "empty" >("pending"); @@ -471,6 +624,44 @@ export default function CanvasEditorPage({ params }: PageProps) { [id, router, searchParams] ); + const openLoginDialog = useCallback((message?: string) => { + setLoginDialogMessage( + String(message || "").trim() || "当前登录状态已失效,请重新登录后继续操作。" + ); + setIsLoginDialogOpen(true); + }, []); + + const redirectToLogin = useCallback(() => { + if (typeof window === "undefined") { + router.push("/login"); + return; + } + + const callbackUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.assign(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`); + }, [router]); + + const checkCanvasAuthSession = useCallback(async () => { + try { + const response = await fetch("/api/auth/session", { cache: "no-store" }); + const session = await response.json().catch(() => null); + const authed = Boolean(session?.user?.id); + setIsAuthenticated(authed); + setAuthSessionChecked(true); + if (!authed) { + openLoginDialog(); + } else { + setIsLoginDialogOpen(false); + } + return authed; + } catch (error) { + console.warn("[Canvas] Failed to verify auth session:", error); + setIsAuthenticated(true); + setAuthSessionChecked(true); + return true; + } + }, [openLoginDialog]); + const handleToggleAgentMode = useCallback(() => { if (isAgentMode) { updateCanvasSearchParams({ @@ -491,6 +682,10 @@ export default function CanvasEditorPage({ params }: PageProps) { }, [agentProjectId, isAgentMode, updateCanvasSearchParams]); const buildCurrentWorkflowPersistedState = useCallback((): AgentWorkflowPersistedState => { + const persistedMaterials = appearanceRecognitionEnabled + ? workflowMaterials + : clearAppearanceSummaryFromMaterials(workflowMaterials); + return { inputs: { primarySku: workflowInputs.primarySku.trim(), @@ -502,7 +697,7 @@ export default function CanvasEditorPage({ params }: PageProps) { fifthCategory: workflowInputs.fifthCategory.trim(), priceBand: workflowInputs.priceBand.trim(), }, - materials: workflowMaterials, + materials: persistedMaterials, appearanceRecognitionEnabled, selections: sanitizedWorkflowSelections, selectedBrandModel, @@ -528,6 +723,26 @@ export default function CanvasEditorPage({ params }: PageProps) { currentWorkflowPersistedStateRef.current = buildCurrentWorkflowPersistedState(); }, [buildCurrentWorkflowPersistedState]); + useEffect(() => { + void checkCanvasAuthSession(); + + const handleWindowFocus = () => { + void checkCanvasAuthSession(); + }; + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") { + void checkCanvasAuthSession(); + } + }; + + window.addEventListener("focus", handleWindowFocus); + document.addEventListener("visibilitychange", handleVisibilityChange); + return () => { + window.removeEventListener("focus", handleWindowFocus); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [checkCanvasAuthSession]); + // 加载画布数据 useLayoutEffect(() => { console.log('[Canvas] Loading project:', id); @@ -576,6 +791,13 @@ export default function CanvasEditorPage({ params }: PageProps) { useCanvasStore.getState().loadCanvas(result.data.canvasData); const persistedWorkflow = result.data.canvasData.workflowState; if (!requestedAgentMode && persistedWorkflow) { + const nextAppearanceRecognitionEnabled = + persistedWorkflow.appearanceRecognitionEnabled !== false; + const nextWorkflowMaterials = nextAppearanceRecognitionEnabled + ? normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + : clearAppearanceSummaryFromMaterials( + normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + ); setWorkflowInputs({ primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), @@ -586,12 +808,8 @@ export default function CanvasEditorPage({ params }: PageProps) { fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), }); - setWorkflowMaterials( - normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) - ); - setAppearanceRecognitionEnabled( - persistedWorkflow.appearanceRecognitionEnabled !== false - ); + setWorkflowMaterials(nextWorkflowMaterials); + setAppearanceRecognitionEnabled(nextAppearanceRecognitionEnabled); setWorkflowSelections(persistedWorkflow.selections || {}); setSelectedBrandModel( persistedWorkflow.selectedBrandModel @@ -639,6 +857,15 @@ export default function CanvasEditorPage({ params }: PageProps) { setWorkflowHydrationSource("empty"); } } else { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + setWorkflowHydrationSource("empty"); + setIsLoading(false); + lastLoadedCanvasIdRef.current = id; + return; + } + // 画布不存在时,创建一个新画布 console.log('[Canvas] 画布不存在,创建新画布:', id); useCanvasStore.getState().setProject(id, requestedAgentMode ? '智能体工作台' : '未命名画布'); @@ -657,7 +884,7 @@ export default function CanvasEditorPage({ params }: PageProps) { }; loadProject(); - }, [id, requestedAgentMode]); + }, [id, openLoginDialog, requestedAgentMode]); useEffect(() => { const updateViewportWidth = () => setViewportWidth(window.innerWidth); @@ -689,6 +916,10 @@ export default function CanvasEditorPage({ params }: PageProps) { ]); const buildPersistedAgentProjectState = useCallback(() => { + const persistedMaterials = appearanceRecognitionEnabled + ? workflowMaterials + : clearAppearanceSummaryFromMaterials(workflowMaterials); + const workflowState: AgentWorkflowPersistedState = { inputs: { primarySku: workflowInputs.primarySku.trim(), @@ -700,7 +931,8 @@ export default function CanvasEditorPage({ params }: PageProps) { fifthCategory: workflowInputs.fifthCategory.trim(), priceBand: workflowInputs.priceBand.trim(), }, - materials: workflowMaterials, + materials: persistedMaterials, + appearanceRecognitionEnabled, selections: sanitizedWorkflowSelections, selectedBrandModel, selectedAgentPresetId: selectedAgentPresetId || null, @@ -754,6 +986,7 @@ export default function CanvasEditorPage({ params }: PageProps) { agentProjectResultCollections, agentWorkspaceState, currentAgentStage, + appearanceRecognitionEnabled, selectedAgentPresetId, selectedBrandModel, workflowInputs, @@ -766,6 +999,71 @@ export default function CanvasEditorPage({ params }: PageProps) { persistedAgentProjectStateRef.current = buildPersistedAgentProjectState(); }, [buildPersistedAgentProjectState]); + const hydrateAgentProjectSnapshot = useCallback( + (params: { + projectId: string; + title?: string | null; + thumbnail?: string | null; + projectState?: AgentGenerationProjectPersistedState | null; + }) => { + const persistedState = normalizeAgentGenerationProjectPersistedState(params.projectState); + const persistedWorkflow = + persistedState.workflowState || createEmptyAgentWorkflowPersistedState(); + const nextAppearanceRecognitionEnabled = + persistedWorkflow.appearanceRecognitionEnabled !== false; + const nextWorkflowMaterials = nextAppearanceRecognitionEnabled + ? normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + : clearAppearanceSummaryFromMaterials( + normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + ); + const nextWorkflowInputs = { + primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), + secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), + brand: String(persistedWorkflow.inputs?.brand || "").trim(), + store: String(persistedWorkflow.inputs?.store || "").trim(), + thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(), + fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(), + fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), + priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), + }; + const nextWorkflowSelections = persistedWorkflow.selections || {}; + const nextSelectedBrandModel = persistedWorkflow.selectedBrandModel || null; + const nextSelectedAgentPresetId = + String(persistedWorkflow.selectedAgentPresetId || "").trim() || null; + const nextCurrentAgentStage = + persistedWorkflow.currentStage === "mode" ? "mode" : "workflow"; + const nextAgentModeState = normalizeAgentModeWorkspaceState( + persistedWorkflow.agentModeState + ); + const nextAgentWorkspaceState = + persistedState.workspaceState || createEmptyAgentGenerationProjectWorkspaceState(); + + workflowInputsRef.current = nextWorkflowInputs; + workflowMaterialsRef.current = nextWorkflowMaterials; + workflowSelectionsRef.current = nextWorkflowSelections; + appearanceRecognitionEnabledRef.current = nextAppearanceRecognitionEnabled; + selectedBrandModelRef.current = nextSelectedBrandModel; + + setWorkflowInputs(nextWorkflowInputs); + setWorkflowMaterials(nextWorkflowMaterials); + setAppearanceRecognitionEnabled(nextAppearanceRecognitionEnabled); + setWorkflowSelections(nextWorkflowSelections); + setSelectedBrandModel(nextSelectedBrandModel); + setSelectedAgentPresetId(nextSelectedAgentPresetId); + setCurrentAgentStage(nextCurrentAgentStage); + setAgentModeState(nextAgentModeState); + setAgentWorkspaceState(nextAgentWorkspaceState); + setAgentProjectTitle(String(params.title || "").trim() || "未命名生图项目"); + setAgentProjectThumbnail(String(params.thumbnail || "").trim() || null); + setAgentProjectResultCollections(persistedState.resultCollections || {}); + setAgentProjectDeletedResultCollections( + persistedState.deletedResultCollections || {} + ); + setLoadedAgentProjectId(params.projectId); + }, + [] + ); + useEffect(() => { if (isAgentMode) return; if (typeof window === "undefined") return; @@ -779,6 +1077,12 @@ export default function CanvasEditorPage({ params }: PageProps) { } const parsed = JSON.parse(raw) as AgentWorkflowPersistedState; + const nextAppearanceRecognitionEnabled = parsed.appearanceRecognitionEnabled !== false; + const nextWorkflowMaterials = nextAppearanceRecognitionEnabled + ? normalizeAgentWorkflowMaterialSelections(parsed.materials) + : clearAppearanceSummaryFromMaterials( + normalizeAgentWorkflowMaterialSelections(parsed.materials) + ); setWorkflowInputs({ primarySku: String(parsed.inputs?.primarySku || "").trim(), secondarySku: String(parsed.inputs?.secondarySku || "").trim(), @@ -789,8 +1093,8 @@ export default function CanvasEditorPage({ params }: PageProps) { fifthCategory: String(parsed.inputs?.fifthCategory || "").trim(), priceBand: String(parsed.inputs?.priceBand || "").trim(), }); - setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials)); - setAppearanceRecognitionEnabled(parsed.appearanceRecognitionEnabled !== false); + setWorkflowMaterials(nextWorkflowMaterials); + setAppearanceRecognitionEnabled(nextAppearanceRecognitionEnabled); setWorkflowSelections(parsed.selections || {}); setSelectedBrandModel( parsed.selectedBrandModel @@ -823,11 +1127,14 @@ export default function CanvasEditorPage({ params }: PageProps) { if (typeof window === "undefined" || workflowHydrationSource === "pending") return; try { + const persistedMaterials = appearanceRecognitionEnabled + ? workflowMaterials + : clearAppearanceSummaryFromMaterials(workflowMaterials); window.localStorage.setItem( workflowStorageKey, JSON.stringify({ inputs: workflowInputs, - materials: workflowMaterials, + materials: persistedMaterials, appearanceRecognitionEnabled, selections: sanitizedWorkflowSelections, selectedBrandModel, @@ -853,6 +1160,45 @@ export default function CanvasEditorPage({ params }: PageProps) { agentModeState, ]); + useEffect(() => { + if (!isAgentMode || !agentProjectId || !hasLoadedCurrentAgentProject) return; + if (typeof window === "undefined") return; + + try { + const draftSnapshot: AgentProjectDraftSnapshot = { + savedAt: Date.now(), + title: agentProjectTitle, + thumbnail: agentProjectThumbnail, + projectState: buildPersistedAgentProjectState(), + }; + + window.localStorage.setItem( + getAgentProjectDraftStorageKey(agentProjectId), + JSON.stringify(draftSnapshot) + ); + } catch (error) { + console.warn("[Agent Project] Failed to persist local draft:", error); + } + }, [ + agentModeState, + agentProjectDeletedResultCollections, + agentProjectId, + agentProjectResultCollections, + agentProjectThumbnail, + agentProjectTitle, + agentWorkspaceState, + appearanceRecognitionEnabled, + buildPersistedAgentProjectState, + currentAgentStage, + hasLoadedCurrentAgentProject, + isAgentMode, + selectedAgentPresetId, + selectedBrandModel, + workflowInputs, + workflowMaterials, + workflowSelections, + ]); + const persistAgentProject = useCallback( async ( reason: "autosave" | "manual" | "switch", @@ -870,6 +1216,10 @@ export default function CanvasEditorPage({ params }: PageProps) { }); if (!result.success) { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + } console.error(`[Agent Project] ${reason} save failed:`, result.error); return false; } @@ -887,6 +1237,7 @@ export default function CanvasEditorPage({ params }: PageProps) { agentProjectTitle, buildPersistedAgentProjectState, isAgentMode, + openLoginDialog, ] ); @@ -899,47 +1250,47 @@ export default function CanvasEditorPage({ params }: PageProps) { setIsAgentProjectLoading(true); setLoadedAgentProjectId(null); try { + const localDraft = readAgentProjectDraftSnapshot(agentProjectId); const result = await agentProjectService.getProject(agentProjectId); if (!result.success || !result.data) { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + if (!cancelled && localDraft?.projectState) { + hydrateAgentProjectSnapshot({ + projectId: agentProjectId, + title: localDraft.title, + thumbnail: localDraft.thumbnail, + projectState: localDraft.projectState, + }); + } + return; + } throw new Error(result.error || "加载生图项目失败"); } if (cancelled) return; - const persistedState = normalizeAgentGenerationProjectPersistedState( - result.data.projectState - ); - const persistedWorkflow = - persistedState.workflowState || createEmptyAgentWorkflowPersistedState(); + const serverUpdatedAt = Number(new Date(result.data.updatedAt).getTime()) || 0; + const shouldUseLocalDraft = + Boolean(localDraft?.projectState) && + Number(localDraft?.savedAt || 0) > serverUpdatedAt; - 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(), - thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(), - fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(), - fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), - priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), + if (shouldUseLocalDraft && localDraft?.projectState) { + hydrateAgentProjectSnapshot({ + projectId: agentProjectId, + title: localDraft.title || result.data.title, + thumbnail: localDraft.thumbnail || result.data.thumbnail, + projectState: localDraft.projectState, + }); + return; + } + + hydrateAgentProjectSnapshot({ + projectId: agentProjectId, + title: result.data.title, + thumbnail: result.data.thumbnail, + projectState: result.data.projectState, }); - setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)); - setAppearanceRecognitionEnabled( - persistedWorkflow.appearanceRecognitionEnabled !== false - ); - 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); - setAgentProjectResultCollections(persistedState.resultCollections || {}); - setAgentProjectDeletedResultCollections( - persistedState.deletedResultCollections || {} - ); - setLoadedAgentProjectId(agentProjectId); } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : "加载生图项目失败"); @@ -955,7 +1306,7 @@ export default function CanvasEditorPage({ params }: PageProps) { return () => { cancelled = true; }; - }, [agentProjectId, requestedAgentMode]); + }, [agentProjectId, hydrateAgentProjectSnapshot, openLoginDialog, requestedAgentMode]); useEffect(() => { if (isAgentProjectSheetOpen) { @@ -968,7 +1319,8 @@ export default function CanvasEditorPage({ params }: PageProps) { !isAgentMode || !agentProjectId || isAgentProjectLoading || - !hasLoadedCurrentAgentProject + !hasLoadedCurrentAgentProject || + (authSessionChecked && !isAuthenticated) ) { return; } @@ -995,6 +1347,9 @@ export default function CanvasEditorPage({ params }: PageProps) { isAgentProjectLoading, hasLoadedCurrentAgentProject, persistAgentProject, + appearanceRecognitionEnabled, + authSessionChecked, + isAuthenticated, selectedAgentPresetId, selectedBrandModel, workflowInputs, @@ -1002,6 +1357,64 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowSelections, ]); + useEffect(() => { + if ( + !isAgentMode || + !agentProjectId || + isAgentProjectLoading || + !hasLoadedCurrentAgentProject + ) { + return; + } + + const handleBeforeUnload = () => { + const draftSnapshot: AgentProjectDraftSnapshot = { + savedAt: Date.now(), + title: agentProjectTitle, + thumbnail: agentProjectThumbnail, + projectState: persistedAgentProjectStateRef.current, + }; + + try { + window.localStorage.setItem( + getAgentProjectDraftStorageKey(agentProjectId), + JSON.stringify(draftSnapshot) + ); + } catch (error) { + console.warn("[Agent Project] Failed to persist local draft before unload:", error); + } + + try { + void fetch(`/api/agent-projects/${agentProjectId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: String(agentProjectTitle || "").trim() || buildDefaultAgentProjectTitle(), + thumbnail: agentProjectThumbnail, + projectState: persistedAgentProjectStateRef.current, + }), + keepalive: true, + }); + } catch (error) { + console.error("[Agent Project] beforeunload save failed:", error); + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + window.addEventListener("pagehide", handleBeforeUnload); + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + window.removeEventListener("pagehide", handleBeforeUnload); + }; + }, [ + agentProjectId, + agentProjectThumbnail, + agentProjectTitle, + hasLoadedCurrentAgentProject, + isAgentMode, + isAgentProjectLoading, + ]); + const handleSaveAsAgentProject = useCallback(async () => { const suggestedTitle = buildSaveAsAgentProjectTitle(agentProjectTitle); const promptedTitle = window.prompt("请输入另存为的新项目名称", suggestedTitle); @@ -1026,6 +1439,10 @@ export default function CanvasEditorPage({ params }: PageProps) { projectState: buildPersistedAgentProjectState(), }); if (!result.success || !result.data) { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + } toast.error(result.error || "另存为新项目失败"); return; } @@ -1043,6 +1460,7 @@ export default function CanvasEditorPage({ params }: PageProps) { agentProjectThumbnail, agentProjectTitle, buildPersistedAgentProjectState, + openLoginDialog, persistAgentProject, updateCanvasSearchParams, ]); @@ -1065,6 +1483,10 @@ export default function CanvasEditorPage({ params }: PageProps) { title: nextTitle, }); if (!result.success || !result.data) { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + } toast.error(result.error || "更新生图项目名称失败"); return; } @@ -1074,7 +1496,7 @@ export default function CanvasEditorPage({ params }: PageProps) { } toast.success("项目名称已更新"); }, - [agentProjectId] + [agentProjectId, openLoginDialog] ); const handleRenameCurrentAgentProject = useCallback(async () => { @@ -1096,6 +1518,10 @@ export default function CanvasEditorPage({ params }: PageProps) { projectState: createEmptyAgentGenerationProjectPersistedState(), }); if (!result.success || !result.data) { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + } toast.error(result.error || "创建生图项目失败"); return; } @@ -1107,6 +1533,7 @@ export default function CanvasEditorPage({ params }: PageProps) { }); }, [ agentProjectId, + openLoginDialog, persistAgentProject, updateCanvasSearchParams, ]); @@ -1129,12 +1556,22 @@ export default function CanvasEditorPage({ params }: PageProps) { async (targetProjectId: string) => { const result = await agentProjectService.deleteProject(targetProjectId); if (!result.success) { + if (isUnauthorizedApiError(result.error)) { + setIsAuthenticated(false); + openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + } toast.error(result.error || "删除生图项目失败"); return; } + removeAgentProjectDraftSnapshot(targetProjectId); + if (targetProjectId === agentProjectId) { const listResult = await agentProjectService.getProjects(); + if (!listResult.success && isUnauthorizedApiError(listResult.error)) { + setIsAuthenticated(false); + openLoginDialog(listResult.error || "当前登录状态已失效,请重新登录后继续操作。"); + } const nextProject = listResult.success ? listResult.data?.find((project) => project.id !== targetProjectId) || null : null; @@ -1155,7 +1592,7 @@ export default function CanvasEditorPage({ params }: PageProps) { toast.success("生图项目已删除"); }, - [agentProjectId, updateCanvasSearchParams] + [agentProjectId, openLoginDialog, updateCanvasSearchParams] ); const handleAgentResultPreviewUrlsChange = useCallback( @@ -1761,8 +2198,21 @@ export default function CanvasEditorPage({ params }: PageProps) { setWorkflowInputs(next); }, []); const handleWorkflowMaterialsChange = useCallback((next: AgentWorkflowMaterialSelections) => { - workflowMaterialsRef.current = next; - setWorkflowMaterials(next); + const nextMaterials = appearanceRecognitionEnabledRef.current + ? next + : clearAppearanceSummaryFromMaterials(next); + workflowMaterialsRef.current = nextMaterials; + setWorkflowMaterials(nextMaterials); + }, []); + const handleAppearanceRecognitionEnabledChange = useCallback((next: boolean) => { + appearanceRecognitionEnabledRef.current = next; + setAppearanceRecognitionEnabled(next); + + if (!next) { + const clearedMaterials = clearAppearanceSummaryFromMaterials(workflowMaterialsRef.current); + workflowMaterialsRef.current = clearedMaterials; + setWorkflowMaterials(clearedMaterials); + } }, []); const handleWorkflowSelectionsChange = useCallback( (next: Partial>) => { @@ -2035,6 +2485,31 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null; + const loginDialogElement = ( + + + + 登录已失效 + {loginDialogMessage} + + + + + + + + ); + const agentProjectSelectionScreen = isAgentMode && !hasActiveAgentProject ? (
+ {agentProjectSelectionScreen} + {loginDialogElement} + + ); } return ( - { - updateCanvasSearchParams({ - mode: null, - }); - }} - onClearPreset={() => setSelectedAgentPresetId(null)} - brandStorePanel={ - - } - workflowStylePanel={ - - } - // 嵌入的智能体选择面板 - presetPanel={ - - } - brandModelPanel={ - - } - materialSelectionPanel={ - - } - /> + <> + { + updateCanvasSearchParams({ + mode: null, + }); + }} + onClearPreset={() => setSelectedAgentPresetId(null)} + brandStorePanel={ + + } + workflowStylePanel={ + + } + // 嵌入的智能体选择面板 + presetPanel={ + + } + brandModelPanel={ + + } + materialSelectionPanel={ + + } + /> + {loginDialogElement} + ); } @@ -2508,6 +2991,7 @@ export default function CanvasEditorPage({ params }: PageProps) { onOpenChange={setSceneFusionDialogOpen} onBatchAddToCanvas={handleBatchAddToCanvas} /> + {loginDialogElement}
);