import type { EditorAssetGenerationInputReference, EditorAssetSnapshot, EditorImageSequenceFrameResult, EditorProjectResourceSnapshot, EditorShowcaseCampaignSnapshot, } from '../../services/image-editor/editorProjectClient'; import { calculateCharacterAnimationPrice, calculateEditorBackgroundMusicPrice, calculateEditorImageGenerationPrice, calculateEditorSoundEffectPrice, calculateEditorVideoPrice, CHARACTER_ANIMATION_MODEL, DEFAULT_BACKGROUND_MUSIC_MODEL, DEFAULT_SOUND_EFFECT_MODEL, DEFAULT_VIDEO_DURATION_SECONDS, DEFAULT_VIDEO_MODEL, } from '../image-editor/ImageCanvasGenerationModel'; export type ShowcaseTabId = 'all' | 'characters' | 'ui' | 'music' | 'marketing'; export type ShowcaseAssetMediaType = 'image' | 'video' | 'audio' | 'image-sequence'; export type ShowcaseAssetPreview = { id: string; label: string; src: string; coverSrc?: string | null; objectKey?: string | null; mediaType: ShowcaseAssetMediaType; width?: number; height?: number; imageSequenceFrames?: EditorImageSequenceFrameResult[]; imageSequenceDurationMs?: number; }; export type ShowcaseAssetItem = { id: string; label: string; previews: ShowcaseAssetPreview[]; prompt: string; author: string; cost: string; likeCount?: number | null; showcaseId?: string | null; campaign?: boolean; }; type ShowcaseAssetGroup = { id: string; label?: string; assets: EditorAssetSnapshot[]; }; const UNKNOWN_META_VALUE = '-'; const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.png'; const SHOWCASE_COST_KEYS = [ 'priceMudPoints', 'costMudPoints', 'generationCostMudPoints', 'mudPointCost', ] as const; const SHOWCASE_AUTHOR_KEYS = [ 'authorDisplayName', 'authorPublicUserCode', ] as const; const USER_PROMPT_INPUT_TITLES = new Set( [ '生成提示词', '视频描述', 'prompt', 'sound', 'gpt_description_prompt', '音效提示词', '背景音乐提示词', '角色设定', '用户输入', '素材描述', '自定义规范提示词', '修改要求', '快速编辑提示词', '重绘提示词', '动作描述', ].map((title) => title.toLowerCase()), ); const STRUCTURED_USER_INPUT_TITLES = new Set( [ '玩法设定', '美术风格', '头身比', '角色视角', '游戏名', '游戏分类', '一句话描述游戏', ].map((title) => title.toLowerCase()), ); export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [ { id: 'all', label: '全部' }, { id: 'characters', label: '角色' }, { id: 'ui', label: 'UI' }, { id: 'music', label: '音乐' }, { id: 'marketing', label: '美宣' }, ]; const SHOWCASE_CATEGORY_IDS = new Set( SHOWCASE_TABS.filter((tab) => tab.id !== 'all').map((tab) => tab.id), ); function assetRecord(asset: EditorAssetSnapshot) { return asset as Record; } function projectResourceRecord(resource: EditorProjectResourceSnapshot) { return resource as unknown as Record; } function normalizeText(value: unknown) { return typeof value === 'string' && value.trim() ? value.trim() : ''; } function normalizeGenerationInputTitle(value: string) { return value.trim().toLowerCase(); } function isBuiltInPromptText(value: string) { const trimmedValue = value.trim(); return ( trimmedValue.startsWith('【系统内置】') || trimmedValue.startsWith('【宣发素材类型】') || trimmedValue.startsWith('参考图1的图标素材规范') || trimmedValue.startsWith('生成玩法UI原型图') || trimmedValue.includes('仅提取被红色框框选的素材') || trimmedValue.includes('图集背景必须使用所选单一纯色抠图背景') ); } function normalizeNumber(value: unknown) { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim()) { const numericValue = Number(value.trim()); return Number.isFinite(numericValue) ? numericValue : null; } return null; } function normalizeInt( value: unknown, fallback: number, minValue = 1, maxValue = Number.MAX_SAFE_INTEGER, ) { const numericValue = normalizeNumber(value); if (numericValue === null) { return fallback; } return Math.min(maxValue, Math.max(minValue, Math.round(numericValue))); } function normalizeAssetKind(asset: EditorAssetSnapshot) { return asset.assetKind?.trim() ?? ''; } function getAssetHaystack(asset: EditorAssetSnapshot) { return [ asset.label, asset.prompt, asset.actualPrompt, asset.model, asset.sourceType, asset.assetKind, ...(asset.generationInputs?.fields ?? []).flatMap((field) => [ field.title, field.value, ]), ...(asset.generationInputs?.references ?? []).flatMap((reference) => [ reference.title, reference.label, reference.refType, reference.refId, ]), ] .filter(Boolean) .join(' ') .toLowerCase(); } function getAssetReferences(asset: EditorAssetSnapshot) { return asset.generationInputs?.references ?? []; } function getReferenceKey(reference: EditorAssetGenerationInputReference) { return `${reference.refType}:${reference.refId}`; } function getSelfAssetKey(asset: EditorAssetSnapshot) { return `asset:${asset.assetId}`; } function getAssetReferenceKeys(asset: EditorAssetSnapshot) { const keys = [getSelfAssetKey(asset)]; if (asset.assetId?.trim()) { keys.push(`project-resource:${asset.assetId.trim()}`); } if (asset.sourceResourceId?.trim()) { keys.push(`project-resource:${asset.sourceResourceId.trim()}`); } if (asset.assetObjectId?.trim()) { keys.push(`asset-object:${asset.assetObjectId.trim()}`); } if (asset.objectKey?.trim()) { keys.push(`object:${asset.objectKey.trim()}`); } return keys; } function projectResourceToShowcaseAsset( resource: EditorProjectResourceSnapshot, ): EditorAssetSnapshot { const record = projectResourceRecord(resource); const fallbackLabel = normalizeText(record.label) || normalizeText(record.title) || resource.assetKind?.trim() || '项目素材'; return { ...record, assetId: resource.assetId?.trim() || resource.showcaseId?.trim() || resource.resourceId, folderId: `project:${resource.projectId}`, label: fallbackLabel, imageSrc: resource.imageSrc, objectKey: resource.objectKey, assetObjectId: resource.assetObjectId, width: resource.width, height: resource.height, sourceType: resource.sourceType, prompt: resource.prompt ?? null, actualPrompt: resource.actualPrompt ?? null, model: resource.model ?? null, taskId: resource.taskId ?? null, imageSequenceFrames: resource.imageSequenceFrames, imageSequenceDurationMs: resource.imageSequenceDurationMs, sourceResourceId: resource.sourceResourceId, assetKind: resource.assetKind, showcaseCategory: normalizeShowcaseCategory( normalizeText(record.showcaseCategory), ), generationInputs: resource.generationInputs, createdAt: resource.createdAt, updatedAt: resource.updatedAt, } as EditorAssetSnapshot; } function normalizeShowcaseCategory(value: string): ShowcaseTabId | null { return SHOWCASE_CATEGORY_IDS.has(value as Exclude) ? (value as ShowcaseTabId) : null; } function resolveAssetShowcaseCategory(asset: EditorAssetSnapshot) { return normalizeShowcaseCategory( normalizeText(assetRecord(asset).showcaseCategory), ); } function normalizeShowcaseMediaRef(value: string | null | undefined) { const trimmedValue = value?.trim().split(/[?#]/u)[0]?.trim() ?? ''; if (!trimmedValue) { return ''; } const pathValue = /^https?:\/\//iu.test(trimmedValue) ? (() => { try { return new URL(trimmedValue).pathname; } catch { return trimmedValue; } })() : trimmedValue; return pathValue.replace(/^\/+/u, ''); } function getProjectResourceMediaKey(resource: EditorProjectResourceSnapshot) { const assetObjectId = resource.assetObjectId?.trim(); if (assetObjectId) { return `asset-object:${assetObjectId}`; } const objectKey = normalizeShowcaseMediaRef(resource.objectKey); if (objectKey) { return `object:${objectKey}`; } const imageSrc = normalizeShowcaseMediaRef(resource.imageSrc); return imageSrc ? `src:${imageSrc}` : ''; } function isCopiedProjectResource( resource: EditorProjectResourceSnapshot, resourceById: Map, ) { const sourceResourceId = resource.sourceResourceId?.trim(); if (!sourceResourceId) { return false; } const sourceResource = resourceById.get(sourceResourceId); if (!sourceResource) { return false; } return ( getProjectResourceMediaKey(sourceResource) === getProjectResourceMediaKey(resource) ); } function buildGeneratedProjectResourceAssets( resources: EditorProjectResourceSnapshot[], ) { const seenResourceIds = new Set(); const resourceById = new Map( resources .filter((resource) => resource.resourceId.trim()) .map((resource) => [resource.resourceId, resource]), ); return resources .filter( (resource) => resource.publicShowcaseEnabled !== false && resource.sourceType === 'generated' && resource.imageSrc.trim(), ) .filter((resource) => { if (seenResourceIds.has(resource.resourceId)) { return false; } seenResourceIds.add(resource.resourceId); return true; }) .filter((resource) => !isCopiedProjectResource(resource, resourceById)) .map(projectResourceToShowcaseAsset); } function findReference( asset: EditorAssetSnapshot, matcher: (reference: EditorAssetGenerationInputReference) => boolean, ) { return getAssetReferences(asset).find(matcher) ?? null; } function findSpecReference(asset: EditorAssetSnapshot) { return findReference(asset, (reference) => /规范/u.test(reference.title)); } function findCharacterReference(asset: EditorAssetSnapshot) { return findReference(asset, (reference) => /角色图片|角色图/u.test(reference.title), ); } function findUiDesignReference(asset: EditorAssetSnapshot) { return findReference(asset, (reference) => /UI设计图|UI 图|UI图片/u.test(reference.title), ); } function isAssetKind(asset: EditorAssetSnapshot, kinds: readonly string[]) { return kinds.includes(normalizeAssetKind(asset)); } function isAudioAsset(asset: EditorAssetSnapshot) { const mediaType = normalizeText(assetRecord(asset).mediaType); return ( mediaType === 'audio' || isAssetKind(asset, ['sound-effect', 'background-music']) || /\.(mp3|wav|m4a|aac|ogg)(?:$|\?)/iu.test(asset.imageSrc) ); } function isVideoAsset(asset: EditorAssetSnapshot) { const mediaType = normalizeText(assetRecord(asset).mediaType); return ( mediaType === 'video' || isAssetKind(asset, ['video']) || /\.(mp4|webm|mov|m4v)(?:$|\?)/iu.test(asset.imageSrc) ); } function resolveAssetMediaType( asset: EditorAssetSnapshot, ): ShowcaseAssetMediaType { if (isCharacterAnimationAsset(asset)) { return 'image-sequence'; } if (isAudioAsset(asset)) { return 'audio'; } if (isVideoAsset(asset)) { return 'video'; } return 'image'; } function isSpecAsset(asset: EditorAssetSnapshot) { return isAssetKind(asset, ['spec', 'icon-spec']); } function isCharacterAsset(asset: EditorAssetSnapshot) { return isAssetKind(asset, ['character']); } function isCharacterAnimationAsset(asset: EditorAssetSnapshot) { return isAssetKind(asset, ['character-animation']); } function isUiAsset(asset: EditorAssetSnapshot) { return isAssetKind(asset, ['ui-design', 'icon', 'icon-spritesheet']); } function isMarketingAsset(asset: EditorAssetSnapshot) { const haystack = getAssetHaystack(asset); return ( isAssetKind(asset, ['publication-material']) || /美宣|宣发|宣传|poster|promo|marketing/u.test(haystack) ); } function addAssetToGroup( groups: Map, groupId: string, asset: EditorAssetSnapshot, label?: string, ) { const group = groups.get(groupId); if (group) { if (label && !group.label) { group.label = label; } group.assets.push(asset); return; } groups.set(groupId, { id: groupId, label, assets: [asset], }); } function mergeAssetAliasGroups( groups: Map, asset: EditorAssetSnapshot, label?: string, ) { const aliasGroups = getAssetReferenceKeys(asset) .map((key) => groups.get(key)) .filter((group): group is ShowcaseAssetGroup => Boolean(group)) .filter( (group, index, list) => list.findIndex((candidate) => candidate.id === group.id) === index, ); const targetGroup = aliasGroups[0] ?? null; if (!targetGroup) { return null; } if (label && !targetGroup.label) { targetGroup.label = label; } aliasGroups.slice(1).forEach((group) => { if (!targetGroup.label && group.label) { targetGroup.label = group.label; } group.assets.forEach((assetInGroup) => { if ( !targetGroup.assets.some( (existingAsset) => existingAsset.assetId === assetInGroup.assetId, ) ) { targetGroup.assets.push(assetInGroup); } }); groups.delete(group.id); }); return targetGroup; } function sortGroupAssets(assets: EditorAssetSnapshot[]) { return [...assets].sort((left, right) => { const leftRank = isSpecAsset(left) || isCharacterAsset(left) || isAssetKind(left, ['ui-design']) ? 0 : 1; const rightRank = isSpecAsset(right) || isCharacterAsset(right) || isAssetKind(right, ['ui-design']) ? 0 : 1; if (leftRank !== rightRank) { return leftRank - rightRank; } return (left.createdAt ?? '').localeCompare(right.createdAt ?? ''); }); } function groupCharacterAssets(assets: EditorAssetSnapshot[]) { const groups = new Map(); assets .filter( (asset) => isCharacterAsset(asset) || isCharacterAnimationAsset(asset) || /角色|character/u.test(getAssetHaystack(asset)), ) .forEach((asset) => { if (isCharacterAsset(asset)) { addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label); return; } const characterReference = findCharacterReference(asset); if (characterReference) { addAssetToGroup( groups, getReferenceKey(characterReference), asset, characterReference.label, ); return; } addAssetToGroup( groups, `standalone:${asset.assetId}`, asset, asset.label, ); }); const characterAssets = assets.filter(isCharacterAsset); characterAssets.forEach((asset) => { const group = mergeAssetAliasGroups(groups, asset, asset.label); if ( group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId) ) { group.assets.unshift(asset); } }); return [...groups.values()].map((group) => ({ ...group, assets: sortGroupAssets(group.assets), })); } function groupUiAssets(assets: EditorAssetSnapshot[]) { const groups = new Map(); assets .filter( (asset) => isUiAsset(asset) || /\bui\b|界面|图标|icon/u.test(getAssetHaystack(asset)), ) .forEach((asset) => { if (isAssetKind(asset, ['ui-design'])) { addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label); return; } const uiReference = findUiDesignReference(asset) ?? findSpecReference(asset); if (uiReference) { addAssetToGroup( groups, getReferenceKey(uiReference), asset, uiReference.label, ); return; } addAssetToGroup( groups, `standalone:${asset.assetId}`, asset, asset.label, ); }); const uiDesignAssets = assets.filter((asset) => isAssetKind(asset, ['ui-design']), ); uiDesignAssets.forEach((asset) => { const group = mergeAssetAliasGroups(groups, asset, asset.label); if ( group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId) ) { group.assets.unshift(asset); } }); return [...groups.values()].map((group) => ({ ...group, assets: sortGroupAssets(group.assets), })); } function groupSingleAssets( assets: EditorAssetSnapshot[], predicate: (asset: EditorAssetSnapshot) => boolean, ) { return assets.filter(predicate).map((asset) => ({ id: `asset:${asset.assetId}`, label: asset.label, assets: [asset], })); } function getGroupsForTab(assets: EditorAssetSnapshot[], tab: ShowcaseTabId) { if (tab === 'all') { return groupSingleAssets(assets, () => true); } if (assets.some((asset) => resolveAssetShowcaseCategory(asset))) { return groupSingleAssets( assets, (asset) => resolveAssetShowcaseCategory(asset) === tab, ); } if (tab === 'characters') { return groupCharacterAssets(assets); } if (tab === 'ui') { return groupUiAssets(assets); } if (tab === 'music') { return groupSingleAssets(assets, isAudioAsset); } if (tab === 'marketing') { return groupSingleAssets(assets, isMarketingAsset); } return []; } function resolveUserGenerationPrompt(asset: EditorAssetSnapshot) { const fields = asset.generationInputs?.fields ?? []; const promptField = fields.find((field) => { const value = String(field.value).trim(); return ( value && USER_PROMPT_INPUT_TITLES.has( normalizeGenerationInputTitle(field.title), ) && !isBuiltInPromptText(value) ); }); const promptValue = promptField ? String(promptField.value).trim() : ''; if (promptValue) { return promptValue; } const structuredLines = fields.flatMap((field) => { if ( !STRUCTURED_USER_INPUT_TITLES.has( normalizeGenerationInputTitle(field.title), ) ) { return []; } const value = String(field.value).trim(); return value && !isBuiltInPromptText(value) ? [`${field.title.trim()}:${value}`] : []; }); return structuredLines.join('\n'); } function extractUserPromptFromAssembledPrompt(value: string) { const trimmedValue = value.trim(); const sectionMatch = trimmedValue.match(/【用户输入】\s*([\s\S]*?)(?=\n+【|$)/u) ?? trimmedValue.match(/【游戏输入】\s*([\s\S]*?)(?=\n+【|$)/u); const sectionValue = sectionMatch?.[1]?.trim(); if (sectionValue && !isBuiltInPromptText(sectionValue)) { return sectionValue; } const roleSettingMatch = trimmedValue.match( /(?:^|\n)角色设定[::]\s*([^\n]+)/u, ); const roleSettingValue = roleSettingMatch?.[1]?.trim(); return roleSettingValue && !isBuiltInPromptText(roleSettingValue) ? roleSettingValue : ''; } function resolveAssetPrompt(asset: EditorAssetSnapshot) { const userPrompt = resolveUserGenerationPrompt(asset); if (userPrompt) { return userPrompt; } for (const value of [asset.prompt, asset.actualPrompt]) { const directPrompt = value?.trim() ?? ''; if (!directPrompt) { continue; } const extractedPrompt = extractUserPromptFromAssembledPrompt(directPrompt); if (extractedPrompt) { return extractedPrompt; } if (!isBuiltInPromptText(directPrompt)) { return directPrompt; } } return UNKNOWN_META_VALUE; } function resolveGroupPrompt(assets: EditorAssetSnapshot[]) { return ( assets .map(resolveAssetPrompt) .find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE ); } function resolveAssetAuthor( asset: EditorAssetSnapshot, fallbackAuthorName?: string | null, ) { const record = assetRecord(asset); for (const key of SHOWCASE_AUTHOR_KEYS) { const value = normalizeText(record[key]); if (value) { return value; } } return fallbackAuthorName?.trim() || UNKNOWN_META_VALUE; } function resolveGroupAuthor( assets: EditorAssetSnapshot[], fallbackAuthorName?: string | null, ) { return ( assets .map((asset) => resolveAssetAuthor(asset, fallbackAuthorName)) .find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE ); } function resolveAssetCost(asset: EditorAssetSnapshot) { const record = assetRecord(asset); for (const key of SHOWCASE_COST_KEYS) { const value = normalizeNumber(record[key]); if (value !== null && value >= 0) { return value; } } const inferredValue = inferAssetCost(asset); if (inferredValue !== null && inferredValue >= 0) { return inferredValue; } return null; } function getAssetCostKey(asset: EditorAssetSnapshot) { if (asset.taskId?.trim()) { return `task:${asset.taskId.trim()}`; } if (asset.objectKey?.trim()) { return `object:${asset.objectKey.trim()}`; } return `asset:${asset.assetId}`; } function inferAssetCost(asset: EditorAssetSnapshot) { const kind = normalizeAssetKind(asset); const model = asset.model; if (kind === 'sound-effect') { return calculateEditorSoundEffectPrice(model ?? DEFAULT_SOUND_EFFECT_MODEL); } if (kind === 'background-music') { return calculateEditorBackgroundMusicPrice( model ?? DEFAULT_BACKGROUND_MUSIC_MODEL, ); } if (kind === 'character-animation') { return calculateCharacterAnimationPrice( model ?? CHARACTER_ANIMATION_MODEL, inferVideoResolution(asset, ['480p', '720p'], '480p') as '480p' | '720p', normalizeInt( asset.imageSequenceDurationMs ? asset.imageSequenceDurationMs / 1_000 : undefined, 4, 4, 6, ), ); } if (kind === 'video' || isVideoAsset(asset)) { return calculateEditorVideoPrice( model ?? DEFAULT_VIDEO_MODEL, inferVideoResolution(asset, ['480p', '720p', '1080p'], '480p') as '480p' | '720p' | '1080p', normalizeInt( inferMediaDurationSeconds(asset), DEFAULT_VIDEO_DURATION_SECONDS, 4, 15, ), ); } if (asset.sourceType === 'generated' || kind) { return calculateEditorImageGenerationPrice({ kind, model }); } return null; } function inferMediaDurationSeconds(asset: EditorAssetSnapshot) { const durationField = asset.generationInputs?.fields.find((field) => { const title = field.title.trim().toLowerCase(); return title === '时长' || title === 'duration'; }); const durationValue = durationField?.value; if (typeof durationValue === 'number') { return Number.isFinite(durationValue) ? durationValue : undefined; } if (typeof durationValue !== 'string') { return undefined; } const matchedValue = durationValue.match(/\d+(?:\.\d+)?/u)?.[0]; return matchedValue ? Number(matchedValue) : undefined; } function inferVideoResolution( asset: EditorAssetSnapshot, allowedValues: string[], fallback: string, ) { const fields = asset.generationInputs?.fields ?? []; const fieldValue = fields .flatMap((field) => [field.title, field.value]) .map((value) => String(value).trim()) .find((value) => /\b(?:480p|720p|1080p)\b/iu.test(value)); const matchedValue = fieldValue?.match(/\b(480p|720p|1080p)\b/iu)?.[1]; if (matchedValue && allowedValues.includes(matchedValue)) { return matchedValue; } return fallback; } function resolveGroupCost(assets: EditorAssetSnapshot[]) { const resolvedCosts = new Map(); assets.forEach((asset) => { const cost = resolveAssetCost(asset); if (cost !== null) { resolvedCosts.set(getAssetCostKey(asset), cost); } }); const costs = [...resolvedCosts.values()]; if (!costs.length) { return UNKNOWN_META_VALUE; } const totalCost = costs.reduce((sum, value) => sum + value, 0); return `${totalCost}泥点`; } function normalizeShowcaseImageSequence(asset: EditorAssetSnapshot) { const frames = asset.imageSequenceFrames; const durationMs = asset.imageSequenceDurationMs; if ( !Array.isArray(frames) || frames.length < 2 || typeof durationMs !== 'number' || !Number.isFinite(durationMs) || durationMs <= 0 ) { return null; } const normalizedFrames = frames.flatMap((frame) => { const imageSrc = frame.imageSrc?.trim() ?? ''; if ( !imageSrc || !Number.isFinite(frame.width) || frame.width <= 0 || !Number.isFinite(frame.height) || frame.height <= 0 ) { return []; } return [ { ...frame, imageSrc, objectKey: frame.objectKey?.trim() || null, assetObjectId: frame.assetObjectId?.trim() || null, }, ]; }); if (normalizedFrames.length !== frames.length) { return null; } return { frames: normalizedFrames, durationMs }; } function toAssetPreview( asset: EditorAssetSnapshot, ): ShowcaseAssetPreview | null { const mediaType = resolveAssetMediaType(asset); if (mediaType === 'image-sequence') { const sequence = normalizeShowcaseImageSequence(asset); if (!sequence) { return null; } const firstFrame = sequence.frames[0]; if (!firstFrame) { return null; } return { id: asset.assetId, label: asset.label, src: firstFrame.imageSrc, coverSrc: null, objectKey: firstFrame.objectKey, mediaType, width: firstFrame.width, height: firstFrame.height, imageSequenceFrames: sequence.frames, imageSequenceDurationMs: sequence.durationMs, }; } return { id: asset.assetId, label: asset.label, src: asset.imageSrc, coverSrc: mediaType === 'audio' ? AUDIO_ASSET_COVER_SRC : null, objectKey: asset.objectKey, mediaType, width: asset.width, height: asset.height, }; } function toLibraryShowcaseItem( group: ShowcaseAssetGroup, fallbackAuthorName?: string | null, ): ShowcaseAssetItem | null { const sortedAssets = sortGroupAssets(group.assets); const previews = sortedAssets .map(toAssetPreview) .filter((preview): preview is ShowcaseAssetPreview => Boolean(preview?.src.trim()), ); if (!previews.length) { return null; } const primaryAsset = sortedAssets[0]; if (!primaryAsset) { return null; } const label = group.label?.trim() || primaryAsset?.label?.trim() || `${previews[0]?.label ?? '素材'}组合`; return { id: group.id, label, previews, prompt: resolveGroupPrompt(sortedAssets), author: resolveGroupAuthor(sortedAssets, fallbackAuthorName), cost: resolveGroupCost(sortedAssets), likeCount: normalizeNumber(assetRecord(primaryAsset).likeCount), showcaseId: normalizeText(assetRecord(primaryAsset).showcaseId), }; } function campaignToShowcaseItem( campaign?: EditorShowcaseCampaignSnapshot | null, ): ShowcaseAssetItem | null { const objectKey = campaign?.imageObjectKey?.trim().replace(/^\/+/u, '') ?? ''; const imageSrc = campaign?.imageSrc.trim() || (objectKey ? `/${objectKey}` : ''); if (!campaign?.enabled || !imageSrc) { return null; } return { id: 'campaign:global', label: campaign.title.trim() || '陶泥儿精选', previews: [ { id: 'campaign:global:image', label: campaign.title.trim() || '陶泥儿精选', src: imageSrc, objectKey: objectKey || null, mediaType: 'image', width: campaign.imageWidth ?? undefined, height: campaign.imageHeight ?? undefined, }, ], prompt: campaign.prompt.trim() || UNKNOWN_META_VALUE, author: campaign.author.trim() || UNKNOWN_META_VALUE, cost: campaign.costText.trim() || UNKNOWN_META_VALUE, campaign: true, }; } export function buildCreationShowcaseItems({ activeTab, projectResources, campaign, fallbackAuthorName, }: { activeTab: ShowcaseTabId; projectResources: EditorProjectResourceSnapshot[]; campaign?: EditorShowcaseCampaignSnapshot | null; fallbackAuthorName?: string | null; }) { const projectItems = getGroupsForTab( buildGeneratedProjectResourceAssets(projectResources), activeTab, ) .map((group) => toLibraryShowcaseItem(group, fallbackAuthorName)) .filter((item): item is ShowcaseAssetItem => item !== null); const campaignItem = campaignToShowcaseItem(campaign); return campaignItem ? [campaignItem, ...projectItems] : projectItems; }