diff --git a/PlotLine/Views/Onboarding/BuildComplete.cshtml b/PlotLine/Views/Onboarding/BuildComplete.cshtml
index 245588d..73b5e21 100644
--- a/PlotLine/Views/Onboarding/BuildComplete.cshtml
+++ b/PlotLine/Views/Onboarding/BuildComplete.cshtml
@@ -5,10 +5,22 @@
+
+ @foreach (var label in new[] { "Welcome", "Writing Preferences", "Project", "Book", "Connect Word", "Scan", "Review", "Build Project", "Complete" })
+ {
+
+
+ @label
+
+ }
+
Build Project
-
Your project structure has been created.
-
@(Model.AlreadyBuilt ? "This manuscript has already been built into PlotDirector." : "PlotDirector has created the approved chapters, scenes and characters.")
+
+ ✓
+
Your project structure has been created.
+
+
@(Model.AlreadyBuilt ? "This manuscript has already been built into PlotDirector, so nothing was duplicated." : "PlotDirector created the approved chapters, scenes, characters and scene appearances.")
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
{
@Model.MarkerWarning
@@ -34,6 +46,11 @@
+
+ Ready for your next pass
+ Your project now has a working story map. Open the overview to inspect the structure, or head straight into the writer workspace to keep drafting.
+
+
Open Project Overview
Open Writer Workspace
diff --git a/PlotLine/Views/Onboarding/Index.cshtml b/PlotLine/Views/Onboarding/Index.cshtml
index 57f84bb..3c390f7 100644
--- a/PlotLine/Views/Onboarding/Index.cshtml
+++ b/PlotLine/Views/Onboarding/Index.cshtml
@@ -2,6 +2,19 @@
@using PlotLine.Models
@{
ViewData["Title"] = "Set up PlotDirector";
+ var currentJourneyOrder = CurrentJourneyOrder(Model);
+ var journeySteps = new[]
+ {
+ new { Label = "Welcome", Order = 1 },
+ new { Label = "Writing Preferences", Order = 2 },
+ new { Label = "Project", Order = 3 },
+ new { Label = "Book", Order = 4 },
+ new { Label = "Connect Word", Order = 5 },
+ new { Label = "Scan", Order = 6 },
+ new { Label = "Review", Order = 7 },
+ new { Label = "Build Project", Order = 8 },
+ new { Label = "Complete", Order = 9 }
+ };
}
+
+ @foreach (var step in journeySteps)
+ {
+
+ @step.Order
+ @step.Label
+
+ }
+
@if (Model.CurrentStep == OnboardingSteps.Welcome)
{
@@ -237,7 +259,7 @@
@if (Model.IsMicrosoftWordPath)
{
Connect your Word Companion
- Open Microsoft Word, open your manuscript, start the PlotDirector Word Companion, and sign in using your PlotDirector account.
+ Open Microsoft Word, open your manuscript, start the PlotDirector Word Companion, and sign in using your PlotDirector account. PlotDirector will stay on this page with you while the scan and build run.
}
else
{
@@ -315,14 +337,22 @@
@if (string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
{
- Next: build project structure.
+
+
+
Review saved. PlotDirector can now create the approved structure.
+
Ready to build chapters, scenes, characters and scene appearances.
+
+
+
+
+
+
Build Project
-
}
@@ -437,4 +467,33 @@
private static string ScanPercent(ManuscriptScanStateViewModel scan)
=> scan.PercentComplete.HasValue ? $"{scan.PercentComplete.Value}%" : string.Empty;
+
+ private static int CurrentJourneyOrder(OnboardingWizardViewModel model)
+ {
+ 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.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,
+ _ => 1
+ };
+ }
+
+ private static string JourneyStepClass(int order, int currentOrder)
+ {
+ if (order < currentOrder)
+ {
+ return "is-complete";
+ }
+
+ return order == currentOrder ? "is-current" : "is-upcoming";
+ }
}
diff --git a/PlotLine/Views/Onboarding/ScanReview.cshtml b/PlotLine/Views/Onboarding/ScanReview.cshtml
index c295598..f128c17 100644
--- a/PlotLine/Views/Onboarding/ScanReview.cshtml
+++ b/PlotLine/Views/Onboarding/ScanReview.cshtml
@@ -23,8 +23,8 @@
@Model.SelectedBookTitle
Review manuscript scan
-
Nothing has been added to your project yet. Review the detected structure, then continue when you're happy.
-
You can tidy this up now, or import the structure and refine it later.
+
Nothing has been added to your project yet. Choose the chapters, scenes and character candidates that should become your first PlotDirector structure.
+
You can keep this light now and refine everything later inside the project.
@if (TempData["OnboardingReviewMessage"] is string reviewMessage)
@@ -145,11 +145,16 @@
Character candidates
@if (!probableCharacters.Any() && !possibleCharacters.Any() && !relationshipTitles.Any())
{
- No repeated character names were found yet.
+ No repeated character names were found yet. You can still build the project structure and add characters later.
}
else
{
var characterIndex = 0;
+
+ Find a candidate
+
+
+ No matching candidates.
@foreach (var group in new[]
{
@@ -168,7 +173,7 @@
@foreach (var candidate in group.Candidates)
{
-
+
@@ -214,7 +219,7 @@
@foreach (var candidate in excludedCharacters)
{
-
+
@@ -262,7 +267,8 @@
diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css
index ff911b6..213c650 100644
--- a/PlotLine/wwwroot/css/onboarding.css
+++ b/PlotLine/wwwroot/css/onboarding.css
@@ -36,6 +36,98 @@
background: var(--bs-primary);
}
+.onboarding-stepper {
+ display: grid;
+ grid-template-columns: repeat(9, minmax(0, 1fr));
+ gap: .45rem;
+ margin: 0 0 2rem;
+ padding: 0;
+ list-style: none;
+}
+
+.onboarding-stepper li {
+ position: relative;
+ display: grid;
+ gap: .35rem;
+ min-width: 0;
+ color: var(--bs-secondary-color);
+}
+
+.onboarding-stepper li::before {
+ content: "";
+ position: absolute;
+ top: .7rem;
+ left: calc(-50% + .7rem);
+ width: calc(100% - .45rem);
+ height: 2px;
+ background: rgba(31, 42, 68, .14);
+}
+
+.onboarding-stepper li:first-child::before {
+ content: none;
+}
+
+.onboarding-stepper span {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ place-items: center;
+ width: 1.45rem;
+ aspect-ratio: 1;
+ border: 1px solid rgba(31, 42, 68, .18);
+ border-radius: 50%;
+ background: var(--bs-body-bg);
+ color: var(--bs-secondary-color);
+ font-size: .72rem;
+ font-weight: 800;
+}
+
+.onboarding-stepper strong {
+ overflow-wrap: anywhere;
+ font-size: .72rem;
+ line-height: 1.2;
+}
+
+.onboarding-stepper .is-complete,
+.onboarding-stepper .is-current {
+ color: var(--bs-body-color);
+}
+
+.onboarding-stepper .is-complete::before,
+.onboarding-stepper .is-current::before {
+ background: rgba(47, 111, 99, .38);
+}
+
+.onboarding-stepper .is-complete span {
+ border-color: #2f6f63;
+ background: #2f6f63;
+ color: #fff;
+}
+
+.onboarding-stepper .is-complete span::before {
+ content: "";
+ width: .45rem;
+ height: .7rem;
+ border: solid currentColor;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+
+.onboarding-stepper .is-complete span {
+ font-size: 0;
+}
+
+.onboarding-stepper .is-current span {
+ border-color: #b68a46;
+ box-shadow: 0 0 0 .22rem rgba(182, 138, 70, .18);
+ color: #755624;
+}
+
+.onboarding-stepper--complete .is-complete:last-child span {
+ border-color: #b68a46;
+ background: #b68a46;
+}
+
.onboarding-copy {
max-width: 680px;
margin-bottom: 1.5rem;
@@ -315,6 +407,35 @@
font-weight: 700;
}
+.onboarding-build-panel {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: .55rem .85rem;
+ align-items: center;
+ padding: .85rem;
+ border: 1px solid rgba(47, 111, 99, .18);
+ border-radius: 8px;
+ background: rgba(47, 111, 99, .07);
+}
+
+.onboarding-build-panel .onboarding-scan-progress {
+ grid-column: 1 / -1;
+}
+
+.onboarding-build-message {
+ margin: .2rem 0 0;
+ color: var(--bs-secondary-color);
+}
+
+.onboarding-build-panel > strong {
+ color: #2f6f63;
+ font-size: 1.1rem;
+}
+
+.onboarding-build-panel.is-running {
+ border-color: rgba(47, 111, 99, .34);
+}
+
.onboarding-review-panel {
width: min(1120px, 100%);
}
@@ -323,6 +444,13 @@
margin-bottom: 1.25rem;
}
+.onboarding-review-panel .onboarding-review-counts {
+ position: sticky;
+ top: .75rem;
+ z-index: 2;
+ backdrop-filter: blur(10px);
+}
+
.onboarding-review-tools {
display: flex;
flex-wrap: wrap;
@@ -347,6 +475,22 @@
font-size: 1.2rem;
}
+.onboarding-character-filter {
+ display: grid;
+ gap: .35rem;
+ color: var(--bs-secondary-color);
+ font-weight: 700;
+}
+
+.onboarding-empty-state {
+ margin: 0;
+ padding: .8rem;
+ border: 1px dashed rgba(31, 42, 68, .2);
+ border-radius: 8px;
+ color: var(--bs-secondary-color);
+ background: rgba(255, 255, 255, .55);
+}
+
.onboarding-review-chapters {
display: grid;
gap: .85rem;
@@ -464,6 +608,48 @@
margin: 0;
}
+.onboarding-success-heading {
+ display: flex;
+ align-items: center;
+ gap: .9rem;
+}
+
+.onboarding-success-heading span {
+ flex: 0 0 auto;
+ display: grid;
+ place-items: center;
+ width: 3rem;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background: #2f6f63;
+ color: #fff;
+ font-size: 1.55rem;
+ font-weight: 800;
+ box-shadow: 0 0 0 .38rem rgba(47, 111, 99, .12);
+}
+
+.onboarding-success-heading h1 {
+ margin: 0;
+}
+
+.onboarding-complete-next {
+ margin-top: 1.25rem;
+ padding: 1rem;
+ border: 1px solid rgba(31, 42, 68, .12);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, .58);
+}
+
+.onboarding-complete-next h2 {
+ margin: 0 0 .35rem;
+ font-size: 1.2rem;
+}
+
+.onboarding-complete-next p {
+ margin: 0;
+ color: var(--bs-secondary-color);
+}
+
.onboarding-dashboard-nudge {
display: flex;
align-items: center;
@@ -533,6 +719,28 @@
grid-template-columns: 1fr;
}
+ .onboarding-shell {
+ padding-top: 1rem;
+ }
+
+ .onboarding-stepper {
+ display: flex;
+ gap: .75rem;
+ overflow-x: auto;
+ padding-bottom: .35rem;
+ scroll-snap-type: x proximity;
+ }
+
+ .onboarding-stepper li {
+ flex: 0 0 7.2rem;
+ scroll-snap-align: start;
+ }
+
+ .onboarding-stepper li::before {
+ width: 6rem;
+ left: -6.35rem;
+ }
+
.onboarding-dashboard-nudge,
.onboarding-companion-card,
.onboarding-review-grid,
@@ -549,4 +757,18 @@
.onboarding-scan-counts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+
+ .onboarding-review-panel .onboarding-review-counts {
+ position: static;
+ }
+
+ .onboarding-build-panel,
+ .onboarding-success-heading {
+ grid-template-columns: 1fr;
+ align-items: start;
+ }
+
+ .onboarding-success-heading {
+ flex-direction: column;
+ }
}
diff --git a/PlotLine/wwwroot/js/word-companion-presence.js b/PlotLine/wwwroot/js/word-companion-presence.js
index 3c395ca..bba30e3 100644
--- a/PlotLine/wwwroot/js/word-companion-presence.js
+++ b/PlotLine/wwwroot/js/word-companion-presence.js
@@ -54,9 +54,9 @@
} else if (!documentOpen) {
node.textContent = "Open your manuscript in Word, then the scan button will appear.";
} else if (currentScanStatus === "Complete") {
- node.textContent = "Scan complete. Review the structure before anything is imported.";
+ node.textContent = "Your scan is ready. Review what PlotDirector found before anything is imported.";
} else if (currentScanStatus === "Running") {
- node.textContent = "The Companion is scanning your manuscript now.";
+ node.textContent = "The Companion is reading your manuscript and sending structure back to PlotDirector.";
} else {
node.textContent = "Your Companion is connected. Scan your manuscript when you are ready.";
}
@@ -97,7 +97,7 @@
const status = field(state, "status", "Status") || "NotStarted";
currentScanStatus = status;
- const message = field(state, "message", "Message") || (status === "Complete" ? "We found:" : "Ready to scan");
+ const message = field(state, "message", "Message") || (status === "Complete" ? "Scan complete. Review what PlotDirector found." : "Ready to scan");
const percent = field(state, "percentComplete", "PercentComplete");
const previewId = field(state, "previewID", "PreviewID") || field(state, "previewId", "PreviewId");
const panel = document.querySelector("[data-onboarding-scan-panel]");
@@ -150,9 +150,19 @@
connection.on("OnboardingScanFailed", applyScanState);
connection.on("OnboardingBuildProgress", (state) => {
const message = field(state, "message", "Message") || "Building project structure...";
+ const percent = field(state, "percentComplete", "PercentComplete");
+ const safePercent = Math.max(0, Math.min(100, Number.parseInt(percent || "0", 10) || 0));
+ document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
+ node.classList.add("is-running");
+ });
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
- const percent = field(state, "percentComplete", "PercentComplete");
- node.textContent = percent === null || percent === undefined ? message : `${message} ${percent}%`;
+ node.textContent = message;
+ });
+ document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => {
+ node.textContent = percent === null || percent === undefined ? "" : `${percent}%`;
+ });
+ document.querySelectorAll("[data-onboarding-build-progress]").forEach((node) => {
+ node.style.width = `${safePercent}%`;
});
});
connection.on("OnboardingBuildCompleted", (result) => {
@@ -193,8 +203,17 @@
return;
}
button.disabled = true;
+ document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
+ node.classList.add("is-running");
+ });
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
- node.textContent = "Creating chapters...";
+ node.textContent = "Creating the approved project structure...";
+ });
+ document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => {
+ node.textContent = "0%";
+ });
+ document.querySelectorAll("[data-onboarding-build-progress]").forEach((node) => {
+ node.style.width = "0%";
});
try {
const result = await connection.invoke("StartOnboardingProjectBuild", previewId);
@@ -202,9 +221,15 @@
window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(returnedPreviewId)}`;
} catch (error) {
button.disabled = false;
+ document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
+ node.classList.remove("is-running");
+ });
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
node.textContent = error?.message || "The project structure could not be built. Check the review and try again.";
});
+ document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => {
+ node.textContent = "";
+ });
}
});
});