Initial commit: OmniAI Web Frontend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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<SetStateAction<CanvasImageNode[]>>;
|
||||
setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>;
|
||||
}
|
||||
|
||||
export function useCanvasGeneration(params: UseCanvasGenerationParams) {
|
||||
const { setImageNodes, setVideoNodes } = params;
|
||||
|
||||
const [textGenerationState, setTextGenerationState] = useState<Record<string, CanvasTextGenerationState>>({});
|
||||
const [imageGenerationState, setImageGenerationState] = useState<Record<string, CanvasImageGenerationState>>({});
|
||||
const [videoGenerationState, setVideoGenerationState] = useState<Record<string, CanvasVideoGenerationState>>({});
|
||||
const [generationToast, setGenerationToast] = useState<string | null>(null);
|
||||
|
||||
const imageGenerationInFlightRef = useRef(new Set<string>());
|
||||
const textGenerationInFlightRef = useRef(new Set<string>());
|
||||
const textGenerationAbortControllersRef = useRef(new Map<string, AbortController>());
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user