500 lines
26 KiB
Plaintext
500 lines
26 KiB
Plaintext
@model OnboardingWizardViewModel
|
|
@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 = "Prepare Chapters", Order = 8 },
|
|
new { Label = "Complete", Order = 9 }
|
|
};
|
|
}
|
|
|
|
<section class="onboarding-shell" aria-labelledby="onboarding-title">
|
|
<div class="onboarding-panel">
|
|
<div class="onboarding-progress" aria-label="Setup progress">
|
|
<span>Step @Model.StepNumber of @Model.TotalSteps</span>
|
|
<div class="onboarding-progress-track">
|
|
<span style="width:@((Model.StepNumber * 100) / Model.TotalSteps)%"></span>
|
|
</div>
|
|
</div>
|
|
<ol class="onboarding-stepper" aria-label="Story setup journey">
|
|
@foreach (var step in journeySteps)
|
|
{
|
|
<li class="@JourneyStepClass(step.Order, currentJourneyOrder)">
|
|
<span>@step.Order</span>
|
|
<strong>@step.Label</strong>
|
|
</li>
|
|
}
|
|
</ol>
|
|
|
|
@if (Model.CurrentStep == OnboardingSteps.Welcome)
|
|
{
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">Story intelligence setup</p>
|
|
<h1 id="onboarding-title">Welcome to PlotDirector</h1>
|
|
<p>Answer a few quick questions so PlotDirector can shape the first setup around the way you write.</p>
|
|
</div>
|
|
<form asp-controller="Onboarding" asp-action="Welcome" method="post" class="onboarding-actions">
|
|
<button class="btn btn-primary btn-lg" type="submit">Continue</button>
|
|
</form>
|
|
}
|
|
else if (Model.CurrentStep == OnboardingSteps.WritingJourney)
|
|
{
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">Your starting point</p>
|
|
<h1 id="onboarding-title">Where are you in your writing journey?</h1>
|
|
</div>
|
|
<form asp-controller="Onboarding" asp-action="WritingJourney" method="post">
|
|
<div class="text-danger mb-3" asp-validation-summary="ModelOnly"></div>
|
|
<div class="onboarding-option-grid">
|
|
@foreach (var option in Model.Options)
|
|
{
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="WritingJourney" value="@option.Value" checked="@(option.Value == Model.WritingJourney)" required />
|
|
<span>
|
|
<strong>@option.Title</strong>
|
|
<small>@option.Description</small>
|
|
</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
<span class="text-danger field-validation-valid" data-valmsg-for="WritingJourney" data-valmsg-replace="true"></span>
|
|
<div class="onboarding-actions">
|
|
<button class="btn btn-outline-secondary" type="submit" formaction="/onboarding/back" formnovalidate name="currentStep" value="@OnboardingSteps.WritingJourney">Back</button>
|
|
<button class="btn btn-primary" type="submit">Continue</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
else if (Model.CurrentStep == OnboardingSteps.WritingSoftware)
|
|
{
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">Writing workflow</p>
|
|
<h1 id="onboarding-title">What software do you write in?</h1>
|
|
</div>
|
|
<form asp-controller="Onboarding" asp-action="WritingSoftware" method="post">
|
|
<div class="text-danger mb-3" asp-validation-summary="ModelOnly"></div>
|
|
<div class="onboarding-option-grid onboarding-option-grid--software">
|
|
@foreach (var option in Model.Options)
|
|
{
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="WritingSoftware" value="@option.Value" checked="@(option.Value == Model.WritingSoftware)" required />
|
|
<span>
|
|
<strong>
|
|
@option.Title
|
|
@if (!string.IsNullOrWhiteSpace(option.Badge))
|
|
{
|
|
<em>@option.Badge</em>
|
|
}
|
|
</strong>
|
|
<small>@option.Description</small>
|
|
</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
<span class="text-danger field-validation-valid" data-valmsg-for="WritingSoftware" data-valmsg-replace="true"></span>
|
|
<div class="onboarding-actions">
|
|
<button class="btn btn-outline-secondary" type="submit" formaction="/onboarding/back" formnovalidate name="currentStep" value="@OnboardingSteps.WritingSoftware">Back</button>
|
|
<button class="btn btn-primary" type="submit">Continue</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
else if (Model.CurrentStep == OnboardingSteps.Project)
|
|
{
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">Story home</p>
|
|
<h1 id="onboarding-title">Create your first project</h1>
|
|
<p>Choose the story space PlotDirector should use for this setup.</p>
|
|
</div>
|
|
<form asp-controller="Onboarding" asp-action="Project" method="post">
|
|
<div class="text-danger mb-3" asp-validation-summary="ModelOnly"></div>
|
|
<div class="onboarding-choice-grid">
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="Mode" value="@OnboardingSelectionModes.Create" checked="@(Model.ProjectForm.Mode == OnboardingSelectionModes.Create)" />
|
|
<span>
|
|
<strong>Create a new project</strong>
|
|
<small>Start a fresh series or standalone story space.</small>
|
|
</span>
|
|
</label>
|
|
@if (Model.ProjectOptions.Any())
|
|
{
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="Mode" value="@OnboardingSelectionModes.Select" checked="@(Model.ProjectForm.Mode == OnboardingSelectionModes.Select)" />
|
|
<span>
|
|
<strong>Use an existing project</strong>
|
|
<small>Continue setup inside one of your current projects.</small>
|
|
</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
|
|
<section class="onboarding-form-section">
|
|
<h2>Create new project</h2>
|
|
<div class="row g-3">
|
|
<div class="col-md-7">
|
|
<label class="form-label" for="ProjectName">Project name</label>
|
|
<input class="form-control" id="ProjectName" name="ProjectName" maxlength="200" value="@Model.ProjectForm.ProjectName" />
|
|
@Html.ValidationMessage("ProjectName", null, new { @class = "text-danger" })
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="Description">Description</label>
|
|
<textarea class="form-control" id="Description" name="Description" rows="3">@Model.ProjectForm.Description</textarea>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@if (Model.ProjectOptions.Any())
|
|
{
|
|
<section class="onboarding-form-section">
|
|
<h2>Use existing project</h2>
|
|
<div class="onboarding-option-grid">
|
|
@foreach (var project in Model.ProjectOptions)
|
|
{
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="SelectedProjectID" value="@project.ProjectID" checked="@(project.ProjectID == Model.ProjectForm.SelectedProjectID)" />
|
|
<span>
|
|
<strong>@project.ProjectName</strong>
|
|
<small>@project.Description</small>
|
|
</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
@Html.ValidationMessage("SelectedProjectID", null, new { @class = "text-danger" })
|
|
</section>
|
|
}
|
|
|
|
<div class="onboarding-actions">
|
|
<button class="btn btn-outline-secondary" type="submit" formaction="/onboarding/back" formnovalidate name="currentStep" value="@OnboardingSteps.Project">Back</button>
|
|
<button class="btn btn-primary" type="submit">Continue</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
else if (Model.CurrentStep == OnboardingSteps.Book)
|
|
{
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">@Model.SelectedProjectName</p>
|
|
<h1 id="onboarding-title">Create your first book</h1>
|
|
<p>Choose the book this onboarding setup should prepare for later manuscript guidance.</p>
|
|
</div>
|
|
<form asp-controller="Onboarding" asp-action="Book" method="post">
|
|
<div class="text-danger mb-3" asp-validation-summary="ModelOnly"></div>
|
|
<div class="onboarding-choice-grid">
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="Mode" value="@OnboardingSelectionModes.Create" checked="@(Model.BookForm.Mode == OnboardingSelectionModes.Create)" />
|
|
<span>
|
|
<strong>Create a new book</strong>
|
|
<small>Add a book shell to @Model.SelectedProjectName.</small>
|
|
</span>
|
|
</label>
|
|
@if (Model.BookOptions.Any())
|
|
{
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="Mode" value="@OnboardingSelectionModes.Select" checked="@(Model.BookForm.Mode == OnboardingSelectionModes.Select)" />
|
|
<span>
|
|
<strong>Use an existing book</strong>
|
|
<small>Continue with a book already in this project.</small>
|
|
</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
|
|
<section class="onboarding-form-section">
|
|
<h2>Create new book</h2>
|
|
<div class="row g-3">
|
|
<div class="col-md-7">
|
|
<label class="form-label" for="BookTitle">Book title</label>
|
|
<input class="form-control" id="BookTitle" name="BookTitle" maxlength="200" value="@Model.BookForm.BookTitle" />
|
|
@Html.ValidationMessage("BookTitle", null, new { @class = "text-danger" })
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label" for="Subtitle">Subtitle</label>
|
|
<input class="form-control" id="Subtitle" name="Subtitle" maxlength="200" value="@Model.BookForm.Subtitle" />
|
|
@Html.ValidationMessage("Subtitle", null, new { @class = "text-danger" })
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label" for="TargetWordCount">Target word count</label>
|
|
<input class="form-control" id="TargetWordCount" name="TargetWordCount" type="number" min="1" value="@Model.BookForm.TargetWordCount" />
|
|
@Html.ValidationMessage("TargetWordCount", null, new { @class = "text-danger" })
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@if (Model.BookOptions.Any())
|
|
{
|
|
<section class="onboarding-form-section">
|
|
<h2>Use existing book</h2>
|
|
<div class="onboarding-option-grid">
|
|
@foreach (var book in Model.BookOptions)
|
|
{
|
|
<label class="onboarding-option-card">
|
|
<input type="radio" name="SelectedBookID" value="@book.BookID" checked="@(book.BookID == Model.BookForm.SelectedBookID)" />
|
|
<span>
|
|
<strong>@book.DisplayTitle</strong>
|
|
<small>@book.Detail</small>
|
|
</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
@Html.ValidationMessage("SelectedBookID", null, new { @class = "text-danger" })
|
|
</section>
|
|
}
|
|
|
|
<div class="onboarding-actions">
|
|
<button class="btn btn-outline-secondary" type="submit" formaction="/onboarding/back" formnovalidate name="currentStep" value="@OnboardingSteps.Book">Back</button>
|
|
<button class="btn btn-primary" type="submit">Continue</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
else
|
|
{
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">Ready for the next phase</p>
|
|
@if (Model.IsMicrosoftWordPath)
|
|
{
|
|
<h1 id="onboarding-title">Connect your Word Companion</h1>
|
|
<p>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.</p>
|
|
}
|
|
else
|
|
{
|
|
<h1 id="onboarding-title">Next: upload a Word export</h1>
|
|
<p>In a later phase, PlotDirector will let you upload a .docx export from your writing tool and connect it to @Model.SelectedBookTitle.</p>
|
|
}
|
|
</div>
|
|
@if (Model.IsMicrosoftWordPath)
|
|
{
|
|
<section class="onboarding-companion-card @(Model.CompanionPresence.IsConnected ? "is-connected" : "is-offline")"
|
|
data-word-companion-presence
|
|
data-word-companion-status="@Model.CompanionPresence.Status"
|
|
aria-label="Word Companion connection">
|
|
<div class="onboarding-companion-mark" aria-hidden="true">
|
|
<span></span>
|
|
</div>
|
|
<div class="onboarding-companion-body">
|
|
<p class="eyebrow">Word Companion</p>
|
|
<h2 data-word-companion-waiting-status>@(Model.CompanionPresence.IsConnected ? "Word Companion Connected" : "Waiting for Word Companion...")</h2>
|
|
<p data-word-companion-connected-copy>
|
|
@CompanionScanMessage(Model.CompanionPresence, Model.ScanState)
|
|
</p>
|
|
<dl class="onboarding-companion-details">
|
|
<div>
|
|
<dt>Version</dt>
|
|
<dd data-word-companion-version>@(Model.CompanionPresence.CompanionVersion ?? "Waiting")</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Document</dt>
|
|
<dd data-word-companion-document>@(Model.CompanionPresence.CurrentDocumentName ?? "No document reported yet")</dd>
|
|
</div>
|
|
</dl>
|
|
<section class="onboarding-scan-panel"
|
|
data-onboarding-scan-panel
|
|
data-scan-status="@Model.ScanState.Status"
|
|
aria-label="Manuscript scan">
|
|
<div class="onboarding-scan-status">
|
|
<span data-onboarding-scan-message>@ScanMessage(Model.ScanState)</span>
|
|
<strong data-onboarding-scan-percent>@ScanPercent(Model.ScanState)</strong>
|
|
</div>
|
|
<div class="onboarding-scan-progress" aria-hidden="true">
|
|
<span data-onboarding-scan-progress style="width:@(Model.ScanState.PercentComplete.GetValueOrDefault())%"></span>
|
|
</div>
|
|
<div class="onboarding-scan-counts">
|
|
<div>
|
|
<span>Chapters</span>
|
|
<strong data-onboarding-scan-chapters>@Model.ScanState.ChapterCount</strong>
|
|
</div>
|
|
<div>
|
|
<span>Scenes</span>
|
|
<strong data-onboarding-scan-scenes>@Model.ScanState.SceneCount</strong>
|
|
</div>
|
|
<div>
|
|
<span>Characters</span>
|
|
<strong data-onboarding-scan-characters>@Model.ScanState.CharacterCandidateCount</strong>
|
|
</div>
|
|
<div>
|
|
<span>Words</span>
|
|
<strong data-onboarding-scan-words>@Model.ScanState.TotalWordCount.ToString("N0")</strong>
|
|
</div>
|
|
</div>
|
|
<div class="onboarding-scan-actions">
|
|
<button class="btn btn-primary"
|
|
type="button"
|
|
data-onboarding-scan-start
|
|
disabled="@(!CanStartScan(Model.CompanionPresence, Model.ScanState))">
|
|
@(Model.ScanState.IsComplete ? "Scan again" : "Scan manuscript")
|
|
</button>
|
|
<a class="btn btn-outline-primary @(Model.ScanState.IsComplete ? string.Empty : "disabled")"
|
|
data-onboarding-scan-review
|
|
href="@(Model.ScanState.PreviewID.HasValue ? Url.Action("ScanReview", "Onboarding", new { previewId = Model.ScanState.PreviewID }) : "#")"
|
|
aria-disabled="@(!Model.ScanState.IsComplete)">
|
|
@(string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal) ? "Edit review" : "Review scan")
|
|
</a>
|
|
</div>
|
|
@if (string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
|
{
|
|
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
|
<div>
|
|
<p class="onboarding-scan-next">Review saved. PlotDirector can now create the approved structure.</p>
|
|
<p class="onboarding-build-message" data-onboarding-build-message>Ready to prepare approved chapters for Story Intelligence.</p>
|
|
</div>
|
|
<strong data-onboarding-build-percent></strong>
|
|
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
|
<span data-onboarding-build-progress></span>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-success"
|
|
type="button"
|
|
data-onboarding-build-start
|
|
data-preview-id="@Model.ScanState.PreviewID">
|
|
Prepare Chapters
|
|
</button>
|
|
}
|
|
</section>
|
|
</div>
|
|
</section>
|
|
}
|
|
<dl class="onboarding-summary">
|
|
<div>
|
|
<dt>Writing journey</dt>
|
|
<dd>@DisplayJourney(Model.WritingJourney)</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Writing software</dt>
|
|
<dd>@DisplaySoftware(Model.WritingSoftware)</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Project</dt>
|
|
<dd>@(Model.SelectedProjectName ?? "Not set")</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Book</dt>
|
|
<dd>@(Model.SelectedBookTitle ?? "Not set")</dd>
|
|
</div>
|
|
</dl>
|
|
<div class="onboarding-actions">
|
|
<form asp-controller="Onboarding" asp-action="Back" method="post">
|
|
<button class="btn btn-outline-secondary" type="submit" name="currentStep" value="@OnboardingSteps.NextPathPreview">Back</button>
|
|
</form>
|
|
@if (Model.ProjectID.HasValue)
|
|
{
|
|
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Go to Project Overview</a>
|
|
}
|
|
<form asp-controller="Onboarding" asp-action="Finish" method="post">
|
|
<button class="btn btn-primary" type="submit">@(Model.IsMicrosoftWordPath ? "Continue" : "Finish setup for now")</button>
|
|
</form>
|
|
</div>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
@section Scripts {
|
|
<partial name="_ValidationScriptsPartial" />
|
|
}
|
|
|
|
@functions {
|
|
private static string DisplayJourney(string? value) => value switch
|
|
{
|
|
WritingJourneyValues.PlanningNewStory => "Planning a brand new story",
|
|
WritingJourneyValues.AlreadyStarted => "Already started writing",
|
|
WritingJourneyValues.ManuscriptMostlyComplete => "Manuscript largely complete",
|
|
_ => "Not set"
|
|
};
|
|
|
|
private static string DisplaySoftware(string? value) => value switch
|
|
{
|
|
WritingSoftwareValues.MicrosoftWord => "Microsoft Word",
|
|
WritingSoftwareValues.GoogleDocs => "Google Docs",
|
|
WritingSoftwareValues.Scrivener => "Scrivener",
|
|
WritingSoftwareValues.LibreOfficeOpenOffice => "LibreOffice / OpenOffice",
|
|
WritingSoftwareValues.MarkdownObsidian => "Markdown / Obsidian",
|
|
WritingSoftwareValues.Other => "Other",
|
|
_ => "Not set"
|
|
};
|
|
|
|
private static bool CanStartScan(CompanionPresenceViewModel presence, ManuscriptScanStateViewModel scan)
|
|
=> presence.IsConnected && presence.DocumentOpen && !scan.IsRunning;
|
|
|
|
private static string CompanionScanMessage(CompanionPresenceViewModel presence, ManuscriptScanStateViewModel scan)
|
|
{
|
|
if (!presence.IsConnected)
|
|
{
|
|
return "Open Word and start the Companion. This page will update automatically.";
|
|
}
|
|
|
|
if (!presence.DocumentOpen)
|
|
{
|
|
return "Open your manuscript in Word, then the scan button will appear.";
|
|
}
|
|
|
|
if (scan.IsComplete)
|
|
{
|
|
return "Scan complete. Review the structure before anything is imported.";
|
|
}
|
|
|
|
if (scan.IsRunning)
|
|
{
|
|
return "The Companion is scanning your manuscript now.";
|
|
}
|
|
|
|
if (scan.IsFailed)
|
|
{
|
|
return "The scan needs another try. Your story data has not been changed.";
|
|
}
|
|
|
|
return "Your Companion is connected. Scan your manuscript when you are ready.";
|
|
}
|
|
|
|
private static string ScanMessage(ManuscriptScanStateViewModel scan)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(scan.Message))
|
|
{
|
|
return scan.Message;
|
|
}
|
|
|
|
return scan.Status switch
|
|
{
|
|
ManuscriptScanStatuses.Complete => "We found:",
|
|
ManuscriptScanStatuses.Running => "Preparing document scan",
|
|
ManuscriptScanStatuses.Failed => "The scan could not finish.",
|
|
_ => "Ready to scan"
|
|
};
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|