570 lines
29 KiB
Plaintext
570 lines
29 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 static string WarningBadgeClass(string severity) => severity switch
|
|
{
|
|
"Critical" => "bg-danger",
|
|
"Warning" => "bg-warning text-dark",
|
|
_ => "bg-info"
|
|
};
|
|
|
|
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>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="DisplayMode" value="ContinuityWarnings" checked="@(Model.Filter.DisplayMode == "ContinuityWarnings")" />
|
|
<span class="form-check-label">Continuity Warnings</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>
|
|
@if (row.LastConfirmedSceneID.HasValue)
|
|
{
|
|
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@row.LastConfirmedSceneID">@row.LastConfirmed</a>
|
|
}
|
|
else
|
|
{
|
|
@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>
|
|
@if (row.LastConfirmedSceneID.HasValue)
|
|
{
|
|
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@row.LastConfirmedSceneID">@row.LastConfirmed</a>
|
|
}
|
|
else
|
|
{
|
|
@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><a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@row.SceneID">@row.SceneDisplayName</a></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><a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@row.SceneID">@row.SceneDisplayName</a></td>
|
|
<td>@row.HolderOrLocation</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}
|
|
</section>
|
|
}
|
|
else if (Model.Filter.DisplayMode == "ContinuityWarnings")
|
|
{
|
|
<section class="story-bible-section">
|
|
<h2>Continuity Warnings</h2>
|
|
@if (!Model.ContinuityWarnings.Any())
|
|
{
|
|
<p class="muted">No continuity concerns detected.</p>
|
|
<p class="muted">Continuity warnings highlight potential issues but cannot replace author judgement.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Severity</th>
|
|
<th>Warning Type</th>
|
|
<th>Message</th>
|
|
<th>Scene</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var warning in Model.ContinuityWarnings)
|
|
{
|
|
<tr>
|
|
<td><span class="badge @WarningBadgeClass(warning.Severity)">@warning.Severity</span></td>
|
|
<td>@warning.WarningType</td>
|
|
<td>@warning.Message</td>
|
|
<td>
|
|
@if (warning.SceneID.HasValue)
|
|
{
|
|
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@warning.SceneID">@warning.SceneDisplayName</a>
|
|
}
|
|
else
|
|
{
|
|
<span class="muted">Project-wide</span>
|
|
}
|
|
</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><a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@row.SceneID">@row.SceneDisplayName</a></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:
|
|
@if (character.LastConfirmedSceneID.HasValue)
|
|
{
|
|
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@character.LastConfirmedSceneID">@character.LastConfirmedSceneDisplayName</a>
|
|
}
|
|
else
|
|
{
|
|
@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:
|
|
@if (asset.LastConfirmedSceneID.HasValue)
|
|
{
|
|
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@asset.LastConfirmedSceneID">@asset.LastConfirmedSceneDisplayName</a>
|
|
}
|
|
else
|
|
{
|
|
@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>
|