import { type Dispatch, type MutableRefObject, type SetStateAction, useEffect, useRef, useState } from "react"; import type { CanvasImageGenerationState, CanvasImageNode, CanvasTextGenerationState, CanvasVideoGenerationState, CanvasVideoNode, } from "./canvasTypes"; import { createCanvasAssetRefFromGeneratedResult, persistCanvasGeneratedResultAsset } from "./canvasAssetPersistence"; import { waitForImageTaskResult, waitForVideoTaskResult } from "./canvasUtils"; const CANVAS_GEN_KEEPALIVE_KEY = "omniai:canvas-gen-tasks"; interface CanvasGenKeepaliveEntry { taskId: string; nodeId: string; nodeKind: "image" | "video"; projectId: string; } function loadCanvasGenKeepalive(): CanvasGenKeepaliveEntry[] { try { const raw = window.localStorage.getItem(CANVAS_GEN_KEEPALIVE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } } function saveCanvasGenKeepalive(entries: CanvasGenKeepaliveEntry[]): void { window.localStorage.setItem(CANVAS_GEN_KEEPALIVE_KEY, JSON.stringify(entries)); } export function addCanvasGenKeepalive(taskId: string, nodeId: string, nodeKind: "image" | "video", projectId: string): void { const entries = loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId); entries.push({ taskId, nodeId, nodeKind, projectId }); saveCanvasGenKeepalive(entries); } export function removeCanvasGenKeepalive(taskId: string): void { saveCanvasGenKeepalive(loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId)); } export interface UseCanvasGenerationParams { setImageNodes: Dispatch>; setVideoNodes: Dispatch>; } export function useCanvasGeneration(params: UseCanvasGenerationParams) { const { setImageNodes, setVideoNodes } = params; const [textGenerationState, setTextGenerationState] = useState>({}); const [imageGenerationState, setImageGenerationState] = useState>({}); const [videoGenerationState, setVideoGenerationState] = useState>({}); const [generationToast, setGenerationToast] = useState(null); const imageGenerationInFlightRef = useRef(new Set()); const textGenerationInFlightRef = useRef(new Set()); const textGenerationAbortControllersRef = useRef(new Map()); const canvasGenKeepaliveRestoredRef = useRef(false); const setTextGenerationStatus = (nodeId: string, state: CanvasTextGenerationState) => { setTextGenerationState((current) => ({ ...current, [nodeId]: state })); }; const setImageGenerationStatus = (nodeId: string, state: CanvasImageGenerationState) => { setImageGenerationState((current) => ({ ...current, [nodeId]: state })); }; const setVideoGenerationStatus = (nodeId: string, state: CanvasVideoGenerationState) => { setVideoGenerationState((current) => ({ ...current, [nodeId]: state })); }; // Toast auto-dismiss useEffect(() => { if (!generationToast) return undefined; const timer = window.setTimeout(() => setGenerationToast(null), 1800); return () => window.clearTimeout(timer); }, [generationToast]); const restoreKeepaliveTasks = ( projectId: string, imageNodesList: CanvasImageNode[], videoNodesList: CanvasVideoNode[], ) => { if (canvasGenKeepaliveRestoredRef.current) return; canvasGenKeepaliveRestoredRef.current = true; const storedTasks = loadCanvasGenKeepalive().filter((e) => e.projectId === projectId); for (const entry of storedTasks) { const nodeExists = [...imageNodesList, ...videoNodesList].some((n) => n.id === entry.nodeId); if (!nodeExists) { removeCanvasGenKeepalive(entry.taskId); continue; } if (entry.nodeKind === "image") { imageGenerationInFlightRef.current.add(entry.nodeId); setImageGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复图片生成", progress: 20 }); void waitForImageTaskResult(entry.taskId, (status) => { const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0))); setImageGenerationStatus(entry.nodeId, { status: "running", message: "图片生成中", progress }); }).then(async (outputUrl) => { removeCanvasGenKeepalive(entry.taskId); setImageGenerationStatus(entry.nodeId, { status: "success", message: "生成完成", progress: 100 }); const ref = createCanvasAssetRefFromGeneratedResult({ url: outputUrl, mediaType: "image/png", resultType: "image", taskId: entry.taskId, originalUrl: outputUrl, }); setImageNodes((current) => current.map((n) => n.id === entry.nodeId ? { ...n, imageUrl: outputUrl, assetRef: ref, taskRef: { taskId: entry.taskId, status: "completed", resultUrl: outputUrl, updatedAt: new Date().toISOString() } } : n )); let durableRef = ref; try { durableRef = await persistCanvasGeneratedResultAsset({ title: "image-node", url: outputUrl, mediaType: "image/png", resultType: "image", taskId: entry.taskId, originalUrl: outputUrl, }); } catch { /* keep temp URL on failure */ } if (durableRef.url !== outputUrl || durableRef.ossKey) { setImageNodes((current) => current.map((n) => n.id === entry.nodeId ? { ...n, imageUrl: durableRef.url, assetRef: durableRef, taskRef: { taskId: entry.taskId, status: "completed", resultUrl: durableRef.url, updatedAt: new Date().toISOString() } } : n )); } }).catch(() => { removeCanvasGenKeepalive(entry.taskId); setImageGenerationStatus(entry.nodeId, { status: "error", message: "图片生成失败" }); }).finally(() => { imageGenerationInFlightRef.current.delete(entry.nodeId); }); } else if (entry.nodeKind === "video") { imageGenerationInFlightRef.current.add(entry.nodeId); setVideoGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复视频生成", progress: 20 }); void waitForVideoTaskResult(entry.taskId, (status) => { const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0))); setVideoGenerationStatus(entry.nodeId, { status: "running", message: "视频生成中", progress }); }).then(async (outputUrl) => { removeCanvasGenKeepalive(entry.taskId); setVideoGenerationStatus(entry.nodeId, { status: "success", message: "生成完成", progress: 100 }); const ref = createCanvasAssetRefFromGeneratedResult({ url: outputUrl, mediaType: "video/mp4", resultType: "video", taskId: entry.taskId, originalUrl: outputUrl, }); setVideoNodes((current) => current.map((n) => n.id === entry.nodeId ? { ...n, videoUrl: outputUrl, assetRef: ref, taskRef: { taskId: entry.taskId, status: "completed", resultUrl: outputUrl, updatedAt: new Date().toISOString() } } : n )); let durableRef = ref; try { durableRef = await persistCanvasGeneratedResultAsset({ title: "video-node", url: outputUrl, mediaType: "video/mp4", resultType: "video", taskId: entry.taskId, originalUrl: outputUrl, }); } catch { /* keep temp URL on failure */ } if (durableRef.url !== outputUrl || durableRef.ossKey) { setVideoNodes((current) => current.map((n) => n.id === entry.nodeId ? { ...n, videoUrl: durableRef.url, assetRef: durableRef, taskRef: { taskId: entry.taskId, status: "completed", resultUrl: durableRef.url, updatedAt: new Date().toISOString() } } : n )); } }).catch(() => { removeCanvasGenKeepalive(entry.taskId); setVideoGenerationStatus(entry.nodeId, { status: "error", message: "视频生成失败" }); }).finally(() => { imageGenerationInFlightRef.current.delete(entry.nodeId); }); } } }; const resetGenerationState = () => { textGenerationAbortControllersRef.current.forEach((c) => c.abort()); textGenerationAbortControllersRef.current.clear(); textGenerationInFlightRef.current.clear(); imageGenerationInFlightRef.current.clear(); setTextGenerationState({}); setImageGenerationState({}); setVideoGenerationState({}); }; return { textGenerationState, imageGenerationState, videoGenerationState, generationToast, setGenerationToast, imageGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef, canvasGenKeepaliveRestoredRef, setTextGenerationStatus, setImageGenerationStatus, setVideoGenerationStatus, restoreKeepaliveTasks, resetGenerationState, }; }