264 lines
14 KiB
Plaintext
264 lines
14 KiB
Plaintext
@model BookDetailViewModel
|
|
@{
|
|
ViewData["Title"] = Model.Book.BookTitle;
|
|
ViewData["ProjectSection"] = "Overview";
|
|
}
|
|
|
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
|
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
|
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
|
<span>@Model.Book.BookTitle</span>
|
|
</nav>
|
|
|
|
<partial name="_ProjectSectionNav" model="Model.Project" />
|
|
|
|
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
|
{
|
|
<div class="alert alert-info">@archiveMessage</div>
|
|
}
|
|
@if (TempData["ArchiveError"] is string archiveError)
|
|
{
|
|
<div class="alert alert-danger">@archiveError</div>
|
|
}
|
|
@if (TempData["ManuscriptMessage"] is string manuscriptMessage)
|
|
{
|
|
<div class="alert alert-info">@manuscriptMessage</div>
|
|
}
|
|
|
|
<div class="page-heading">
|
|
<div>
|
|
<p class="eyebrow"><a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a></p>
|
|
<h1>Book @Model.Book.BookNumber: @Model.Book.BookTitle <help-icon key="books.overview" /></h1>
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.Subtitle))
|
|
{
|
|
<p class="lead-text">@Model.Book.Subtitle</p>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.Tagline))
|
|
{
|
|
<p class="muted">@Model.Book.Tagline</p>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.Description))
|
|
{
|
|
<p class="lead-text">@Model.Book.Description</p>
|
|
}
|
|
</div>
|
|
<div class="button-row">
|
|
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Book.BookID">Edit book</a>
|
|
<a class="btn btn-primary" asp-controller="Chapters" asp-action="Create" asp-route-bookId="@Model.Book.BookID">Add chapter</a>
|
|
<div class="dropdown pw-actions">
|
|
<button class="btn pw-actions__toggle dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
Actions
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-end">
|
|
<a class="dropdown-item" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Story health</a>
|
|
<div class="dropdown-divider"></div>
|
|
<form asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this book? This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
|
|
<input type="hidden" name="id" value="@Model.Book.BookID" />
|
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
|
<button class="dropdown-item text-danger" type="submit">Archive Book</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (Model.StoryIntelligenceAction.HasCanonicalSceneSources || Model.StoryIntelligencePipeline is not null)
|
|
{
|
|
var pipeline = Model.StoryIntelligencePipeline;
|
|
var action = Model.StoryIntelligenceAction;
|
|
<section class="list-section">
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
|
|
<div>
|
|
<p class="eyebrow">Story Intelligence</p>
|
|
<h2>@action.StatusLabel</h2>
|
|
<p class="muted mb-0">@(pipeline is null ? action.Description : StoryIntelligenceDescription(pipeline))</p>
|
|
</div>
|
|
@if (pipeline is null)
|
|
{
|
|
<form asp-controller="Onboarding" asp-action="StartBookStoryIntelligence" asp-route-bookId="@Model.Book.BookID" method="post">
|
|
<button class="btn btn-primary" type="submit" disabled="@(!action.CanStart)">@action.ActionLabel</button>
|
|
</form>
|
|
}
|
|
else
|
|
{
|
|
@if (action.CanContinue)
|
|
{
|
|
<a class="btn btn-primary"
|
|
asp-controller="Onboarding"
|
|
asp-action="ContinueStoryIntelligence"
|
|
asp-route-bookId="@Model.Book.BookID">@action.ActionLabel</a>
|
|
}
|
|
else
|
|
{
|
|
<span class="status-pill">Analysis running</span>
|
|
}
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
<section class="list-section">
|
|
<h2>Book metadata</h2>
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<span class="status-pill">@Model.Book.Status</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@Model.Book.CurrentWordCount.ToString("N0")</strong>
|
|
<span class="muted">current words</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.TargetWordCount?.ToString("N0") ?? "Not set")</strong>
|
|
<span class="muted">target words</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.ProgressPercentage.HasValue ? $"{Model.Book.ProgressPercentage:0.#}%" : "Not set")</strong>
|
|
<span class="muted">progress</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@Model.Book.ChapterCount.ToString("N0")</strong>
|
|
<span class="muted">chapters</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@Model.Book.SceneCount.ToString("N0")</strong>
|
|
<span class="muted">scenes</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.WritingStartDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
|
<span class="muted">writing start</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.TargetCompletionDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
|
<span class="muted">target completion</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.PublicationDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
|
<span class="muted">publication</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
@if (Model.ManuscriptDocument is null)
|
|
{
|
|
<strong>No manuscript linked</strong>
|
|
}
|
|
else
|
|
{
|
|
<strong>Manuscript linked</strong>
|
|
<span class="muted">Last synced: @(Model.ManuscriptDocument.LastSyncUtc?.ToString("dd MMM yyyy HH:mm") ?? "Never")</span>
|
|
<form asp-action="UnlinkManuscript" method="post" class="mt-2" data-confirm-message="This will unlink the Word manuscript from this Book in PlotDirector. It will not delete chapters, scenes, characters, assets, locations, or manuscript text. If the Word document is still open, you should also unlink it from the Word Companion so the hidden document metadata is removed. Continue?">
|
|
<input type="hidden" name="id" value="@Model.Book.BookID" />
|
|
<button type="submit" class="btn btn-outline-danger btn-sm">Unlink Manuscript</button>
|
|
</form>
|
|
}
|
|
<span class="muted">Manuscript Status</span>
|
|
</div>
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.ShortDescription))
|
|
{
|
|
<div class="col-12">
|
|
<p>@Model.Book.ShortDescription</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="list-section">
|
|
<h2>Chapters <help-icon key="books.chapters" /></h2>
|
|
@if (!Model.Chapters.Any())
|
|
{
|
|
<p class="muted">No chapters yet.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Chapter</th>
|
|
<th>Title</th>
|
|
<th>Status</th>
|
|
<th>Manuscript</th>
|
|
<th>Summary</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var chapter in Model.Chapters)
|
|
{
|
|
<tr>
|
|
<td class="narrow">@chapter.ChapterNumber</td>
|
|
<td><a asp-controller="Chapters" asp-action="Details" asp-route-id="@chapter.ChapterID">@chapter.ChapterTitle</a></td>
|
|
<td>
|
|
<span class="status-pill">@chapter.RevisionStatusName</span>
|
|
@if (!string.IsNullOrWhiteSpace(chapter.ChapterPurposeName))
|
|
{
|
|
<span class="status-pill">@chapter.ChapterPurposeName</span>
|
|
}
|
|
</td>
|
|
<td><span class="status-pill">@(chapter.IsLinkedToManuscript ? "🔗 Linked" : "📝 Planned")</span></td>
|
|
<td>@chapter.Summary</td>
|
|
<td class="text-end">
|
|
<a class="btn btn-outline-secondary btn-sm" asp-controller="Chapters" asp-action="Edit" asp-route-id="@chapter.ChapterID">Edit</a>
|
|
<form asp-controller="Chapters" asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this chapter? This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
|
|
<input type="hidden" name="id" value="@chapter.ChapterID" />
|
|
<input type="hidden" name="bookId" value="@Model.Book.BookID" />
|
|
<button class="btn btn-outline-danger btn-sm" type="submit">Archive</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
@functions {
|
|
private static string StoryIntelligenceDescription(StoryIntelligenceBookPipelineState pipeline)
|
|
{
|
|
if (pipeline.IsComplete)
|
|
{
|
|
return "Story Intelligence review is saved for this book.";
|
|
}
|
|
|
|
if (string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "Story Intelligence is analysing this book in the background.";
|
|
}
|
|
|
|
return pipeline.CurrentReviewStage switch
|
|
{
|
|
StoryIntelligencePipelineStages.NarrativeReview => "Open the Review Centre and choose which saved category to handle.",
|
|
StoryIntelligencePipelineStages.RelationshipReview => "Open the Review Centre and choose which saved category to handle.",
|
|
StoryIntelligencePipelineStages.AssetReview => "Open the Review Centre and choose which saved category to handle.",
|
|
StoryIntelligencePipelineStages.LocationReview => "Open the Review Centre and choose which saved category to handle.",
|
|
StoryIntelligencePipelineStages.CharacterReview => "Analysis is complete. Resolve character identities before reviewing dependent story intelligence.",
|
|
StoryIntelligencePipelineStages.SceneReview => "Story Intelligence analysis has not finished for this book.",
|
|
_ when StoryIntelligenceReadyForNarrative(pipeline) => "Open the Review Centre and choose which saved category to handle.",
|
|
_ when StoryIntelligenceReadyForRelationships(pipeline) => "Open the Review Centre and choose which saved category to handle.",
|
|
_ when StoryIntelligenceReadyForAssets(pipeline) => "Open the Review Centre and choose which saved category to handle.",
|
|
_ when StoryIntelligenceReadyForLocations(pipeline) => "Open the Review Centre and choose which saved category to handle.",
|
|
_ => "PlotDirector will open the Review Centre for this book."
|
|
};
|
|
}
|
|
|
|
private static bool StoryIntelligenceReadyForNarrative(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.KnowledgeImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.NarrativeReview or StoryIntelligencePipelineStages.NarrativeImport;
|
|
|
|
private static bool StoryIntelligenceReadyForRelationships(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.AssetImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport;
|
|
|
|
private static bool StoryIntelligenceReadyForAssets(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.LocationImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
|
|
or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
|
|
|| (string.Equals(pipeline.CurrentStage, StoryIntelligencePipelineStages.Complete, StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.RelationshipImport, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private static bool StoryIntelligenceReadyForLocations(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.CharacterImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.LocationReview or StoryIntelligencePipelineStages.LocationImport
|
|
or StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
|
|
or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport;
|
|
}
|