2026-06-05 21:48:27 +01:00

410 lines
20 KiB
Plaintext

@model StoryBibleViewModel
@{
ViewData["Title"] = $"Story Bible - {Model.Project.ProjectName}";
var timelineByBook = Model.Timeline.GroupBy(x => new { x.BookID, x.BookTitle });
var relationshipsByCharacter = Model.Relationships.GroupBy(x => new { x.CharacterAID, x.CharacterAName });
var revisionByGroup = Model.RevisionItems.GroupBy(x => x.GroupName);
}
<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>Story Bible</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Project workspace</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a class="active" asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
<a asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Relationship Map</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading story-bible-heading">
<div>
<p class="eyebrow">Reference</p>
<h1>Story Bible <help-icon key="storyBible.overview" /></h1>
<p class="lead-text">A project-level reference for people, threads, places, assets, continuity, and revision attention.</p>
</div>
<form class="story-bible-search" asp-action="Index" method="get">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input class="form-control" name="q" value="@Model.Query" placeholder="Search story bible" />
<button class="btn btn-primary" type="submit">Search</button>
</form>
</div>
<div class="button-row export-action-row">
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID">Printable Story Bible</a>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID" asp-route-format="markdown">Markdown</a>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID" asp-route-format="text">Text</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="NarrativeTimeline" asp-route-projectId="@Model.Project.ProjectID">Timeline export</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="ResearchPack" asp-route-projectId="@Model.Project.ProjectID">Research pack</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Open Relationship Map</a>
</div>
<nav class="story-bible-nav" aria-label="Story Bible sections">
<a href="#characters">Characters</a>
<a href="#threads">Plot Threads</a>
<a href="#assets">Story Assets</a>
<a href="#relationships">Relationships</a>
<a href="#locations">Locations</a>
<a href="#timeline-summary">Timeline Summary</a>
<a href="#attention">Things that may need attention</a>
<a href="#revision">Revision Status</a>
</nav>
@if (!string.IsNullOrWhiteSpace(Model.Query))
{
<section class="story-bible-section" id="search-results">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Search</p>
<h2>Results for "@Model.Query"</h2>
</div>
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Clear search</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="Search" asp-route-projectId="@Model.Project.ProjectID" asp-route-q="@Model.Query" asp-route-format="markdown">Export results</a>
</div>
@if (!Model.SearchResults.Any())
{
<p class="muted">No matching story bible entries. Try a character name, location, asset, plot thread, or scene title.</p>
}
else
{
<div class="story-bible-search-results">
@foreach (var group in Model.SearchResults.GroupBy(x => x.ResultType))
{
<article class="story-bible-card">
<h3>@group.Key</h3>
<ul class="story-bible-link-list">
@foreach (var result in group)
{
<li>
<a href="@StoryBibleLink(result, Model.Project.ProjectID)">@result.Title</a>
@if (!string.IsNullOrWhiteSpace(result.Detail))
{
<span>@result.Detail</span>
}
</li>
}
</ul>
</article>
}
</div>
}
</section>
}
<section class="story-bible-section" id="characters">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">People</p>
<h2>Characters</h2>
</div>
<span class="soft-count">@Model.Characters.Count characters</span>
</div>
<div class="story-bible-grid">
@foreach (var character in Model.Characters)
{
<article class="story-bible-card">
<div class="story-bible-card-title">
<h3>@character.CharacterName</h3>
<span>@character.SceneCount scenes</span>
</div>
<p>@(character.Description ?? "No profile description yet.")</p>
<div class="story-bible-meta">
@if (character.AgeAtSeriesStart.HasValue) { <span>Age @character.AgeAtSeriesStart</span> }
@if (!string.IsNullOrWhiteSpace(character.EyeColour)) { <span>@character.EyeColour eyes</span> }
@if (!string.IsNullOrWhiteSpace(character.Height)) { <span>@character.Height</span> }
<span>@character.PovSceneCount POV scenes</span>
<span>@character.RelationshipCount relationships</span>
<span>@character.KnowledgeCount knowledge notes</span>
</div>
<dl class="story-bible-facts">
<dt>First</dt><dd>@(character.FirstSceneLabel ?? "Not placed")</dd>
<dt>Last</dt><dd>@(character.LastSceneLabel ?? "Not placed")</dd>
<dt>Locations</dt><dd>@(character.LocationHistory ?? "No location history yet")</dd>
</dl>
@if (character.MissingLocationCount > 0)
{
<span class="badge text-bg-warning">@character.MissingLocationCount missing locations</span>
}
<div class="story-bible-actions">
<a asp-controller="Characters" asp-action="Details" asp-route-id="@character.CharacterID">Open profile</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-focusCharacterId="@character.CharacterID">Show on timeline</a>
</div>
</article>
}
</div>
</section>
<section class="story-bible-section" id="threads">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Questions and promises</p>
<h2>Plot Threads</h2>
</div>
<span class="soft-count">@Model.PlotThreads.Count threads</span>
</div>
<div class="story-bible-grid">
@foreach (var thread in Model.PlotThreads)
{
<article class="story-bible-card">
<div class="story-bible-card-title">
<h3>@thread.ThreadTitle</h3>
<span>@thread.ThreadStatusName</span>
</div>
<div class="story-bible-meta">
<span>@thread.PlotLineName</span>
<span>@thread.ThreadTypeName</span>
<span>Importance @thread.Importance</span>
<span>@thread.EventCount events</span>
</div>
<dl class="story-bible-facts">
<dt>Introduced</dt><dd>@(thread.IntroducedSceneLabel ?? "Not set")</dd>
<dt>Last touched</dt><dd>@(thread.LastTouchedSceneLabel ?? "No events yet")</dd>
<dt>Planned end</dt><dd>@(thread.PlannedResolutionSceneLabel ?? "Not planned")</dd>
<dt>Actual end</dt><dd>@(thread.ActualResolutionSceneLabel ?? "Not resolved")</dd>
<dt>History</dt><dd>@(thread.EventHistory ?? "No event history yet")</dd>
</dl>
@if (thread.WarningCount > 0)
{
<span class="badge text-bg-warning">@thread.WarningCount warnings</span>
}
<div class="story-bible-actions">
<a asp-controller="PlotThreads" asp-action="Edit" asp-route-id="@thread.PlotThreadID">Edit thread</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-focusPlotThreadId="@thread.PlotThreadID">Show on timeline</a>
</div>
</article>
}
</div>
</section>
<section class="story-bible-section" id="assets">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Objects and evidence</p>
<h2>Story Assets</h2>
</div>
<span class="soft-count">@Model.Assets.Count assets</span>
</div>
<div class="story-bible-grid">
@foreach (var asset in Model.Assets)
{
<article class="story-bible-card">
<div class="story-bible-card-title">
<h3>@asset.AssetName</h3>
<span>@(asset.IsResolved ? "Resolved" : "Open")</span>
</div>
<div class="story-bible-meta">
<span>@asset.KindName</span>
<span>@(asset.CurrentStateName ?? "No state")</span>
<span>Importance @asset.Importance</span>
<span>@asset.EventCount events</span>
</div>
<dl class="story-bible-facts">
<dt>Location</dt><dd>@(asset.CurrentLocationPath ?? "No current location")</dd>
<dt>Dependencies</dt><dd>@(asset.DependencySummary ?? "None")</dd>
<dt>Custody</dt><dd>@(asset.CustodySummary ?? "No custody history")</dd>
<dt>History</dt><dd>@(asset.EventHistory ?? "No event history yet")</dd>
</dl>
@if (asset.WarningCount > 0)
{
<span class="badge text-bg-warning">@asset.WarningCount warnings</span>
}
<div class="story-bible-actions">
<a asp-controller="StoryAssets" asp-action="Details" asp-route-id="@asset.StoryAssetID">Open asset</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-focusAssetId="@asset.StoryAssetID">Show on timeline</a>
</div>
</article>
}
</div>
</section>
<section class="story-bible-section" id="relationships">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Character web</p>
<h2>Relationships</h2>
</div>
<span class="soft-count">@Model.Relationships.Count relationships</span>
</div>
<div class="story-bible-grid">
@foreach (var categoryGroup in Model.Relationships.GroupBy(x => new { x.RelationshipCategoryName, x.CategorySortOrder }).OrderBy(x => x.Key.CategorySortOrder).ThenBy(x => x.Key.RelationshipCategoryName))
{
<article class="story-bible-card">
<h3>@categoryGroup.Key.RelationshipCategoryName</h3>
<ul class="story-bible-link-list">
@foreach (var relationship in categoryGroup.OrderBy(x => x.CharacterAName).ThenBy(x => x.CharacterBName).ThenBy(x => x.RelationshipTypeSortOrder))
{
<li>
<a asp-controller="Characters" asp-action="Details" asp-route-id="@relationship.CharacterBID">@relationship.CharacterAName / @relationship.CharacterBName</a>
<span>@relationship.RelationshipTypeName <span class="status-pill">@relationship.RelationshipCategoryName</span></span>
<span>Initial: @(relationship.InitialRelationshipName ?? relationship.RelationshipTypeName)@if (!string.IsNullOrWhiteSpace(relationship.InitialStateName)) { <text> / @relationship.InitialStateName</text> }@if (relationship.InitialIntensity.HasValue) { <text> / Intensity @relationship.InitialIntensity</text> }</span>
<span>Current: @(relationship.CurrentStateName ?? "No current state")@if (relationship.CurrentIntensity.HasValue) { <text> / Intensity @relationship.CurrentIntensity</text> }</span>
@if (!string.IsNullOrWhiteSpace(relationship.EventHistory))
{
<span>Relationship Event History: @relationship.EventHistory</span>
}
</li>
}
</ul>
</article>
}
</div>
</section>
<section class="story-bible-section" id="locations">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Place</p>
<h2>Locations</h2>
</div>
<span class="soft-count">@Model.Locations.Count locations</span>
</div>
<div class="story-bible-grid story-bible-grid-compact">
@foreach (var location in Model.Locations)
{
<article class="story-bible-card">
<div class="story-bible-card-title">
<h3>@location.LocationName</h3>
<span>@(location.LocationTypeName ?? "Location")</span>
</div>
<p>@location.LocationPath</p>
<div class="story-bible-meta">
<span>@location.SceneCount scenes</span>
<span>@location.CharacterCount characters</span>
<span>@location.AssetCount assets</span>
<span>@location.RelationshipCount links</span>
</div>
@if (!string.IsNullOrWhiteSpace(location.RelatedLocations))
{
<p class="muted">Linked to @location.RelatedLocations</p>
}
<div class="story-bible-actions">
<a asp-controller="Locations" asp-action="Details" asp-route-id="@location.LocationID">Open location</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-focusLocationId="@location.LocationID">Show on timeline</a>
</div>
</article>
}
</div>
</section>
<section class="story-bible-section" id="timeline-summary">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Narrative order</p>
<h2>Timeline Summary</h2>
</div>
<a class="btn btn-outline-primary btn-sm" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Open timeline</a>
</div>
<div class="story-bible-timeline-summary">
@foreach (var book in timelineByBook)
{
<article class="story-bible-card story-bible-wide-card">
<h3>@book.Key.BookTitle</h3>
@foreach (var chapter in book.GroupBy(x => new { x.ChapterID, x.ChapterNumber, x.ChapterTitle }))
{
<div class="story-bible-chapter-summary">
<h4>Chapter @chapter.Key.ChapterNumber: @chapter.Key.ChapterTitle</h4>
@foreach (var scene in chapter)
{
<div class="story-bible-scene-summary">
<strong>Scene @scene.SceneNumber: @scene.SceneTitle</strong>
<span>@scene.RevisionStatusName</span>
<span>POV: @(scene.PovCharacterName ?? "Not set")</span>
<span>Location: @(scene.LocationPath ?? "Not set")</span>
@if (!string.IsNullOrWhiteSpace(scene.Summary))
{
<p>@scene.Summary</p>
}
</div>
}
</div>
}
</article>
}
</div>
</section>
<section class="story-bible-section" id="attention">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Open Questions</p>
<h2>Things that may need attention</h2>
</div>
<span class="soft-count">@Model.OpenQuestions.Count items</span>
</div>
<div class="attention-list">
@foreach (var item in Model.OpenQuestions)
{
<article>
<span class="badge text-bg-light">@item.Category</span>
<strong>@item.Title</strong>
@if (!string.IsNullOrWhiteSpace(item.Detail))
{
<p>@item.Detail</p>
}
@if (item.SceneID.HasValue)
{
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open scene</a>
}
</article>
}
</div>
</section>
<section class="story-bible-section" id="revision">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Revision</p>
<h2>Revision Status</h2>
</div>
</div>
<div class="story-bible-grid story-bible-grid-compact">
@foreach (var group in revisionByGroup)
{
<article class="story-bible-card">
<div class="story-bible-card-title">
<h3>@group.Key</h3>
<span>@group.Sum(x => x.ItemCount)</span>
</div>
<ul class="story-bible-link-list">
@foreach (var item in group)
{
<li>
<span>@(item.Detail ?? item.GroupName)</span>
<strong>@item.ItemCount</strong>
</li>
}
</ul>
</article>
}
</div>
</section>
@functions {
private string StoryBibleLink(StoryBibleSearchResult result, int projectId)
{
return result.ResultType switch
{
"Scene" => Url.Action("Index", "Timeline", new { projectId, selectedSceneId = result.SceneID ?? result.EntityID }) ?? "#",
"Character" => Url.Action("Details", "Characters", new { id = result.EntityID }) ?? "#",
"Story Asset" => Url.Action("Details", "StoryAssets", new { id = result.EntityID }) ?? "#",
"Plot Thread" => Url.Action("Edit", "PlotThreads", new { id = result.EntityID }) ?? "#",
"Location" => Url.Action("Details", "Locations", new { id = result.EntityID }) ?? "#",
"Warning" => Url.Action("Index", "Warnings", new { projectId, sceneId = result.SceneID }) ?? "#",
_ => Url.Action("Index", "StoryBible", new { projectId }) ?? "#"
};
}
}