Phase 20AQ – Story Intelligence Review UX Polish

This commit is contained in:
Nick Beckley 2026-07-09 21:24:22 +01:00
parent d44217162c
commit ab3b591e4d
11 changed files with 91 additions and 105 deletions

View File

@ -137,12 +137,12 @@ public sealed class StoryIntelligenceAssetImportService(
var requestedName = Clean(choice.ImportName); var requestedName = Clean(choice.ImportName);
var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName; var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName;
var forceCreateSeparate = string.Equals(choice.Action, StoryIntelligenceAssetImportActions.CreateSeparate, StringComparison.OrdinalIgnoreCase); var linkExisting = string.Equals(choice.Action, StoryIntelligenceAssetImportActions.LinkExisting, StringComparison.OrdinalIgnoreCase);
var matchedExistingId = forceCreateSeparate var matchedExistingId = linkExisting
? null ? candidate.ExistingAssetID
: candidate.ExistingAssetID
?? existingIndex.Find(importName)?.StoryAssetID ?? existingIndex.Find(importName)?.StoryAssetID
?? existingIndex.Find(candidate.DisplayName)?.StoryAssetID; ?? existingIndex.Find(candidate.DisplayName)?.StoryAssetID
: null;
var assetId = matchedExistingId; var assetId = matchedExistingId;
if (!assetId.HasValue) if (!assetId.HasValue)

View File

@ -142,12 +142,12 @@ public sealed class StoryIntelligenceCharacterImportService(
var requestedName = Clean(choice.ImportName); var requestedName = Clean(choice.ImportName);
var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName; var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName;
var forceCreateSeparate = string.Equals(choice.Action, StoryIntelligenceCharacterImportActions.CreateSeparate, StringComparison.OrdinalIgnoreCase); var linkExisting = string.Equals(choice.Action, StoryIntelligenceCharacterImportActions.LinkExisting, StringComparison.OrdinalIgnoreCase);
var matchedExistingId = forceCreateSeparate var matchedExistingId = linkExisting
? null ? candidate.ExistingCharacterID
: candidate.ExistingCharacterID
?? existingIndex.Find(importName)?.CharacterID ?? existingIndex.Find(importName)?.CharacterID
?? existingIndex.Find(candidate.DisplayName)?.CharacterID; ?? existingIndex.Find(candidate.DisplayName)?.CharacterID
: null;
var characterId = matchedExistingId; var characterId = matchedExistingId;
if (!characterId.HasValue) if (!characterId.HasValue)

View File

@ -132,12 +132,12 @@ public sealed class StoryIntelligenceLocationImportService(
var requestedName = Clean(choice.ImportName); var requestedName = Clean(choice.ImportName);
var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName; var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName;
var forceCreateSeparate = string.Equals(choice.Action, StoryIntelligenceLocationImportActions.CreateSeparate, StringComparison.OrdinalIgnoreCase); var linkExisting = string.Equals(choice.Action, StoryIntelligenceLocationImportActions.LinkExisting, StringComparison.OrdinalIgnoreCase);
var matchedExistingId = forceCreateSeparate var matchedExistingId = linkExisting
? null ? candidate.ExistingLocationID
: candidate.ExistingLocationID
?? existingIndex.Find(importName)?.LocationID ?? existingIndex.Find(importName)?.LocationID
?? existingIndex.Find(candidate.DisplayName)?.LocationID; ?? existingIndex.Find(candidate.DisplayName)?.LocationID
: null;
var locationId = matchedExistingId; var locationId = matchedExistingId;
if (!locationId.HasValue) if (!locationId.HasValue)

View File

@ -212,16 +212,16 @@ public sealed class StoryIntelligenceCharacterImportForm
public sealed class StoryIntelligenceCharacterImportChoiceForm public sealed class StoryIntelligenceCharacterImportChoiceForm
{ {
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;
public string Action { get; set; } = StoryIntelligenceCharacterImportActions.Approve; public string Action { get; set; } = StoryIntelligenceCharacterImportActions.CreateNew;
public string? ImportName { get; set; } public string? ImportName { get; set; }
public string? AliasTargetKey { get; set; } public string? AliasTargetKey { get; set; }
} }
public static class StoryIntelligenceCharacterImportActions public static class StoryIntelligenceCharacterImportActions
{ {
public const string Approve = "Approve"; public const string CreateNew = "CreateNew";
public const string LinkExisting = "LinkExisting";
public const string Ignore = "Ignore"; public const string Ignore = "Ignore";
public const string CreateSeparate = "CreateSeparate";
public const string Alias = "Alias"; public const string Alias = "Alias";
} }
@ -260,16 +260,16 @@ public sealed class StoryIntelligenceLocationImportForm
public sealed class StoryIntelligenceLocationImportChoiceForm public sealed class StoryIntelligenceLocationImportChoiceForm
{ {
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;
public string Action { get; set; } = StoryIntelligenceLocationImportActions.Approve; public string Action { get; set; } = StoryIntelligenceLocationImportActions.CreateNew;
public string? ImportName { get; set; } public string? ImportName { get; set; }
public string? AliasTargetKey { get; set; } public string? AliasTargetKey { get; set; }
} }
public static class StoryIntelligenceLocationImportActions public static class StoryIntelligenceLocationImportActions
{ {
public const string Approve = "Approve"; public const string CreateNew = "CreateNew";
public const string LinkExisting = "LinkExisting";
public const string Ignore = "Ignore"; public const string Ignore = "Ignore";
public const string CreateSeparate = "CreateSeparate";
public const string Alias = "Alias"; public const string Alias = "Alias";
} }
@ -309,16 +309,16 @@ public sealed class StoryIntelligenceAssetImportForm
public sealed class StoryIntelligenceAssetImportChoiceForm public sealed class StoryIntelligenceAssetImportChoiceForm
{ {
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;
public string Action { get; set; } = StoryIntelligenceAssetImportActions.Approve; public string Action { get; set; } = StoryIntelligenceAssetImportActions.CreateNew;
public string? ImportName { get; set; } public string? ImportName { get; set; }
public string? AliasTargetKey { get; set; } public string? AliasTargetKey { get; set; }
} }
public static class StoryIntelligenceAssetImportActions public static class StoryIntelligenceAssetImportActions
{ {
public const string Approve = "Approve"; public const string CreateNew = "CreateNew";
public const string LinkExisting = "LinkExisting";
public const string Ignore = "Ignore"; public const string Ignore = "Ignore";
public const string CreateSeparate = "CreateSeparate";
public const string Alias = "Alias"; public const string Alias = "Alias";
} }

View File

@ -5,7 +5,7 @@
<section class="onboarding-shell" aria-labelledby="story-asset-complete-title"> <section class="onboarding-shell" aria-labelledby="story-asset-complete-title">
<div class="onboarding-panel onboarding-review-panel"> <div class="onboarding-panel onboarding-review-panel">
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Create Assets" })" /> <partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Complete" })" />
<div class="onboarding-copy"> <div class="onboarding-copy">
<p class="eyebrow">Build Story Database</p> <p class="eyebrow">Build Story Database</p>
<div class="onboarding-success-heading"> <div class="onboarding-success-heading">

View File

@ -47,9 +47,9 @@
else else
{ {
<div class="story-review-card-actions"> <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-primary btn-sm" type="button" data-asset-bulk="create-all">Create 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="link-existing">Link existing matches</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="ignore-all">Ignore all</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="expand">Expand all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="collapse">Collapse all</button> <button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="collapse">Collapse all</button>
</div> </div>
@ -62,7 +62,6 @@
var candidate = Model.AssetReview.Candidates[i]; var candidate = Model.AssetReview.Candidates[i];
<details class="story-character-card" open data-asset-card> <details class="story-character-card" open data-asset-card>
<summary> <summary>
<input type="checkbox" checked data-asset-selected aria-label="Select @candidate.AssetName" />
<span> <span>
<strong>@candidate.AssetName</strong> <strong>@candidate.AssetName</strong>
<small>@candidate.Category · @candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small> <small>@candidate.Category · @candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small>
@ -106,19 +105,16 @@
<fieldset class="story-character-actions"> <fieldset class="story-character-actions">
<legend>Decision</legend> <legend>Decision</legend>
<label> <label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Approve" checked data-asset-action /> <input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.CreateNew" checked data-asset-action />
@(candidate.IsExistingMatch ? "Link to existing" : "Create new asset") Create new asset
</label> </label>
@if (candidate.IsExistingMatch)
{
<label> <label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.CreateSeparate" data-asset-action /> <input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.LinkExisting" data-asset-action @(candidate.IsExistingMatch ? string.Empty : "disabled") />
Create separate asset Link existing asset
</label> </label>
}
<label> <label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Alias" data-asset-action /> <input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Alias" data-asset-action />
Alias/variant of another asset Alias of another asset
</label> </label>
<label> <label>
<input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Ignore" data-asset-action /> <input type="radio" name="Assets[@i].Action" value="@StoryIntelligenceAssetImportActions.Ignore" data-asset-action />
@ -170,7 +166,7 @@
updateCard(card); updateCard(card);
} }
}; };
const actionFor = (card) => card.querySelector("[data-asset-action]:checked")?.value || "@StoryIntelligenceAssetImportActions.Approve"; const actionFor = (card) => card.querySelector("[data-asset-action]:checked")?.value || "@StoryIntelligenceAssetImportActions.CreateNew";
const isCanonicalTarget = (card) => { const isCanonicalTarget = (card) => {
const action = actionFor(card); const action = actionFor(card);
return action !== "@StoryIntelligenceAssetImportActions.Ignore" && action !== "@StoryIntelligenceAssetImportActions.Alias"; return action !== "@StoryIntelligenceAssetImportActions.Ignore" && action !== "@StoryIntelligenceAssetImportActions.Alias";
@ -207,10 +203,13 @@
document.querySelectorAll("[data-asset-bulk]").forEach(button => { document.querySelectorAll("[data-asset-bulk]").forEach(button => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const action = button.getAttribute("data-asset-bulk"); const action = button.getAttribute("data-asset-bulk");
const selectedCards = cards().filter(card => card.querySelector("[data-asset-selected]")?.checked); if (action === "create-all") cards().forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.CreateNew"));
if (action === "approve-all") cards().forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Approve")); if (action === "link-existing") cards().forEach(card => {
if (action === "approve-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Approve")); if (card.querySelector(`[data-asset-action][value="@StoryIntelligenceAssetImportActions.LinkExisting"]:not(:disabled)`)) {
if (action === "ignore-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Ignore")); setAction(card, "@StoryIntelligenceAssetImportActions.LinkExisting");
}
});
if (action === "ignore-all") cards().forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Ignore"));
if (action === "expand") cards().forEach(card => card.open = true); if (action === "expand") cards().forEach(card => card.open = true);
if (action === "collapse") cards().forEach(card => card.open = false); if (action === "collapse") cards().forEach(card => card.open = false);
}); });
@ -219,11 +218,6 @@
input.addEventListener("change", () => updateCard(input.closest("[data-asset-card]"))); input.addEventListener("change", () => updateCard(input.closest("[data-asset-card]")));
}); });
cards().forEach(updateCard); cards().forEach(updateCard);
form.addEventListener("submit", () => {
cards()
.filter(card => !card.querySelector("[data-asset-selected]")?.checked)
.forEach(card => setAction(card, "@StoryIntelligenceAssetImportActions.Ignore"));
});
})(); })();
</script> </script>
} }

View File

@ -5,7 +5,7 @@
<section class="onboarding-shell" aria-labelledby="story-character-complete-title"> <section class="onboarding-shell" aria-labelledby="story-character-complete-title">
<div class="onboarding-panel onboarding-review-panel"> <div class="onboarding-panel onboarding-review-panel">
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Create Characters" })" /> <partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Locations" })" />
<div class="onboarding-copy"> <div class="onboarding-copy">
<p class="eyebrow">Build Story Database</p> <p class="eyebrow">Build Story Database</p>
<div class="onboarding-success-heading"> <div class="onboarding-success-heading">

View File

@ -38,15 +38,15 @@
</div> </div>
<div class="onboarding-actions"> <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-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> <a class="btn btn-primary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID">Continue</a>
</div> </div>
} }
else else
{ {
<div class="story-review-card-actions"> <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-primary btn-sm" type="button" data-character-bulk="create-all">Create 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="link-existing">Link existing matches</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="ignore-all">Ignore all</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="expand">Expand all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="collapse">Collapse all</button> <button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="collapse">Collapse all</button>
</div> </div>
@ -59,7 +59,6 @@
var candidate = Model.CharacterReview.Candidates[i]; var candidate = Model.CharacterReview.Candidates[i];
<details class="story-character-card" open data-character-card> <details class="story-character-card" open data-character-card>
<summary> <summary>
<input type="checkbox" checked data-character-selected aria-label="Select @candidate.CharacterName" />
<span> <span>
<strong>@candidate.CharacterName</strong> <strong>@candidate.CharacterName</strong>
<small>@candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small> <small>@candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small>
@ -87,24 +86,21 @@
<fieldset class="story-character-actions"> <fieldset class="story-character-actions">
<legend>Decision</legend> <legend>Decision</legend>
<label> <label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Approve" checked data-character-action /> <input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.CreateNew" checked data-character-action />
@(candidate.IsExistingMatch ? "Link to existing" : "Create new character") Create new character
</label> </label>
@if (candidate.IsExistingMatch)
{
<label> <label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.CreateSeparate" data-character-action /> <input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.LinkExisting" data-character-action @(candidate.IsExistingMatch ? string.Empty : "disabled") />
Create separate character Link existing character
</label>
}
<label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Ignore" data-character-action />
Ignore
</label> </label>
<label> <label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Alias" data-character-action /> <input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Alias" data-character-action />
Alias of another character Alias of another character
</label> </label>
<label>
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Ignore" data-character-action />
Ignore
</label>
</fieldset> </fieldset>
<div data-character-import-name-panel> <div data-character-import-name-panel>
@ -151,7 +147,7 @@
updateCard(card); updateCard(card);
} }
}; };
const actionFor = (card) => card.querySelector("[data-character-action]:checked")?.value || "@StoryIntelligenceCharacterImportActions.Approve"; const actionFor = (card) => card.querySelector("[data-character-action]:checked")?.value || "@StoryIntelligenceCharacterImportActions.CreateNew";
const isCanonicalTarget = (card) => { const isCanonicalTarget = (card) => {
const action = actionFor(card); const action = actionFor(card);
return action !== "@StoryIntelligenceCharacterImportActions.Ignore" && action !== "@StoryIntelligenceCharacterImportActions.Alias"; return action !== "@StoryIntelligenceCharacterImportActions.Ignore" && action !== "@StoryIntelligenceCharacterImportActions.Alias";
@ -188,10 +184,13 @@
document.querySelectorAll("[data-character-bulk]").forEach(button => { document.querySelectorAll("[data-character-bulk]").forEach(button => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const action = button.getAttribute("data-character-bulk"); const action = button.getAttribute("data-character-bulk");
const selectedCards = cards().filter(card => card.querySelector("[data-character-selected]")?.checked); if (action === "create-all") cards().forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.CreateNew"));
if (action === "approve-all") cards().forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Approve")); if (action === "link-existing") cards().forEach(card => {
if (action === "approve-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Approve")); if (card.querySelector(`[data-character-action][value="@StoryIntelligenceCharacterImportActions.LinkExisting"]:not(:disabled)`)) {
if (action === "ignore-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore")); setAction(card, "@StoryIntelligenceCharacterImportActions.LinkExisting");
}
});
if (action === "ignore-all") cards().forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore"));
if (action === "expand") cards().forEach(card => card.open = true); if (action === "expand") cards().forEach(card => card.open = true);
if (action === "collapse") cards().forEach(card => card.open = false); if (action === "collapse") cards().forEach(card => card.open = false);
}); });
@ -200,11 +199,6 @@
input.addEventListener("change", () => updateCard(input.closest("[data-character-card]"))); input.addEventListener("change", () => updateCard(input.closest("[data-character-card]")));
}); });
cards().forEach(updateCard); cards().forEach(updateCard);
form.addEventListener("submit", () => {
cards()
.filter(card => !card.querySelector("[data-character-selected]")?.checked)
.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore"));
});
})(); })();
</script> </script>
} }

View File

@ -5,7 +5,7 @@
<section class="onboarding-shell" aria-labelledby="story-location-complete-title"> <section class="onboarding-shell" aria-labelledby="story-location-complete-title">
<div class="onboarding-panel onboarding-review-panel"> <div class="onboarding-panel onboarding-review-panel">
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Create Locations" })" /> <partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Assets" })" />
<div class="onboarding-copy"> <div class="onboarding-copy">
<p class="eyebrow">Build Story Database</p> <p class="eyebrow">Build Story Database</p>
<div class="onboarding-success-heading"> <div class="onboarding-success-heading">

View File

@ -44,9 +44,9 @@
else else
{ {
<div class="story-review-card-actions"> <div class="story-review-card-actions">
<button class="btn btn-outline-primary btn-sm" type="button" data-location-bulk="approve-all">Approve all</button> <button class="btn btn-outline-primary btn-sm" type="button" data-location-bulk="create-all">Create all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="approve-selected">Approve selected</button> <button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="link-existing">Link existing matches</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="ignore-selected">Ignore selected</button> <button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="ignore-all">Ignore all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="expand">Expand all</button> <button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="expand">Expand all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="collapse">Collapse all</button> <button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="collapse">Collapse all</button>
</div> </div>
@ -59,7 +59,6 @@
var candidate = Model.LocationReview.Candidates[i]; var candidate = Model.LocationReview.Candidates[i];
<details class="story-character-card" open data-location-card> <details class="story-character-card" open data-location-card>
<summary> <summary>
<input type="checkbox" checked data-location-selected aria-label="Select @candidate.LocationName" />
<span> <span>
<strong>@candidate.LocationName</strong> <strong>@candidate.LocationName</strong>
<small>@candidate.Category · @candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small> <small>@candidate.Category · @candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small>
@ -95,19 +94,16 @@
<fieldset class="story-character-actions"> <fieldset class="story-character-actions">
<legend>Decision</legend> <legend>Decision</legend>
<label> <label>
<input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.Approve" checked data-location-action /> <input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.CreateNew" checked data-location-action />
@(candidate.IsExistingMatch ? "Link to existing" : "Create new location") Create new location
</label> </label>
@if (candidate.IsExistingMatch)
{
<label> <label>
<input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.CreateSeparate" data-location-action /> <input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.LinkExisting" data-location-action @(candidate.IsExistingMatch ? string.Empty : "disabled") />
Create separate location Link existing location
</label> </label>
}
<label> <label>
<input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.Alias" data-location-action /> <input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.Alias" data-location-action />
Alias/variant of another location Alias of another location
</label> </label>
<label> <label>
<input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.Ignore" data-location-action /> <input type="radio" name="Locations[@i].Action" value="@StoryIntelligenceLocationImportActions.Ignore" data-location-action />
@ -159,7 +155,7 @@
updateCard(card); updateCard(card);
} }
}; };
const actionFor = (card) => card.querySelector("[data-location-action]:checked")?.value || "@StoryIntelligenceLocationImportActions.Approve"; const actionFor = (card) => card.querySelector("[data-location-action]:checked")?.value || "@StoryIntelligenceLocationImportActions.CreateNew";
const isCanonicalTarget = (card) => { const isCanonicalTarget = (card) => {
const action = actionFor(card); const action = actionFor(card);
return action !== "@StoryIntelligenceLocationImportActions.Ignore" && action !== "@StoryIntelligenceLocationImportActions.Alias"; return action !== "@StoryIntelligenceLocationImportActions.Ignore" && action !== "@StoryIntelligenceLocationImportActions.Alias";
@ -196,10 +192,13 @@
document.querySelectorAll("[data-location-bulk]").forEach(button => { document.querySelectorAll("[data-location-bulk]").forEach(button => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const action = button.getAttribute("data-location-bulk"); const action = button.getAttribute("data-location-bulk");
const selectedCards = cards().filter(card => card.querySelector("[data-location-selected]")?.checked); if (action === "create-all") cards().forEach(card => setAction(card, "@StoryIntelligenceLocationImportActions.CreateNew"));
if (action === "approve-all") cards().forEach(card => setAction(card, "@StoryIntelligenceLocationImportActions.Approve")); if (action === "link-existing") cards().forEach(card => {
if (action === "approve-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceLocationImportActions.Approve")); if (card.querySelector(`[data-location-action][value="@StoryIntelligenceLocationImportActions.LinkExisting"]:not(:disabled)`)) {
if (action === "ignore-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceLocationImportActions.Ignore")); setAction(card, "@StoryIntelligenceLocationImportActions.LinkExisting");
}
});
if (action === "ignore-all") cards().forEach(card => setAction(card, "@StoryIntelligenceLocationImportActions.Ignore"));
if (action === "expand") cards().forEach(card => card.open = true); if (action === "expand") cards().forEach(card => card.open = true);
if (action === "collapse") cards().forEach(card => card.open = false); if (action === "collapse") cards().forEach(card => card.open = false);
}); });
@ -208,11 +207,6 @@
input.addEventListener("change", () => updateCard(input.closest("[data-location-card]"))); input.addEventListener("change", () => updateCard(input.closest("[data-location-card]")));
}); });
cards().forEach(updateCard); cards().forEach(updateCard);
form.addEventListener("submit", () => {
cards()
.filter(card => !card.querySelector("[data-location-selected]")?.checked)
.forEach(card => setAction(card, "@StoryIntelligenceLocationImportActions.Ignore"));
});
})(); })();
</script> </script>
} }

View File

@ -2,14 +2,18 @@
@{ @{
var phases = new[] var phases = new[]
{ {
new { Name = "Phase 1", Label = "Prepare manuscript", Stages = new[] { "Connect Word Companion", "Scan Manuscript", "Review Chapters" } }, new { Name = "Phase 1", Label = "Prepare manuscript", Stages = new[] { "Welcome", "Writing Preferences", "Project", "Book", "Connect Word", "Scan Manuscript", "Review Chapters" } },
new { Name = "Phase 2", Label = "Analyse manuscript", Stages = new[] { "Analyse Manuscript" } }, new { Name = "Phase 2", Label = "Analyse manuscript", Stages = new[] { "Analyse Manuscript" } },
new { Name = "Phase 3", Label = "Review Story Intelligence", Stages = new[] { "Review Scenes", "Review Characters", "Review Locations", "Review Assets", "Review Relationships", "Review Knowledge" } }, new { Name = "Phase 3", Label = "Review Story Intelligence", Stages = new[] { "Review Scenes", "Review Characters", "Review Locations", "Review Assets" } },
new { Name = "Phase 4", Label = "Build Story Database", Stages = new[] { "Create Scenes", "Create Characters", "Create Locations", "Create Assets", "Complete" } } new { Name = "Phase 4", Label = "Build Story Database", Stages = new[] { "Complete" } }
}; };
var flatStages = phases.SelectMany(phase => phase.Stages).ToList(); var flatStages = phases.SelectMany(phase => phase.Stages).ToList();
var currentIndex = Math.Max(0, flatStages.FindIndex(stage => string.Equals(stage, Model.CurrentStage, StringComparison.OrdinalIgnoreCase))); var currentIndex = Math.Max(0, flatStages.FindIndex(stage => string.Equals(stage, Model.CurrentStage, StringComparison.OrdinalIgnoreCase)));
var percent = flatStages.Count <= 1 ? 100 : Math.Clamp((currentIndex * 100) / (flatStages.Count - 1), 0, 100); var implementedReviewStages = new[] { "Review Scenes", "Review Characters", "Review Locations", "Review Assets", "Complete" };
var reviewIndex = Array.FindIndex(implementedReviewStages, stage => string.Equals(stage, Model.CurrentStage, StringComparison.OrdinalIgnoreCase));
var percent = reviewIndex >= 0
? Math.Clamp((reviewIndex * 100) / (implementedReviewStages.Length - 1), 0, 100)
: flatStages.Count <= 1 ? 100 : Math.Clamp((currentIndex * 100) / (flatStages.Count - 1), 0, 100);
} }
<div class="onboarding-progress story-pipeline-progress" aria-label="Story Intelligence pipeline progress"> <div class="onboarding-progress story-pipeline-progress" aria-label="Story Intelligence pipeline progress">