import { ArrowLeft, CheckCircle2, Eye, History, ImagePlus, LayoutTemplate, Loader2, MessageSquareText, Play, Plus, Sparkles, Trash2, Wand2, X, } from 'lucide-react'; import { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import type { CreativeDraftEditResult } from '../../../packages/shared/src/contracts/creativeAgent'; import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions'; import type { PuzzleDraftLevel, PuzzleResultDraft, } from '../../../packages/shared/src/contracts/puzzleAgentDraft'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import { updatePuzzleWork } from '../../services/puzzle-works'; import { resolvePuzzleUiBackgroundSource } from '../../services/puzzle-runtime/puzzleUiBackgroundSource'; import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage'; import { useAuthUi } from '../auth/AuthUiContext'; import PuzzleHistoryAssetPickerDialog from '../puzzle-agent/PuzzleHistoryAssetPickerDialog'; import { PUZZLE_IMAGE_MODEL_GPT_IMAGE_2, type PuzzleImageModelId, } from '../puzzle-agent/puzzleImageModelOptions'; import { PuzzleImageModelPicker } from '../puzzle-agent/PuzzleImageModelPicker'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; type PuzzleResultViewProps = { session: PuzzleAgentSessionSnapshot; profileId?: string | null; isBusy?: boolean; error?: string | null; onBack: () => void; onExecuteAction: (payload: PuzzleAgentActionRequest) => void; onStartTestRun?: (draft: PuzzleResultDraft) => void; creativeDraftEdit?: { isBusy: boolean; error: string | null; onSubmit: (payload: { instruction: string; currentDraft: PuzzleResultDraft; }) => Promise | void; } | null; }; type PuzzleAutoSaveState = 'idle' | 'saving' | 'saved' | 'error'; type PuzzleResultTab = 'levels' | 'work' | 'assets'; type PuzzleAssetConfigTabId = 'ui'; type DraftEditState = { workTitle: string; workDescription: string; themeTags: string[]; levels: PuzzleDraftLevel[]; }; const PUZZLE_MIN_THEME_TAG_COUNT = 3; const PUZZLE_MAX_THEME_TAG_COUNT = 6; const PUZZLE_AUTOSAVE_DEBOUNCE_MS = 600; const PUZZLE_IMAGE_GENERATION_POINT_COST = 2; const PUZZLE_PUBLISH_POINT_COST = 1; const PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS = 90; const PUZZLE_UI_BACKGROUND_REFERENCE_SRC = '/ui-previews/puzzle-image-compact-ui-2026-05-08.png'; const PUZZLE_RESULT_TABS: Array<{ id: PuzzleResultTab; label: string }> = [ { id: 'levels', label: '拼图关卡' }, { id: 'work', label: '作品信息' }, { id: 'assets', label: '素材配置' }, ]; const PUZZLE_ASSET_CONFIG_TABS: Array<{ id: PuzzleAssetConfigTabId; label: string; }> = [ { id: 'ui', label: 'UI' }, ]; type PuzzleLevelGenerationRuntime = { startedAtMs: number; estimateSeconds: number; }; function resolvePuzzleLevelGenerationProgress( level: PuzzleDraftLevel, runtime: PuzzleLevelGenerationRuntime | null, nowMs: number, ) { if (level.generationStatus !== 'generating') { return { isGenerating: false, progressPercent: 0, secondsLeft: 0, }; } const estimateSeconds = runtime?.estimateSeconds ?? PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS; const elapsedSeconds = runtime ? Math.max(0, Math.floor((nowMs - runtime.startedAtMs) / 1000)) : 0; const secondsLeft = Math.max(0, estimateSeconds - elapsedSeconds); const progressPercent = Math.min( 96, Math.max( 6, Math.round(((estimateSeconds - secondsLeft) / estimateSeconds) * 100), ), ); return { isGenerating: true, progressPercent, secondsLeft, }; } function normalizeThemeTagInput(value: string) { return [ ...new Set( value .split(/[\n,,、]/u) .map((entry) => entry.trim()) .filter(Boolean), ), ]; } function buildDefaultPuzzleUiBackgroundPrompt( editState: DraftEditState, level: PuzzleDraftLevel | null, ) { const tags = editState.themeTags .map((tag) => tag.trim()) .filter(Boolean) .join(','); return [ editState.workTitle.trim(), editState.workDescription.trim(), level?.levelName.trim(), level?.pictureDescription.trim(), tags, '移动端拼图游戏 UI 背景,中央正方形拼图区边界清晰,拼图区外氛围与作品名称一致', ] .filter(Boolean) .join('。'); } function resolveLevelFormalImageSrc(level: PuzzleDraftLevel) { const selectedCandidate = level.candidates.find( (candidate) => candidate.selected || (level.selectedCandidateId ? candidate.candidateId === level.selectedCandidateId : false), ) ?? level.candidates[level.candidates.length - 1] ?? null; return ( selectedCandidate?.imageSrc?.trim() || level.coverImageSrc?.trim() || '' ); } function buildFallbackLevelFromDraft( draft: PuzzleResultDraft, ): PuzzleDraftLevel { return { levelId: 'puzzle-level-1', levelName: draft.levelName || '', pictureDescription: draft.summary, candidates: draft.candidates, selectedCandidateId: draft.selectedCandidateId, coverImageSrc: draft.coverImageSrc, coverAssetId: draft.coverAssetId, generationStatus: draft.generationStatus, }; } function normalizeDraftLevels(draft: PuzzleResultDraft) { const sourceLevels = draft.levels && draft.levels.length > 0 ? draft.levels : [buildFallbackLevelFromDraft(draft)]; return sourceLevels.map((level, index) => ({ ...level, levelId: level.levelId?.trim() || `puzzle-level-${index + 1}`, levelName: level.levelName?.trim() || '', pictureDescription: level.pictureDescription?.trim() || draft.summary, pictureReference: level.pictureReference ?? null, uiBackgroundPrompt: level.uiBackgroundPrompt ?? null, uiBackgroundImageSrc: level.uiBackgroundImageSrc ?? null, uiBackgroundImageObjectKey: level.uiBackgroundImageObjectKey ?? null, candidates: level.candidates ?? [], selectedCandidateId: level.selectedCandidateId ?? null, coverImageSrc: level.coverImageSrc ?? null, coverAssetId: level.coverAssetId ?? null, backgroundMusic: level.backgroundMusic ?? null, generationStatus: level.generationStatus || 'idle', })); } function syncDraftFromEditState( draft: PuzzleResultDraft, editState: DraftEditState, ): PuzzleResultDraft { const levels = editState.levels; const primaryLevel = levels[0] ?? buildFallbackLevelFromDraft(draft); return { ...draft, workTitle: editState.workTitle.trim(), workDescription: editState.workDescription.trim(), levelName: primaryLevel.levelName, summary: editState.workDescription.trim(), themeTags: editState.themeTags, candidates: primaryLevel.candidates, selectedCandidateId: primaryLevel.selectedCandidateId, coverImageSrc: primaryLevel.coverImageSrc, coverAssetId: primaryLevel.coverAssetId, generationStatus: primaryLevel.generationStatus, levels, }; } function createDraftEditState(draft: PuzzleResultDraft): DraftEditState { return { workTitle: draft.workTitle ?? '', workDescription: draft.workDescription ?? '', themeTags: normalizeThemeTagInput(draft.themeTags.join(',')), levels: normalizeDraftLevels(draft), }; } function mergeDraftEditStateWithIncomingState( currentState: DraftEditState | null, incomingState: DraftEditState, ): DraftEditState { if (!currentState) { return incomingState; } const incomingLevelsById = new Map( incomingState.levels.map((level) => [level.levelId, level]), ); const shouldPreserveLocalEdits = currentState.levels.some((level) => { const incomingLevel = incomingLevelsById.get(level.levelId); return ( level.generationStatus === 'generating' && Boolean(incomingLevel) && incomingLevel?.generationStatus !== 'generating' ); }); if (!shouldPreserveLocalEdits) { return incomingState; } const mergedLevels = currentState.levels.map((level) => { const incomingLevel = incomingLevelsById.get(level.levelId); if ( !incomingLevel || level.generationStatus !== 'generating' || incomingLevel.generationStatus === 'generating' ) { return level; } return { ...level, candidates: incomingLevel.candidates, selectedCandidateId: incomingLevel.selectedCandidateId, coverImageSrc: incomingLevel.coverImageSrc, coverAssetId: incomingLevel.coverAssetId, pictureReference: incomingLevel.pictureReference ?? level.pictureReference, uiBackgroundPrompt: incomingLevel.uiBackgroundPrompt ?? level.uiBackgroundPrompt, uiBackgroundImageSrc: incomingLevel.uiBackgroundImageSrc ?? level.uiBackgroundImageSrc, uiBackgroundImageObjectKey: incomingLevel.uiBackgroundImageObjectKey ?? level.uiBackgroundImageObjectKey, backgroundMusic: incomingLevel.backgroundMusic ?? level.backgroundMusic, generationStatus: incomingLevel.generationStatus || 'ready', }; }); const mergedLevelIds = new Set(mergedLevels.map((level) => level.levelId)); const appendedIncomingLevels = incomingState.levels.filter( (level) => !mergedLevelIds.has(level.levelId), ); return { ...currentState, levels: [...mergedLevels, ...appendedIncomingLevels], }; } function createBlankPuzzleLevel( existingLevels: PuzzleDraftLevel[], ): PuzzleDraftLevel { const nextIndex = existingLevels.length + 1; return { levelId: `puzzle-level-${Date.now()}-${nextIndex}`, levelName: '', pictureDescription: '', pictureReference: null, uiBackgroundPrompt: null, uiBackgroundImageSrc: null, uiBackgroundImageObjectKey: null, candidates: [], selectedCandidateId: null, coverImageSrc: null, coverAssetId: null, backgroundMusic: null, generationStatus: 'idle', }; } function buildPublishReady( session: PuzzleAgentSessionSnapshot, editState: DraftEditState, ) { const preservedBlockers = session.resultPreview?.blockers .filter( (entry) => ![ 'MISSING_LEVEL_NAME', 'INVALID_TAG_COUNT', 'MISSING_COVER_IMAGE', ].includes(entry.code), ) .map((entry) => entry.message) ?? []; const levels = editState.levels; const hasGeneratingLevel = levels.some( (level) => level.generationStatus === 'generating', ); const blockers = [ ...(session.resultPreview ? [] : ['等待结果页草稿完成后再发布。']), ...preservedBlockers, ...(editState.workTitle.trim() ? [] : ['作品名称不能为空。']), ...(editState.workDescription.trim() ? [] : ['作品描述不能为空。']), ...(editState.themeTags.length >= PUZZLE_MIN_THEME_TAG_COUNT && editState.themeTags.length <= PUZZLE_MAX_THEME_TAG_COUNT ? [] : [ `正式标签数量必须在 ${PUZZLE_MIN_THEME_TAG_COUNT} 到 ${PUZZLE_MAX_THEME_TAG_COUNT} 之间。`, ]), ...(levels.length > 0 ? [] : ['至少需要一个拼图关卡。']), ...levels.flatMap((level, index) => [ ...(level.levelName.trim() ? [] : [`第${index + 1}关名称不能为空。`]), ...(resolveLevelFormalImageSrc(level) ? [] : [`第${index + 1}关缺少正式图。`]), ...(level.generationStatus === 'generating' ? [`第${index + 1}关画面正在生成。`] : []), ]), ...(hasGeneratingLevel ? ['还有关卡画面正在生成。'] : []), ]; return { blockers: [...new Set(blockers.filter(Boolean))], publishReady: blockers.filter(Boolean).length === 0, }; } function PuzzleResultHeader({ autoSaveState, isBusy, onBack, }: { autoSaveState: PuzzleAutoSaveState; isBusy: boolean; onBack: () => void; }) { const autoSaveBadge = autoSaveState === 'saving' ? (
保存中
) : autoSaveState === 'saved' ? (
已自动保存
) : autoSaveState === 'error' ? (
保存失败
) : null; return (
{autoSaveBadge}
); } function PuzzleResultTabs({ activeTab, onChange, }: { activeTab: PuzzleResultTab; onChange: (tab: PuzzleResultTab) => void; }) { return (
{PUZZLE_RESULT_TABS.map((tab) => ( ))}
); } function PuzzleAssetConfigTabs({ activeTab, onChange, }: { activeTab: PuzzleAssetConfigTabId; onChange: (tab: PuzzleAssetConfigTabId) => void; }) { return (
{PUZZLE_ASSET_CONFIG_TABS.map((tab) => ( ))}
); } function PuzzleThemeTagEditor({ editState, isBusy, error, onChange, onGenerateTags, }: { editState: DraftEditState; isBusy: boolean; error: string | null; onChange: (nextState: DraftEditState) => void; onGenerateTags: () => void; }) { const [newTagText, setNewTagText] = useState(''); const [isAddingTag, setIsAddingTag] = useState(false); const addTags = () => { const nextTags = normalizeThemeTagInput(newTagText); if (nextTags.length <= 0) { setIsAddingTag(false); setNewTagText(''); return; } onChange({ ...editState, themeTags: [...new Set([...editState.themeTags, ...nextTags])], }); setNewTagText(''); setIsAddingTag(false); }; return (
作品标签
{!isAddingTag ? ( ) : null}
{editState.themeTags.map((tag) => ( {tag} ))} {editState.themeTags.length <= 0 ? ( 暂无标签 ) : null}
{isAddingTag ? (
setNewTagText(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); addTags(); } if (event.key === 'Escape') { setIsAddingTag(false); setNewTagText(''); } }} className="min-h-10 flex-1 rounded-[0.8rem] border border-transparent bg-white/92 px-3 text-sm text-[var(--platform-text-strong)] outline-none" placeholder="输入新标签" aria-label="新题材标签" />
) : null} {error ? (
{error}
) : null}
); } function PuzzleLevelDetailDialog({ draft, generationNowMs, generationRuntime, imageRefreshKey, isBusy, level, onClose, onGenerate, onLevelChange, onStartTestRun, }: { draft: PuzzleResultDraft; generationNowMs: number; generationRuntime: PuzzleLevelGenerationRuntime | null; imageRefreshKey: string; isBusy: boolean; level: PuzzleDraftLevel; onClose: () => void; onGenerate: ( level: PuzzleDraftLevel, promptText?: string | null, referenceImageSrc?: string | null, imageModel?: PuzzleImageModelId | null, ) => void; onLevelChange: (nextLevel: PuzzleDraftLevel) => void; onStartTestRun?: (level: PuzzleDraftLevel) => void; }) { const platformTheme = useAuthUi()?.platformTheme ?? 'light'; const [referenceImageSrc, setReferenceImageSrc] = useState(''); const [referenceImageLabel, setReferenceImageLabel] = useState(''); const [referenceImageError, setReferenceImageError] = useState( null, ); const [isHistoryPickerOpen, setIsHistoryPickerOpen] = useState(false); const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false); const [imageModel, setImageModel] = useState( PUZZLE_IMAGE_MODEL_GPT_IMAGE_2, ); const formalImageSrc = resolveLevelFormalImageSrc(level); const hasFormalImage = Boolean(formalImageSrc); const effectiveReferenceImageSrc = referenceImageSrc.trim() || level.pictureReference?.trim() || ''; const displayImageSrc = formalImageSrc || effectiveReferenceImageSrc; const displayImageAlt = formalImageSrc ? level.levelName || draft.workTitle || '拼图关卡' : '拼图参考图'; const generationProgress = resolvePuzzleLevelGenerationProgress( level, generationRuntime, generationNowMs, ); const handleReferenceImageChange = async ( event: ChangeEvent, ) => { const file = event.target.files?.[0]; event.currentTarget.value = ''; if (!file) { return; } try { const dataUrl = await readPuzzleReferenceImageAsDataUrl(file); setReferenceImageSrc(dataUrl); setReferenceImageLabel(file.name.trim() || '本地参考图'); setReferenceImageError(null); } catch (uploadError) { setReferenceImageError( uploadError instanceof Error ? uploadError.message : '参考图读取失败,请重试。', ); } }; const executeGeneration = () => { const nextLevel = { ...level, generationStatus: 'generating' as const, }; setIsCostConfirmOpen(false); onGenerate( nextLevel, nextLevel.pictureDescription.trim() || undefined, effectiveReferenceImageSrc || undefined, imageModel, ); }; if (typeof document === 'undefined') { return null; } return createPortal(
{ if (event.target === event.currentTarget) { onClose(); } }} >
event.stopPropagation()} >
{level.levelName || '关卡详情'}
关卡名称
onLevelChange({ ...level, levelName: event.target.value }) } className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none" aria-label="关卡名称" />
画面图
{ void handleReferenceImageChange(event); }} className="sr-only" /> {displayImageSrc ? ( ) : ( )} {generationProgress.isGenerating ? (
生成中
) : null}
{effectiveReferenceImageSrc ? (
{referenceImageLabel || '已选择参考图'}
) : null} {referenceImageError ? (
{referenceImageError}
) : null}
画面描述