Phase 20AJ – Rationalise Story Intelligence Entry Points
This commit is contained in:
parent
058b3d10dc
commit
5b4c78c040
@ -7,7 +7,7 @@ using PlotLine.ViewModels;
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public sealed class BooksController(IBookService books) : Controller
|
||||
public sealed class BooksController(IBookService books, IOnboardingService onboarding) : Controller
|
||||
{
|
||||
public async Task<IActionResult> Details(int id)
|
||||
{
|
||||
@ -31,6 +31,11 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Save(BookEditViewModel model)
|
||||
{
|
||||
if (model.BeginMode is not (BookBeginModes.Manual or BookBeginModes.Import))
|
||||
{
|
||||
model.BeginMode = BookBeginModes.Manual;
|
||||
}
|
||||
|
||||
if (!BookStatuses.IsValid(model.Status))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Status), "Select a valid book status.");
|
||||
@ -68,6 +73,7 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
||||
createModel.PublicationDate = model.PublicationDate;
|
||||
createModel.UsesExplicitScenes = model.UsesExplicitScenes;
|
||||
createModel.BeginMode = model.BeginMode;
|
||||
await books.PopulateEditContextAsync(createModel);
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
@ -80,6 +86,12 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
if (model.BookID == 0 && string.Equals(model.BeginMode, BookBeginModes.Import, StringComparison.Ordinal))
|
||||
{
|
||||
await onboarding.BeginManuscriptImportForBookAsync(model.ProjectID, result.BookID);
|
||||
return RedirectToAction("Index", "Onboarding");
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id = result.BookID });
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,22 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost("start-manual")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> StartManual()
|
||||
{
|
||||
await onboarding.StartManualSetupAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpPost("start-import")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> StartImport()
|
||||
{
|
||||
await onboarding.StartManuscriptImportSetupAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpGet("scan-review")]
|
||||
public async Task<IActionResult> ScanReview(Guid? previewId)
|
||||
{
|
||||
|
||||
@ -6,6 +6,7 @@ namespace PlotLine.Services;
|
||||
public interface IManuscriptScanPreviewStore
|
||||
{
|
||||
Task<ManuscriptScanState> StartAsync(int userId, int onboardingId, int projectId, int bookId);
|
||||
Task ClearAsync(int userId, int onboardingId);
|
||||
Task<ManuscriptScanState> ProgressAsync(int userId, ManuscriptScanProgress progress);
|
||||
Task<ManuscriptScanState> CompleteAsync(int userId, ManuscriptScanPreview preview);
|
||||
Task<ManuscriptScanState> FailAsync(int userId, int onboardingId, string message);
|
||||
@ -45,6 +46,12 @@ public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore
|
||||
return Task.FromResult(ToState(session));
|
||||
}
|
||||
|
||||
public Task ClearAsync(int userId, int onboardingId)
|
||||
{
|
||||
sessions.TryRemove((userId, onboardingId), out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<ManuscriptScanState> ProgressAsync(int userId, ManuscriptScanProgress progress)
|
||||
{
|
||||
var key = (userId, progress.OnboardingID);
|
||||
|
||||
@ -16,6 +16,9 @@ public interface IOnboardingService
|
||||
Task<OnboardingNudgeViewModel> GetDashboardNudgeAsync();
|
||||
Task<bool> ShouldShowWordCompanionHeaderAsync();
|
||||
Task MoveToNextStepAsync();
|
||||
Task StartManualSetupAsync();
|
||||
Task StartManuscriptImportSetupAsync();
|
||||
Task BeginManuscriptImportForBookAsync(int projectId, int bookId);
|
||||
Task SetCurrentStepAsync(string currentStep);
|
||||
Task SaveWritingJourneyAsync(string writingJourney);
|
||||
Task SaveWritingSoftwareAsync(string writingSoftware);
|
||||
@ -572,6 +575,42 @@ public sealed class OnboardingService(
|
||||
await onboarding.SetCurrentStepAsync(RequireUserId(), nextStep);
|
||||
}
|
||||
|
||||
public async Task StartManualSetupAsync()
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.PlanningNewStory);
|
||||
var state = await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.Other);
|
||||
await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID);
|
||||
await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.Project);
|
||||
}
|
||||
|
||||
public async Task StartManuscriptImportSetupAsync()
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.ManuscriptMostlyComplete);
|
||||
var state = await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.MicrosoftWord);
|
||||
await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID);
|
||||
await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.Project);
|
||||
}
|
||||
|
||||
public async Task BeginManuscriptImportForBookAsync(int projectId, int bookId)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
var project = await projects.GetForUserAsync(projectId, userId);
|
||||
var book = await books.GetAsync(bookId);
|
||||
if (project is null || !project.IsOwner || project.IsArchived || book is null || book.ProjectID != project.ProjectID || book.IsArchived)
|
||||
{
|
||||
throw new InvalidOperationException("Choose one of your active books before importing a manuscript.");
|
||||
}
|
||||
|
||||
await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.ManuscriptMostlyComplete);
|
||||
await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.MicrosoftWord);
|
||||
await onboarding.SaveProjectAsync(userId, project.ProjectID);
|
||||
var state = await onboarding.SaveBookAsync(userId, book.BookID);
|
||||
await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID);
|
||||
await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.NextPathPreview);
|
||||
}
|
||||
|
||||
public async Task SetCurrentStepAsync(string currentStep)
|
||||
{
|
||||
if (currentStep is not (OnboardingSteps.Welcome
|
||||
|
||||
@ -28,6 +28,7 @@ public sealed class ProjectIndexViewModel
|
||||
public TrialVisibilityViewModel? Trial { get; init; }
|
||||
public OnboardingNudgeViewModel OnboardingNudge { get; init; } = new();
|
||||
public StoryIntelligenceDashboardViewModel StoryIntelligence { get; init; } = new();
|
||||
public bool HasAnyProjects => ActiveProjects.Any() || ArchivedProjects.Any();
|
||||
}
|
||||
|
||||
public sealed class ProjectIndexCardViewModel
|
||||
@ -228,6 +229,13 @@ public sealed class BookEditViewModel
|
||||
public string? Description { get; set; }
|
||||
public Project? Project { get; set; }
|
||||
public IReadOnlyList<SelectListItem> StatusOptions { get; set; } = [];
|
||||
public string BeginMode { get; set; } = BookBeginModes.Manual;
|
||||
}
|
||||
|
||||
public static class BookBeginModes
|
||||
{
|
||||
public const string Manual = "Manual";
|
||||
public const string Import = "Import";
|
||||
}
|
||||
|
||||
public sealed class BookDetailViewModel
|
||||
|
||||
@ -14,7 +14,38 @@
|
||||
<form asp-action="Save" method="post" enctype="multipart/form-data" class="edit-panel">
|
||||
<input asp-for="BookID" type="hidden" />
|
||||
<input asp-for="ProjectID" type="hidden" />
|
||||
@if (Model.BookID != 0)
|
||||
{
|
||||
<input asp-for="BeginMode" type="hidden" value="@BookBeginModes.Manual" />
|
||||
}
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
@if (Model.BookID == 0)
|
||||
{
|
||||
<section class="mb-4" aria-label="Choose how to begin this book">
|
||||
<p class="eyebrow">New book</p>
|
||||
<h2 class="h5">How would you like to begin?</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="onboarding-option-card h-100">
|
||||
<input type="radio" name="BeginMode" value="@BookBeginModes.Manual" checked="@(Model.BeginMode != BookBeginModes.Import)" />
|
||||
<span>
|
||||
<strong>Start with an empty book</strong>
|
||||
<span>Create chapters and scenes manually.</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="onboarding-option-card h-100">
|
||||
<input type="radio" name="BeginMode" value="@BookBeginModes.Import" checked="@(Model.BeginMode == BookBeginModes.Import)" />
|
||||
<span>
|
||||
<strong>Import an existing manuscript</strong>
|
||||
<span>Use the Word Companion and Story Intelligence.</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label asp-for="BookNumber" class="form-label">Book number <help-icon key="books.fields.bookNumber" mode="inline" /></label>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
@{
|
||||
ViewData["Title"] = "Projects";
|
||||
var trial = Model.Trial;
|
||||
var hasAnyProjects = Model.HasAnyProjects;
|
||||
}
|
||||
|
||||
<div class="page-heading">
|
||||
@ -33,33 +34,6 @@
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (Model.OnboardingNudge.ShouldShow && !Model.StoryIntelligence.ShouldShow)
|
||||
{
|
||||
<section class="onboarding-dashboard-nudge @(Model.OnboardingNudge.CompanionConnected ? "is-connected" : string.Empty)"
|
||||
data-word-companion-presence="@(Model.OnboardingNudge.IsCompanionPresence.ToString().ToLowerInvariant())"
|
||||
aria-label="PlotDirector setup">
|
||||
<div>
|
||||
<p class="eyebrow">First setup</p>
|
||||
<h2 data-word-companion-dashboard-title>@Model.OnboardingNudge.Title</h2>
|
||||
<p data-word-companion-dashboard-description>@Model.OnboardingNudge.Description</p>
|
||||
</div>
|
||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="Index">@Model.OnboardingNudge.ButtonText</a>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (Model.StoryIntelligence.ShouldShow)
|
||||
{
|
||||
<section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact"
|
||||
aria-label="Story Intelligence">
|
||||
<div>
|
||||
<p class="eyebrow">Manuscript import</p>
|
||||
<h2>@Model.StoryIntelligence.Title</h2>
|
||||
<p>@Model.StoryIntelligence.Description</p>
|
||||
</div>
|
||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="Index">@Model.StoryIntelligence.ButtonText</a>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
||||
{
|
||||
<div class="alert alert-success">@archiveMessage</div>
|
||||
@ -73,12 +47,19 @@
|
||||
<div class="alert alert-warning">@projectExportMessage</div>
|
||||
}
|
||||
|
||||
@if (!Model.ActiveProjects.Any() && !Model.ArchivedProjects.Any())
|
||||
@if (!hasAnyProjects)
|
||||
{
|
||||
<section class="empty-panel">
|
||||
<h2>Start with a series or standalone novel</h2>
|
||||
<p>Create a project, then add books, chapters and scenes in narrative order.</p>
|
||||
<a class="btn btn-primary" asp-action="Create">Create your first project</a>
|
||||
<h2>Welcome to PlotDirector</h2>
|
||||
<p>Let's create your first project.</p>
|
||||
<div class="button-row justify-content-center">
|
||||
<form asp-controller="Onboarding" asp-action="StartManual" method="post">
|
||||
<button class="btn btn-primary" type="submit">Start writing manually</button>
|
||||
</form>
|
||||
<form asp-controller="Onboarding" asp-action="StartImport" method="post">
|
||||
<button class="btn btn-outline-primary" type="submit">Import an existing manuscript</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user