Codex/generation task reliability #20

Merged
stringadmin merged 20 commits from codex/generation-task-reliability into master 2026-06-08 05:56:38 +00:00
6 changed files with 143 additions and 56 deletions
Showing only changes of commit b8b3b8f137 - Show all commits
+27
View File
@@ -127,6 +127,27 @@ const VIEW_KEYS = new Set<WebViewKey>([
]); ]);
const PUBLIC_VIEW_SET = new Set<WebViewKey>(["home", "login", "community", "more", "dialogGenerator", "userAgreement", "privacyPolicy", "not-found"]); const PUBLIC_VIEW_SET = new Set<WebViewKey>(["home", "login", "community", "more", "dialogGenerator", "userAgreement", "privacyPolicy", "not-found"]);
const LEGACY_PAGE_STYLE_VIEWS = new Set<WebViewKey>([
"login",
"workbench",
"canvas",
"community",
"communityReview",
"communityCaseAdd",
"assets",
"ecommerce",
"ecommerceHub",
"digitalHuman",
"characterMix",
"more",
]);
let legacyPageStylesPromise: Promise<unknown> | null = null;
function loadLegacyPageStyles(): Promise<unknown> {
legacyPageStylesPromise ??= import("./styles/pages/legacy-pages.css");
return legacyPageStylesPromise;
}
function normalizeViewKey(rawView: string): WebViewKey { function normalizeViewKey(rawView: string): WebViewKey {
const normalized = const normalized =
@@ -357,6 +378,12 @@ function App() {
if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true); if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
}, [isEcommerceActive]); // eslint-disable-line react-hooks/exhaustive-deps }, [isEcommerceActive]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (LEGACY_PAGE_STYLE_VIEWS.has(activeView) || ecommerceEverMounted) {
void loadLegacyPageStyles();
}
}, [activeView, ecommerceEverMounted]);
// Dismiss boot splash after first render // Dismiss boot splash after first render
useEffect(() => { useEffect(() => {
const splash = document.getElementById("app-boot-splash"); const splash = document.getElementById("app-boot-splash");
+28 -28
View File
@@ -41,6 +41,32 @@ interface AppShellProps {
} }
const BRAND_LOGO_URL = ossAssets.brand.logo; const BRAND_LOGO_URL = ossAssets.brand.logo;
const TOOL_SURFACE_VIEW_SET = new Set<WebViewKey>([
"workbench",
"canvas",
"more",
"scriptTokens",
"tokenUsage",
"ecommerceTemplates",
"sizeTemplate",
"imageWorkbench",
"resolutionUpscale",
"digitalHuman",
"dialogGenerator",
"avatarConsole",
"characterMix",
] as WebViewKey[]);
const PRIMARY_NAV_ORDER: WebViewKey[] = [
"workbench",
"ecommerce",
"sizeTemplate",
"canvas",
"scriptTokens",
"tokenUsage",
"community",
"assets",
"more",
];
function formatBalance(cents: number): string { function formatBalance(cents: number): string {
const value = Math.max(0, cents) / 100; const value = Math.max(0, cents) / 100;
@@ -76,37 +102,11 @@ function AppShell({
const isAuthView = activeView === "login"; const isAuthView = activeView === "login";
const isImmersiveView = activeView === "agent" || activeView === "avatarConsole"; const isImmersiveView = activeView === "agent" || activeView === "avatarConsole";
const showFloatingNav = (!isAuthView || !!session) && !isImmersiveView && activeView !== "home"; const showFloatingNav = (!isAuthView || !!session) && !isImmersiveView && activeView !== "home";
const toolSurfaceViews = [ const showPageScrollActions = showFloatingNav && !TOOL_SURFACE_VIEW_SET.has(activeView);
"workbench",
"canvas",
"more",
"scriptTokens",
"tokenUsage",
"ecommerceTemplates",
"sizeTemplate",
"imageWorkbench",
"resolutionUpscale",
"digitalHuman",
"dialogGenerator",
"avatarConsole",
"characterMix",
] as WebViewKey[];
const showPageScrollActions = showFloatingNav && !toolSurfaceViews.includes(activeView);
const visibleNavItems = useMemo( const visibleNavItems = useMemo(
() => { () => {
const orderedKeys: WebViewKey[] = [ return PRIMARY_NAV_ORDER
"workbench",
"ecommerce",
"sizeTemplate",
"canvas",
"scriptTokens",
"tokenUsage",
"community",
"assets",
"more",
];
return orderedKeys
.map((key) => navItems.find((item) => item.key === key)) .map((key) => navItems.find((item) => item.key === key))
.filter((item): item is WebNavItem => Boolean(item)); .filter((item): item is WebNavItem => Boolean(item));
}, },
+31 -6
View File
@@ -578,7 +578,17 @@ function CanvasPage({
// 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
const canvasAssets = serverAssets.filter((asset) => asset.imageUrl); const canvasAssets = useMemo(
() => serverAssets.filter((asset) => asset.imageUrl),
[serverAssets],
);
const assetCountsByCategory = useMemo(() => {
const counts = new Map<string, number>();
for (const asset of serverAssets) {
counts.set(asset.type, (counts.get(asset.type) ?? 0) + 1);
}
return counts;
}, [serverAssets]);
const shouldShowEmptyProjectState = const shouldShowEmptyProjectState =
projectsLoaded && projects.length === 0 && !projectId && workflow.source === "blank" && workflow.nodes.length === 0; projectsLoaded && projects.length === 0 && !projectId && workflow.source === "blank" && workflow.nodes.length === 0;
const isWaitingForProjects = isAuthenticated && !projectsLoaded; const isWaitingForProjects = isAuthenticated && !projectsLoaded;
@@ -2640,10 +2650,13 @@ function CanvasPage({
setConnectorDrag(null); setConnectorDrag(null);
}; };
const collapsedPackageNodeKeys = new Set( const collapsedPackageNodeKeys = useMemo(
() => new Set(
nodePackages.flatMap((nodePackage) => nodePackages.flatMap((nodePackage) =>
nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : [] nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : []
) )
),
[nodePackages],
); );
const isNodeCollapsedInPackage = (kind: CanvasNodeKind, id: string) => const isNodeCollapsedInPackage = (kind: CanvasNodeKind, id: string) =>
collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind, id })); collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind, id }));
@@ -2684,6 +2697,18 @@ function CanvasPage({
return positionedLink ? [positionedLink] : []; return positionedLink ? [positionedLink] : [];
}), }),
].filter((link) => !isLinkCollapsedInPackage(link)); ].filter((link) => !isLinkCollapsedInPackage(link));
const visibleTextNodes = useMemo(
() => textNodes.filter((textNode) => !isNodeCollapsedInPackage("text", textNode.id)),
[collapsedPackageNodeKeys, textNodes],
);
const visibleImageNodes = useMemo(
() => imageNodes.filter((imageNode) => !isNodeCollapsedInPackage("image", imageNode.id)),
[collapsedPackageNodeKeys, imageNodes],
);
const visibleVideoNodes = useMemo(
() => videoNodes.filter((videoNode) => !isNodeCollapsedInPackage("video", videoNode.id)),
[collapsedPackageNodeKeys, videoNodes],
);
const pendingLinkPreview = const pendingLinkPreview =
pendingLinkPort && pendingLinkPreviewPoint pendingLinkPort && pendingLinkPreviewPoint
? (() => { ? (() => {
@@ -4002,7 +4027,7 @@ function CanvasPage({
) : null} ) : null}
</svg> </svg>
) : null} ) : null}
{textNodes.filter((textNode) => !isNodeCollapsedInPackage("text", textNode.id)).map((textNode) => { {visibleTextNodes.map((textNode) => {
const textNodeSelected = isSelectedNode("text", textNode.id); const textNodeSelected = isSelectedNode("text", textNode.id);
const textNodeActive = isActiveSelectedNode("text", textNode.id); const textNodeActive = isActiveSelectedNode("text", textNode.id);
const textNodeResizing = nodeResizeDrag?.kind === "text" && nodeResizeDrag.nodeId === textNode.id; const textNodeResizing = nodeResizeDrag?.kind === "text" && nodeResizeDrag.nodeId === textNode.id;
@@ -4270,7 +4295,7 @@ function CanvasPage({
</div> </div>
); );
})} })}
{imageNodes.filter((imageNode) => !isNodeCollapsedInPackage("image", imageNode.id)).map((imageNode) => { {visibleImageNodes.map((imageNode) => {
const imageNodeSelected = isSelectedNode("image", imageNode.id); const imageNodeSelected = isSelectedNode("image", imageNode.id);
const imageNodeActive = isActiveSelectedNode("image", imageNode.id); const imageNodeActive = isActiveSelectedNode("image", imageNode.id);
const imageNodeResizing = nodeResizeDrag?.kind === "image" && nodeResizeDrag.nodeId === imageNode.id; const imageNodeResizing = nodeResizeDrag?.kind === "image" && nodeResizeDrag.nodeId === imageNode.id;
@@ -4774,7 +4799,7 @@ function CanvasPage({
</div> </div>
); );
})} })}
{videoNodes.filter((videoNode) => !isNodeCollapsedInPackage("video", videoNode.id)).map((videoNode) => { {visibleVideoNodes.map((videoNode) => {
const videoNodeSelected = isSelectedNode("video", videoNode.id); const videoNodeSelected = isSelectedNode("video", videoNode.id);
const videoNodeActive = isActiveSelectedNode("video", videoNode.id); const videoNodeActive = isActiveSelectedNode("video", videoNode.id);
const videoNodeResizing = nodeResizeDrag?.kind === "video" && nodeResizeDrag.nodeId === videoNode.id; const videoNodeResizing = nodeResizeDrag?.kind === "video" && nodeResizeDrag.nodeId === videoNode.id;
@@ -5485,7 +5510,7 @@ function CanvasPage({
onClick={() => setSelectedExistingCategory(category.key)} onClick={() => setSelectedExistingCategory(category.key)}
> >
{category.label} {category.label}
<span>{serverAssets.filter((asset) => asset.type === category.key).length} </span> <span>{assetCountsByCategory.get(category.key) ?? 0} </span>
</button> </button>
))} ))}
</div> </div>
+49 -13
View File
@@ -901,25 +901,58 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [selectedDetailModules, setSelectedDetailModules] = useState<string[]>(defaultDetailModuleIds); const [selectedDetailModules, setSelectedDetailModules] = useState<string[]>(defaultDetailModuleIds);
const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle"); const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle");
const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null); const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null);
const productSetRatioOptions = getPlatformRatioOptions(productSetPlatform, productSetOutput); const productSetRatioOptions = useMemo(
const hotUploadedRatioOption = cloneOutput === "hot" ? formatUploadedImageRatio(cloneReferenceImages[0]) : null; () => getPlatformRatioOptions(productSetPlatform, productSetOutput),
const baseCloneRatioOptions = getPlatformRatioOptions(platform, cloneOutput); [productSetOutput, productSetPlatform],
const cloneRatioOptions = hotUploadedRatioOption );
const hotUploadedRatioOption = useMemo(
() => cloneOutput === "hot" ? formatUploadedImageRatio(cloneReferenceImages[0]) : null,
[cloneOutput, cloneReferenceImages],
);
const baseCloneRatioOptions = useMemo(
() => getPlatformRatioOptions(platform, cloneOutput),
[cloneOutput, platform],
);
const cloneRatioOptions = useMemo(
() => hotUploadedRatioOption
? getUniqueRatioOptions([...baseCloneRatioOptions, hotUploadedRatioOption]) ? getUniqueRatioOptions([...baseCloneRatioOptions, hotUploadedRatioOption])
: baseCloneRatioOptions; : baseCloneRatioOptions,
const productSetLanguageOptions = getPlatformLanguageOptions(productSetPlatform, productSetMarket); [baseCloneRatioOptions, hotUploadedRatioOption],
const cloneLanguageOptions = getPlatformLanguageOptions(platform, market); );
const detailLanguageOptions = getPlatformLanguageOptions(detailPlatform, detailMarket); const productSetLanguageOptions = useMemo(
() => getPlatformLanguageOptions(productSetPlatform, productSetMarket),
[productSetMarket, productSetPlatform],
);
const cloneLanguageOptions = useMemo(
() => getPlatformLanguageOptions(platform, market),
[market, platform],
);
const detailLanguageOptions = useMemo(
() => getPlatformLanguageOptions(detailPlatform, detailMarket),
[detailMarket, detailPlatform],
);
const ecommerceMentionImages: MentionImageOption[] = [ const ecommerceMentionImages: MentionImageOption[] = [
...productImages.map((image, index) => ({ ...image, label: `商品图 ${index + 1}` })), ...productImages.map((image, index) => ({ ...image, label: `商品图 ${index + 1}` })),
...cloneReferenceImages.map((image, index) => ({ ...image, label: `参考图 ${index + 1}` })), ...cloneReferenceImages.map((image, index) => ({ ...image, label: `参考图 ${index + 1}` })),
]; ];
const ecommerceVideoImageDataUrls = useMemo(
() => productImages.map((img) => img.src),
[productImages],
);
const ecommerceVideoImageFiles = useMemo(
() => productImages.map((img) => img.file),
[productImages],
);
const selectedProductSetOutput = const selectedProductSetOutput =
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!; productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!; const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
const productSetPreviewReady = productSetStatus === "done"; const productSetPreviewReady = productSetStatus === "done";
const cloneSetTotal = Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0); const cloneSetTotal = useMemo(
() => Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0),
[cloneSetCounts],
);
const canGenerateSet = setImages.length > 0 && productSetStatus !== "generating"; const canGenerateSet = setImages.length > 0 && productSetStatus !== "generating";
const canGenerate = (cloneOutput === "video-outfit" const canGenerate = (cloneOutput === "video-outfit"
? Boolean(videoOutfitVideoFile && videoOutfitRefFile) ? Boolean(videoOutfitVideoFile && videoOutfitRefFile)
@@ -928,9 +961,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const canGenerateDetail = detailProductImages.length > 0 && detailStatus !== "generating"; const canGenerateDetail = detailProductImages.length > 0 && detailStatus !== "generating";
const cloneVideoDurationProgress = const cloneVideoDurationProgress =
((cloneVideoDuration - cloneVideoDurationMin) / (cloneVideoDurationMax - cloneVideoDurationMin)) * 100; ((cloneVideoDuration - cloneVideoDurationMin) / (cloneVideoDurationMax - cloneVideoDurationMin)) * 100;
const cloneVideoDurationStyle: CSSProperties = { const cloneVideoDurationStyle: CSSProperties = useMemo(
() => ({
"--clone-video-duration-progress": `${cloneVideoDurationProgress}%`, "--clone-video-duration-progress": `${cloneVideoDurationProgress}%`,
} as CSSProperties; }) as CSSProperties,
[cloneVideoDurationProgress],
);
const trackEcommerceTask = (taskId: string) => { const trackEcommerceTask = (taskId: string) => {
activeEcommerceTaskIdsRef.current.add(taskId); activeEcommerceTaskIdsRef.current.add(taskId);
@@ -2647,8 +2683,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<main className="product-clone-preview product-clone-preview--video" style={{ padding: 0, overflow: "hidden" }}> <main className="product-clone-preview product-clone-preview--video" style={{ padding: 0, overflow: "hidden" }}>
<EcommerceVideoWorkspace <EcommerceVideoWorkspace
isAuthenticated={Boolean((_props as Record<string, unknown>).isAuthenticated)} isAuthenticated={Boolean((_props as Record<string, unknown>).isAuthenticated)}
productImageDataUrls={productImages.map((img) => img.src)} productImageDataUrls={ecommerceVideoImageDataUrls}
productImageFiles={productImages.map((img) => img.file)} productImageFiles={ecommerceVideoImageFiles}
requirement={requirement} requirement={requirement}
platform={platform} platform={platform}
aspectRatio={ratio.includes("916") || ratio.includes("9:16") ? "9:16" : ratio.includes("169") || ratio.includes("16:9") ? "16:9" : ratio.includes("34") || ratio.includes("3:4") ? "3:4" : "9:16"} aspectRatio={ratio.includes("916") || ratio.includes("9:16") ? "9:16" : ratio.includes("169") || ratio.includes("16:9") ? "16:9" : ratio.includes("34") || ratio.includes("3:4") ? "3:4" : "9:16"}
-1
View File
@@ -12,7 +12,6 @@
@import "./pages/more-tools.css"; @import "./pages/more-tools.css";
@import "./pages/studio-layout.css"; @import "./pages/studio-layout.css";
@import "./pages/size-template.css"; @import "./pages/size-template.css";
@import "./pages/legacy-pages.css";
@import "./components/recharge-modal.css"; @import "./components/recharge-modal.css";
@import "./components/dropzone.css"; @import "./components/dropzone.css";
@import "./components/skeleton.css"; @import "./components/skeleton.css";
+3 -3
View File
@@ -3,6 +3,8 @@
* Falls back gracefully when Notification API is unavailable. * Falls back gracefully when Notification API is unavailable.
*/ */
import { toast } from "../components/toast/toastStore";
let permissionGranted = false; let permissionGranted = false;
async function requestPermission(): Promise<boolean> { async function requestPermission(): Promise<boolean> {
@@ -35,9 +37,7 @@ export function notifyTaskCompleted(label: string, mode: "image" | "video" = "im
// Use the existing toast system for in-app notifications // Use the existing toast system for in-app notifications
function dispatchGenToast(msg: string) { function dispatchGenToast(msg: string) {
try { toast(msg, "success");
import("../components/toast/toastStore").then((m) => m.toast(msg, "success"));
} catch { /* toast system not loaded */ }
} }
/** Call once on app init to pre-warm permission. */ /** Call once on app init to pre-warm permission. */