import { ArrowLeft, CheckCircle2, Eye, ImageIcon, ImagePlus, Loader2, Music, Play, Plus, Send, Trash2, Wand2, X, } from 'lucide-react'; import { type ChangeEvent, type ReactNode, useEffect, useMemo, useState, } from 'react'; import { createPortal } from 'react-dom'; import type { CreationAudioAsset } from '../../../packages/shared/src/contracts/creationAudio'; import type { Match3DResultDraft } from '../../../packages/shared/src/contracts/match3dAgent'; import type { Match3DGeneratedItemAsset, Match3DWorkProfile, PutMatch3DWorkRequest, } from '../../../packages/shared/src/contracts/match3dWorks'; import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; import { isGeneratedLegacyPath } from '../../services/assetReadUrlService'; import { createBackgroundMusicTask, createSoundEffectTask, publishBackgroundMusicAsset, publishSoundEffectAsset, waitForGeneratedAudioAsset, } from '../../services/creation-audio'; import { generateMatch3DBackgroundImage, generateMatch3DCoverImage, generateMatch3DItemAssets, generateMatch3DWorkTags, publishMatch3DWork, updateMatch3DGeneratedItemAssets, updateMatch3DWork, } from '../../services/match3d-works'; import { getMatch3DGeneratedImageViewSources, resolveMatch3DGeneratedImageAssetSource, resolveMatch3DGeneratedModelAssetSource, } from '../../services/match3dGeneratedModelCache'; import { useAuthUi } from '../auth/AuthUiContext'; import { MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS, MATCH3D_RUNTIME_GLASS_SPINNER_CLASS, MATCH3D_RUNTIME_GLASS_TIMER_CLASS, MATCH3D_RUNTIME_GLASS_TRAY_CLASS, MATCH3D_RUNTIME_GLASS_TRAY_SLOT_CLASS, } from '../match3d-runtime/match3dRuntimeUiStyles'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; type Match3DResultViewProps = { profile: Match3DWorkProfile; draft?: Match3DResultDraft | null; isBusy?: boolean; error?: string | null; onBack: () => void; onSaved?: (profile: Match3DWorkProfile) => void; onPublished?: (profile: Match3DWorkProfile) => void; onStartTestRun: ( profile: Match3DWorkProfile, options?: { itemTypeCountOverride?: number }, ) => void; }; type Match3DAutoSaveState = 'idle' | 'saving' | 'saved' | 'error'; type Match3DResultTab = 'work' | 'config' | 'assets'; type Match3DAssetConfigTab = 'items' | 'ui' | 'music'; type Match3DAssetTaskStatus = | 'idle' | 'submitting' | 'waiting' | 'generating' | 'image_ready' | 'done' | 'failed' | 'unknown'; type Match3DBatchItemGenerationState = { phase: 'idle' | 'generating' | 'done' | 'failed'; progress: number | null; itemNames: string[]; message: string | null; error: string | null; }; type Match3DItemAssetDraft = { id: string; name: string; usage: string; prompt: string; referenceImageSrc: string; imageViews: Match3DGeneratedItemAsset['imageViews']; taskUuid: string | null; subscriptionKey: string | null; status: Match3DAssetTaskStatus; progress: number | null; downloads: Array<{ name: string; url: string }>; soundPrompt: string; backgroundMusicTitle: string | null; backgroundMusicStyle: string | null; backgroundMusicPrompt: string | null; backgroundMusic: CreationAudioAsset | null; clickSound: CreationAudioAsset | null; backgroundAsset: Match3DGeneratedItemAsset['backgroundAsset'] | null; error: string | null; updatedAt: string | null; }; type Match3DResultEditState = { gameName: string; summary: string; tagsText: string; coverImageSrc: string; themeText: string; clearCountText: string; difficultyText: string; }; type Match3DCoverSourceAsset = { id: string; label: string; imageSrc: string; kind: 'item' | 'ui'; }; const MATCH3D_MIN_TAG_COUNT = 3; const MATCH3D_MAX_TAG_COUNT = 6; const MATCH3D_AUTOSAVE_DEBOUNCE_MS = 600; const MATCH3D_DEFAULT_ASSET_COUNT = 6; const MATCH3D_BACKGROUND_MUSIC_ASSET_KIND = 'match3d_background_music'; const MATCH3D_CLICK_SOUND_ASSET_KIND = 'match3d_click_sound'; const MATCH3D_BACKGROUND_MUSIC_POINTS_COST = 5; const MATCH3D_CLICK_SOUND_POINTS_COST = 10; const MATCH3D_UI_BACKGROUND_POINTS_COST = 2; const MATCH3D_ITEM_ASSETS_POINTS_PER_BATCH = 2; const MATCH3D_ITEM_ASSETS_BILLING_BATCH_SIZE = 5; const MATCH3D_RESULT_TABS: Array<{ id: Match3DResultTab; label: string }> = [ { id: 'work', label: '作品信息' }, { id: 'config', label: '难度配置' }, { id: 'assets', label: '素材配置' }, ]; const MATCH3D_ASSET_CONFIG_TABS: Array<{ id: Match3DAssetConfigTab; label: string; }> = [ { id: 'items', label: '物品' }, { id: 'ui', label: 'UI' }, { id: 'music', label: '背景音乐' }, ]; // 中文注释:结果页难度配置必须与创作入口页保持同一组派生参数。 const MATCH3D_DIFFICULTY_OPTIONS = [ { id: 'easy', label: '轻松', clearCount: 8, difficulty: 2, itemTypeCount: 3 }, { id: 'standard', label: '标准', clearCount: 12, difficulty: 4, itemTypeCount: 9, }, { id: 'advanced', label: '进阶', clearCount: 16, difficulty: 6, itemTypeCount: 15, }, { id: 'hardcore', label: '硬核', clearCount: 21, difficulty: 8, itemTypeCount: 21, }, ] as const; type Match3DDifficultyOptionId = (typeof MATCH3D_DIFFICULTY_OPTIONS)[number]['id']; type Match3DDifficultyOption = (typeof MATCH3D_DIFFICULTY_OPTIONS)[number]; const MATCH3D_CONTAINER_REFERENCE_PREVIEW_SRC = '/match3d-background-references/pot-fused-reference.png'; const MATCH3D_MATERIAL_TAB_BUTTON_CLASS = 'min-h-10 rounded-[0.9rem] px-3 text-sm font-bold transition'; function resolveMatch3DDifficultyOptionId( difficulty: number | null | undefined, clearCount: number | null | undefined, ): Match3DDifficultyOptionId { const byClearCount = MATCH3D_DIFFICULTY_OPTIONS.find( (option) => option.clearCount === clearCount, ); if (byClearCount) { return byClearCount.id; } if (typeof difficulty !== 'number' || !Number.isFinite(difficulty)) { return 'standard'; } const normalizedDifficulty = Math.max( 1, Math.min(10, Math.round(difficulty)), ); return MATCH3D_DIFFICULTY_OPTIONS.reduce( (nearestOption, option) => Math.abs(option.difficulty - normalizedDifficulty) < Math.abs(nearestOption.difficulty - normalizedDifficulty) ? option : nearestOption, MATCH3D_DIFFICULTY_OPTIONS[1], ).id; } function getMatch3DDifficultyOption(optionId: Match3DDifficultyOptionId) { return ( MATCH3D_DIFFICULTY_OPTIONS.find((option) => option.id === optionId) ?? MATCH3D_DIFFICULTY_OPTIONS[1] ); } function getMatch3DReadyItemTypeCount( generatedItemAssets: readonly Match3DGeneratedItemAsset[], ) { return generatedItemAssets.filter(hasMatch3DGeneratedFiveViewImageSource) .length; } function getMatch3DPlayableItemTypeCount( targetItemTypeCount: number, generatedItemAssets: readonly Match3DGeneratedItemAsset[], ) { return Math.max( 1, Math.min( targetItemTypeCount, getMatch3DReadyItemTypeCount(generatedItemAssets), ), ); } function getMatch3DDifficultyOptionFromEditState( editState: Match3DResultEditState, ) { return getMatch3DDifficultyOption( resolveMatch3DDifficultyOptionId( normalizeDifficulty(editState.difficultyText), normalizePositiveInteger(editState.clearCountText), ), ); } function resolveMatch3DBackgroundPreviewSource( profile: Match3DWorkProfile, draft: Match3DResultDraft | null, generatedItemAssets: readonly Match3DGeneratedItemAsset[], ) { return ( draft?.backgroundImageSrc?.trim() || draft?.generatedBackgroundAsset?.imageSrc?.trim() || draft?.backgroundImageObjectKey?.trim() || draft?.generatedBackgroundAsset?.imageObjectKey?.trim() || profile.backgroundImageSrc?.trim() || profile.generatedBackgroundAsset?.imageSrc?.trim() || profile.backgroundImageObjectKey?.trim() || profile.generatedBackgroundAsset?.imageObjectKey?.trim() || generatedItemAssets .map( (asset) => asset.backgroundAsset?.imageSrc?.trim() || asset.backgroundAsset?.imageObjectKey?.trim() || '', ) .find(Boolean) || '' ); } function resolveMatch3DBackgroundPrompt( profile: Match3DWorkProfile, draft: Match3DResultDraft | null, generatedItemAssets: readonly Match3DGeneratedItemAsset[], ) { return ( draft?.backgroundPrompt?.trim() || draft?.generatedBackgroundAsset?.prompt?.trim() || profile.backgroundPrompt?.trim() || profile.generatedBackgroundAsset?.prompt?.trim() || generatedItemAssets .map((asset) => asset.backgroundAsset?.prompt?.trim() || '') .find(Boolean) || buildFallbackMatch3DBackgroundPrompt(profile.themeText) ); } function resolveMatch3DContainerPreviewSource( generatedItemAssets: readonly Match3DGeneratedItemAsset[], ) { return ( generatedItemAssets .map( (asset) => asset.backgroundAsset?.containerImageSrc?.trim() || asset.backgroundAsset?.containerImageObjectKey?.trim() || '', ) .find(Boolean) || '' ); } function buildFallbackMatch3DBackgroundPrompt(themeText: string) { const theme = themeText.trim() || '抓大鹅'; return `${theme}题材抓大鹅游戏竖屏纯背景图,表现题材环境、绿色纵向渐变和轻快休闲氛围,中央区域保持干净通透,无锅、无圆盘、无托盘、无拼图槽、无物品槽、无文字、无水印、无 UI、无按钮、无倒计时、无物品。`; } function normalizeTags(value: string) { return [ ...new Set( value .split(/[\n,,、]/u) .map((entry) => normalizeMatch3DTag(entry)) .filter(Boolean), ), ]; } function buildMatch3DAssetPrompt( profile: Match3DWorkProfile, assetName: string, usage: string, ) { return buildMatch3DAssetPromptFromDraft(profile.themeText, assetName, usage); } function buildMatch3DAssetPromptFromDraft( themeText: string, assetName: string, usage: string, ) { const normalizedTheme = themeText.trim() || '主题'; return [ `${normalizedTheme}题材抓大鹅游戏内2D素材:${assetName}`, usage, '适合移动端游戏直接显示,主体清晰,五视角一致,干净背景。', ] .filter(Boolean) .join(','); } function buildFallbackMatch3DClickSoundPrompt( profile: Pick, assetName: string, ) { const normalizedTheme = profile.themeText.trim() || '抓大鹅'; const normalizedName = assetName.trim() || '物品'; return `${normalizedTheme}题材抓大鹅中“${normalizedName}”被点击并消除时的短促反馈音效,清脆、可爱、有轻微弹跳感,适合移动端休闲游戏。`; } function normalizeMatch3DTag(value: string) { return value .trim() .replace(/^[\s#"'`,。、“”《》;:!?,.、]+/u, '') .replace(/[\s#"'`,。、“”《》;:!?,.、]+$/u, '') .slice(0, 12); } function normalizeMatch3DTagListText(value: string) { return [ ...new Set( value .split(/[\n,,、]/u) .map((entry) => normalizeMatch3DTag(entry)) .filter(Boolean), ), ]; } function normalizeMatch3DItemName(value: string) { return value .trim() .replace(/^[-*•\d.、)\s]+/u, '') .slice(0, 12); } function normalizeMatch3DItemNameList(values: readonly string[]) { return [...new Set(values.map(normalizeMatch3DItemName).filter(Boolean))]; } function calculateMatch3DItemAssetsPointsCost(itemCount: number) { if (itemCount <= 0) { return 0; } return ( Math.ceil(itemCount / MATCH3D_ITEM_ASSETS_BILLING_BATCH_SIZE) * MATCH3D_ITEM_ASSETS_POINTS_PER_BATCH ); } function hasMatch3DGeneratedModelSource(asset: Match3DGeneratedItemAsset) { return Boolean(asset.modelSrc?.trim() || asset.modelObjectKey?.trim()); } function hasMatch3DGeneratedImageSource(asset: Match3DGeneratedItemAsset) { return getMatch3DGeneratedImageViewSources(asset).length > 0; } function hasMatch3DGeneratedFiveViewImageSource( asset: Match3DGeneratedItemAsset, ) { return ( (asset.imageViews ?? []).filter( (view) => Boolean(view.imageSrc?.trim()) || Boolean(view.imageObjectKey?.trim()), ).length >= 5 ); } function resolveMatch3DGeneratedImageViewSourceFromDraft( view: NonNullable[number], ) { return view.imageObjectKey?.trim() || view.imageSrc?.trim() || ''; } function resolveMatch3DAssetDraftImageViewSources( asset: Match3DItemAssetDraft, ) { return [ ...new Set( (asset.imageViews ?? []) .map(resolveMatch3DGeneratedImageViewSourceFromDraft) .filter(Boolean), ), ]; } function resolveMatch3DAssetDraftPreviewSources(asset: Match3DItemAssetDraft) { const imageViewSources = resolveMatch3DAssetDraftImageViewSources(asset); if (imageViewSources.length > 0) { return imageViewSources.slice(0, 5); } const fallbackSource = asset.referenceImageSrc.trim(); return fallbackSource ? [fallbackSource] : []; } function hasPersistableMatch3DGeneratedItemAsset( asset: Match3DGeneratedItemAsset, ) { return Boolean( asset.imageSrc?.trim() || asset.imageObjectKey?.trim() || getMatch3DGeneratedImageViewSources(asset).length > 0 || asset.modelSrc?.trim() || asset.modelObjectKey?.trim() || asset.taskUuid?.trim() || asset.subscriptionKey?.trim() || asset.backgroundAsset?.imageSrc?.trim() || asset.backgroundAsset?.imageObjectKey?.trim() || asset.backgroundAsset?.containerImageSrc?.trim() || asset.backgroundAsset?.containerImageObjectKey?.trim() || asset.backgroundAsset?.prompt?.trim() || asset.backgroundMusic || asset.clickSound, ); } function getMatch3DGeneratedItemAssetPersistenceSignature( asset: Match3DGeneratedItemAsset, ) { return [ asset.itemId.trim(), asset.itemName.trim(), asset.imageSrc?.trim() ?? '', asset.imageObjectKey?.trim() ?? '', ...(asset.imageViews ?? []).flatMap((view) => [ view.viewId.trim(), String(view.viewIndex), view.imageSrc?.trim() ?? '', view.imageObjectKey?.trim() ?? '', ]), asset.modelSrc?.trim() ?? '', asset.modelObjectKey?.trim() ?? '', asset.modelFileName?.trim() ?? '', asset.taskUuid?.trim() ?? '', asset.subscriptionKey?.trim() ?? '', asset.status.trim(), asset.soundPrompt?.trim() ?? '', asset.backgroundMusicTitle?.trim() ?? '', asset.backgroundMusicStyle?.trim() ?? '', asset.backgroundMusicPrompt?.trim() ?? '', asset.backgroundMusic?.audioSrc?.trim() ?? asset.backgroundMusic?.assetObjectId?.trim() ?? asset.backgroundMusic?.taskId?.trim() ?? '', asset.backgroundAsset?.prompt?.trim() ?? '', asset.backgroundAsset?.imageSrc?.trim() ?? '', asset.backgroundAsset?.imageObjectKey?.trim() ?? '', asset.backgroundAsset?.containerPrompt?.trim() ?? '', asset.backgroundAsset?.containerImageSrc?.trim() ?? '', asset.backgroundAsset?.containerImageObjectKey?.trim() ?? '', asset.backgroundAsset?.status?.trim() ?? '', asset.backgroundAsset?.error?.trim() ?? '', asset.clickSound?.audioSrc?.trim() ?? asset.clickSound?.assetObjectId?.trim() ?? asset.clickSound?.taskId?.trim() ?? '', asset.error?.trim() ?? '', ].join('\u001f'); } function shouldPersistGeneratedItemAssets( currentAssets: readonly Match3DGeneratedItemAsset[], savedAssets: readonly Match3DGeneratedItemAsset[] = [], ) { if (currentAssets.length <= 0) { return false; } if (currentAssets.length !== savedAssets.length) { return true; } return currentAssets.some((asset, index) => { const savedAsset = savedAssets[index]; return ( !savedAsset || getMatch3DGeneratedItemAssetPersistenceSignature(asset) !== getMatch3DGeneratedItemAssetPersistenceSignature(savedAsset) ); }); } function mergeMatch3DGeneratedItemAsset( base: Match3DGeneratedItemAsset, override: Match3DGeneratedItemAsset, ): Match3DGeneratedItemAsset { const overrideHasModel = hasMatch3DGeneratedModelSource(override); const overrideHasImages = hasMatch3DGeneratedImageSource(override); return { ...base, itemName: override.itemName.trim() || base.itemName, imageSrc: override.imageSrc?.trim() ? override.imageSrc : (base.imageSrc ?? null), imageObjectKey: override.imageObjectKey?.trim() ? override.imageObjectKey : (base.imageObjectKey ?? null), imageViews: override.imageViews && override.imageViews.length > 0 ? override.imageViews : (base.imageViews ?? []), modelSrc: override.modelSrc?.trim() ? override.modelSrc : (base.modelSrc ?? null), modelObjectKey: override.modelObjectKey?.trim() ? override.modelObjectKey : (base.modelObjectKey ?? null), modelFileName: override.modelFileName?.trim() ? override.modelFileName : (base.modelFileName ?? null), taskUuid: override.taskUuid?.trim() ? override.taskUuid : (base.taskUuid ?? null), subscriptionKey: override.subscriptionKey?.trim() ? override.subscriptionKey : (base.subscriptionKey ?? null), backgroundMusic: override.backgroundMusic ?? base.backgroundMusic ?? null, clickSound: override.clickSound ?? base.clickSound ?? null, backgroundAsset: override.backgroundAsset ?? base.backgroundAsset ?? null, soundPrompt: override.soundPrompt?.trim() ? override.soundPrompt : (base.soundPrompt ?? null), backgroundMusicTitle: override.backgroundMusicTitle?.trim() ? override.backgroundMusicTitle : (base.backgroundMusicTitle ?? null), backgroundMusicStyle: override.backgroundMusicStyle?.trim() ? override.backgroundMusicStyle : (base.backgroundMusicStyle ?? null), backgroundMusicPrompt: override.backgroundMusicPrompt?.trim() ? override.backgroundMusicPrompt : (base.backgroundMusicPrompt ?? null), // 中文注释:新草稿以 2D 多视角图片为正式素材;历史模型字段只做兼容保留。 status: overrideHasModel && base.status !== 'model_ready' ? 'model_ready' : overrideHasImages ? 'image_ready' : base.status, error: override.error ?? base.error ?? null, }; } function createMatch3DAssetDrafts( profile: Match3DWorkProfile, draft: Match3DResultDraft | null = null, ): Match3DItemAssetDraft[] { const generatedAssets = resolveMatch3DResultGeneratedItemAssets( profile, draft, ); if (generatedAssets?.length) { return generatedAssets.map((asset) => createMatch3DAssetDraftFromGeneratedAsset(profile, asset), ); } const theme = profile.themeText.trim() || '主题'; const seeds = [ { id: 'primary-item', name: `${theme}核心物件`, usage: '局内主要点击消除物件', }, { id: 'rare-item', name: `${theme}稀有物件`, usage: '用于增强辨识度的高价值物件', }, { id: 'bonus-item', name: `${theme}奖励物件`, usage: '通关或连消反馈奖励', }, { id: 'obstacle-item', name: `${theme}干扰物件`, usage: '堆叠层中的视觉遮挡物', }, { id: 'tray-prop', name: `${theme}托盘道具`, usage: '备选栏和结算展示道具', }, { id: 'scene-prop', name: `${theme}场景小物`, usage: '圆形空间周边装饰物', }, ].slice(0, MATCH3D_DEFAULT_ASSET_COUNT); return seeds.map((seed) => ({ ...seed, prompt: buildMatch3DAssetPrompt(profile, seed.name, seed.usage), referenceImageSrc: profile.referenceImageSrc ?? profile.coverImageSrc ?? '', imageViews: [], taskUuid: null, subscriptionKey: null, status: 'idle', progress: null, downloads: [], soundPrompt: buildFallbackMatch3DClickSoundPrompt(profile, seed.name), backgroundMusicTitle: null, backgroundMusicStyle: null, backgroundMusicPrompt: null, backgroundMusic: null, clickSound: null, backgroundAsset: null, error: null, updatedAt: null, })); } function createMatch3DAssetDraftFromGeneratedAsset( profile: Match3DWorkProfile, asset: Match3DGeneratedItemAsset, ): Match3DItemAssetDraft { const modelSource = resolveMatch3DGeneratedModelAssetSource(asset); const imageSource = resolveMatch3DGeneratedImageAssetSource(asset); const downloads = modelSource ? [ { name: asset.modelFileName ?? `${asset.itemName}.glb`, url: modelSource, }, ] : []; return { id: asset.itemId, name: asset.itemName, usage: '局内点击消除物件', prompt: buildMatch3DAssetPrompt( profile, asset.itemName, '局内点击消除物件', ), referenceImageSrc: imageSource || asset.imageSrc || profile.referenceImageSrc || profile.coverImageSrc || '', imageViews: asset.imageViews ?? [], taskUuid: asset.taskUuid ?? null, subscriptionKey: asset.subscriptionKey ?? null, status: asset.status === 'model_ready' ? 'done' : normalizeMatch3DAssetStatus(asset.status), progress: asset.status === 'model_ready' ? 1 : null, downloads, soundPrompt: asset.soundPrompt?.trim() || asset.clickSound?.prompt?.trim() || buildFallbackMatch3DClickSoundPrompt(profile, asset.itemName), backgroundMusicTitle: asset.backgroundMusicTitle ?? asset.backgroundMusic?.title ?? null, backgroundMusicStyle: asset.backgroundMusicStyle ?? null, backgroundMusicPrompt: asset.backgroundMusicPrompt ?? asset.backgroundMusic?.prompt ?? null, backgroundMusic: asset.backgroundMusic ?? null, backgroundAsset: asset.backgroundAsset ?? null, clickSound: asset.clickSound ?? null, error: asset.error ?? null, updatedAt: profile.updatedAt, }; } function createGeneratedAssetsFromDrafts( assetDrafts: Match3DItemAssetDraft[], existingAssets: readonly Match3DGeneratedItemAsset[] = [], ): Match3DGeneratedItemAsset[] { const existingById = new Map( existingAssets.map((asset) => [asset.itemId, asset]), ); return assetDrafts.map((asset) => { const existing = existingById.get(asset.id); const modelFile = asset.downloads.find((file) => file.url.trim()) ?? null; const modelSource = modelFile?.url.trim() || existing?.modelSrc?.trim() || existing?.modelObjectKey?.trim() || null; const modelObjectKey = modelFile?.url && isGeneratedLegacyPath(modelFile.url) ? modelFile.url.trim().replace(/^\/+/u, '') : (existing?.modelObjectKey ?? null); return { itemId: asset.id, itemName: asset.name, imageSrc: existing?.imageSrc ?? (asset.referenceImageSrc || null), imageObjectKey: existing?.imageObjectKey ?? null, imageViews: asset.imageViews ?? existing?.imageViews ?? [], modelSrc: modelSource, modelObjectKey, modelFileName: modelFile?.name?.trim() || existing?.modelFileName || null, taskUuid: asset.taskUuid, subscriptionKey: asset.subscriptionKey, soundPrompt: asset.soundPrompt.trim() || existing?.soundPrompt || null, backgroundMusicTitle: asset.backgroundMusicTitle ?? existing?.backgroundMusicTitle ?? null, backgroundMusicStyle: asset.backgroundMusicStyle ?? existing?.backgroundMusicStyle ?? null, backgroundMusicPrompt: asset.backgroundMusicPrompt ?? existing?.backgroundMusicPrompt ?? null, backgroundMusic: asset.backgroundMusic, clickSound: asset.clickSound, backgroundAsset: asset.backgroundAsset ?? existing?.backgroundAsset ?? (asset.id === assetDrafts[0]?.id ? (existingAssets .map((candidate) => candidate.backgroundAsset ?? null) .find(Boolean) ?? null) : null), // 中文注释:当前主链路只要求 2D 图片素材;但历史草稿若已有平台模型字段,保存时不能把模型状态降回图片态。 status: modelSource?.trim() ? 'model_ready' : hasMatch3DGeneratedImageSource( existing ?? ({} as Match3DGeneratedItemAsset), ) ? 'image_ready' : asset.status === 'done' ? 'model_ready' : asset.status, error: asset.error, }; }); } function normalizeMatch3DAssetStatus(status: string): Match3DAssetTaskStatus { const normalized = status.trim().toLowerCase(); if ( normalized === 'waiting' || normalized === 'pending' || normalized === 'queued' ) { return 'waiting'; } if ( normalized === 'generating' || normalized === 'running' || normalized === 'processing' ) { return 'generating'; } if (normalized === 'image_ready') { return 'image_ready'; } if ( normalized === 'done' || normalized === 'succeeded' || normalized === 'success' || normalized === 'completed' ) { return 'done'; } if ( normalized === 'failed' || normalized === 'error' || normalized === 'canceled' || normalized === 'cancelled' ) { return 'failed'; } return 'unknown'; } function Match3DAudioProgress({ label, progress, }: { label: string; progress: number; }) { const normalizedProgress = Math.max(0, Math.min(1, progress)); return (
{label} {Math.round(normalizedProgress * 100)}%
); } function Match3DResolvedAudio({ ariaLabel, src, }: { ariaLabel?: string; src: string; }) { const { resolvedUrl } = useResolvedAssetReadUrl(src, { expireSeconds: 300, }); if (!resolvedUrl) { return (
音频已绑定
); } return (