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

@@ -253,6 +253,7 @@ fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() {
image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()),
ai_redraw: None,
candidate_count: Some(1),
should_auto_name_level: None,
candidate_id: None,
level_id: Some("puzzle-level-1".to_string()),
work_title: Some("暖灯猫街作品".to_string()),
@@ -376,6 +377,21 @@ fn puzzle_level_name_image_data_url_downsizes_generated_image() {
assert!(data_url.len() > "data:image/png;base64,".len());
}
#[test]
fn puzzle_uploaded_cover_can_reuse_resolved_history_image() {
let resolved = PuzzleResolvedReferenceImage {
mime_type: "image/png".to_string(),
bytes_len: 8,
bytes: b"pngbytes".to_vec(),
};
let downloaded = PuzzleDownloadedImage::from_resolved_reference_image(resolved);
assert_eq!(downloaded.extension, "png");
assert_eq!(downloaded.mime_type, "image/png");
assert_eq!(downloaded.bytes, b"pngbytes");
}
#[test]
fn puzzle_first_level_name_snapshot_defaults_work_title() {
let levels_json = serde_json::to_string(&vec![json!({
@@ -397,6 +413,7 @@ fn puzzle_first_level_name_snapshot_defaults_work_title() {
image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()),
ai_redraw: None,
candidate_count: Some(1),
should_auto_name_level: None,
candidate_id: None,
level_id: Some("puzzle-level-1".to_string()),
work_title: Some("猫画面".to_string()),
@@ -619,7 +636,7 @@ fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() {
selected_candidate_id: Some("candidate-1".to_string()),
cover_image_src: Some("/generated-puzzle-assets/session/cover.png".to_string()),
cover_asset_id: Some("asset-1".to_string()),
generation_status: "ready".to_string(),
generation_status: "generating".to_string(),
};
let response = map_puzzle_work_summary_response(
@@ -654,6 +671,7 @@ fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() {
assert_eq!(response.levels.len(), 1);
assert_eq!(response.generation_status.as_deref(), Some("ready"));
assert_eq!(response.levels[0].generation_status, "ready");
assert_eq!(
response.levels[0].cover_image_src.as_deref(),
Some("/generated-puzzle-assets/session/cover.png")