PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml

160 lines
9.7 KiB
Plaintext

@model StoryIntelligenceProgressViewModel
@{
ViewData["Title"] = "Review characters";
}
<section class="onboarding-shell" aria-labelledby="story-character-title">
<div class="onboarding-panel onboarding-review-panel story-review-page">
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Characters" })" />
<div class="onboarding-copy">
<p class="eyebrow">Review Story Intelligence</p>
<h1 id="story-character-title">Review Characters</h1>
<p>Approve the characters PlotDirector should create or link from your imported scenes. You stay in control of every character record.</p>
</div>
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
{
<div class="alert alert-danger">@error</div>
}
@if (TempData["OnboardingStoryIntelligenceMessage"] is string message)
{
<div class="alert alert-success">@message</div>
}
<partial name="_StoryIntelligencePipelineSummary" model="Model.PipelineDashboard" />
<section class="story-review-chapter-list" aria-label="Character review">
@if (Model.CharacterReview.Candidates.Count == 0)
{
<div class="story-review-note">
<strong>No character decisions are waiting.</strong>
<p class="mb-0">Characters are already up to date, or there were no character suggestions ready to import.</p>
</div>
<div class="story-future-stage-grid">
@FutureStage("Review Locations")
@FutureStage("Review Assets")
@FutureStage("Review Relationships")
@FutureStage("Review Knowledge")
</div>
<div class="onboarding-actions">
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">Back to scenes</a>
<a class="btn btn-primary" asp-action="StoryIntelligenceComplete" asp-route-batchId="@Model.BatchID">Continue</a>
</div>
}
else
{
<div class="story-review-card-actions">
<button class="btn btn-outline-primary btn-sm" type="button" data-character-bulk="approve-all">Approve all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="approve-selected">Approve selected</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="ignore-selected">Ignore selected</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="expand">Expand all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="collapse">Collapse all</button>
</div>
<form asp-action="ImportStoryIntelligenceCharacters" method="post" data-character-review-form>
<input type="hidden" name="BatchID" value="@Model.BatchID" />
<div class="story-character-card-grid">
@for (var i = 0; i < Model.CharacterReview.Candidates.Count; i++)
{
var candidate = Model.CharacterReview.Candidates[i];
<details class="story-character-card" open data-character-card>
<summary>
<input type="checkbox" checked data-character-selected aria-label="Select @candidate.CharacterName" />
<span>
<strong>@candidate.CharacterName</strong>
<small>@candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small>
</span>
<em>@candidate.Confidence</em>
</summary>
<input type="hidden" name="Characters[@i].Key" value="@candidate.Key" />
<div class="story-character-card__body">
<dl>
<div><dt>First appearance</dt><dd>@Display(candidate.ExampleFirstAppearance)</dd></div>
<div><dt>Possible aliases</dt><dd>@(candidate.PossibleAliases.Count == 0 ? "None detected" : string.Join(", ", candidate.PossibleAliases))</dd></div>
<div><dt>Existing match</dt><dd>@(candidate.ExistingCharacterName ?? "None")</dd></div>
</dl>
@if (candidate.IsExistingMatch)
{
<div class="story-review-note">
<strong>Possible existing character</strong>
<p>@candidate.CharacterName may already be @candidate.ExistingCharacterName. Choose whether to link them or create a separate character.</p>
</div>
}
<fieldset class="story-character-actions">
<legend>Decision</legend>
<label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Approve" checked data-character-action />
@(candidate.IsExistingMatch ? "Link to existing" : "Create new character")
</label>
@if (candidate.IsExistingMatch)
{
<label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.CreateSeparate" data-character-action />
Create separate character
</label>
}
<label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Ignore" data-character-action />
Ignore
</label>
</fieldset>
<label class="form-label" for="character-import-name-@i">Import name</label>
<input id="character-import-name-@i" class="form-control" name="Characters[@i].ImportName" value="@candidate.ImportName" />
</div>
</details>
}
</div>
<div class="onboarding-actions">
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">Back to scenes</a>
<button class="btn btn-primary" type="submit">Create characters</button>
</div>
</form>
}
</section>
</div>
</section>
@section Scripts {
<script>
(() => {
const form = document.querySelector("[data-character-review-form]");
if (!form) return;
const cards = () => Array.from(form.querySelectorAll("[data-character-card]"));
const setAction = (card, action) => {
const input = card.querySelector(`[data-character-action][value="${action}"]`);
if (input) input.checked = true;
};
document.querySelectorAll("[data-character-bulk]").forEach(button => {
button.addEventListener("click", () => {
const action = button.getAttribute("data-character-bulk");
const selectedCards = cards().filter(card => card.querySelector("[data-character-selected]")?.checked);
if (action === "approve-all") cards().forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Approve"));
if (action === "approve-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Approve"));
if (action === "ignore-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore"));
if (action === "expand") cards().forEach(card => card.open = true);
if (action === "collapse") cards().forEach(card => card.open = false);
});
});
form.addEventListener("submit", () => {
cards()
.filter(card => !card.querySelector("[data-character-selected]")?.checked)
.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore"));
});
})();
</script>
}
@functions {
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value;
private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label)
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
}