import { useEffect, useMemo, useState } from 'react'; import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent'; import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; import type { CreationEntryConfig } from '../../services/creationEntryConfigService'; import type { CustomWorldProfile } from '../../types'; import type { PlatformCreationTypeCard, PlatformCreationTypeId, } from '../platform-entry/platformEntryCreationTypes'; import { isPlatformCreationTypeVisible } from '../platform-entry/platformEntryCreationTypes'; import { buildCreationWorkShelfItems, type CreationWorkShelfItem, type CreationWorkShelfMetricId, } from './creationWorkShelf'; import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard'; import { CustomWorldWorkCard } from './CustomWorldWorkCard'; import { type CustomWorldWorkFilter, CustomWorldWorkTabs, } from './CustomWorldWorkTabs'; // 中文注释:草稿在手机端保持双列,已发布卡片由卡片自身跨两列展示公开指标。 const WORK_GRID_CLASS = 'grid grid-cols-2 gap-2.5 sm:gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4'; const WORK_METRIC_CACHE_KEY = 'genarrative.creationHub.publishedMetrics.v1'; type WorkMetricSnapshot = Record< string, Partial> >; type CustomWorldCreationHubProps = { items: CustomWorldWorkSummary[]; loading: boolean; error: string | null; onRetry: () => void; createError?: string | null; createBusy?: boolean; entryConfig: CreationEntryConfig; creationTypes: readonly PlatformCreationTypeCard[]; onCreateType: (type: PlatformCreationTypeId) => void; onOpenDraft: (item: CustomWorldWorkSummary) => void; onEnterPublished: (profileId: string) => void; onDeletePublished?: ((item: CustomWorldWorkSummary) => void) | null; deletingWorkId?: string | null; rpgLibraryEntries?: CustomWorldLibraryEntry[]; bigFishItems?: BigFishWorkSummary[]; onOpenBigFishDetail?: (item: BigFishWorkSummary) => void; onDeleteBigFish?: ((item: BigFishWorkSummary) => void) | null; match3dItems?: Match3DWorkSummary[]; onOpenMatch3DDetail?: (item: Match3DWorkSummary) => void; onDeleteMatch3D?: ((item: Match3DWorkSummary) => void) | null; squareHoleItems?: SquareHoleWorkSummary[]; onOpenSquareHoleDetail?: (item: SquareHoleWorkSummary) => void; onDeleteSquareHole?: ((item: SquareHoleWorkSummary) => void) | null; puzzleItems?: PuzzleWorkSummary[]; onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void; onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null; onClaimPuzzlePointIncentive?: ((item: PuzzleWorkSummary) => void) | null; claimingPuzzleProfileId?: string | null; visualNovelItems?: VisualNovelWorkSummary[]; onOpenVisualNovelDetail?: ((item: VisualNovelWorkSummary) => void) | null; onDeleteVisualNovel?: ((item: VisualNovelWorkSummary) => void) | null; getWorkState?: ( item: CreationWorkShelfItem, ) => { isGenerating?: boolean; hasUnreadUpdate?: boolean } | null; onOpenShelfItem?: (item: CreationWorkShelfItem) => void; mode?: 'full' | 'start-only' | 'works-only'; }; function EmptyState({ title }: { title: string }) { return (
{title}
); } function buildWorkMetricCacheItemKey(item: CreationWorkShelfItem) { return `${item.kind}:${item.id}`; } function readWorkMetricSnapshot(): WorkMetricSnapshot { if (typeof window === 'undefined') { return {}; } try { const rawSnapshot = window.sessionStorage.getItem(WORK_METRIC_CACHE_KEY); if (!rawSnapshot) { return {}; } const parsed = JSON.parse(rawSnapshot) as WorkMetricSnapshot; return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } } function writeWorkMetricSnapshot(items: CreationWorkShelfItem[]) { if (typeof window === 'undefined') { return; } const snapshot: WorkMetricSnapshot = {}; for (const item of items) { if (item.status !== 'published' || item.metrics.length === 0) { continue; } snapshot[buildWorkMetricCacheItemKey(item)] = Object.fromEntries( item.metrics.map((metric) => [metric.id, metric.value]), ); } // 中文注释:缓存只作为下一次进入创作页的数字动画起点,真实展示值仍以接口返回为准。 if (Object.keys(snapshot).length === 0) { return; } try { window.sessionStorage.setItem( WORK_METRIC_CACHE_KEY, JSON.stringify(snapshot), ); } catch { // 中文注释:浏览器禁用 sessionStorage 时降级为无缓存动画,不影响作品列表使用。 } } export function CustomWorldCreationHub({ items, loading, error, onRetry, createError = null, createBusy = false, entryConfig, creationTypes, onCreateType, onOpenDraft, onEnterPublished, onDeletePublished = null, deletingWorkId = null, rpgLibraryEntries = [], bigFishItems = [], onOpenBigFishDetail, onDeleteBigFish = null, match3dItems = [], onOpenMatch3DDetail, onDeleteMatch3D = null, squareHoleItems = [], onOpenSquareHoleDetail, onDeleteSquareHole = null, puzzleItems = [], onOpenPuzzleDetail, onDeletePuzzle = null, onClaimPuzzlePointIncentive = null, claimingPuzzleProfileId = null, visualNovelItems = [], onOpenVisualNovelDetail = null, onDeleteVisualNovel = null, getWorkState, onOpenShelfItem, mode = 'full', }: CustomWorldCreationHubProps) { const [activeFilter, setActiveFilter] = useState('all'); const isSquareHoleCreationVisible = isPlatformCreationTypeVisible( creationTypes, 'square-hole', ); const shelfItems = useMemo( () => buildCreationWorkShelfItems({ rpgItems: items, rpgLibraryEntries, bigFishItems, match3dItems, squareHoleItems: isSquareHoleCreationVisible ? squareHoleItems : [], puzzleItems, visualNovelItems, canDeleteRpg: Boolean(onDeletePublished), canDeleteBigFish: Boolean(onDeleteBigFish), canDeleteMatch3D: Boolean(onDeleteMatch3D), canDeleteSquareHole: isSquareHoleCreationVisible && Boolean(onDeleteSquareHole), canDeletePuzzle: Boolean(onDeletePuzzle), canDeleteVisualNovel: Boolean(onDeleteVisualNovel), onOpenRpgDraft: onOpenDraft, onEnterRpgPublished: onEnterPublished, onDeleteRpg: onDeletePublished ?? undefined, onOpenBigFishDetail, onDeleteBigFish: onDeleteBigFish ?? undefined, onOpenMatch3DDetail, onDeleteMatch3D: onDeleteMatch3D ?? undefined, onOpenSquareHoleDetail, onDeleteSquareHole: onDeleteSquareHole ?? undefined, onOpenPuzzleDetail, onDeletePuzzle: onDeletePuzzle ?? undefined, onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined, onOpenVisualNovelDetail: onOpenVisualNovelDetail ?? undefined, onDeleteVisualNovel: onDeleteVisualNovel ?? undefined, getItemState: getWorkState, }), [ bigFishItems, isSquareHoleCreationVisible, items, match3dItems, onDeleteBigFish, onDeleteMatch3D, onDeleteSquareHole, onDeletePublished, onDeletePuzzle, onDeleteVisualNovel, onClaimPuzzlePointIncentive, onOpenBigFishDetail, onOpenDraft, onOpenMatch3DDetail, onOpenPuzzleDetail, onOpenSquareHoleDetail, onOpenVisualNovelDetail, onEnterPublished, getWorkState, puzzleItems, rpgLibraryEntries, squareHoleItems, visualNovelItems, ], ); const [metricSnapshot] = useState(() => readWorkMetricSnapshot(), ); useEffect(() => { writeWorkMetricSnapshot(shelfItems); }, [shelfItems]); const draftCount = shelfItems.filter( (entry) => entry.status === 'draft', ).length; const publishedCount = shelfItems.filter( (entry) => entry.status === 'published', ).length; const filteredItems = useMemo( () => shelfItems.filter((entry) => activeFilter === 'all' ? true : entry.status === activeFilter, ), [activeFilter, shelfItems], ); function buildDeleteAction(item: CreationWorkShelfItem) { if (!item.canDelete) { return null; } return item.actions.delete ?? null; } function buildPointIncentiveAction(item: CreationWorkShelfItem) { return item.actions.claimPointIncentive ?? null; } const showStartCard = mode !== 'works-only'; const showWorkShelf = mode !== 'start-only'; return (
{showStartCard ? ( ) : null} {showWorkShelf ? ( ) : null} {showWorkShelf && error ? (
{error}
) : null} {showWorkShelf ? ( loading ? (
{Array.from({ length: 3 }).map((_, index) => (
))}
) : filteredItems.length > 0 ? (
{filteredItems.map((item) => ( { onOpenShelfItem?.(item); item.actions.open(); }} onDelete={buildDeleteAction(item)} deleteBusy={deletingWorkId === item.id} onClaimPointIncentive={buildPointIncentiveAction(item)} pointIncentiveBusy={ item.source.kind === 'puzzle' && claimingPuzzleProfileId === item.source.item.profileId } /> ))}
) : shelfItems.length === 0 ? ( ) : ( ) ) : null}
); } export type { CustomWorldWorkFilter };