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:
2026-05-19 10:02:13 +08:00
parent 5e03b3d2f2
commit 7b37271f17
16 changed files with 653 additions and 73 deletions

View File

@@ -96,6 +96,7 @@ pub(super) fn map_puzzle_form_draft_response(
pub(super) fn map_puzzle_draft_level_response(
level: PuzzleDraftLevelRecord,
) -> PuzzleDraftLevelResponse {
let generation_status = resolve_puzzle_level_generation_status(&level);
PuzzleDraftLevelResponse {
level_id: level.level_id,
level_name: level.level_name,
@@ -115,7 +116,7 @@ pub(super) fn map_puzzle_draft_level_response(
selected_candidate_id: level.selected_candidate_id,
cover_image_src: level.cover_image_src,
cover_asset_id: level.cover_asset_id,
generation_status: level.generation_status,
generation_status,
}
}
@@ -279,23 +280,66 @@ pub(super) fn map_puzzle_result_preview_finding_response(
}
fn resolve_puzzle_work_generation_status(item: &PuzzleWorkProfileRecord) -> Option<String> {
let has_viewable_result = item
.cover_image_src
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty())
|| item.levels.iter().any(has_puzzle_level_image);
if has_viewable_result {
return Some("ready".to_string());
}
item.levels
.iter()
.map(|level| level.generation_status.trim())
.find(|status| *status == "generating")
.map(resolve_puzzle_level_generation_status)
.find(|status| status.as_str() == "generating")
.or_else(|| {
item.levels
.iter()
.map(|level| level.generation_status.trim())
.find(|status| *status == "ready")
.map(resolve_puzzle_level_generation_status)
.find(|status| status.as_str() == "ready")
})
.or_else(|| {
item.levels
.iter()
.map(|level| level.generation_status.trim())
.map(resolve_puzzle_level_generation_status)
.find(|status| !status.is_empty())
})
.map(str::to_string)
}
fn resolve_puzzle_level_generation_status(level: &PuzzleDraftLevelRecord) -> String {
if level.generation_status.trim() == "generating" && has_puzzle_level_image(level) {
return "ready".to_string();
}
level.generation_status.trim().to_string()
}
fn has_puzzle_level_image(level: &PuzzleDraftLevelRecord) -> bool {
let has_cover = level
.cover_image_src
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty());
let has_selected_candidate = level
.selected_candidate_id
.as_deref()
.and_then(|candidate_id| {
level
.candidates
.iter()
.find(|candidate| candidate.candidate_id == candidate_id)
})
.map(|candidate| candidate.image_src.trim())
.is_some_and(|value| !value.is_empty());
let has_fallback_candidate = level
.candidates
.last()
.map(|candidate| candidate.image_src.trim())
.is_some_and(|value| !value.is_empty());
has_cover || has_selected_candidate || has_fallback_candidate
}
pub(super) fn map_puzzle_work_summary_response(
@@ -338,7 +382,11 @@ pub(super) fn map_puzzle_work_summary_response(
.saturating_sub(item.point_incentive_claimed_points),
publish_ready: item.publish_ready,
generation_status,
levels: item.levels.iter().map(|x|map_puzzle_draft_level_response(x.clone())).collect(),
levels: item
.levels
.iter()
.map(|x| map_puzzle_draft_level_response(x.clone()))
.collect(),
}
}
@@ -603,4 +651,4 @@ pub(super) fn build_puzzle_welcome_text(seed_text: &str) -> String {
}
"拼图创作信息已准备好。".to_string()
}
}