diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index 2e304ec..25c8fb3 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -48,6 +48,22 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return View(model); } + [HttpGet("scan-characters")] + public async Task ScanCharacters(Guid? previewId) + { + var model = await onboarding.GetScanReviewAsync(previewId) + ?? (previewId.HasValue ? await onboarding.GetScanReviewAsync() : null); + if (model is null) + { + TempData["ArchiveError"] = "The character review could not be opened. Please scan the manuscript again."; + await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview); + return RedirectToAction(nameof(Index)); + } + + ViewData["ScanReviewStep"] = "characters"; + return View("ScanReview", model); + } + [HttpGet("build-complete")] public async Task BuildComplete(Guid previewId) { @@ -499,6 +515,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard [ValidateAntiForgeryToken] public async Task SaveScanReview(ManuscriptScanReviewForm form, string intent = "save") { + var continueToCharacters = string.Equals(intent, "characters", StringComparison.OrdinalIgnoreCase); var readyToImport = string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase); try { @@ -510,9 +527,11 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData["OnboardingReviewMessage"] = readyToImport ? "Review saved. Next: prepare chapters and analyse manuscript." + : continueToCharacters ? "Chapter and scene review saved. Next: review detected characters." : "Review choices saved."; return readyToImport ? RedirectToAction(nameof(StoryIntelligence)) + : continueToCharacters ? RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID }) : RedirectToAction(nameof(ScanReview), new { previewId = form.PreviewID }); } catch (InvalidOperationException ex) @@ -524,6 +543,11 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return NotFound(); } + if (readyToImport) + { + ViewData["ScanReviewStep"] = "characters"; + } + return View("ScanReview", model); } } diff --git a/PlotLine/Data/OnboardingRepository.cs b/PlotLine/Data/OnboardingRepository.cs index cab4886..920b002 100644 --- a/PlotLine/Data/OnboardingRepository.cs +++ b/PlotLine/Data/OnboardingRepository.cs @@ -11,6 +11,7 @@ public interface IOnboardingRepository Task SetCurrentStepAsync(int userId, string currentStep); Task SaveWritingJourneyAsync(int userId, string writingJourney); Task SaveWritingSoftwareAsync(int userId, string writingSoftware); + Task ClearWritingSoftwareAsync(int userId); Task SaveProjectAsync(int userId, int projectId); Task SaveBookAsync(int userId, int bookId); Task MarkCompletedAsync(int userId); @@ -64,6 +65,26 @@ public sealed class OnboardingRepository(ISqlConnectionFactory connectionFactory commandType: CommandType.StoredProcedure); } + public async Task ClearWritingSoftwareAsync(int userId) + { + using var connection = connectionFactory.CreateConnection(); + await connection.ExecuteAsync( + """ + UPDATE dbo.UserOnboardingState + SET WritingSoftware = NULL, + UsesWordCompanion = 0, + CurrentStep = N'WritingSoftware', + UpdatedUtc = SYSUTCDATETIME() + WHERE UserID = @UserID; + """, + new { UserID = userId }); + + return await connection.QuerySingleAsync( + "dbo.UserOnboarding_Get", + new { UserID = userId }, + commandType: CommandType.StoredProcedure); + } + public async Task SaveProjectAsync(int userId, int projectId) { using var connection = connectionFactory.CreateConnection(); diff --git a/PlotLine/Services/OnboardingService.cs b/PlotLine/Services/OnboardingService.cs index 55bdcca..c8007ab 100644 --- a/PlotLine/Services/OnboardingService.cs +++ b/PlotLine/Services/OnboardingService.cs @@ -80,7 +80,7 @@ public sealed class OnboardingService( { CurrentStep = step, StepNumber = StepNumber(step), - TotalSteps = 9, + TotalSteps = 10, WritingJourney = state.WritingJourney, WritingSoftware = state.WritingSoftware, ProjectID = state.ProjectID, @@ -659,10 +659,10 @@ public sealed class OnboardingService( public async Task StartManuscriptImportSetupAsync() { var userId = RequireUserId(); - await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.ManuscriptMostlyComplete); - var state = await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.MicrosoftWord); + var state = await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.ManuscriptMostlyComplete); + state = await onboarding.ClearWritingSoftwareAsync(userId); await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID); - await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.Project); + await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.WritingSoftware); } public async Task BeginManuscriptImportForBookAsync(int projectId, int bookId) diff --git a/PlotLine/Views/Onboarding/Index.cshtml b/PlotLine/Views/Onboarding/Index.cshtml index c57bd24..873a3c2 100644 --- a/PlotLine/Views/Onboarding/Index.cshtml +++ b/PlotLine/Views/Onboarding/Index.cshtml @@ -229,7 +229,7 @@ else {
-

Step @currentJourneyOrder of 9

+

Step @currentJourneyOrder of 10

@if (Model.IsMicrosoftWordPath) {

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

@@ -460,16 +460,17 @@ return model.CurrentStep switch { OnboardingSteps.Welcome => 1, - OnboardingSteps.WritingJourney or OnboardingSteps.WritingSoftware => 2, - OnboardingSteps.Project => 3, - OnboardingSteps.Book => 4, - OnboardingSteps.NextPathPreview when !model.IsMicrosoftWordPath => 9, - OnboardingSteps.NextPathPreview when string.Equals(model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal) => 8, + OnboardingSteps.WritingJourney => 2, + OnboardingSteps.WritingSoftware => 3, + OnboardingSteps.Project => 4, + OnboardingSteps.Book => 5, + OnboardingSteps.NextPathPreview when !model.IsMicrosoftWordPath => 10, + OnboardingSteps.NextPathPreview when string.Equals(model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal) => 9, OnboardingSteps.NextPathPreview when model.ScanState.IsComplete => 7, OnboardingSteps.NextPathPreview when model.ScanState.IsRunning || model.ScanState.IsFailed || model.ScanState.PercentComplete.HasValue => 6, OnboardingSteps.NextPathPreview when model.CompanionPresence.IsConnected => 6, - OnboardingSteps.NextPathPreview => 5, - OnboardingSteps.Complete => 9, + OnboardingSteps.NextPathPreview => 6, + OnboardingSteps.Complete => 10, _ => 1 }; } diff --git a/PlotLine/Views/Onboarding/ScanReview.cshtml b/PlotLine/Views/Onboarding/ScanReview.cshtml index 8c800ce..3a9f935 100644 --- a/PlotLine/Views/Onboarding/ScanReview.cshtml +++ b/PlotLine/Views/Onboarding/ScanReview.cshtml @@ -18,18 +18,20 @@ .OrderBy(candidate => candidate.ReviewName) .ToList(); var visibleCharacterCount = probableCharacters.Count + possibleCharacters.Count + relationshipTitles.Count; + var isCharacterStep = string.Equals(ViewData["ScanReviewStep"] as string, "characters", StringComparison.OrdinalIgnoreCase); + var currentStep = isCharacterStep ? 8 : 7; }
@Html.AntiForgeryToken() - +
-

Step 7 of 9

-

Review Manuscript Scan

-

Confirm the chapter structure first, then review the detected character names and aliases before Story Intelligence begins.

+

Step @currentStep of 10

+

@(isCharacterStep ? "Review Characters" : "Review Chapters")

+

@(isCharacterStep ? "Review detected character names, aliases and existing character matches before Story Intelligence begins." : "Confirm the chapter and scene structure before reviewing detected characters.")

@if (TempData["OnboardingReviewMessage"] is string reviewMessage) @@ -57,21 +59,19 @@
-
- - - - - -
- -
-
- - + @if (!isCharacterStep) + { +
+ + + + +
+ } -
+
+ }
- +
-