PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceRelationships.cshtml

244 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@model StoryIntelligenceProgressViewModel
@{
ViewData["Title"] = "Review relationships";
const int previewLimit = 25;
var relationshipCandidates = Model.RelationshipReview.Candidates.Take(previewLimit).ToList();
var hiddenCandidateCount = Math.Max(0, Model.RelationshipReview.PendingCandidateCount - relationshipCandidates.Count);
}
<section class="onboarding-shell" aria-labelledby="story-relationship-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-relationship-title">Review Relationships <help-icon key="storyIntelligence.relationships" /></h1>
<p>Review meaningful relationship facts, states and developments scene by scene. Direction runs from Character A to Character B unless marked reciprocal. Accepted proposals appear in the existing scene relationship timeline.</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="Relationship review">
@if (Model.RelationshipReview.Candidates.Count == 0)
{
<div class="story-review-note">
<strong>No relationship decisions are waiting.</strong>
<p class="mb-0">Relationships are already up to date, or there were no relationship suggestions ready to import from the stored scene analysis.</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="ImportStoryIntelligenceRelationships" 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 relationships...">Mark reviewed</button>
</form>
</div>
}
else
{
<div class="story-review-card-actions">
@if (hiddenCandidateCount > 0)
{
<span class="story-review-page-status">Showing @relationshipCandidates.Count.ToString("N0") of @Model.RelationshipReview.PendingCandidateCount.ToString("N0") pending</span>
}
<button class="btn btn-outline-primary btn-sm" type="button" data-relationship-bulk="create-all">Accept all on this page</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-relationship-bulk="ignore-all">Ignore all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-relationship-bulk="expand">Expand all</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-relationship-bulk="collapse">Collapse all</button>
</div>
<form asp-action="ImportStoryIntelligenceRelationships" method="post" data-relationship-review-form data-story-review-form>
<input type="hidden" name="BatchID" value="@Model.BatchID" />
<div class="story-character-card-grid">
@for (var i = 0; i < relationshipCandidates.Count; i++)
{
var candidate = relationshipCandidates[i];
@if (candidate.FirstChapterNumber.HasValue && (i == 0 || candidate.FirstChapterNumber != relationshipCandidates[i - 1].FirstChapterNumber))
{
<h2 class="h5 mt-3">Chapter @candidate.FirstChapterNumber.Value.ToString("0.##") <help-icon key="storyIntelligence.relationships.decisions" /></h2>
}
<details class="story-character-card" open data-relationship-card>
<summary class="story-review-card-header">
<span>
<small>@Display(candidate.FirstAppearance)</small>
<strong>@candidate.CharacterAName / @candidate.CharacterBName</strong>
<small>@candidate.ProposalKind — @candidate.RelationshipState</small>
</span>
<em>@candidate.Confidence</em>
</summary>
<input type="hidden" name="Relationships[@i].Key" value="@candidate.Key" />
<div class="story-character-card__body" data-relationship-key="@candidate.Key">
<dl>
<div><dt>Character A</dt><dd>@candidate.CharacterAName</dd></div>
<div><dt>Character B</dt><dd>@candidate.CharacterBName</dd></div>
<div><dt>First seen</dt><dd>@Display(candidate.FirstAppearance)</dd></div>
<div><dt>Direction</dt><dd>@candidate.CharacterAName @(candidate.IsReciprocal ? "↔" : "→") @candidate.CharacterBName</dd></div>
<div><dt>Existing match</dt><dd>@(candidate.ExistingRelationshipLabel ?? "None")</dd></div>
</dl>
@if (!string.IsNullOrWhiteSpace(candidate.ExampleScene))
{
<div class="story-review-note">
<strong>Scene</strong>
<p>@candidate.ExampleScene</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(candidate.ExampleContext))
{
<div class="story-review-note">
<strong>Supporting context</strong>
<p>@candidate.ExampleContext</p>
</div>
}
@if (candidate.NeedsDirectionReview)
{
<div class="story-review-note">This stored analysis predates explicit direction. Check which character feels or acts towards the other and whether the state is mutual.</div>
}
<fieldset class="story-character-actions">
<legend>Decision for this scene proposal <help-icon key="storyIntelligence.relationships.decisions" mode="inline" /></legend>
<label><input type="radio" name="Relationships[@i].Action" value="@StoryIntelligenceRelationshipImportActions.CreateNew" checked data-relationship-action /> Accept development / fact</label>
<label><input type="radio" name="Relationships[@i].Action" value="@StoryIntelligenceRelationshipImportActions.Ignore" data-relationship-action /> Ignore this proposal</label>
</fieldset>
<div data-relationship-type-panel>
<label class="form-label" for="relationship-type-@i">@(candidate.ProposalKind == "Factual" ? "Factual relationship type" : "Relationship track (Other allows changing states)") <help-icon key="storyIntelligence.relationships.track" mode="inline" /></label>
<select id="relationship-type-@i" class="form-select" name="Relationships[@i].RelationshipTypeID">
@foreach (var option in Model.RelationshipReview.RelationshipTypeOptions)
{
<option value="@option.Value" selected="@(option.Value == candidate.RelationshipTypeID.ToString())">@option.Text</option>
}
</select>
<label class="form-label mt-2" for="relationship-state-@i">State in this scene <help-icon key="storyIntelligence.relationships.state" mode="inline" /></label>
<select id="relationship-state-@i" class="form-select" name="Relationships[@i].RelationshipStateID">
@foreach (var option in Model.RelationshipReview.RelationshipStateOptions)
{
<option value="@option.Value" selected="@(option.Value == candidate.RelationshipStateID.ToString())">@option.Text</option>
}
</select>
<label class="form-label mt-2" for="relationship-direction-@i">Direction <help-icon key="storyIntelligence.relationships.direction" mode="inline" /></label>
<select id="relationship-direction-@i" class="form-select" name="Relationships[@i].IsReciprocal">
<option value="false" selected="@(!candidate.IsReciprocal)">@candidate.CharacterAName → @candidate.CharacterBName</option>
<option value="true" selected="@candidate.IsReciprocal">Mutual / reciprocal</option>
</select>
<label class="mt-2"><input type="checkbox" name="Relationships[@i].ReverseDirection" value="true" /> Reverse A and B (also reverses who knows) <help-icon key="storyIntelligence.relationships.reverse" mode="inline" /></label>
<label class="form-label mt-2" for="relationship-known-@i">Does @candidate.CharacterBName know this feeling / state? <help-icon key="storyIntelligence.relationships.awareness" mode="inline" /></label>
<select id="relationship-known-@i" class="form-select" name="Relationships[@i].IsKnownToOtherCharacter">
<option value="false" selected="@(!candidate.IsKnownToOtherCharacter)">No / not established</option>
<option value="true" selected="@candidate.IsKnownToOtherCharacter">Yes</option>
</select>
<label class="form-label mt-2" for="relationship-intensity-@i">Emotional intensity (1–10; blank if unknown) <help-icon key="storyIntelligence.relationships.intensity" mode="inline" /></label>
<input id="relationship-intensity-@i" class="form-control" type="number" min="1" max="10" name="Relationships[@i].Intensity" value="@candidate.Intensity" />
<label class="form-label mt-2" for="relationship-development-@i">Development / factual discovery <help-icon key="storyIntelligence.relationships.development" mode="inline" /></label>
<textarea id="relationship-development-@i" class="form-control" name="Relationships[@i].Development" rows="3">@candidate.Development</textarea>
<p class="text-muted">Acceptance reuses a matching factual or directional relationship and adds this scene event. Existing history is preserved.</p>
</div>
</div>
</details>
}
</div>
<div class="onboarding-actions">
@if (hiddenCandidateCount > 0)
{
<span class="text-muted">Save this batch to continue with @hiddenCandidateCount.ToString("N0") more relationship decision@(hiddenCandidateCount == 1 ? string.Empty : "s").</span>
}
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
@if (Model.RelationshipReview.HasPreviousPage)
{
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceRelationships" asp-route-batchId="@Model.BatchID" asp-route-page="@(Model.RelationshipReview.PageNumber - 1)">Previous</a>
}
@if (Model.RelationshipReview.HasNextPage)
{
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceRelationships" asp-route-batchId="@Model.BatchID" asp-route-page="@(Model.RelationshipReview.PageNumber + 1)">Next</a>
}
<button class="btn btn-primary" type="submit" data-busy-label="Saving relationships...">Save relationship decisions</button>
</div>
</form>
}
</section>
</div>
</section>
@section Scripts {
<script>
(() => {
const form = document.querySelector("[data-relationship-review-form]");
if (!form) return;
const cards = () => Array.from(form.querySelectorAll("[data-relationship-card]"));
const setAction = (card, action) => {
const input = card.querySelector(`[data-relationship-action][value="${action}"]`);
if (input) {
input.checked = true;
updateCard(card);
}
};
const actionFor = (card) => card.querySelector("[data-relationship-action]:checked")?.value || "@StoryIntelligenceRelationshipImportActions.CreateNew";
const isCanonicalTarget = (card) => {
const action = actionFor(card);
return action !== "@StoryIntelligenceRelationshipImportActions.Ignore" && action !== "@StoryIntelligenceRelationshipImportActions.Alias";
};
const updateAliasTargets = () => {
const allCards = cards();
for (const card of allCards) {
const body = card.querySelector("[data-relationship-key]");
const ownKey = body?.getAttribute("data-relationship-key") || "";
const select = card.querySelector("[data-relationship-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-relationship-key]")?.getAttribute("data-relationship-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 typePanel = card.querySelector("[data-relationship-type-panel]");
const aliasPanel = card.querySelector("[data-relationship-alias-panel]");
if (typePanel) typePanel.hidden = action === "@StoryIntelligenceRelationshipImportActions.Ignore" || action === "@StoryIntelligenceRelationshipImportActions.Alias";
if (aliasPanel) aliasPanel.hidden = action !== "@StoryIntelligenceRelationshipImportActions.Alias";
updateAliasTargets();
};
document.querySelectorAll("[data-relationship-bulk]").forEach(button => {
button.addEventListener("click", () => {
const action = button.getAttribute("data-relationship-bulk");
if (action === "create-all") cards().forEach(card => setAction(card, "@StoryIntelligenceRelationshipImportActions.CreateNew"));
if (action === "link-existing") cards().forEach(card => {
if (card.querySelector(`[data-relationship-action][value="@StoryIntelligenceRelationshipImportActions.LinkExisting"]:not(:disabled)`)) {
setAction(card, "@StoryIntelligenceRelationshipImportActions.LinkExisting");
}
});
if (action === "ignore-all") cards().forEach(card => setAction(card, "@StoryIntelligenceRelationshipImportActions.Ignore"));
if (action === "expand") cards().forEach(card => card.open = true);
if (action === "collapse") cards().forEach(card => card.open = false);
});
});
form.querySelectorAll("[data-relationship-action]").forEach(input => {
input.addEventListener("change", () => updateCard(input.closest("[data-relationship-card]")));
});
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;
}