237 lines
14 KiB
Plaintext
237 lines
14 KiB
Plaintext
@model StoryIntelligenceProgressViewModel
|
|
@{
|
|
ViewData["Title"] = "Review assets";
|
|
}
|
|
|
|
<section class="onboarding-shell" aria-labelledby="story-asset-title">
|
|
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
|
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Assets" })" />
|
|
<div class="onboarding-copy">
|
|
<p class="eyebrow">Review Story Intelligence</p>
|
|
<h1 id="story-asset-title">Review Assets</h1>
|
|
<p>Approve the important objects PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.</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="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="story-future-stage-grid">
|
|
@FutureStage("Review Relationships")
|
|
@FutureStage("Review Knowledge")
|
|
@FutureStage("Review Continuity")
|
|
@FutureStage("Timeline Events")
|
|
</div>
|
|
<div class="onboarding-actions">
|
|
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID">Back to locations</a>
|
|
<form asp-action="ImportStoryIntelligenceAssets" method="post">
|
|
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
|
<button class="btn btn-primary" type="submit">Continue</button>
|
|
</form>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="story-review-card-actions">
|
|
<button class="btn btn-outline-primary btn-sm" type="button" data-asset-bulk="approve-all">Approve all</button>
|
|
<button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="approve-selected">Approve selected</button>
|
|
<button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="ignore-selected">Ignore selected</button>
|
|
<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>
|
|
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
|
<div class="story-character-card-grid">
|
|
@for (var i = 0; i < Model.AssetReview.Candidates.Count; i++)
|
|
{
|
|
var candidate = Model.AssetReview.Candidates[i];
|
|
<details class="story-character-card" open data-asset-card>
|
|
<summary>
|
|
<input type="checkbox" checked data-asset-selected aria-label="Select @candidate.AssetName" />
|
|
<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>Possible existing asset</strong>
|
|
<p>@candidate.AssetName may already be @candidate.ExistingAssetName. Choose whether to link them or create a separate asset.</p>
|
|
</div>
|
|
}
|
|
|
|
<fieldset class="story-character-actions">
|
|
<legend>Decision</legend>
|
|
<label>
|
|
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Approve" checked data-asset-action />
|
|
@(candidate.IsExistingMatch ? "Link to existing" : "Create new asset")
|
|
</label>
|
|
@if (candidate.IsExistingMatch)
|
|
{
|
|
<label>
|
|
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.CreateSeparate" data-asset-action />
|
|
Create separate asset
|
|
</label>
|
|
}
|
|
<label>
|
|
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Alias" data-asset-action />
|
|
Alias/variant of another asset
|
|
</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</label>
|
|
<input id="asset-import-name-@i" class="form-control" name="Assets[@i].ImportName" value="@candidate.ImportName" />
|
|
</div>
|
|
|
|
<div data-asset-alias-panel hidden>
|
|
<label class="form-label" for="asset-alias-target-@i">Alias/variant target</label>
|
|
<select id="asset-alias-target-@i" class="form-select" name="Assets[@i].AliasTargetKey" data-asset-alias-target>
|
|
<option value="">Choose asset...</option>
|
|
@foreach (var target in Model.AssetReview.Candidates.Where(target => !string.Equals(target.Key, candidate.Key, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
<option value="@target.Key">@target.AssetName@(target.IsExistingMatch ? $" -> {target.ExistingAssetName}" : string.Empty)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
}
|
|
</div>
|
|
|
|
<div class="onboarding-actions">
|
|
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID">Back to locations</a>
|
|
<button class="btn btn-primary" type="submit">Create assets</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.Approve";
|
|
const isCanonicalTarget = (card) => {
|
|
const action = actionFor(card);
|
|
return action !== "@StoryIntelligenceAssetImportActions.Ignore" && action !== "@StoryIntelligenceAssetImportActions.Alias";
|
|
};
|
|
const updateAliasTargets = () => {
|
|
const allCards = cards();
|
|
for (const card of allCards) {
|
|
const body = card.querySelector("[data-asset-key]");
|
|
const ownKey = body?.getAttribute("data-asset-key") || "";
|
|
const select = card.querySelector("[data-asset-alias-target]");
|
|
if (!select) continue;
|
|
|
|
const previous = select.value;
|
|
for (const option of Array.from(select.options)) {
|
|
if (!option.value) continue;
|
|
const targetCard = allCards.find(item => item.querySelector("[data-asset-key]")?.getAttribute("data-asset-key") === option.value);
|
|
option.disabled = option.value === ownKey || !targetCard || !isCanonicalTarget(targetCard);
|
|
}
|
|
if (select.selectedOptions[0]?.disabled) {
|
|
select.value = "";
|
|
} else {
|
|
select.value = previous;
|
|
}
|
|
}
|
|
};
|
|
const updateCard = (card) => {
|
|
const action = actionFor(card);
|
|
const namePanel = card.querySelector("[data-asset-import-name-panel]");
|
|
const aliasPanel = card.querySelector("[data-asset-alias-panel]");
|
|
if (namePanel) namePanel.hidden = action === "@StoryIntelligenceAssetImportActions.Ignore" || action === "@StoryIntelligenceAssetImportActions.Alias";
|
|
if (aliasPanel) aliasPanel.hidden = action !== "@StoryIntelligenceAssetImportActions.Alias";
|
|
updateAliasTargets();
|
|
};
|
|
document.querySelectorAll("[data-asset-bulk]").forEach(button => {
|
|
button.addEventListener("click", () => {
|
|
const action = button.getAttribute("data-asset-bulk");
|
|
const selectedCards = cards().filter(card => card.querySelector("[data-asset-selected]")?.checked);
|
|
if (action === "approve-all") cards().forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Approve"));
|
|
if (action === "approve-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Approve"));
|
|
if (action === "ignore-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Ignore"));
|
|
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]")));
|
|
});
|
|
cards().forEach(updateCard);
|
|
form.addEventListener("submit", () => {
|
|
cards()
|
|
.filter(card => !card.querySelector("[data-asset-selected]")?.checked)
|
|
.forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.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>");
|
|
}
|