Fix basic location onboarding wizard UI

This commit is contained in:
Nick Beckley 2026-08-31 16:48:13 +00:00
parent 66e28b7f0e
commit b6caf9df8e
8 changed files with 210 additions and 33 deletions

View File

@ -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()

View File

@ -93,6 +93,7 @@ public sealed class OnboardingController(
}
[HttpGet("scan-locations")]
[HttpGet("scan/locations")]
public async Task<IActionResult> 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<IActionResult> SaveBasicLocations(CoreImportBasicLocationReviewForm form, string intent = "save")

View File

@ -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,

View File

@ -6,7 +6,7 @@
<section class="onboarding-shell" aria-labelledby="build-complete-title">
<div class="onboarding-panel onboarding-review-panel">
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = coreComplete ? 10 : 8, TotalSteps = 10 })" />
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = coreComplete ? 11 : 8, TotalSteps = 11 })" />
<div class="onboarding-copy">
<p class="eyebrow">@(coreComplete ? "Core Import Complete" : "Scene Detection Complete")</p>
<div class="onboarding-success-heading">
@ -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.
}
</p>
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
@ -87,8 +87,8 @@
else
{
<section class="onboarding-complete-next">
<h2>Next: review characters</h2>
<p>Character detection remains part of core import. Review detected names, aliases and existing matches to finish importing the manuscript.</p>
<h2>Next: review locations</h2>
<p>Basic location review is part of core import. Review detected places before reviewing characters and finishing the manuscript import.</p>
</section>
<section class="onboarding-build-panel onboarding-marker-resync-panel" data-onboarding-build-panel>

View File

@ -4,10 +4,13 @@
}
<section class="onboarding-shell">
<div class="onboarding-header">
<div class="onboarding-panel onboarding-review-panel onboarding-location-review-panel">
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 9, TotalSteps = 11 })" />
<div class="onboarding-copy">
<p class="eyebrow">Core import</p>
<h1>Review locations</h1>
<p class="lede">@Model.DocumentTitle - @Model.SelectedProjectName - @Model.SelectedBookTitle</p>
<h1 id="scan-location-review-title">Review Locations</h1>
<p>@Model.DocumentTitle - @Model.SelectedProjectName - @Model.SelectedBookTitle</p>
</div>
@if (TempData["OnboardingReviewMessage"] is string message)
@ -19,7 +22,6 @@
<div class="alert alert-danger">@error</div>
}
<div class="onboarding-panel">
<div class="review-summary-grid">
<div>
<span class="metric-label">Raw observations</span>
@ -42,8 +44,11 @@
<article class="location-review-row">
<input type="hidden" name="Locations[@i].StableKey" value="@candidate.StableKey" />
<div class="location-review-main">
<div class="location-review-title-row">
<h2>@candidate.ProposedName</h2>
<p>@candidate.ObservationCount observation@(candidate.ObservationCount == 1 ? "" : "s") - first seen in Chapter @candidate.FirstChapterNumber, Scene @candidate.FirstSceneNumber</p>
<span>@candidate.ObservationCount observation@(candidate.ObservationCount == 1 ? "" : "s")</span>
</div>
<p>First seen in Chapter @candidate.FirstChapterNumber, Scene @candidate.FirstSceneNumber</p>
@if (candidate.KnownNames.Count > 1)
{
<p>Known names: @string.Join(", ", candidate.KnownNames)</p>
@ -51,7 +56,7 @@
<div class="location-evidence">
@foreach (var evidence in candidate.Evidence.Take(3))
{
<blockquote><strong>@evidence.Label:</strong> @evidence.EvidenceText</blockquote>
<blockquote><strong>@evidence.Label</strong><span>@evidence.EvidenceText</span></blockquote>
}
</div>
</div>
@ -119,7 +124,7 @@
}
</div>
<div class="form-actions">
<div class="onboarding-actions location-review-actions">
<button class="btn btn-secondary" type="submit" name="intent" value="save">Save choices</button>
<button class="btn btn-outline-secondary" type="submit" name="intent" value="skip">Skip locations</button>
<button class="btn btn-primary" type="submit" name="intent" value="continue">@Model.ContinueButtonText</button>

View File

@ -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))

View File

@ -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)

View File

@ -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;
}