From 24f2120cb208fc42c241a23494c8cc285b4d15e7 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Tue, 25 Aug 2026 18:55:51 +0000 Subject: [PATCH] Fix onboarding import wizard sequencing --- PlotLine.Tests/Program.cs | 12 ++++-- PlotLine/Controllers/OnboardingController.cs | 21 ++++++++++- PlotLine/Hubs/WordCompanionFollowHub.cs | 2 +- PlotLine/Services/OnboardingService.cs | 12 +++++- .../OnboardingStoryIntelligenceService.cs | 6 +-- PlotLine/ViewModels/OnboardingViewModels.cs | 6 +-- .../Views/Onboarding/BuildComplete.cshtml | 10 +---- PlotLine/Views/Onboarding/Index.cshtml | 37 ++++++++++++++----- PlotLine/Views/Onboarding/ScanReview.cshtml | 10 ++--- .../_OnboardingJourneyHeader.cshtml | 9 +++-- .../wwwroot/js/word-companion-presence.js | 1 + 11 files changed, 85 insertions(+), 41 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 2aac677..a11f20d 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1589,7 +1589,12 @@ static void CoreImportWizardOrdersScenesBeforeCharacters() Assert(review.Contains("value=\"structure\"", StringComparison.Ordinal), "Chapter review should submit to structural import, not character review."); Assert(controller.Contains("var readyToImport = string.Equals(intent, \"structure\"", StringComparison.Ordinal), "Controller should treat structure intent as the scene-creation gate."); + Assert(controller.Contains("Import the approved chapters and scenes before reviewing characters.", StringComparison.Ordinal), "Character review should be guarded until structural import exists."); + Assert(controller.Contains("continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport", StringComparison.Ordinal), "Structure-save errors should remain on chapter review rather than jumping to characters."); Assert(controller.Contains("FinaliseCharacterReviewAsync", StringComparison.Ordinal), "Character review should have a distinct post-structure finalisation path."); + Assert(onboarding.Contains("ValidateScanStructure(review, preview);\n review = new ManuscriptScanReviewDecision", StringComparison.Ordinal), "Saving chapter review should validate only structure before scene import."); + Assert(onboarding.Contains("ValidateScanStructure(review, preview);\n\n var chapterDecisions", StringComparison.Ordinal), "Building approved structure should not validate unresolved character decisions."); + Assert(onboarding.Contains("ValidateScanReview(new ManuscriptScanReviewDecision", StringComparison.Ordinal), "Character validation should run when finalising character review."); Assert(onboarding.Contains("Import the approved chapters and scenes before reviewing characters.", StringComparison.Ordinal), "Character finalisation should refuse to run before structural scenes exist."); Assert(complete.Contains("Review characters", StringComparison.Ordinal), "After Word sync, the next onboarding step should be character review."); Assert(complete.Contains("Word may now be closed; keep this browser workflow open", StringComparison.Ordinal), "Word safe-close must be distinct from browser/core-import completion."); @@ -1889,10 +1894,11 @@ static void OnboardingWizardKeepsCharacterPagesInsideOneMainStep() var storyService = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/OnboardingStoryIntelligenceService.cs")); Assert(!header.Contains("BuildDynamicSteps", StringComparison.Ordinal), "Onboarding header should not create one top-level step per character page."); - Assert(header.Contains("new(\"Review Characters\", 8)", StringComparison.Ordinal), "Onboarding header should keep character review as one main step."); - Assert(review.Contains("var currentStep = isCharacterStep ? 8 : 7;", StringComparison.Ordinal), "Scan review should keep every character page on the same main wizard step."); + Assert(header.Contains("new(\"Import Scenes\", 8)", StringComparison.Ordinal), "Onboarding header should include scene import as a distinct main step."); + Assert(header.Contains("new(\"Review Characters\", 9)", StringComparison.Ordinal), "Onboarding header should keep character review as one main step."); + Assert(review.Contains("var currentStep = isCharacterStep ? 9 : 7;", StringComparison.Ordinal), "Scan review should keep every character page on the same main wizard step."); Assert(review.Contains("onboarding-subprogress", StringComparison.Ordinal), "Character pagination should be shown as section-level progress."); - Assert(storyService.Contains("const int totalWizardSteps = 11;", StringComparison.Ordinal), "Story Intelligence summary/context should use the fixed wizard step count."); + Assert(storyService.Contains("const int totalWizardSteps = 12;", StringComparison.Ordinal), "Story Intelligence summary/context should use the fixed wizard step count."); } static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel() diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index 138a890..a14109a 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -60,6 +60,14 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(Index)); } + var build = await onboarding.GetBuildResultAsync(model.PreviewID); + if (build is null || build.SceneMappings.Count == 0) + { + TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing characters."; + await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview); + return RedirectToAction(nameof(Index)); + } + ViewData["ScanReviewStep"] = "characters"; ViewData["CharacterReviewPage"] = Math.Max(1, page); return View("ScanReview", model); @@ -622,6 +630,17 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return NotFound(); } + if (continueToCharacters) + { + var build = await onboarding.GetBuildResultAsync(form.PreviewID); + if (build is null || build.SceneMappings.Count == 0) + { + TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing characters."; + await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview); + return RedirectToAction(nameof(Index)); + } + } + TempData["OnboardingReviewMessage"] = readyToImport ? "Chapter review saved. Next: import scenes and update Word." : continueToCharacters ? "Chapter and scene review saved. Next: review detected characters." @@ -642,7 +661,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return NotFound(); } - if (readyToImport || continueCharacterReview || backCharacterReview) + if (continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport) { ViewData["ScanReviewStep"] = "characters"; ViewData["CharacterReviewPage"] = readyToImport diff --git a/PlotLine/Hubs/WordCompanionFollowHub.cs b/PlotLine/Hubs/WordCompanionFollowHub.cs index a5e1753..6347f95 100644 --- a/PlotLine/Hubs/WordCompanionFollowHub.cs +++ b/PlotLine/Hubs/WordCompanionFollowHub.cs @@ -253,7 +253,7 @@ public sealed class WordCompanionFollowHub( await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress { Status = "Complete", - Message = "Manuscript structure has been created and saved. Word and this browser page may now be closed.", + Message = "Manuscript structure has been created and saved. Word may now be closed; keep this browser workflow open to review characters.", PercentComplete = 100 }); await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildMarkerSyncCompleted", new { buildId }); diff --git a/PlotLine/Services/OnboardingService.cs b/PlotLine/Services/OnboardingService.cs index d9694e8..4e4a792 100644 --- a/PlotLine/Services/OnboardingService.cs +++ b/PlotLine/Services/OnboardingService.cs @@ -202,7 +202,7 @@ public sealed class OnboardingService( if (readyToImport) { await scanPreviews.SaveReviewAsync(userId, review); - ValidateScanReview(review, preview); + ValidateScanStructure(review, preview); review = new ManuscriptScanReviewDecision { PreviewID = review.PreviewID, @@ -236,7 +236,7 @@ public sealed class OnboardingService( } await PublishBuildProgress(userId, preview.PreviewID, "Creating chapters...", 10, progress); - ValidateScanReview(review, preview); + ValidateScanStructure(review, preview); var chapterDecisions = review.Chapters.Where(chapter => chapter.Include).OrderBy(chapter => chapter.ChapterNumber).ToList(); @@ -382,6 +382,14 @@ public sealed class OnboardingService( }; }) .ToList(); + ValidateScanReview(new ManuscriptScanReviewDecision + { + PreviewID = review.PreviewID, + Status = review.Status, + Chapters = review.Chapters, + Scenes = review.Scenes, + Characters = characterDecisions + }, preview); var characterMappings = new List(); foreach (var decision in characterDecisions.Where(IsIncludedCanonicalCharacter).OrderBy(item => item.Name).ThenBy(item => item.TemporaryCharacterKey)) diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs index ff92b7b..445e931 100644 --- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs +++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs @@ -71,9 +71,9 @@ public sealed class OnboardingStoryIntelligenceService( var included = IncludedChapters(preview, review); var existingBatch = await batchStore.GetLatestForPreviewAsync(userId, preview.PreviewID); var missingTextCount = included.Count(chapter => string.IsNullOrWhiteSpace(chapter.Preview.ChapterText)); - const int storySummaryStep = 9; - const int storyContextStep = 10; - const int totalWizardSteps = 11; + const int storySummaryStep = 10; + const int storyContextStep = 11; + const int totalWizardSteps = 12; return new StoryIntelligenceOverviewViewModel { PreviewID = preview.PreviewID, diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index fc123a8..f59e696 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -127,9 +127,9 @@ public sealed class StoryIntelligenceOverviewViewModel public int ApprovedChapterCount { get; init; } public int ApprovedWordCount { get; init; } public int MissingChapterTextCount { get; init; } - public int WizardCurrentStep { get; init; } = 9; - public int WizardContextStep { get; init; } = 10; - public int WizardTotalSteps { get; init; } = 10; + public int WizardCurrentStep { get; init; } = 10; + public int WizardContextStep { get; init; } = 11; + public int WizardTotalSteps { get; init; } = 12; public string? Message { get; init; } public Guid? ExistingBatchID { get; init; } public StoryIntelligenceJobProgress? ExistingJob { get; init; } diff --git a/PlotLine/Views/Onboarding/BuildComplete.cshtml b/PlotLine/Views/Onboarding/BuildComplete.cshtml index 14f777f..9572fbd 100644 --- a/PlotLine/Views/Onboarding/BuildComplete.cshtml +++ b/PlotLine/Views/Onboarding/BuildComplete.cshtml @@ -6,15 +6,7 @@
-
    - @foreach (var label in new[] { "Welcome", "Writing Preferences", "Project", "Book", "Connect Word", "Scan", "Review", "Create Structure", "Complete" }) - { -
  1. - - @label -
  2. - } -
+

Core Import Complete

diff --git a/PlotLine/Views/Onboarding/Index.cshtml b/PlotLine/Views/Onboarding/Index.cshtml index 6ddde7a..65a0adb 100644 --- a/PlotLine/Views/Onboarding/Index.cshtml +++ b/PlotLine/Views/Onboarding/Index.cshtml @@ -3,11 +3,21 @@ @{ ViewData["Title"] = "Set up PlotDirector"; var currentJourneyOrder = CurrentJourneyOrder(Model); + var structureReadyToImport = string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal); + var reviewedScanAvailable = Model.ScanState.IsComplete || structureReadyToImport; }
+ @if (TempData["OnboardingReviewMessage"] is string reviewMessage) + { +
@reviewMessage
+ } + @if (TempData["ArchiveError"] is string archiveError) + { +
@archiveError
+ } @if (Model.CurrentStep == OnboardingSteps.Welcome) { @@ -229,11 +239,11 @@ else {
-

Step @currentJourneyOrder of 10

+

Step @currentJourneyOrder of 12

@if (Model.IsMicrosoftWordPath) { -

@(currentJourneyOrder >= 6 ? "Scan Manuscript" : "Connect your Word Companion")

-

Open your manuscript in Word and start the Companion. When it is connected, use the wizard button below to scan and continue.

+

@(structureReadyToImport ? "Import scenes and Word markers" : currentJourneyOrder >= 6 ? "Scan Manuscript" : "Connect your Word Companion")

+

@(structureReadyToImport ? "Keep Word and this browser page open while PlotDirector detects scene boundaries, creates the manuscript structure, inserts bookmark IDs, and saves the document." : "Open your manuscript in Word and start the Companion. When it is connected, use the wizard button below to scan and continue.")

} else { @@ -314,10 +324,17 @@
}
-
- -
- @if (Model.IsMicrosoftWordPath && string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal)) + @if (Model.IsMicrosoftWordPath && reviewedScanAvailable && Model.ScanState.PreviewID.HasValue) + { + Back to chapter review + } + else + { +
+ +
+ } + @if (Model.IsMicrosoftWordPath && structureReadyToImport) {
}
- +
@@ -364,7 +364,7 @@ else { Back to setup - + }
diff --git a/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml b/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml index 2e33e94..3e0b5ae 100644 --- a/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml +++ b/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml @@ -36,10 +36,11 @@ new("Book", 5), new("Connect Word", 6), new("Review Chapters", 7), - new("Review Characters", 8), - new("Story Summary", 9), - new("Story Context", 10), - new("Complete", 11) + new("Import Scenes", 8), + new("Review Characters", 9), + new("Story Summary", 10), + new("Story Context", 11), + new("Complete", 12) ]; private static string StepClass(int order, int currentStep) diff --git a/PlotLine/wwwroot/js/word-companion-presence.js b/PlotLine/wwwroot/js/word-companion-presence.js index 94c82d3..057bd06 100644 --- a/PlotLine/wwwroot/js/word-companion-presence.js +++ b/PlotLine/wwwroot/js/word-companion-presence.js @@ -220,6 +220,7 @@ if (!previewId) { return; } + pendingBuildPreviewId = previewId; button.disabled = true; document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => { node.classList.add("is-running");