Puzzle: support history images & partial generation
Allow history-generated image paths to be submitted where Data URLs were previously required and avoid treating partial/result-page generations as blocking the whole draft. Backend: resolve history /generated-* references via resolve_puzzle_reference_image_as_data_url and convert to PuzzleDownloadedImage; add PuzzleDownloadedImage::from_resolved_reference_image; extend draft handling to apply generated level metadata (auto-naming) and normalize generation_status to treat levels with images as ready. API: add shouldAutoNameLevel to action contracts and use it to request/refine generated level names. Spacetime/module and mappers: normalize completed level statuses when saving/reading so result-page background or per-level generation doesn't mask completed drafts. Frontend: expose resolver helpers, only mark a work as generating when no usable cover or ready level exists, keep level controls enabled during UI-background regeneration, and add tests covering history-image submission, auto-naming, and UI-background/partial-generation behaviors.
This commit is contained in:
@@ -93,6 +93,11 @@ type PuzzleLevelGenerationRuntime = {
|
||||
estimateSeconds: number;
|
||||
};
|
||||
|
||||
type PuzzleUiBackgroundGenerationState = {
|
||||
levelId: string;
|
||||
prompt: string;
|
||||
} | null;
|
||||
|
||||
function resolvePuzzleLevelGenerationProgress(
|
||||
level: PuzzleDraftLevel,
|
||||
runtime: PuzzleLevelGenerationRuntime | null,
|
||||
@@ -1409,16 +1414,22 @@ function PuzzleUiAssetsTab({
|
||||
editState,
|
||||
imageRefreshKey,
|
||||
isBusy,
|
||||
uiBackgroundGeneration,
|
||||
onChange,
|
||||
onGenerate,
|
||||
}: {
|
||||
editState: DraftEditState;
|
||||
imageRefreshKey: string;
|
||||
isBusy: boolean;
|
||||
uiBackgroundGeneration: PuzzleUiBackgroundGenerationState;
|
||||
onChange: (nextState: DraftEditState) => void;
|
||||
onGenerate: (prompt: string) => void;
|
||||
}) {
|
||||
const firstLevel = editState.levels[0] ?? null;
|
||||
const isGeneratingUiBackground = Boolean(
|
||||
firstLevel &&
|
||||
uiBackgroundGeneration?.levelId === firstLevel.levelId,
|
||||
);
|
||||
const formalImageSrc = firstLevel ? resolveLevelFormalImageSrc(firstLevel) : '';
|
||||
const defaultPrompt = buildDefaultPuzzleUiBackgroundPrompt(
|
||||
editState,
|
||||
@@ -1490,21 +1501,30 @@ function PuzzleUiAssetsTab({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!firstLevel || !normalizedPrompt || isBusy}
|
||||
disabled={
|
||||
!firstLevel ||
|
||||
!normalizedPrompt ||
|
||||
isBusy ||
|
||||
isGeneratingUiBackground
|
||||
}
|
||||
onClick={() => {
|
||||
if (!firstLevel || !normalizedPrompt) {
|
||||
return;
|
||||
}
|
||||
setIsCostConfirmOpen(true);
|
||||
}}
|
||||
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-4 py-3 ${!firstLevel || !normalizedPrompt || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-4 py-3 ${!firstLevel || !normalizedPrompt || isBusy || isGeneratingUiBackground ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
{isBusy ? (
|
||||
{isBusy || isGeneratingUiBackground ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="h-4 w-4" />
|
||||
)}
|
||||
{hasGeneratedUiBackground ? '重新生成' : '生成UI背景'} · {PUZZLE_IMAGE_GENERATION_POINT_COST}泥点
|
||||
{isGeneratingUiBackground
|
||||
? '生成中'
|
||||
: hasGeneratedUiBackground
|
||||
? '重新生成'
|
||||
: '生成UI背景'} · {PUZZLE_IMAGE_GENERATION_POINT_COST}泥点
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1547,7 +1567,12 @@ function PuzzleUiAssetsTab({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!firstLevel || !normalizedPrompt || isBusy}
|
||||
disabled={
|
||||
!firstLevel ||
|
||||
!normalizedPrompt ||
|
||||
isBusy ||
|
||||
isGeneratingUiBackground
|
||||
}
|
||||
onClick={() => {
|
||||
if (!firstLevel || !normalizedPrompt) {
|
||||
return;
|
||||
@@ -1559,7 +1584,7 @@ function PuzzleUiAssetsTab({
|
||||
setIsCostConfirmOpen(false);
|
||||
onGenerate(normalizedPrompt);
|
||||
}}
|
||||
className={`platform-button platform-button--primary justify-center ${!firstLevel || !normalizedPrompt || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
className={`platform-button platform-button--primary justify-center ${!firstLevel || !normalizedPrompt || isBusy || isGeneratingUiBackground ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
@@ -1696,6 +1721,7 @@ function PuzzleAssetConfigTab({
|
||||
editState,
|
||||
imageRefreshKey,
|
||||
isBusy,
|
||||
uiBackgroundGeneration,
|
||||
onAssetConfigTabChange,
|
||||
onChange,
|
||||
onGenerateUiBackground,
|
||||
@@ -1704,6 +1730,7 @@ function PuzzleAssetConfigTab({
|
||||
editState: DraftEditState;
|
||||
imageRefreshKey: string;
|
||||
isBusy: boolean;
|
||||
uiBackgroundGeneration: PuzzleUiBackgroundGenerationState;
|
||||
onAssetConfigTabChange: (tab: PuzzleAssetConfigTabId) => void;
|
||||
onChange: (nextState: DraftEditState) => void;
|
||||
onGenerateUiBackground: (prompt: string) => void;
|
||||
@@ -1719,6 +1746,7 @@ function PuzzleAssetConfigTab({
|
||||
editState={editState}
|
||||
imageRefreshKey={imageRefreshKey}
|
||||
isBusy={isBusy}
|
||||
uiBackgroundGeneration={uiBackgroundGeneration}
|
||||
onChange={onChange}
|
||||
onGenerate={onGenerateUiBackground}
|
||||
/>
|
||||
@@ -1829,6 +1857,8 @@ export function PuzzleResultView({
|
||||
const [tagGenerationError, setTagGenerationError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [uiBackgroundGeneration, setUiBackgroundGeneration] =
|
||||
useState<PuzzleUiBackgroundGenerationState>(null);
|
||||
const [generationRuntimeByLevelId, setGenerationRuntimeByLevelId] = useState<
|
||||
Record<string, PuzzleLevelGenerationRuntime>
|
||||
>({});
|
||||
@@ -1844,11 +1874,18 @@ export function PuzzleResultView({
|
||||
latestEditStateRef.current = editState;
|
||||
}, [editState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setUiBackgroundGeneration(null);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draft) {
|
||||
setEditState(null);
|
||||
latestEditStateRef.current = null;
|
||||
setActiveLevelId(null);
|
||||
setUiBackgroundGeneration(null);
|
||||
setAutoSaveState('idle');
|
||||
setAutoSaveError(null);
|
||||
setTagGenerationError(null);
|
||||
@@ -1884,6 +1921,19 @@ export function PuzzleResultView({
|
||||
setAutoSaveState('idle');
|
||||
setAutoSaveError(null);
|
||||
setTagGenerationError(null);
|
||||
setUiBackgroundGeneration((current) => {
|
||||
if (
|
||||
current &&
|
||||
mergedState.levels.some(
|
||||
(level) =>
|
||||
level.levelId === current.levelId &&
|
||||
resolvePuzzleUiBackgroundSource(level),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}, [draft]);
|
||||
|
||||
const syncedDraft = useMemo(() => {
|
||||
@@ -2163,6 +2213,7 @@ export function PuzzleResultView({
|
||||
editState={editState}
|
||||
imageRefreshKey={imageRefreshKey}
|
||||
isBusy={isBusy}
|
||||
uiBackgroundGeneration={uiBackgroundGeneration}
|
||||
onAssetConfigTabChange={setActiveAssetConfigTab}
|
||||
onChange={setEditState}
|
||||
onGenerateUiBackground={(prompt) => {
|
||||
@@ -2170,6 +2221,10 @@ export function PuzzleResultView({
|
||||
if (!firstLevel) {
|
||||
return;
|
||||
}
|
||||
setUiBackgroundGeneration({
|
||||
levelId: firstLevel.levelId,
|
||||
prompt,
|
||||
});
|
||||
onExecuteAction({
|
||||
action: 'generate_puzzle_ui_background',
|
||||
levelId: firstLevel.levelId,
|
||||
@@ -2256,6 +2311,7 @@ export function PuzzleResultView({
|
||||
imageModel: imageModel ?? PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aiRedraw: true,
|
||||
candidateCount: 1,
|
||||
shouldAutoNameLevel: !nextLevel.levelName.trim(),
|
||||
workTitle: editState.workTitle.trim(),
|
||||
workDescription: editState.workDescription.trim(),
|
||||
summary: editState.workDescription.trim(),
|
||||
|
||||
Reference in New Issue
Block a user