465 lines
23 KiB
Plaintext

@model ContinuityExplorerViewModel
@{
ViewData["Title"] = $"Continuity Explorer - {Model.Project.ProjectName}";
ViewData["ProjectSection"] = "Continuity Explorer";
}
@functions {
private static string StateBadgeClass(string state) => state switch
{
"Explicit" => "bg-success",
"Inferred" => "bg-secondary",
_ => "bg-warning text-dark"
};
private bool IsEntitySelected(string entityType) =>
Model.Filter.EntityTypes.Contains(entityType, StringComparer.OrdinalIgnoreCase);
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<span>Continuity Explorer</span>
</nav>
<partial name="_ProjectSectionNav" model="Model.Project" />
<div class="page-heading">
<div>
<p class="eyebrow">Continuity</p>
<h1>Continuity Explorer</h1>
<p class="lead-text">Investigate where characters, assets, and locations stand across the story.</p>
</div>
</div>
<section class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Filters</strong>
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#continuity-filter-panel" aria-expanded="true" aria-controls="continuity-filter-panel">Toggle</button>
</div>
<div class="collapse show" id="continuity-filter-panel">
<div class="card-body">
<form asp-action="ApplyFilters" method="post">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<div class="row g-3">
<div class="col-lg-3">
<label class="form-label">Entity types</label>
<div class="d-grid gap-1">
<label class="form-check">
<input class="form-check-input" type="checkbox" name="EntityTypes" value="Characters" checked="@IsEntitySelected("Characters")" />
<span class="form-check-label">Characters</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" name="EntityTypes" value="Assets" checked="@IsEntitySelected("Assets")" />
<span class="form-check-label">Assets</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" name="EntityTypes" value="Locations" checked="@IsEntitySelected("Locations")" />
<span class="form-check-label">Locations</span>
</label>
</div>
</div>
<div class="col-lg-9">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label" for="CharacterIDs">Characters</label>
<select class="form-select form-select-sm" id="CharacterIDs" name="CharacterIDs" asp-items="Model.CharacterOptions" multiple size="6"></select>
</div>
<div class="col-md-4">
<label class="form-label" for="AssetIDs">Assets</label>
<select class="form-select form-select-sm" id="AssetIDs" name="AssetIDs" asp-items="Model.AssetOptions" multiple size="6"></select>
</div>
<div class="col-md-4">
<label class="form-label" for="LocationIDs">Locations</label>
<select class="form-select form-select-sm" id="LocationIDs" name="LocationIDs" asp-items="Model.LocationOptions" multiple size="6"></select>
</div>
</div>
</div>
<div class="col-lg-4">
<label class="form-label">Scene range</label>
<div class="d-grid gap-1">
<label class="form-check">
<input class="form-check-input" type="radio" name="SceneRangeMode" value="EntireProject" checked="@(Model.Filter.SceneRangeMode == "EntireProject")" />
<span class="form-check-label">Entire Project</span>
</label>
<label class="form-check">
<input class="form-check-input" type="radio" name="SceneRangeMode" value="Book" checked="@(Model.Filter.SceneRangeMode == "Book")" />
<span class="form-check-label">Book</span>
</label>
<label class="form-check">
<input class="form-check-input" type="radio" name="SceneRangeMode" value="Chapter" checked="@(Model.Filter.SceneRangeMode == "Chapter")" />
<span class="form-check-label">Chapter</span>
</label>
<label class="form-check">
<input class="form-check-input" type="radio" name="SceneRangeMode" value="Custom" checked="@(Model.Filter.SceneRangeMode == "Custom")" />
<span class="form-check-label">Custom Scene Range</span>
</label>
</div>
</div>
<div class="col-lg-8">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label" for="BookID">Book</label>
<select class="form-select form-select-sm" id="BookID" name="BookID" asp-items="Model.BookOptions">
<option value="">Any book</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="ChapterID">Chapter</label>
<select class="form-select form-select-sm" id="ChapterID" name="ChapterID" asp-items="Model.ChapterOptions">
<option value="">Any chapter</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="StartSceneID">Start scene</label>
<select class="form-select form-select-sm" id="StartSceneID" name="StartSceneID" asp-items="Model.SceneOptions">
<option value="">First scene</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="EndSceneID">End scene</label>
<select class="form-select form-select-sm" id="EndSceneID" name="EndSceneID" asp-items="Model.SceneOptions">
<option value="">Last scene</option>
</select>
</div>
</div>
</div>
<div class="col-lg-4">
<label class="form-label">Display mode</label>
<div class="d-grid gap-1">
<label class="form-check">
<input class="form-check-input" type="radio" name="DisplayMode" value="StoryState" checked="@(Model.Filter.DisplayMode == "StoryState")" />
<span class="form-check-label">Story State</span>
</label>
<label class="form-check">
<input class="form-check-input" type="radio" name="DisplayMode" value="Journey" checked="@(Model.Filter.DisplayMode == "Journey")" />
<span class="form-check-label">Journey</span>
</label>
<label class="form-check">
<input class="form-check-input" type="radio" name="DisplayMode" value="LocationActivity" checked="@(Model.Filter.DisplayMode == "LocationActivity")" />
<span class="form-check-label">Location Activity</span>
</label>
</div>
</div>
<div class="col-lg-8">
<label class="form-label" for="SelectedSceneID">Story State scene</label>
<select class="form-select form-select-sm" id="SelectedSceneID" name="SelectedSceneID" asp-items="Model.SceneOptions"></select>
</div>
</div>
<div class="button-row mt-3">
<button class="btn btn-primary" type="submit">Apply filters</button>
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Reset</a>
</div>
</form>
</div>
</div>
</section>
@if (Model.Filter.DisplayMode == "StoryState")
{
<section class="story-bible-section">
<h2>Story State</h2>
@if (!Model.StoryState.Characters.Any() && !Model.StoryState.Assets.Any() && !Model.StoryState.Locations.Any())
{
<p class="muted">No matching continuity information found.</p>
}
else
{
@if (Model.Filter.IncludeCharacters)
{
<h3>Characters</h3>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle">
<thead><tr><th>Character</th><th>Current Location</th><th>State</th><th>Last Confirmed</th></tr></thead>
<tbody>
@foreach (var row in Model.StoryState.Characters)
{
<tr><td>@row.CharacterName</td><td>@row.CurrentLocation</td><td><span class="badge @StateBadgeClass(row.State)">@row.State</span></td><td>@row.LastConfirmed</td></tr>
}
</tbody>
</table>
</div>
}
@if (Model.Filter.IncludeAssets)
{
<h3>Assets</h3>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle">
<thead><tr><th>Asset</th><th>Holder / Location</th><th>State</th><th>Last Confirmed</th></tr></thead>
<tbody>
@foreach (var row in Model.StoryState.Assets)
{
<tr><td>@row.AssetName</td><td>@row.CurrentHolderOrLocation</td><td><span class="badge @StateBadgeClass(row.State)">@row.State</span></td><td>@row.LastConfirmed</td></tr>
}
</tbody>
</table>
</div>
}
@if (Model.Filter.IncludeLocations)
{
<h3>Locations</h3>
@if (!Model.StoryState.Locations.Any())
{
<p class="muted">No matching location presence found.</p>
}
else
{
<div class="row g-3">
@foreach (var location in Model.StoryState.Locations)
{
<div class="col-lg-6">
<article class="story-bible-card h-100">
<h3>@location.LocationPath</h3>
<p><strong>Characters:</strong> @(location.CharactersPresent.Any() ? string.Join(", ", location.CharactersPresent) : "None")</p>
<p><strong>Assets:</strong> @(location.AssetsPresent.Any() ? string.Join(", ", location.AssetsPresent) : "None")</p>
</article>
</div>
}
</div>
}
}
}
</section>
}
else if (Model.Filter.DisplayMode == "Journey")
{
<section class="story-bible-section">
<h2>Journey</h2>
@if (!Model.CharacterJourney.Any() && !Model.AssetJourney.Any())
{
<p class="muted">No movement recorded.</p>
}
else
{
@foreach (var group in Model.CharacterJourney.GroupBy(x => new { x.CharacterID, x.CharacterName }))
{
<h3>@group.Key.CharacterName</h3>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle">
<thead>
<tr>
@if (Model.ShowBookContext)
{
<th>Book</th>
}
@if (Model.ShowChapterContext)
{
<th>Chapter</th>
}
<th>Scene</th>
<th>Location</th>
</tr>
</thead>
<tbody>
@foreach (var row in group)
{
<tr>
@if (Model.ShowBookContext)
{
<td>@row.BookDisplayName</td>
}
@if (Model.ShowChapterContext)
{
<td>@row.ChapterDisplayName</td>
}
<td>@row.SceneDisplayName</td>
<td>@row.Location</td>
</tr>
}
</tbody>
</table>
</div>
}
@foreach (var group in Model.AssetJourney.GroupBy(x => new { x.StoryAssetID, x.AssetName }))
{
<h3>@group.Key.AssetName</h3>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle">
<thead>
<tr>
@if (Model.ShowBookContext)
{
<th>Book</th>
}
@if (Model.ShowChapterContext)
{
<th>Chapter</th>
}
<th>Scene</th>
<th>Holder / Location</th>
</tr>
</thead>
<tbody>
@foreach (var row in group)
{
<tr>
@if (Model.ShowBookContext)
{
<td>@row.BookDisplayName</td>
}
@if (Model.ShowChapterContext)
{
<td>@row.ChapterDisplayName</td>
}
<td>@row.SceneDisplayName</td>
<td>@row.HolderOrLocation</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</section>
}
else
{
<section class="story-bible-section">
<h2>Location Activity</h2>
@if (!Model.LocationActivity.Any())
{
<p class="muted">No matching continuity information found.</p>
}
else
{
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Location</th>
@if (Model.ShowBookContext)
{
<th>Book</th>
}
@if (Model.ShowChapterContext)
{
<th>Chapter</th>
}
<th>Scene</th>
<th>Characters Present</th>
<th>Assets Present</th>
</tr>
</thead>
<tbody>
@foreach (var row in Model.LocationActivity)
{
<tr>
<td>@row.LocationName</td>
@if (Model.ShowBookContext)
{
<td>@row.BookDisplayName</td>
}
@if (Model.ShowChapterContext)
{
<td>@row.ChapterDisplayName</td>
}
<td>@row.SceneDisplayName</td>
<td>
@if (!row.Characters.Any())
{
<span class="muted">None</span>
}
else
{
<div class="d-grid gap-1">
@foreach (var character in row.Characters)
{
<div>
<span>@character.EntityName</span>
<span class="badge @StateBadgeClass(character.StateType)">@character.StateType</span>
@if (!string.IsNullOrWhiteSpace(character.LastConfirmedSceneDisplayName))
{
<small class="text-muted d-block">Last confirmed: @character.LastConfirmedSceneDisplayName</small>
}
</div>
}
</div>
}
</td>
<td>
@if (!row.Assets.Any())
{
<span class="muted">None</span>
}
else
{
<div class="d-grid gap-1">
@foreach (var asset in row.Assets)
{
<div>
<span>@asset.EntityName</span>
<span class="badge @StateBadgeClass(asset.StateType)">@asset.StateType</span>
@if (!string.IsNullOrWhiteSpace(asset.Note))
{
<small class="text-muted"> @asset.Note</small>
}
@if (!string.IsNullOrWhiteSpace(asset.LastConfirmedSceneDisplayName))
{
<small class="text-muted d-block">Last confirmed: @asset.LastConfirmedSceneDisplayName</small>
}
</div>
}
</div>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
}
<script>
(() => {
const setDisabled = (id, disabled) => {
const element = document.getElementById(id);
if (element) {
element.disabled = disabled;
}
};
const selectedRadioValue = name => {
const selected = document.querySelector(`input[name="${name}"]:checked`);
return selected ? selected.value : "";
};
const isEntityChecked = value => {
const checkbox = document.querySelector(`input[name="EntityTypes"][value="${value}"]`);
return checkbox ? checkbox.checked : false;
};
const updateContinuityFilterControls = () => {
setDisabled("CharacterIDs", !isEntityChecked("Characters"));
setDisabled("AssetIDs", !isEntityChecked("Assets"));
setDisabled("LocationIDs", !isEntityChecked("Locations"));
const sceneRangeMode = selectedRadioValue("SceneRangeMode");
setDisabled("BookID", sceneRangeMode === "EntireProject");
setDisabled("ChapterID", sceneRangeMode !== "Chapter");
setDisabled("StartSceneID", sceneRangeMode !== "Custom");
setDisabled("EndSceneID", sceneRangeMode !== "Custom");
setDisabled("SelectedSceneID", selectedRadioValue("DisplayMode") !== "StoryState");
};
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('input[name="EntityTypes"], input[name="SceneRangeMode"], input[name="DisplayMode"]')
.forEach(input => input.addEventListener("change", updateContinuityFilterControls));
updateContinuityFilterControls();
});
})();
</script>