diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index bcf1de0..21fe204 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -3930,8 +3930,8 @@ static void CoreImportWizardOrdersScenesBeforeCharacters() 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("Detected scene structure", StringComparison.Ordinal), "Scene detection result page should show detected counts by chapter."); - Assert(complete.Contains("Review characters", StringComparison.Ordinal), "After Word sync, the next onboarding step should be character review."); - Assert(complete.Contains("Keep this browser workflow open to review characters.", StringComparison.Ordinal), "Scene detection completion should continue to character review without marking optional Story Intelligence as required."); + Assert(complete.Contains("Review locations", StringComparison.Ordinal), "After Word sync, the next onboarding step should be location review."); + Assert(complete.Contains("Keep this browser workflow open to review locations.", StringComparison.Ordinal), "Scene detection completion should continue to location review without marking optional Story Intelligence as required."); } static void CoreImportRemapsCharactersAfterReview() @@ -4533,12 +4533,13 @@ static void OnboardingWizardKeepsCharacterPagesInsideOneMainStep() Assert(!header.Contains("BuildDynamicSteps", StringComparison.Ordinal), "Onboarding header should not create one top-level step per character page."); Assert(header.Contains("new(\"Detect Scenes\", 8)", StringComparison.Ordinal), "Onboarding header should include scene detection 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(header.Contains("new(\"Review Locations\", 9)", StringComparison.Ordinal), "Onboarding header should include basic location review as a distinct main step."); + Assert(header.Contains("new(\"Review Characters\", 10)", StringComparison.Ordinal), "Onboarding header should keep character review as one main step."); Assert(!header.Contains("Story Summary", StringComparison.Ordinal), "Core onboarding header should not present Story Summary as a mandatory import step."); Assert(!header.Contains("Story Context", StringComparison.Ordinal), "Core onboarding header should not present Story Context as a mandatory import 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("var currentStep = isCharacterStep ? 10 : 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 = 10;", StringComparison.Ordinal), "Story Intelligence summary/context should use the fixed core wizard step count."); + Assert(storyService.Contains("const int totalWizardSteps = 11;", StringComparison.Ordinal), "Story Intelligence summary/context should use the fixed core wizard step count."); } static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel() diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index 4459eba..0390f62 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -93,6 +93,7 @@ public sealed class OnboardingController( } [HttpGet("scan-locations")] + [HttpGet("scan/locations")] public async Task ScanLocations(Guid previewId) { var model = await onboarding.GetBasicLocationReviewAsync(previewId); @@ -112,6 +113,7 @@ public sealed class OnboardingController( } [HttpPost("scan-locations")] + [HttpPost("scan/locations")] [RequestFormLimits(ValueCountLimit = 20000, KeyLengthLimit = 4096, ValueLengthLimit = 1024 * 1024)] [ValidateAntiForgeryToken] public async Task SaveBasicLocations(CoreImportBasicLocationReviewForm form, string intent = "save") diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs index 29a0f47..25c4c4f 100644 --- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs +++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs @@ -88,7 +88,7 @@ public sealed class OnboardingStoryIntelligenceService( var missingTextCount = included.Count(chapter => string.IsNullOrWhiteSpace(chapter.Preview.ChapterText)); const int storySummaryStep = 10; const int storyContextStep = 10; - const int totalWizardSteps = 10; + const int totalWizardSteps = 11; return new StoryIntelligenceOverviewViewModel { PreviewID = preview.PreviewID, diff --git a/PlotLine/Views/Onboarding/BuildComplete.cshtml b/PlotLine/Views/Onboarding/BuildComplete.cshtml index a28e141..624bdba 100644 --- a/PlotLine/Views/Onboarding/BuildComplete.cshtml +++ b/PlotLine/Views/Onboarding/BuildComplete.cshtml @@ -6,7 +6,7 @@
- +

@(coreComplete ? "Core Import Complete" : "Scene Detection Complete")

@@ -20,11 +20,11 @@ } else if (Model.AlreadyBuilt) { - @:This manuscript structure has already been created in PlotDirector, so nothing was duplicated. Review characters to complete the core import. + @:This manuscript structure has already been created in PlotDirector, so nothing was duplicated. Review locations before reviewing characters to complete the core import. } else { - @:@Model.ScenesCreated.ToString("N0") scenes imported across @Model.ChaptersCreated.ToString("N0") chapters. Keep this browser workflow open to review characters. + @:@Model.ScenesCreated.ToString("N0") scenes imported across @Model.ChaptersCreated.ToString("N0") chapters. Keep this browser workflow open to review locations. }

@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning)) @@ -87,8 +87,8 @@ else {
-

Next: review characters

-

Character detection remains part of core import. Review detected names, aliases and existing matches to finish importing the manuscript.

+

Next: review locations

+

Basic location review is part of core import. Review detected places before reviewing characters and finishing the manuscript import.

diff --git a/PlotLine/Views/Onboarding/ScanLocations.cshtml b/PlotLine/Views/Onboarding/ScanLocations.cshtml index 262f823..afd3393 100644 --- a/PlotLine/Views/Onboarding/ScanLocations.cshtml +++ b/PlotLine/Views/Onboarding/ScanLocations.cshtml @@ -4,22 +4,24 @@ }
-
-

Core import

-

Review locations

-

@Model.DocumentTitle - @Model.SelectedProjectName - @Model.SelectedBookTitle

-
+
+ - @if (TempData["OnboardingReviewMessage"] is string message) - { -
@message
- } - @if (TempData["ArchiveError"] is string error) - { -
@error
- } +
+

Core import

+

Review Locations

+

@Model.DocumentTitle - @Model.SelectedProjectName - @Model.SelectedBookTitle

+
+ + @if (TempData["OnboardingReviewMessage"] is string message) + { +
@message
+ } + @if (TempData["ArchiveError"] is string error) + { +
@error
+ } -
Raw observations @@ -42,8 +44,11 @@
-

@candidate.ProposedName

-

@candidate.ObservationCount observation@(candidate.ObservationCount == 1 ? "" : "s") - first seen in Chapter @candidate.FirstChapterNumber, Scene @candidate.FirstSceneNumber

+
+

@candidate.ProposedName

+ @candidate.ObservationCount observation@(candidate.ObservationCount == 1 ? "" : "s") +
+

First seen in Chapter @candidate.FirstChapterNumber, Scene @candidate.FirstSceneNumber

@if (candidate.KnownNames.Count > 1) {

Known names: @string.Join(", ", candidate.KnownNames)

@@ -51,7 +56,7 @@
@foreach (var evidence in candidate.Evidence.Take(3)) { -
@evidence.Label: @evidence.EvidenceText
+
@evidence.Label@evidence.EvidenceText
}
@@ -119,7 +124,7 @@ }
-
+
diff --git a/PlotLine/Views/Onboarding/ScanReview.cshtml b/PlotLine/Views/Onboarding/ScanReview.cshtml index 6d432be..be9a1ea 100644 --- a/PlotLine/Views/Onboarding/ScanReview.cshtml +++ b/PlotLine/Views/Onboarding/ScanReview.cshtml @@ -31,8 +31,8 @@ var requestedCharacterPage = ViewData["CharacterReviewPage"] is int requestedPage ? requestedPage : 1; var characterPage = Math.Clamp(requestedCharacterPage, 1, characterReviewPageCount); var currentCharacters = primaryCharacters.Skip((characterPage - 1) * characterPageSize).Take(characterPageSize).ToList(); - var currentStep = isCharacterStep ? 9 : 7; - const int totalSteps = 10; + var currentStep = isCharacterStep ? 10 : 7; + const int totalSteps = 11; var characterPageTitle = $"Review Characters {characterPage} of {primaryCharacterPageCount}"; var duplicateCanonicalNames = primaryCharacters .Where(candidate => candidate.Include && string.IsNullOrWhiteSpace(candidate.AliasTargetTemporaryCharacterKey)) diff --git a/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml b/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml index bdc4648..18ee752 100644 --- a/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml +++ b/PlotLine/Views/Onboarding/_OnboardingJourneyHeader.cshtml @@ -37,8 +37,9 @@ new("Connect Word", 6), new("Review Chapters", 7), new("Detect Scenes", 8), - new("Review Characters", 9), - new("Complete", 10) + new("Review Locations", 9), + new("Review Characters", 10), + new("Complete", 11) ]; private static string StepClass(int order, int currentStep) diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css index 6a19a9d..440f80c 100644 --- a/PlotLine/wwwroot/css/onboarding.css +++ b/PlotLine/wwwroot/css/onboarding.css @@ -747,6 +747,153 @@ color: var(--bs-secondary-color); } +.onboarding-location-review-panel { + width: min(1240px, 100%); +} + +.onboarding-location-review-panel .review-summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .75rem; + margin-bottom: 1rem; +} + +.onboarding-location-review-panel .review-summary-grid div { + display: grid; + gap: .15rem; + padding: .85rem 1rem; + border: 1px solid rgba(31, 42, 68, .1); + border-radius: 8px; + background: rgba(255, 255, 255, .58); +} + +.onboarding-location-review-panel .metric-label { + color: var(--bs-secondary-color); + font-size: .78rem; + font-weight: 800; + text-transform: uppercase; +} + +.onboarding-location-review-panel .review-summary-grid strong { + font-size: 1.45rem; + line-height: 1.1; +} + +.location-review-list { + display: grid; + gap: .85rem; +} + +.location-review-row { + display: grid; + grid-template-columns: minmax(18rem, .95fr) minmax(28rem, 1.35fr); + gap: 1rem; + align-items: start; + border: 1px solid rgba(31, 42, 68, .12); + border-radius: 8px; + padding: 1rem; + background: rgba(255, 255, 255, .62); +} + +.location-review-title-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: .75rem; +} + +.location-review-title-row h2 { + margin: 0; + font-size: 1.25rem; + line-height: 1.2; +} + +.location-review-title-row span { + flex: 0 0 auto; + color: var(--bs-secondary-color); + font-size: .78rem; + font-weight: 800; + white-space: nowrap; +} + +.location-review-main > p { + margin: .3rem 0 0; + color: var(--bs-secondary-color); +} + +.location-evidence { + display: grid; + gap: .45rem; + margin-top: .75rem; +} + +.location-evidence blockquote { + display: grid; + gap: .25rem; + margin: 0; + border-left: 3px solid rgba(47, 111, 99, .34); + border-radius: 6px; + padding: .55rem .65rem; + background: rgba(47, 111, 99, .06); +} + +.location-evidence strong { + color: var(--bs-body-color); + font-size: .78rem; +} + +.location-evidence span { + color: var(--bs-secondary-color); + font-size: .9rem; + line-height: 1.4; +} + +.location-review-controls { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .75rem; +} + +.location-review-controls label { + display: grid; + gap: .3rem; + margin: 0; + color: var(--bs-secondary-color); + font-size: .82rem; + font-weight: 800; +} + +.location-review-controls input, +.location-review-controls select { + width: 100%; + min-height: 2.35rem; + border: 1px solid rgba(31, 42, 68, .18); + border-radius: 6px; + padding: .45rem .6rem; + background-color: var(--bs-body-bg); + color: var(--bs-body-color); + font-size: .95rem; + font-weight: 500; +} + +.location-review-controls input:focus, +.location-review-controls select:focus { + border-color: var(--bs-primary); + box-shadow: 0 0 0 .2rem rgba(47, 111, 99, .16); + outline: 0; +} + +.location-review-actions { + position: sticky; + bottom: 0; + z-index: 2; + margin: 1rem calc(clamp(1.25rem, 3vw, 2.5rem) * -1) calc(clamp(1.25rem, 3vw, 2.5rem) * -1); + border-top: 1px solid rgba(31, 42, 68, .1); + padding: 1rem clamp(1.25rem, 3vw, 2.5rem); + background: rgba(255, 255, 255, .92); + backdrop-filter: blur(10px); +} + [data-story-intelligence-onboarding] .onboarding-review-counts { position: static; margin-bottom: 1rem; @@ -2132,6 +2279,27 @@ summary.story-review-chapter-heading { grid-template-columns: 1fr; } + .onboarding-location-review-panel .review-summary-grid, + .location-review-row, + .location-review-controls { + grid-template-columns: 1fr; + } + + .location-review-title-row { + align-items: flex-start; + flex-direction: column; + gap: .35rem; + } + + .location-review-actions { + position: static; + justify-content: stretch; + } + + .location-review-actions .btn { + width: 100%; + } + .onboarding-companion-details { grid-template-columns: 1fr; }