1
This commit is contained in:
@@ -18,8 +18,8 @@ import {
|
||||
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 { CreationAudioAsset } from '../../../packages/shared/src/contracts/creationAudio';
|
||||
import type { CreativeDraftEditResult } from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
|
||||
import type {
|
||||
PuzzleDraftLevel,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '../../services/creation-audio';
|
||||
import { updatePuzzleWork } from '../../services/puzzle-works';
|
||||
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
|
||||
import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import PuzzleHistoryAssetPickerDialog from '../puzzle-agent/PuzzleHistoryAssetPickerDialog';
|
||||
import {
|
||||
@@ -61,7 +62,8 @@ type PuzzleResultViewProps = {
|
||||
};
|
||||
|
||||
type PuzzleAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
type PuzzleResultTab = 'levels' | 'work' | 'ui' | 'music';
|
||||
type PuzzleResultTab = 'levels' | 'work' | 'assets';
|
||||
type PuzzleAssetConfigTabId = 'ui' | 'music';
|
||||
|
||||
type DraftEditState = {
|
||||
workTitle: string;
|
||||
@@ -74,12 +76,27 @@ 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_BACKGROUND_MUSIC_POINT_COST = 5;
|
||||
const PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS = 90;
|
||||
const PUZZLE_BACKGROUND_MUSIC_ASSET_KIND = 'puzzle_background_music';
|
||||
const PUZZLE_BACKGROUND_MUSIC_SLOT = 'background_music';
|
||||
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' },
|
||||
{ id: 'music', label: '背景音乐' },
|
||||
];
|
||||
|
||||
type PuzzleLevelGenerationRuntime = {
|
||||
startedAtMs: number;
|
||||
estimateSeconds: number;
|
||||
@@ -419,13 +436,8 @@ function PuzzleResultTabs({
|
||||
onChange: (tab: PuzzleResultTab) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-3 grid grid-cols-4 gap-2 rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-1">
|
||||
{[
|
||||
{ id: 'levels' as const, label: '拼图关卡' },
|
||||
{ id: 'work' as const, label: '作品信息' },
|
||||
{ id: 'ui' as const, label: 'UI' },
|
||||
{ id: 'music' as const, label: '音乐' },
|
||||
].map((tab) => (
|
||||
<div className="mb-3 grid grid-cols-3 gap-2 rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-1">
|
||||
{PUZZLE_RESULT_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
@@ -444,6 +456,34 @@ function PuzzleResultTabs({
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleAssetConfigTabs({
|
||||
activeTab,
|
||||
onChange,
|
||||
}: {
|
||||
activeTab: PuzzleAssetConfigTabId;
|
||||
onChange: (tab: PuzzleAssetConfigTabId) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-3 grid grid-cols-2 gap-2 rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-1">
|
||||
{PUZZLE_ASSET_CONFIG_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => onChange(tab.id)}
|
||||
className={`min-h-10 rounded-[0.9rem] px-3 text-sm font-bold transition ${
|
||||
activeTab === tab.id
|
||||
? 'bg-white text-[var(--platform-text-strong)] shadow-sm'
|
||||
: 'text-[var(--platform-text-base)] hover:bg-white/60'
|
||||
}`}
|
||||
aria-pressed={activeTab === tab.id}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleThemeTagEditor({
|
||||
editState,
|
||||
isBusy,
|
||||
@@ -1467,7 +1507,7 @@ function PuzzleUiAssetsTab({
|
||||
) : (
|
||||
<Wand2 className="h-4 w-4" />
|
||||
)}
|
||||
{firstLevel?.uiBackgroundImageSrc ? '重新生成' : '生成UI背景'}
|
||||
{firstLevel?.uiBackgroundImageSrc ? '重新生成' : '生成UI背景'} · {PUZZLE_IMAGE_GENERATION_POINT_COST}光点
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1543,6 +1583,7 @@ function PuzzleUiRuntimePreviewPanel({
|
||||
src={backgroundPreviewSrc}
|
||||
refreshKey={`${imageRefreshKey}:ui-runtime-preview`}
|
||||
alt=""
|
||||
data-testid="puzzle-ui-runtime-preview-background"
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
@@ -1632,6 +1673,10 @@ function PuzzleMusicTab({
|
||||
const [statusText, setStatusText] = useState<string | null>(null);
|
||||
const [errorText, setErrorText] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const { resolvedUrl: resolvedMusicSrc } = useResolvedAssetReadUrl(
|
||||
currentMusic?.audioSrc,
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
|
||||
const canGenerate = title.trim().length > 0;
|
||||
const writeMusic = (music: CreationAudioAsset) => {
|
||||
@@ -1708,12 +1753,17 @@ function PuzzleMusicTab({
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{currentMusic?.audioSrc ? (
|
||||
{currentMusic?.audioSrc && resolvedMusicSrc ? (
|
||||
<audio
|
||||
className="mt-3 w-full"
|
||||
controls
|
||||
src={currentMusic.audioSrc}
|
||||
src={resolvedMusicSrc}
|
||||
aria-label="拼图背景音乐"
|
||||
/>
|
||||
) : currentMusic?.audioSrc ? (
|
||||
<div className="mt-3 rounded-[0.9rem] border border-[var(--platform-subpanel-border)] bg-white/62 px-3 py-3 text-sm font-semibold text-[var(--platform-text-soft)]">
|
||||
音频已绑定
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 flex h-12 items-center gap-2 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/62 px-3 text-sm font-semibold text-[var(--platform-text-soft)]">
|
||||
<Music className="h-4 w-4" />
|
||||
@@ -1758,7 +1808,7 @@ function PuzzleMusicTab({
|
||||
) : (
|
||||
<Music className="h-4 w-4" />
|
||||
)}
|
||||
{currentMusic ? '重新生成音乐' : '生成音乐'}
|
||||
{currentMusic ? '重新生成音乐' : '生成音乐'} · {PUZZLE_BACKGROUND_MUSIC_POINT_COST}光点
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -1771,22 +1821,75 @@ function PuzzleMusicTab({
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleAssetConfigTab({
|
||||
activeAssetConfigTab,
|
||||
editState,
|
||||
imageRefreshKey,
|
||||
isBusy,
|
||||
profileId,
|
||||
sessionId,
|
||||
onAssetConfigTabChange,
|
||||
onChange,
|
||||
onGenerateUiBackground,
|
||||
}: {
|
||||
activeAssetConfigTab: PuzzleAssetConfigTabId;
|
||||
editState: DraftEditState;
|
||||
imageRefreshKey: string;
|
||||
isBusy: boolean;
|
||||
profileId: string | null;
|
||||
sessionId: string;
|
||||
onAssetConfigTabChange: (tab: PuzzleAssetConfigTabId) => void;
|
||||
onChange: (nextState: DraftEditState) => void;
|
||||
onGenerateUiBackground: (prompt: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-0">
|
||||
<PuzzleAssetConfigTabs
|
||||
activeTab={activeAssetConfigTab}
|
||||
onChange={onAssetConfigTabChange}
|
||||
/>
|
||||
{activeAssetConfigTab === 'ui' ? (
|
||||
<PuzzleUiAssetsTab
|
||||
editState={editState}
|
||||
imageRefreshKey={imageRefreshKey}
|
||||
isBusy={isBusy}
|
||||
onChange={onChange}
|
||||
onGenerate={onGenerateUiBackground}
|
||||
/>
|
||||
) : null}
|
||||
{activeAssetConfigTab === 'music' ? (
|
||||
<PuzzleMusicTab
|
||||
editState={editState}
|
||||
profileId={profileId}
|
||||
sessionId={sessionId}
|
||||
isBusy={isBusy}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleResultActionBar({
|
||||
actionError,
|
||||
editState,
|
||||
imageRefreshKey,
|
||||
isBusy,
|
||||
canStartTestRun,
|
||||
publishReady,
|
||||
publishBlockers,
|
||||
onPublish,
|
||||
onStartTestRun,
|
||||
}: {
|
||||
actionError: string | null;
|
||||
editState: DraftEditState;
|
||||
imageRefreshKey: string;
|
||||
isBusy: boolean;
|
||||
canStartTestRun: boolean;
|
||||
publishReady: boolean;
|
||||
publishBlockers: string[];
|
||||
onPublish: () => void;
|
||||
onStartTestRun?: () => void;
|
||||
}) {
|
||||
const [showPublishDialog, setShowPublishDialog] = useState(false);
|
||||
const [hasAttemptedPublish, setHasAttemptedPublish] = useState(false);
|
||||
@@ -1798,6 +1901,19 @@ function PuzzleResultActionBar({
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-end gap-3 pb-[max(0.25rem,env(safe-area-inset-bottom))]">
|
||||
{onStartTestRun ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStartTestRun}
|
||||
disabled={isBusy || !canStartTestRun}
|
||||
className={`platform-button platform-button--ghost ${isBusy || !canStartTestRun ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Play className="h-4 w-4" />
|
||||
试玩
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1844,6 +1960,8 @@ export function PuzzleResultView({
|
||||
}: PuzzleResultViewProps) {
|
||||
const draft = session.draft;
|
||||
const [activeTab, setActiveTab] = useState<PuzzleResultTab>('levels');
|
||||
const [activeAssetConfigTab, setActiveAssetConfigTab] =
|
||||
useState<PuzzleAssetConfigTabId>('ui');
|
||||
const [activeLevelId, setActiveLevelId] = useState<string | null>(null);
|
||||
const [editState, setEditState] = useState<DraftEditState | null>(
|
||||
draft ? createDraftEditState(draft) : null,
|
||||
@@ -2093,6 +2211,7 @@ export function PuzzleResultView({
|
||||
generationStatus: level.generationStatus,
|
||||
levels: [level],
|
||||
});
|
||||
const canStartTestRun = Boolean(onStartTestRun && primaryImageSrc);
|
||||
|
||||
return (
|
||||
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full flex-col xl:max-w-[min(100%,98rem)] xl:px-1 2xl:max-w-[min(100%,112rem)]">
|
||||
@@ -2174,13 +2293,17 @@ export function PuzzleResultView({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'ui' ? (
|
||||
<PuzzleUiAssetsTab
|
||||
{activeTab === 'assets' ? (
|
||||
<PuzzleAssetConfigTab
|
||||
activeAssetConfigTab={activeAssetConfigTab}
|
||||
editState={editState}
|
||||
imageRefreshKey={imageRefreshKey}
|
||||
isBusy={isBusy}
|
||||
profileId={profileId ?? null}
|
||||
sessionId={session.sessionId}
|
||||
onAssetConfigTabChange={setActiveAssetConfigTab}
|
||||
onChange={setEditState}
|
||||
onGenerate={(prompt) => {
|
||||
onGenerateUiBackground={(prompt) => {
|
||||
const firstLevel = editState.levels[0] ?? null;
|
||||
if (!firstLevel) {
|
||||
return;
|
||||
@@ -2207,15 +2330,6 @@ export function PuzzleResultView({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'music' ? (
|
||||
<PuzzleMusicTab
|
||||
editState={editState}
|
||||
profileId={profileId ?? null}
|
||||
sessionId={session.sessionId}
|
||||
isBusy={isBusy}
|
||||
onChange={setEditState}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
@@ -2234,8 +2348,14 @@ export function PuzzleResultView({
|
||||
editState={editState}
|
||||
imageRefreshKey={imageRefreshKey}
|
||||
isBusy={isBusy}
|
||||
canStartTestRun={canStartTestRun}
|
||||
publishReady={publishState.publishReady}
|
||||
publishBlockers={publishState.blockers}
|
||||
onStartTestRun={
|
||||
onStartTestRun
|
||||
? () => onStartTestRun(syncedDraft)
|
||||
: undefined
|
||||
}
|
||||
onPublish={() => {
|
||||
if (!publishState.publishReady) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user