Codex/generation task reliability #20
+111
-49
@@ -15,6 +15,7 @@ import {
|
|||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { reportError } from "./utils/errorReporting";
|
import { reportError } from "./utils/errorReporting";
|
||||||
import { initNotificationPermission } from "./utils/generationNotifier";
|
import { initNotificationPermission } from "./utils/generationNotifier";
|
||||||
@@ -233,61 +234,122 @@ function App() {
|
|||||||
const canvasAutoOpenedRecentRef = useRef(false);
|
const canvasAutoOpenedRecentRef = useRef(false);
|
||||||
|
|
||||||
// Session store
|
// Session store
|
||||||
const session = useSessionStore((s) => s.session);
|
const {
|
||||||
const loginPromptOpen = useSessionStore((s) => s.loginPromptOpen);
|
session,
|
||||||
const pendingAction = useSessionStore((s) => s.pendingAction);
|
loginPromptOpen,
|
||||||
const sessionReplacedOpen = useSessionStore((s) => s.sessionReplacedOpen);
|
pendingAction,
|
||||||
const sessionReplacedMessage = useSessionStore((s) => s.sessionReplacedMessage);
|
sessionReplacedOpen,
|
||||||
const setSession = useSessionStore((s) => s.setSession);
|
sessionReplacedMessage,
|
||||||
const openLoginPrompt = useSessionStore((s) => s.openLoginPrompt);
|
setSession,
|
||||||
const closeLoginPrompt = useSessionStore((s) => s.closeLoginPrompt);
|
openLoginPrompt,
|
||||||
const showSessionReplaced = useSessionStore((s) => s.showSessionReplaced);
|
closeLoginPrompt,
|
||||||
const hideSessionReplaced = useSessionStore((s) => s.hideSessionReplaced);
|
showSessionReplaced,
|
||||||
const clearSessionState = useSessionStore((s) => s.clearSession);
|
hideSessionReplaced,
|
||||||
|
clearSession: clearSessionState,
|
||||||
|
} = useSessionStore(useShallow((s) => ({
|
||||||
|
session: s.session,
|
||||||
|
loginPromptOpen: s.loginPromptOpen,
|
||||||
|
pendingAction: s.pendingAction,
|
||||||
|
sessionReplacedOpen: s.sessionReplacedOpen,
|
||||||
|
sessionReplacedMessage: s.sessionReplacedMessage,
|
||||||
|
setSession: s.setSession,
|
||||||
|
openLoginPrompt: s.openLoginPrompt,
|
||||||
|
closeLoginPrompt: s.closeLoginPrompt,
|
||||||
|
showSessionReplaced: s.showSessionReplaced,
|
||||||
|
hideSessionReplaced: s.hideSessionReplaced,
|
||||||
|
clearSession: s.clearSession,
|
||||||
|
})));
|
||||||
|
|
||||||
// Project store
|
// Project store
|
||||||
const projects = useProjectStore((s) => s.projects);
|
const {
|
||||||
const projectsLoaded = useProjectStore((s) => s.projectsLoaded);
|
projects,
|
||||||
const canvasWorkflow = useProjectStore((s) => s.canvasWorkflow);
|
projectsLoaded,
|
||||||
const currentCanvasProjectId = useProjectStore((s) => s.currentCanvasProjectId);
|
canvasWorkflow,
|
||||||
const pendingDeleteProject = useProjectStore((s) => s.pendingDeleteProject);
|
currentCanvasProjectId,
|
||||||
const deleteProjectSubmitting = useProjectStore((s) => s.deleteProjectSubmitting);
|
pendingDeleteProject,
|
||||||
const setProjects = useProjectStore((s) => s.setProjects);
|
deleteProjectSubmitting,
|
||||||
const setProjectsLoaded = useProjectStore((s) => s.setProjectsLoaded);
|
setProjects,
|
||||||
const setCanvasWorkflow = useProjectStore((s) => s.setCanvasWorkflow);
|
setProjectsLoaded,
|
||||||
const setCurrentCanvasProjectId = useProjectStore((s) => s.setCurrentCanvasProjectId);
|
setCanvasWorkflow,
|
||||||
const openDeleteProjectModal = useProjectStore((s) => s.openDeleteProject);
|
setCurrentCanvasProjectId,
|
||||||
const closeDeleteProjectModal = useProjectStore((s) => s.closeDeleteProject);
|
openDeleteProject: openDeleteProjectModal,
|
||||||
const setDeleteProjectSubmitting = useProjectStore((s) => s.setDeleteProjectSubmitting);
|
closeDeleteProject: closeDeleteProjectModal,
|
||||||
const clearProjectState = useProjectStore((s) => s.clearProjectState);
|
setDeleteProjectSubmitting,
|
||||||
|
clearProjectState,
|
||||||
|
} = useProjectStore(useShallow((s) => ({
|
||||||
|
projects: s.projects,
|
||||||
|
projectsLoaded: s.projectsLoaded,
|
||||||
|
canvasWorkflow: s.canvasWorkflow,
|
||||||
|
currentCanvasProjectId: s.currentCanvasProjectId,
|
||||||
|
pendingDeleteProject: s.pendingDeleteProject,
|
||||||
|
deleteProjectSubmitting: s.deleteProjectSubmitting,
|
||||||
|
setProjects: s.setProjects,
|
||||||
|
setProjectsLoaded: s.setProjectsLoaded,
|
||||||
|
setCanvasWorkflow: s.setCanvasWorkflow,
|
||||||
|
setCurrentCanvasProjectId: s.setCurrentCanvasProjectId,
|
||||||
|
openDeleteProject: s.openDeleteProject,
|
||||||
|
closeDeleteProject: s.closeDeleteProject,
|
||||||
|
setDeleteProjectSubmitting: s.setDeleteProjectSubmitting,
|
||||||
|
clearProjectState: s.clearProjectState,
|
||||||
|
})));
|
||||||
|
|
||||||
// Task store
|
// Task store
|
||||||
const tasks = useTaskStore((s) => s.tasks);
|
const {
|
||||||
const appendTask = useTaskStore((s) => s.appendTask);
|
tasks,
|
||||||
const mergeServerTasks = useTaskStore((s) => s.mergeServerTasks);
|
appendTask,
|
||||||
const clearTasks = useTaskStore((s) => s.clearTasks);
|
mergeServerTasks,
|
||||||
|
clearTasks,
|
||||||
|
} = useTaskStore(useShallow((s) => ({
|
||||||
|
tasks: s.tasks,
|
||||||
|
appendTask: s.appendTask,
|
||||||
|
mergeServerTasks: s.mergeServerTasks,
|
||||||
|
clearTasks: s.clearTasks,
|
||||||
|
})));
|
||||||
|
|
||||||
// App store
|
// App store
|
||||||
const usage = useAppStore((s) => s.usage);
|
const {
|
||||||
const runtimeNotifications = useAppStore((s) => s.runtimeNotifications);
|
usage,
|
||||||
const serverNotifications = useAppStore((s) => s.serverNotifications);
|
runtimeNotifications,
|
||||||
const activeView = useAppStore((s) => s.activeView);
|
serverNotifications,
|
||||||
const workspaceExpanded = useAppStore((s) => s.workspaceExpanded);
|
activeView,
|
||||||
const imageWorkbenchTool = useAppStore((s) => s.imageWorkbenchTool);
|
workspaceExpanded,
|
||||||
const pendingEcommerceTemplate = useAppStore((s) => s.pendingEcommerceTemplate);
|
imageWorkbenchTool,
|
||||||
const backendHealth = useAppStore((s) => s.backendHealth);
|
pendingEcommerceTemplate,
|
||||||
const setUsage = useAppStore((s) => s.setUsage);
|
backendHealth,
|
||||||
const pushNotification = useAppStore((s) => s.pushNotification);
|
setUsage,
|
||||||
const setRuntimeNotifications = useAppStore((s) => s.setRuntimeNotifications);
|
pushNotification,
|
||||||
const setServerNotifications = useAppStore((s) => s.setServerNotifications);
|
setRuntimeNotifications,
|
||||||
const setView = useAppStore((s) => s.setView);
|
setServerNotifications,
|
||||||
const setWorkspaceExpanded = useAppStore((s) => s.setWorkspaceExpanded);
|
setView,
|
||||||
const setImageWorkbenchTool = useAppStore((s) => s.setImageWorkbenchTool);
|
setWorkspaceExpanded,
|
||||||
const setPendingEcommerceTemplate = useAppStore((s) => s.setPendingEcommerceTemplate);
|
setImageWorkbenchTool,
|
||||||
const setBackendHealth = useAppStore((s) => s.setBackendHealth);
|
setPendingEcommerceTemplate,
|
||||||
const markNotificationRead = useAppStore((s) => s.markNotificationRead);
|
setBackendHealth,
|
||||||
const markAllNotificationsRead = useAppStore((s) => s.markAllNotificationsRead);
|
markNotificationRead,
|
||||||
const clearAppState = useAppStore((s) => s.clearAppState);
|
markAllNotificationsRead,
|
||||||
|
clearAppState,
|
||||||
|
} = useAppStore(useShallow((s) => ({
|
||||||
|
usage: s.usage,
|
||||||
|
runtimeNotifications: s.runtimeNotifications,
|
||||||
|
serverNotifications: s.serverNotifications,
|
||||||
|
activeView: s.activeView,
|
||||||
|
workspaceExpanded: s.workspaceExpanded,
|
||||||
|
imageWorkbenchTool: s.imageWorkbenchTool,
|
||||||
|
pendingEcommerceTemplate: s.pendingEcommerceTemplate,
|
||||||
|
backendHealth: s.backendHealth,
|
||||||
|
setUsage: s.setUsage,
|
||||||
|
pushNotification: s.pushNotification,
|
||||||
|
setRuntimeNotifications: s.setRuntimeNotifications,
|
||||||
|
setServerNotifications: s.setServerNotifications,
|
||||||
|
setView: s.setView,
|
||||||
|
setWorkspaceExpanded: s.setWorkspaceExpanded,
|
||||||
|
setImageWorkbenchTool: s.setImageWorkbenchTool,
|
||||||
|
setPendingEcommerceTemplate: s.setPendingEcommerceTemplate,
|
||||||
|
setBackendHealth: s.setBackendHealth,
|
||||||
|
markNotificationRead: s.markNotificationRead,
|
||||||
|
markAllNotificationsRead: s.markAllNotificationsRead,
|
||||||
|
clearAppState: s.clearAppState,
|
||||||
|
})));
|
||||||
|
|
||||||
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
|
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
|
||||||
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
|
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
|
||||||
|
|||||||
@@ -397,10 +397,12 @@ function CanvasPage({
|
|||||||
const suppressNextPaneClickRef = useRef(false);
|
const suppressNextPaneClickRef = useRef(false);
|
||||||
const canvasAutoSaveTimerRef = useRef<number | null>(null);
|
const canvasAutoSaveTimerRef = useRef<number | null>(null);
|
||||||
const canvasAutoSaveIdleHandleRef = useRef<number | null>(null);
|
const canvasAutoSaveIdleHandleRef = useRef<number | null>(null);
|
||||||
|
const canvasAutoSaveRetryTimerRef = useRef<number | null>(null);
|
||||||
const canvasAutoSaveInFlightRef = useRef(false);
|
const canvasAutoSaveInFlightRef = useRef(false);
|
||||||
const canvasAutoSavePendingRef = useRef(false);
|
const canvasAutoSavePendingRef = useRef(false);
|
||||||
const lastAutoSavedWorkflowFingerprintRef = useRef("");
|
const lastAutoSavedWorkflowFingerprintRef = useRef("");
|
||||||
const canvasAutoSaveHydrationRef = useRef(true);
|
const canvasAutoSaveHydrationRef = useRef(true);
|
||||||
|
const textNodeMentionFocusTimerRef = useRef<number | null>(null);
|
||||||
const textNodeIdRef = useRef(9);
|
const textNodeIdRef = useRef(9);
|
||||||
const imageNodeIdRef = useRef(1);
|
const imageNodeIdRef = useRef(1);
|
||||||
const videoNodeIdRef = useRef(1);
|
const videoNodeIdRef = useRef(1);
|
||||||
@@ -519,7 +521,11 @@ function CanvasPage({
|
|||||||
else if (kind === "video") updateVideoNodePrompt(nodeId, nextValue);
|
else if (kind === "video") updateVideoNodePrompt(nodeId, nextValue);
|
||||||
else updateTextNodePrompt(nodeId, nextValue);
|
else updateTextNodePrompt(nodeId, nextValue);
|
||||||
closeTextNodeMention(nodeId);
|
closeTextNodeMention(nodeId);
|
||||||
setTimeout(() => {
|
if (textNodeMentionFocusTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(textNodeMentionFocusTimerRef.current);
|
||||||
|
}
|
||||||
|
textNodeMentionFocusTimerRef.current = window.setTimeout(() => {
|
||||||
|
textNodeMentionFocusTimerRef.current = null;
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.setSelectionRange(nextCaret, nextCaret);
|
textarea.setSelectionRange(nextCaret, nextCaret);
|
||||||
@@ -555,6 +561,18 @@ function CanvasPage({
|
|||||||
const [autoSaveStatus, setAutoSaveStatus] = useState<"saved" | "saving" | "error" | "idle">("idle");
|
const [autoSaveStatus, setAutoSaveStatus] = useState<"saved" | "saving" | "error" | "idle">("idle");
|
||||||
const autoSaveStatusTimerRef = useRef<number | null>(null);
|
const autoSaveStatusTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (canvasAutoSaveTimerRef.current !== null) window.clearTimeout(canvasAutoSaveTimerRef.current);
|
||||||
|
if (canvasAutoSaveRetryTimerRef.current !== null) window.clearTimeout(canvasAutoSaveRetryTimerRef.current);
|
||||||
|
if (autoSaveStatusTimerRef.current !== null) window.clearTimeout(autoSaveStatusTimerRef.current);
|
||||||
|
if (textNodeMentionFocusTimerRef.current !== null) window.clearTimeout(textNodeMentionFocusTimerRef.current);
|
||||||
|
if (canvasAutoSaveIdleHandleRef.current !== null && "cancelIdleCallback" in window) {
|
||||||
|
window.cancelIdleCallback(canvasAutoSaveIdleHandleRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Save immediately when user leaves page or switches tab (placed after runCanvasAutoSave definition)
|
// Save immediately when user leaves page or switches tab (placed after runCanvasAutoSave definition)
|
||||||
// — see useEffect below near runCanvasAutoSave
|
// — see useEffect below near runCanvasAutoSave
|
||||||
|
|
||||||
@@ -3159,7 +3177,13 @@ function CanvasPage({
|
|||||||
canvasAutoSaveInFlightRef.current = false;
|
canvasAutoSaveInFlightRef.current = false;
|
||||||
if (canvasAutoSavePendingRef.current) {
|
if (canvasAutoSavePendingRef.current) {
|
||||||
canvasAutoSavePendingRef.current = false;
|
canvasAutoSavePendingRef.current = false;
|
||||||
window.setTimeout(() => void runCanvasAutoSave(), canvasAutoSaveIdleTimeoutMs);
|
if (canvasAutoSaveRetryTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(canvasAutoSaveRetryTimerRef.current);
|
||||||
|
}
|
||||||
|
canvasAutoSaveRetryTimerRef.current = window.setTimeout(() => {
|
||||||
|
canvasAutoSaveRetryTimerRef.current = null;
|
||||||
|
void runCanvasAutoSave();
|
||||||
|
}, canvasAutoSaveIdleTimeoutMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -3228,7 +3252,13 @@ function CanvasPage({
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.setTimeout(() => void runCanvasAutoSave(), canvasAutoSaveIdleTimeoutMs);
|
if (canvasAutoSaveRetryTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(canvasAutoSaveRetryTimerRef.current);
|
||||||
|
}
|
||||||
|
canvasAutoSaveRetryTimerRef.current = window.setTimeout(() => {
|
||||||
|
canvasAutoSaveRetryTimerRef.current = null;
|
||||||
|
void runCanvasAutoSave();
|
||||||
|
}, canvasAutoSaveIdleTimeoutMs);
|
||||||
}, canvasAutoSaveDebounceMs);
|
}, canvasAutoSaveDebounceMs);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -3240,6 +3270,10 @@ function CanvasPage({
|
|||||||
window.cancelIdleCallback(canvasAutoSaveIdleHandleRef.current);
|
window.cancelIdleCallback(canvasAutoSaveIdleHandleRef.current);
|
||||||
canvasAutoSaveIdleHandleRef.current = null;
|
canvasAutoSaveIdleHandleRef.current = null;
|
||||||
}
|
}
|
||||||
|
if (canvasAutoSaveRetryTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(canvasAutoSaveRetryTimerRef.current);
|
||||||
|
canvasAutoSaveRetryTimerRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
|||||||
@@ -1098,6 +1098,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearCloneSetCountHold = () => {
|
const clearCloneSetCountHold = () => {
|
||||||
|
window.removeEventListener("pointerup", clearCloneSetCountHold);
|
||||||
|
window.removeEventListener("pointercancel", clearCloneSetCountHold);
|
||||||
if (countHoldTimeoutRef.current !== null) {
|
if (countHoldTimeoutRef.current !== null) {
|
||||||
window.clearTimeout(countHoldTimeoutRef.current);
|
window.clearTimeout(countHoldTimeoutRef.current);
|
||||||
countHoldTimeoutRef.current = null;
|
countHoldTimeoutRef.current = null;
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export default function EcommerceVideoWorkspace({
|
|||||||
const [previewMedia, setPreviewMedia] = useState<{ url: string; type: "image" | "video" } | null>(null);
|
const [previewMedia, setPreviewMedia] = useState<{ url: string; type: "image" | "video" } | null>(null);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const renderAbortRef = useRef({ current: false });
|
const renderAbortRef = useRef({ current: false });
|
||||||
|
const actionNoticeTimerRef = useRef<number | null>(null);
|
||||||
const setView = useAppStore((s) => s.setView);
|
const setView = useAppStore((s) => s.setView);
|
||||||
const keepaliveRestoredFingerprintRef = useRef<string | null>(null);
|
const keepaliveRestoredFingerprintRef = useRef<string | null>(null);
|
||||||
const keepalivePollingStartedRef = useRef(false);
|
const keepalivePollingStartedRef = useRef(false);
|
||||||
@@ -276,9 +277,23 @@ export default function EcommerceVideoWorkspace({
|
|||||||
// Note: keep-alive is NOT cleared on completion — results persist across page switches.
|
// Note: keep-alive is NOT cleared on completion — results persist across page switches.
|
||||||
// Only cleared when user explicitly starts a new plan via handlePlan.
|
// Only cleared when user explicitly starts a new plan via handlePlan.
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (actionNoticeTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(actionNoticeTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const showNotice = (msg: string) => {
|
const showNotice = (msg: string) => {
|
||||||
setActionNotice(msg);
|
setActionNotice(msg);
|
||||||
setTimeout(() => setActionNotice(null), 3000);
|
if (actionNoticeTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(actionNoticeTimerRef.current);
|
||||||
|
}
|
||||||
|
actionNoticeTimerRef.current = window.setTimeout(() => {
|
||||||
|
actionNoticeTimerRef.current = null;
|
||||||
|
setActionNotice(null);
|
||||||
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (url: string) => {
|
const handleDownload = async (url: string) => {
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ function ScriptReviewShowcase() {
|
|||||||
const scoreRef = useRef<HTMLSpanElement>(null);
|
const scoreRef = useRef<HTMLSpanElement>(null);
|
||||||
const barRefs = useRef<(HTMLDivElement | null)[]>([]);
|
const barRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
const scoreValRefs = useRef<(HTMLSpanElement | null)[]>([]);
|
const scoreValRefs = useRef<(HTMLSpanElement | null)[]>([]);
|
||||||
|
const animationTimersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
|
|
||||||
|
const clearAnimationTimers = () => {
|
||||||
|
animationTimersRef.current.forEach((timer) => clearTimeout(timer));
|
||||||
|
animationTimersRef.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = document.getElementById("script-review-showcase");
|
const el = document.getElementById("script-review-showcase");
|
||||||
@@ -69,18 +75,23 @@ function ScriptReviewShowcase() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!animated) return;
|
if (!animated) return;
|
||||||
const timer = setTimeout(() => {
|
clearAnimationTimers();
|
||||||
|
const scheduleAnimation = (callback: () => void, delay: number) => {
|
||||||
|
const timer = setTimeout(callback, delay);
|
||||||
|
animationTimersRef.current.push(timer);
|
||||||
|
};
|
||||||
|
scheduleAnimation(() => {
|
||||||
animateNumber(scoreRef.current, 77, 1400);
|
animateNumber(scoreRef.current, 77, 1400);
|
||||||
barRefs.current.forEach((bar, i) => {
|
barRefs.current.forEach((bar, i) => {
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
const pct = parseFloat(bar.dataset.pct ?? "0");
|
const pct = parseFloat(bar.dataset.pct ?? "0");
|
||||||
setTimeout(() => { bar.style.height = `${pct}%`; }, i * 100 + 400);
|
scheduleAnimation(() => { bar.style.height = `${pct}%`; }, i * 100 + 400);
|
||||||
});
|
});
|
||||||
scoreValRefs.current.forEach((el, i) => {
|
scoreValRefs.current.forEach((el, i) => {
|
||||||
setTimeout(() => animateNumber(el, parseInt(el?.dataset.target ?? "0"), 800), i * 100 + 400);
|
scheduleAnimation(() => animateNumber(el, parseInt(el?.dataset.target ?? "0"), 800), i * 100 + 400);
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return clearAnimationTimers;
|
||||||
}, [animated]);
|
}, [animated]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useCallback } from "react";
|
import { useEffect, useMemo, useRef, useCallback } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import type { GenerationQueueItem } from "../stores/useGenerationStore";
|
import type { GenerationQueueItem } from "../stores/useGenerationStore";
|
||||||
import { useGenerationStore } from "../stores/useGenerationStore";
|
import { useGenerationStore } from "../stores/useGenerationStore";
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +14,17 @@ interface UseGenerationTasksOptions {
|
|||||||
|
|
||||||
export function useGenerationTasks(options: UseGenerationTasksOptions) {
|
export function useGenerationTasks(options: UseGenerationTasksOptions) {
|
||||||
const { sourceView, autoResume = true } = options;
|
const { sourceView, autoResume = true } = options;
|
||||||
const store = useGenerationStore();
|
const {
|
||||||
|
queue,
|
||||||
|
addTask,
|
||||||
|
updateTask: updateStoredTask,
|
||||||
|
getRunningTasks,
|
||||||
|
} = useGenerationStore(useShallow((s) => ({
|
||||||
|
queue: s.queue,
|
||||||
|
addTask: s.addTask,
|
||||||
|
updateTask: s.updateTask,
|
||||||
|
getRunningTasks: s.getRunningTasks,
|
||||||
|
})));
|
||||||
const pollingStartedRef = useRef(false);
|
const pollingStartedRef = useRef(false);
|
||||||
|
|
||||||
// ── Auto-resume: re-subscribe to running tasks on mount ────
|
// ── Auto-resume: re-subscribe to running tasks on mount ────
|
||||||
@@ -21,7 +32,7 @@ export function useGenerationTasks(options: UseGenerationTasksOptions) {
|
|||||||
if (!autoResume || pollingStartedRef.current) return;
|
if (!autoResume || pollingStartedRef.current) return;
|
||||||
pollingStartedRef.current = true;
|
pollingStartedRef.current = true;
|
||||||
|
|
||||||
const active = store.getRunningTasks().filter((t) => t.sourceView === sourceView);
|
const active = getRunningTasks().filter((t) => t.sourceView === sourceView);
|
||||||
if (active.length > 0) {
|
if (active.length > 0) {
|
||||||
startBackgroundPolling();
|
startBackgroundPolling();
|
||||||
}
|
}
|
||||||
@@ -29,19 +40,19 @@ export function useGenerationTasks(options: UseGenerationTasksOptions) {
|
|||||||
return () => {
|
return () => {
|
||||||
pollingStartedRef.current = false;
|
pollingStartedRef.current = false;
|
||||||
};
|
};
|
||||||
}, [autoResume, sourceView, store]);
|
}, [autoResume, sourceView, getRunningTasks]);
|
||||||
|
|
||||||
// ── Subscribe to live updates ───────────────────────────
|
// ── Subscribe to live updates ───────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return subscribeToTaskUpdates((updated) => {
|
return subscribeToTaskUpdates((updated) => {
|
||||||
store.updateTask(updated.id, updated);
|
updateStoredTask(updated.id, updated);
|
||||||
});
|
});
|
||||||
}, [store]);
|
}, [updateStoredTask]);
|
||||||
|
|
||||||
// ── View-scoped computed lists ──────────────────────────
|
// ── View-scoped computed lists ──────────────────────────
|
||||||
const myTasks = useMemo(
|
const myTasks = useMemo(
|
||||||
() => store.queue.filter((t) => t.sourceView === sourceView),
|
() => queue.filter((t) => t.sourceView === sourceView),
|
||||||
[store.queue, sourceView],
|
[queue, sourceView],
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeTasks = useMemo(
|
const activeTasks = useMemo(
|
||||||
@@ -63,41 +74,41 @@ export function useGenerationTasks(options: UseGenerationTasksOptions) {
|
|||||||
const submitTask = useCallback(
|
const submitTask = useCallback(
|
||||||
(task: Omit<GenerationQueueItem, "id" | "createdAt">) => {
|
(task: Omit<GenerationQueueItem, "id" | "createdAt">) => {
|
||||||
const id = `gen-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
const id = `gen-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
store.addTask({ ...task, id, createdAt: Date.now() });
|
addTask({ ...task, id, createdAt: Date.now() });
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
[store],
|
[addTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateTask = useCallback(
|
const updateTask = useCallback(
|
||||||
(id: string, patch: Partial<GenerationQueueItem>) => {
|
(id: string, patch: Partial<GenerationQueueItem>) => {
|
||||||
store.updateTask(id, patch);
|
updateStoredTask(id, patch);
|
||||||
},
|
},
|
||||||
[store],
|
[updateStoredTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
const markCompleted = useCallback(
|
const markCompleted = useCallback(
|
||||||
(id: string, resultUrl: string) => {
|
(id: string, resultUrl: string) => {
|
||||||
store.updateTask(id, { status: "completed", progress: 100, resultUrl });
|
updateStoredTask(id, { status: "completed", progress: 100, resultUrl });
|
||||||
},
|
},
|
||||||
[store],
|
[updateStoredTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
const markFailed = useCallback(
|
const markFailed = useCallback(
|
||||||
(id: string, error: string) => {
|
(id: string, error: string) => {
|
||||||
store.updateTask(id, { status: "failed", error });
|
updateStoredTask(id, { status: "failed", error });
|
||||||
},
|
},
|
||||||
[store],
|
[updateStoredTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
const retryTask = useCallback(
|
const retryTask = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const task = store.queue.find((t) => t.id === id);
|
const task = queue.find((t) => t.id === id);
|
||||||
if (task) {
|
if (task) {
|
||||||
store.updateTask(id, { status: "pending", progress: 0, error: null });
|
updateStoredTask(id, { status: "pending", progress: 0, error: null });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[store],
|
[queue, updateStoredTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user