Streamline core import wizard steps

This commit is contained in:
Nick Beckley 2026-08-27 19:32:27 +00:00
parent ea41ec3aac
commit 4f82dc308a
5 changed files with 173 additions and 33 deletions

View File

@ -93,6 +93,8 @@ var tests = new (string Name, Action Test)[]
("Core import scene titles ride existing structural AI call", CoreImportSceneTitlesRideExistingStructuralAiCall),
("Core import scene title preference is review scoped", CoreImportSceneTitlePreferenceIsReviewScoped),
("Core import scene title preference moved to Detect Scenes", CoreImportSceneTitlePreferenceMovedToDetectScenes),
("Core import Review Chapters is compact and chapter only", CoreImportReviewChaptersIsCompactAndChapterOnly),
("Core import Detect Scenes is compact and owns title option", CoreImportDetectScenesIsCompactAndOwnsTitleOption),
("Book context is canonical import metadata", BookContextIsCanonicalImportMetadata),
("Names defaults from selected book context", NamesDefaultsFromSelectedBookContext),
("Story Intelligence starts from core import complete", StoryIntelligenceStartsFromCoreImportComplete),
@ -2579,6 +2581,49 @@ static void CoreImportSceneTitlePreferenceMovedToDetectScenes()
Assert(hub.Contains("StartOnboardingProjectBuild(Guid previewId, bool generateSceneTitles = true)", StringComparison.Ordinal), "Build hub should accept the title preference without a separate AI call.");
}
static void CoreImportReviewChaptersIsCompactAndChapterOnly()
{
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
var review = File.ReadAllText(Path.Combine(root, "Views/Onboarding/ScanReview.cshtml"));
Assert(!review.Contains("Generate descriptive scene titles", StringComparison.Ordinal), "Review Chapters should not render the scene-title option.");
Assert(review.Contains("chapters selected", StringComparison.Ordinal), "Review Chapters should render the selected chapter summary.");
Assert(review.Contains("onboarding-review-chapter-row", StringComparison.Ordinal), "Review Chapters should render compact chapter rows.");
Assert(!review.Contains("<details class=\"onboarding-review-chapter\"", StringComparison.Ordinal), "Review Chapters should not use large disclosure cards for chapter selection.");
Assert(review.Contains("data-bulk-target=\"chapter\" data-bulk-state=\"true\"", StringComparison.Ordinal), "Review Chapters should retain Include all chapters.");
Assert(review.Contains("data-bulk-target=\"chapter\" data-bulk-state=\"false\"", StringComparison.Ordinal), "Review Chapters should retain Exclude all chapters.");
Assert(review.Contains("type=\"reset\">Restore scan defaults", StringComparison.Ordinal), "Review Chapters should retain Restore scan defaults.");
Assert(review.Contains("Continue to Scene Detection", StringComparison.Ordinal), "Review Chapters should retain Continue to Scene Detection.");
Assert(review.Contains("name=\"Chapters[@chapterIndex].Include\"", StringComparison.Ordinal), "Compact rows should still post chapter inclusion choices.");
Assert(review.Contains("name=\"Chapters[@chapterIndex].Title\"", StringComparison.Ordinal), "Compact rows should still post reviewed chapter titles.");
var checkboxIndex = review.IndexOf("type=\"checkbox\" name=\"Chapters[@chapterIndex].Include\"", StringComparison.Ordinal);
var hiddenIndex = review.IndexOf("type=\"hidden\" name=\"Chapters[@chapterIndex].Include\"", StringComparison.Ordinal);
Assert(checkboxIndex >= 0 && hiddenIndex > checkboxIndex, "Chapter include checkbox should post before the hidden false value.");
}
static void CoreImportDetectScenesIsCompactAndOwnsTitleOption()
{
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
var index = File.ReadAllText(Path.Combine(root, "Views/Onboarding/Index.cshtml"));
var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/word-companion-presence.js"));
var analyzer = File.ReadAllText(Path.Combine(root, "Services/ManuscriptStructureAnalysisService.cs"));
var detectScenesBlock = Between(index, "@if (structureReadyToImport)", "else\n {");
Assert(CountOccurrences(index, "Generate descriptive scene titles") == 1, "Detect Scenes should render the scene-title option exactly once.");
Assert(detectScenesBlock.Contains("data-onboarding-generate-scene-titles checked", StringComparison.Ordinal), "Detect Scenes should keep the existing ON default.");
Assert(detectScenesBlock.Contains("data-word-companion-compact-status-label", StringComparison.Ordinal), "Detect Scenes should render compact Word connection status.");
Assert(detectScenesBlock.Contains("data-word-companion-document", StringComparison.Ordinal), "Detect Scenes should include the current Word document name.");
Assert(detectScenesBlock.Contains("Keep Word open while PlotDirector detects scenes and writes the scene markers.", StringComparison.Ordinal), "Detect Scenes should give the required concise Word-open instruction.");
Assert(detectScenesBlock.Contains("data-onboarding-build-panel hidden", StringComparison.Ordinal), "Detect Scenes should hide processing progress until detection starts.");
Assert(!detectScenesBlock.Contains("onboarding-companion-card", StringComparison.Ordinal), "Detect Scenes should not render the large Word Companion card.");
Assert(!detectScenesBlock.Contains("Word Companion Connected", StringComparison.Ordinal), "Detect Scenes should not render the old dashboard heading.");
Assert(!detectScenesBlock.Contains("data-onboarding-scan-counts", StringComparison.Ordinal), "Detect Scenes should not render old scan counters.");
Assert(!detectScenesBlock.Contains("Ready to detect scenes.", StringComparison.Ordinal), "Detect Scenes should not render the old ready panel.");
Assert(script.Contains("StartOnboardingProjectBuild\", previewId, generateSceneTitles", StringComparison.Ordinal), "Detect Scenes should start the existing canonical scene-detection operation.");
Assert(script.Contains("node.hidden = false;", StringComparison.Ordinal), "Processing state should reveal the build progress panel only when work starts.");
Assert(!analyzer.Contains("SceneTitle-Prompt", StringComparison.Ordinal), "Scene title generation should remain inside the existing chapter-structure call.");
}
static void BookContextIsCanonicalImportMetadata()
{
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
@ -3503,6 +3548,32 @@ static void WordCompanionReplaysLiveCurrentSceneToWritingPanes()
static int CountCsvRows(string path)
=> File.ReadLines(path).Skip(1).Count(line => !string.IsNullOrWhiteSpace(line));
static int CountOccurrences(string value, string search)
{
var count = 0;
var index = 0;
while ((index = value.IndexOf(search, index, StringComparison.Ordinal)) >= 0)
{
count++;
index += search.Length;
}
return count;
}
static string Between(string value, string start, string end)
{
var startIndex = value.IndexOf(start, StringComparison.Ordinal);
if (startIndex < 0)
{
return string.Empty;
}
startIndex += start.Length;
var endIndex = value.IndexOf(end, startIndex, StringComparison.Ordinal);
return endIndex < 0 ? value[startIndex..] : value[startIndex..endIndex];
}
static JsonSerializerOptions JsonOptions()
=> new()
{

View File

@ -265,7 +265,7 @@
@if (Model.IsMicrosoftWordPath)
{
<h1 id="onboarding-title">@(structureReadyToImport ? "Detect Scenes" : currentJourneyOrder >= 6 ? "Scan Manuscript" : "Connect your Word Companion")</h1>
<p>@(structureReadyToImport ? "PlotDirector will now analyse the approved chapters to determine where each scene begins and ends. Keep Word open while scene markers are added and the document is saved." : "Open your manuscript in Word and start the Companion. When it is connected, use the wizard button below to scan and continue.")</p>
<p>@(structureReadyToImport ? "PlotDirector will analyse the selected chapters to determine where each scene begins and ends." : "Open your manuscript in Word and start the Companion. When it is connected, use the wizard button below to scan and continue.")</p>
}
else
{
@ -282,22 +282,22 @@
data-word-companion-status="@Model.CompanionPresence.Status"
aria-label="Scene detection">
<p class="onboarding-compact-status">
<span data-word-companion-waiting-status>@(Model.CompanionPresence.IsConnected ? "Word connected" : "Waiting for Word")</span>
&middot; @Model.ScanState.ChapterCount.ToString("N0") chapter@(Model.ScanState.ChapterCount == 1 ? string.Empty : "s")
&middot; @Model.ScanState.TotalWordCount.ToString("N0") words
<span aria-hidden="true">@(Model.CompanionPresence.IsConnected ? "\u2713" : "!")</span>
<span data-word-companion-compact-status-label>@(Model.CompanionPresence.IsConnected ? "Word Companion connected" : "Word Companion disconnected")</span>
&middot; <span data-word-companion-document>@(Model.CompanionPresence.CurrentDocumentName ?? "No document reported yet")</span>
</p>
<label class="onboarding-inline-choice">
<input type="checkbox" data-onboarding-generate-scene-titles checked />
<span>
<strong>Generate descriptive scene titles</strong>
<small>Short working titles used only within PlotDirector.</small>
<small>Adds optional PlotDirector-only titles such as "Behind the Fire Exit". Titles can be edited later and do not change the manuscript.</small>
</span>
</label>
<p>Keep Word open until scene markers have been added. If Word cannot save automatically, PlotDirector will ask you to save and confirm.</p>
<div class="onboarding-build-panel" data-onboarding-build-panel>
<p>Keep Word open while PlotDirector detects scenes and writes the scene markers.</p>
<div class="onboarding-build-panel" data-onboarding-build-panel hidden>
<div>
<p class="onboarding-scan-next">Ready to detect scenes.</p>
<p class="onboarding-build-message" data-onboarding-build-message>Scene detection has not started yet.</p>
<p class="onboarding-scan-next">Detecting scenes</p>
<p class="onboarding-build-message" data-onboarding-build-message>Analysing scene boundaries...</p>
</div>
<strong data-onboarding-build-percent></strong>
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
@ -388,7 +388,8 @@
type="button"
data-onboarding-build-start
data-onboarding-build-action
data-preview-id="@Model.ScanState.PreviewID">
data-preview-id="@Model.ScanState.PreviewID"
disabled="@(!CanStartSceneDetection(Model.CompanionPresence))">
Detect Scenes
</button>
}
@ -452,6 +453,9 @@
private static bool CanStartScan(CompanionPresenceViewModel presence, ManuscriptScanStateViewModel scan)
=> presence.IsConnected && presence.DocumentOpen && !scan.IsRunning;
private static bool CanStartSceneDetection(CompanionPresenceViewModel presence)
=> presence.IsConnected && presence.DocumentOpen;
private static string CompanionScanMessage(CompanionPresenceViewModel presence, ManuscriptScanStateViewModel scan)
{
if (!presence.IsConnected)

View File

@ -127,24 +127,18 @@
}
@foreach (var chapter in Model.Chapters)
{
<details class="onboarding-review-chapter">
<summary>
<span>Chapter @chapter.ChapterNumber</span>
<strong>@chapter.ReviewTitle</strong>
<small>@chapter.WordCount.ToString("N0") words</small>
</summary>
<label class="onboarding-review-chapter-row">
<input type="hidden" name="Chapters[@chapterIndex].TemporaryChapterKey" value="@chapter.TemporaryChapterKey" />
<input type="hidden" name="Chapters[@chapterIndex].ChapterNumber" value="@chapter.ChapterNumber" />
<label class="onboarding-review-toggle">
<input type="checkbox" name="Chapters[@chapterIndex].Include" value="true" checked="@chapter.Include" data-review-kind="chapter" />
<input type="hidden" name="Chapters[@chapterIndex].Include" value="false" />
Include chapter
</label>
<label class="onboarding-review-field">
Chapter title
<input class="form-control" name="Chapters[@chapterIndex].Title" value="@chapter.ReviewTitle" />
</label>
</details>
<input type="checkbox" name="Chapters[@chapterIndex].Include" value="true" checked="@chapter.Include" data-review-kind="chapter" />
<input type="hidden" name="Chapters[@chapterIndex].Include" value="false" />
<input type="hidden" name="Chapters[@chapterIndex].Title" value="@chapter.ReviewTitle" />
<span class="onboarding-review-chapter-row__title">
<strong>Chapter @chapter.ChapterNumber</strong>
<span>@chapter.ReviewTitle</span>
</span>
<small>@chapter.WordCount.ToString("N0") words</small>
</label>
chapterIndex++;
}
</div>

View File

@ -427,12 +427,8 @@
.onboarding-detect-scenes {
display: grid;
gap: .85rem;
margin: 1rem 0 1.25rem;
padding: 1rem;
border: 1px solid rgba(31, 42, 68, .12);
border-radius: 8px;
background: rgba(255, 255, 255, .72);
gap: .75rem;
margin: .75rem 0 1rem;
}
.onboarding-detect-scenes > p:not(.onboarding-compact-status) {
@ -632,7 +628,68 @@
.onboarding-review-chapters {
display: grid;
gap: .85rem;
gap: .35rem;
}
.onboarding-review-chapter-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: .65rem;
min-height: 2.55rem;
margin: 0;
border: 1px solid rgba(31, 42, 68, .1);
border-radius: 8px;
padding: .45rem .65rem;
background: rgba(255, 255, 255, .58);
cursor: pointer;
}
.onboarding-review-chapter-row:hover {
border-color: rgba(47, 111, 99, .28);
background: rgba(47, 111, 99, .055);
}
.onboarding-review-chapter-row:has(input[type="checkbox"]:checked) {
border-color: rgba(47, 111, 99, .34);
background: rgba(47, 111, 99, .08);
}
.onboarding-review-chapter-row input[type="checkbox"] {
width: 1.05rem;
height: 1.05rem;
margin: 0;
}
.onboarding-review-chapter-row__title {
display: flex;
align-items: baseline;
gap: .45rem;
min-width: 0;
}
.onboarding-review-chapter-row__title strong,
.onboarding-review-chapter-row__title span {
min-width: 0;
}
.onboarding-review-chapter-row__title strong {
flex: 0 0 auto;
font-size: .92rem;
}
.onboarding-review-chapter-row__title span {
overflow: hidden;
color: var(--bs-body-color);
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.onboarding-review-chapter-row small {
color: var(--bs-secondary-color);
font-weight: 800;
white-space: nowrap;
}
.onboarding-review-chapter {

View File

@ -46,6 +46,9 @@
document.querySelectorAll("[data-word-companion-status-label]").forEach((node) => {
node.textContent = label;
});
document.querySelectorAll("[data-word-companion-compact-status-label]").forEach((node) => {
node.textContent = connected ? "Word Companion connected" : "Word Companion disconnected";
});
document.querySelectorAll("[data-word-companion-waiting-status]").forEach((node) => {
node.textContent = connected ? "Word Companion Connected" : "Word Companion disconnected. Waiting for reconnection...";
});
@ -90,6 +93,13 @@
button.disabled = openingReview || !companionConnected || !companionDocumentOpen || currentScanStatus === "Running";
button.textContent = openingReview ? "Opening chapter review..." : currentScanStatus === "Complete" ? "Scan again" : "Scan manuscript";
});
document.querySelectorAll("[data-onboarding-build-start]").forEach((button) => {
if (button.dataset.buildMode === "confirm-save") {
return;
}
button.disabled = !companionConnected || !companionDocumentOpen;
});
};
const applyScanState = (state) => {
@ -195,6 +205,7 @@
pendingBuildPreviewId = pendingManualSavePreviewId || pendingBuildPreviewId;
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
node.hidden = false;
node.classList.add("is-running", "is-manual-save");
});
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
@ -284,6 +295,7 @@
return;
}
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
node.hidden = false;
node.classList.add("is-running");
node.classList.remove("is-manual-save");
});
@ -353,6 +365,7 @@
clearManualSaveRequired();
button.disabled = true;
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
node.hidden = false;
node.classList.add("is-running");
});
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
@ -373,6 +386,7 @@
} catch (error) {
button.disabled = false;
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
node.hidden = false;
node.classList.remove("is-running");
});
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {