PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceAssets.cshtml

239 lines
15 KiB
Plaintext

@model StoryIntelligenceProgressViewModel
@{
ViewData["Title"] = "Review assets";
var assetCandidates = Model.AssetReview.Candidates.ToList();
}
<section class="onboarding-shell" aria-labelledby="story-asset-title">
<div class="onboarding-panel onboarding-review-panel story-review-page">
<div class="onboarding-copy story-review-heading">
<p class="eyebrow">Story Intelligence</p>
<h1 id="story-asset-title">Review Assets <help-icon key="storyIntelligence.assets" /></h1>
<p>Review important object suggestions that need a create, same-asset, or ignore decision.</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>
}
<section class="story-review-chapter-list" aria-label="Asset review">
@if (Model.AssetReview.Candidates.Count == 0)
{
<div class="story-review-note">
<strong>No asset decisions are waiting.</strong>
<p class="mb-0">Assets are already up to date, or there were no story-significant asset suggestions ready to import.</p>
</div>
<div class="onboarding-actions">
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
<form asp-action="ImportStoryIntelligenceAssets" method="post" data-story-review-form>
<input type="hidden" name="BatchID" value="@Model.BatchID" />
<button class="btn btn-primary" type="submit" data-busy-label="Saving assets...">Mark reviewed</button>
</form>
</div>
}
else
{
<div class="story-review-card-actions">
<span class="story-review-page-status">
Page @Model.AssetReview.PageNumber.ToString("N0") of @Model.AssetReview.PageCount.ToString("N0")
&middot; Showing @Model.AssetReview.ShowingStart.ToString("N0")-@Model.AssetReview.ShowingEnd.ToString("N0") of @Model.AssetReview.PendingCandidateCount.ToString("N0") pending
</span>
<button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="expand">Expand all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="collapse">Collapse all</button>
</div>
<form asp-action="ImportStoryIntelligenceAssets" method="post" data-asset-review-form data-story-review-form>
<input type="hidden" name="BatchID" value="@Model.BatchID" />
<input type="hidden" name="PageNumber" value="@Model.AssetReview.PageNumber" />
<div class="story-character-card-grid">
@for (var i = 0; i < assetCandidates.Count; i++)
{
var candidate = assetCandidates[i];
<details class="story-character-card" open data-asset-card data-asset-existing-match="@candidate.IsExistingMatch.ToString().ToLowerInvariant()">
<summary class="story-review-card-header">
<span>
<strong>@candidate.AssetName</strong>
<small>@candidate.Category · @candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small>
</span>
<em>@candidate.Confidence</em>
</summary>
<input type="hidden" name="Assets[@i].Key" value="@candidate.Key" />
<div class="story-character-card__body" data-asset-key="@candidate.Key" data-asset-name="@candidate.AssetName">
<dl>
<div><dt>First appearance</dt><dd>@Display(candidate.ExampleFirstAppearance)</dd></div>
<div><dt>Possible owner</dt><dd>@Display(candidate.PossibleOwner)</dd></div>
<div><dt>Existing match</dt><dd>@(candidate.ExistingAssetName ?? "None")</dd></div>
</dl>
@if (candidate.PossibleAliases.Any())
{
<div class="story-review-note">
<strong>Possible aliases</strong>
<p>@string.Join(", ", candidate.PossibleAliases)</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(candidate.ExampleContext))
{
<div class="story-review-note">
<strong>Scene context</strong>
<p>@candidate.ExampleContext</p>
</div>
}
@if (candidate.IsExistingMatch)
{
<div class="story-review-note">
<strong>Existing asset matched</strong>
<p>@candidate.AssetName is linked to @candidate.ExistingAssetName.</p>
</div>
}
@if (candidate.IsExistingMatch)
{
<input type="hidden" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.LinkExisting" data-asset-action />
<input type="hidden" name="Assets[@i].ExistingAssetID" value="@candidate.ExistingAssetID" />
}
else
{
<fieldset class="story-character-actions">
<legend>Decision <help-icon key="storyIntelligence.assets.decisions" mode="inline" /></legend>
<label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.CreateNew" checked data-asset-action />
Create new asset
</label>
<label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.LinkExisting" data-asset-action />
Same asset as
</label>
<label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Ignore" data-asset-action />
Ignore
</label>
</fieldset>
<div data-asset-import-name-panel>
<label class="form-label" for="asset-import-name-@i">Import name <help-icon key="storyIntelligence.review.importName" mode="inline" /></label>
<input id="asset-import-name-@i" class="form-control" name="Assets[@i].ImportName" value="@candidate.ImportName" />
</div>
<div data-asset-same-panel hidden>
<label class="form-label" for="asset-same-target-@i">Same asset as <help-icon key="storyIntelligence.assets.sameAsset" mode="inline" /></label>
<select id="asset-same-target-@i" class="form-select" data-asset-unified-target>
<option value="">Choose asset...</option>
@foreach (var group in Model.AssetReview.AssetTargetOptions
.Where(target => !IsSelfTarget(target, candidate))
.GroupBy(target => target.GroupName))
{
<optgroup label="@group.Key">
@foreach (var target in group)
{
<option value="@target.Value">@TargetLabel(target)</option>
}
</optgroup>
}
</select>
<input type="hidden" name="Assets[@i].ExistingAssetID" data-asset-existing-target />
<input type="hidden" name="Assets[@i].AliasTargetKey" data-asset-alias-target />
</div>
}
</div>
</details>
}
</div>
<div class="onboarding-actions">
<span class="text-muted">
Page @Model.AssetReview.PageNumber.ToString("N0") of @Model.AssetReview.PageCount.ToString("N0")
&middot; Showing @Model.AssetReview.ShowingStart.ToString("N0")-@Model.AssetReview.ShowingEnd.ToString("N0") of @Model.AssetReview.PendingCandidateCount.ToString("N0") pending
</span>
@if (Model.AssetReview.HasPreviousPage)
{
<button class="btn btn-outline-secondary" type="submit" name="intent" value="previous" data-busy-label="Saving page...">Previous</button>
}
@if (Model.AssetReview.HasNextPage)
{
<button class="btn btn-primary" type="submit" name="intent" value="next" data-busy-label="Saving page...">Next</button>
}
else
{
<button class="btn btn-primary" type="submit" name="intent" value="save" data-busy-label="Saving assets...">Save asset decisions</button>
}
</div>
</form>
}
</section>
</div>
</section>
@section Scripts {
<script>
(() => {
const form = document.querySelector("[data-asset-review-form]");
if (!form) return;
const cards = () => Array.from(form.querySelectorAll("[data-asset-card]"));
const setAction = (card, action) => {
const input = card.querySelector(`[data-asset-action][value="${action}"]`);
if (input) {
input.checked = true;
updateCard(card);
}
};
const actionFor = (card) => card.querySelector("[data-asset-action]:checked")?.value || "@StoryIntelligenceAssetImportActions.CreateNew";
const applyUnifiedTarget = (card) => {
const selector = card.querySelector("[data-asset-unified-target]");
const existing = card.querySelector("[data-asset-existing-target]");
const pending = card.querySelector("[data-asset-alias-target]");
const value = selector?.value || "";
if (existing && selector) existing.value = value.startsWith("existing:") ? value.substring("existing:".length) : "";
if (pending && selector) pending.value = value.startsWith("candidate:") ? value.substring("candidate:".length) : "";
};
const updateAliasTargets = () => {
cards().forEach(applyUnifiedTarget);
};
const updateCard = (card) => {
const action = actionFor(card);
const namePanel = card.querySelector("[data-asset-import-name-panel]");
const samePanel = card.querySelector("[data-asset-same-panel]");
applyUnifiedTarget(card);
if (namePanel) namePanel.hidden = action === "@StoryIntelligenceAssetImportActions.Ignore" || action === "@StoryIntelligenceAssetImportActions.LinkExisting";
if (samePanel) samePanel.hidden = action !== "@StoryIntelligenceAssetImportActions.LinkExisting";
updateAliasTargets();
};
document.querySelectorAll("[data-asset-bulk]").forEach(button => {
button.addEventListener("click", () => {
const action = button.getAttribute("data-asset-bulk");
if (action === "expand") cards().forEach(card => card.open = true);
if (action === "collapse") cards().forEach(card => card.open = false);
});
});
form.querySelectorAll("[data-asset-action]").forEach(input => {
input.addEventListener("change", () => updateCard(input.closest("[data-asset-card]")));
});
form.querySelectorAll("[data-asset-unified-target]").forEach(select => {
select.addEventListener("change", () => cards().forEach(updateCard));
});
cards().forEach(updateCard);
})();
</script>
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
}
@functions {
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value;
private static string TargetLabel(StoryIntelligenceAssetTargetOptionViewModel target)
=> string.IsNullOrWhiteSpace(target.AliasSummary) ? target.DisplayName : $"{target.DisplayName} ({target.AliasSummary})";
private static bool IsSelfTarget(StoryIntelligenceAssetTargetOptionViewModel target, StoryIntelligenceAssetReviewCandidateViewModel candidate)
=> (target.AssetID.HasValue && candidate.ExistingAssetID == target.AssetID)
|| (!string.IsNullOrWhiteSpace(target.CandidateKey) && string.Equals(target.CandidateKey, candidate.Key, StringComparison.OrdinalIgnoreCase));
}