import { ArrowLeft, CheckCircle2, ImagePlus, Images, Loader2, Play, Plus, Sparkles, Trash2, X, } from 'lucide-react'; import { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; 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 { puzzleAssetClient, type PuzzleHistoryAsset, } from '../../services/puzzle-works/puzzleAssetClient'; import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage'; import { useAuthUi } from '../auth/AuthUiContext'; 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; }; type PuzzleAutoSaveState = 'idle' | 'saving' | 'saved' | 'error'; type PuzzleResultTab = 'levels' | 'work'; 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; function normalizeThemeTagInput(value: string) { return [ ...new Set( value .split(/[\n,,、]/u) .map((entry) => entry.trim()) .filter(Boolean), ), ]; } 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, candidates: level.candidates ?? [], selectedCandidateId: level.selectedCandidateId ?? null, coverImageSrc: level.coverImageSrc ?? null, coverAssetId: level.coverAssetId ?? 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() || draft.workTitle, workDescription: editState.workDescription.trim(), levelName: primaryLevel.levelName, summary: primaryLevel.pictureDescription, 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 || draft.levelName, workDescription: draft.workDescription || '', themeTags: normalizeThemeTagInput(draft.themeTags.join(',')), levels: normalizeDraftLevels(draft), }; } function createBlankPuzzleLevel(existingLevels: PuzzleDraftLevel[]): PuzzleDraftLevel { const nextIndex = existingLevels.length + 1; return { levelId: `puzzle-level-${Date.now()}-${nextIndex}`, levelName: '', pictureDescription: '', candidates: [], selectedCandidateId: null, coverImageSrc: null, coverAssetId: null, generationStatus: 'idle', }; } function formatHistoryAssetDate(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value || ''; } return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } 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 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}关缺少正式图。`]), ]), ]; return { blockers: [...new Set(blockers.filter(Boolean))], publishReady: Boolean(session.resultPreview?.publishReady) && Boolean(editState.workTitle.trim()) && Boolean(editState.workDescription.trim()) && editState.themeTags.length >= PUZZLE_MIN_THEME_TAG_COUNT && editState.themeTags.length <= PUZZLE_MAX_THEME_TAG_COUNT && levels.length > 0 && levels.every( (level) => level.levelName.trim() && resolveLevelFormalImageSrc(level), ), }; } 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 (
{[ { id: 'levels' as const, label: '拼图关卡' }, { id: 'work' as const, label: '作品信息' }, ].map((tab) => ( ))}
); } function PuzzleThemeTagEditor({ editState, isBusy, onChange, }: { editState: DraftEditState; isBusy: boolean; onChange: (nextState: DraftEditState) => 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}
); } function PuzzleHistoryAssetPickerDialog({ isBusy, onClose, onSelect, }: { isBusy: boolean; onClose: () => void; onSelect: (asset: PuzzleHistoryAsset) => void; }) { const platformTheme = useAuthUi()?.platformTheme ?? 'light'; const [assets, setAssets] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setIsLoading(true); setError(null); puzzleAssetClient .listHistoryAssets({ limit: 120 }) .then((nextAssets) => { if (!cancelled) { setAssets(nextAssets); } }) .catch((loadError) => { if (!cancelled) { setError( loadError instanceof Error ? loadError.message : '历史拼图素材读取失败。', ); } }) .finally(() => { if (!cancelled) { setIsLoading(false); } }); return () => { cancelled = true; }; }, []); if (typeof document === 'undefined') { return null; } return createPortal(
{ if (event.target === event.currentTarget) { onClose(); } }} >
event.stopPropagation()} >
选择历史拼图素材
{error ? (
{error}
) : null} {isLoading ? (
读取中...
) : null} {!isLoading && !error && assets.length <= 0 ? (
暂无历史拼图素材
) : null} {!isLoading && assets.length > 0 ? (
{assets.map((asset) => ( ))}
) : null}
, document.body, ); } function PuzzleLevelDetailDialog({ draft, imageRefreshKey, isBusy, level, onClose, onGenerate, onLevelChange, onStartTestRun, }: { draft: PuzzleResultDraft; imageRefreshKey: string; isBusy: boolean; level: PuzzleDraftLevel; onClose: () => void; onGenerate: ( levelId: string, promptText?: string | null, referenceImageSrc?: string | 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 formalImageSrc = resolveLevelFormalImageSrc(level); const hasFormalImage = Boolean(formalImageSrc); 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 : '参考图读取失败,请重试。', ); } }; 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="关卡名称" />
{hasFormalImage ? (
画面图
) : null}
画面描述