2026-06-01 09:03:59 +01:00

371 lines
22 KiB
Plaintext

@model TimelineViewModel
@{
ViewData["Title"] = "Timeline";
ViewData["ShellClass"] = "plotline-shell-wide";
var selectedBook = Model.Books.FirstOrDefault(x => x.Book.BookID == Model.SelectedBookID)?.Book;
var timelineMetricScenes = Model.Books
.SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => new
{
sceneId = scene.SceneID,
axisLabel = $"{chapter.Chapter.ChapterNumber:g}.{scene.SceneNumber:g}",
chapterLabel = $"Chapter {chapter.Chapter.ChapterNumber:g}",
sceneNumber = scene.SceneNumber.ToString("g"),
sceneTitle = scene.SceneTitle,
bookTitle = book.Book.BookTitle,
timelineUrl = Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = book.Book.BookID, selectedSceneId = scene.SceneID })
})))
.ToList();
var timelineMetricShapePayload = new
{
scenes = timelineMetricScenes,
metrics = Model.MetricGraphs.Select(graph => new
{
metricName = graph.MetricName,
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
})
};
}
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Workspace context</span>
<strong>@Model.Project.ProjectName@if (selectedBook is not null) { <span> / @selectedBook.BookTitle</span> }</strong>
</div>
<nav class="context-nav" aria-label="Workspace navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">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="workspace-heading">
<div>
<p class="eyebrow">Story timeline</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<form method="get" class="timeline-filter">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<select name="bookId" asp-items="Model.BookOptions" class="form-select">
<option value="">All books</option>
</select>
<button class="btn btn-outline-primary" type="submit">View</button>
</form>
<div class="button-row">
<form asp-controller="Warnings" asp-action="ValidateProject" method="post">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
<button class="btn btn-outline-primary" type="submit">Validate project</button>
</form>
@if (Model.SelectedBookID.HasValue)
{
<form asp-controller="Warnings" asp-action="ValidateBook" method="post">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
<button class="btn btn-outline-primary" type="submit">Validate book</button>
</form>
}
<a class="btn btn-outline-secondary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Warnings</a>
<a class="btn btn-outline-secondary" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
</div>
</div>
@if (!Model.Books.Any())
{
<section class="empty-panel">
<h2>No timeline yet</h2>
<p>Add books, chapters and scenes to see them arranged here in narrative order.</p>
</section>
}
else
{
<div class="workspace-layout">
<main class="timeline-workspace">
<section class="timeline-shell workspace-timeline">
@foreach (var book in Model.Books)
{
<div class="timeline-book">
<div class="timeline-book-row">
<div class="timeline-book-title">Book @book.Book.BookNumber: @book.Book.BookTitle</div>
<div class="timeline-chapters">
@foreach (var chapter in book.Chapters)
{
<div class="timeline-chapter" style="--chapter-scene-count:@Math.Max(chapter.Scenes.Count, 1)">
<div class="timeline-chapter-title">
<span>Chapter @chapter.Chapter.ChapterNumber</span>
<strong>@chapter.Chapter.ChapterTitle</strong>
</div>
<div class="timeline-scenes">
@if (!chapter.Scenes.Any())
{
<div class="timeline-empty-scene">No scenes</div>
}
else
{
@foreach (var scene in chapter.Scenes)
{
var selectedClass = scene.SceneID == Model.SelectedSceneID ? "selected" : "";
<a class="timeline-scene-card @selectedClass"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@scene.SceneID">
<span class="scene-number">Scene @scene.SceneNumber</span>
<strong>@scene.SceneTitle</strong>
<div class="scene-badge-row">
<span class="status-pill">@scene.RevisionStatusName</span>
@if (scene.WarningCount > 0)
{
<span class="warning-placeholder active">@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s")</span>
}
else
{
<span class="warning-placeholder">Warnings 0</span>
}
@if (scene.DependencyCount > 0)
{
<span class="dependency-placeholder">@scene.DependencyCount dep@(scene.DependencyCount == 1 ? "" : "s")</span>
}
</div>
<span class="time-label">@scene.TimeLabel</span>
@if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationName))
{
<span class="location-pill" title="@scene.PrimaryLocationPath">@scene.PrimaryLocationName</span>
}
@if (scene.PurposeLabels.Any())
{
<div class="purpose-chip-row">
@foreach (var purpose in scene.PurposeLabels.Take(3))
{
<span class="purpose-chip">@purpose.PurposeName</span>
}
</div>
}
<span class="scene-summary">@scene.Summary</span>
</a>
}
}
</div>
</div>
}
</div>
</div>
</div>
}
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
{
<div class="plot-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<span>Plot line lanes</span>
<small>Thread event markers by scene</small>
</div>
@foreach (var lane in Model.PlotLanes)
{
<div class="plot-lane-row" style="--plot-colour:@lane.PlotLine.Colour">
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
<strong>@lane.PlotLine.PlotLineName</strong>
</a>
<div class="plot-lane-slots">
@foreach (var slot in lane.SceneSlots)
{
<div class="plot-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
@foreach (var threadEvent in slot.Events)
{
var markerClass = threadEvent.EventTypeName is "Resolved" or "Payoff"
? "resolved"
: threadEvent.EventTypeName is "Contradicted"
? "contradicted"
: "";
<a class="thread-marker @markerClass"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@slot.Scene.SceneID"
title="@threadEvent.ThreadTitle: @threadEvent.EventTitle">
@threadEvent.MarkerText
</a>
}
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="future-lane-placeholder">
Add visible plot lines and thread events to see narrative lanes here.
</div>
}
@if (Model.AssetLanes.Any() && Model.OrderedScenes.Any())
{
<div class="asset-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<span>Story asset lanes</span>
<small>Asset event markers by scene</small>
</div>
@foreach (var lane in Model.AssetLanes)
{
<div class="asset-lane-row">
<a class="asset-lane-label" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
<strong>@lane.Asset.AssetName</strong>
<span>@lane.Asset.KindName / @lane.Asset.Importance</span>
</a>
<div class="asset-lane-slots">
@foreach (var slot in lane.SceneSlots)
{
<div class="asset-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
@foreach (var assetEvent in slot.Events)
{
<a class="asset-marker"
asp-controller="StoryAssets"
asp-action="Details"
asp-route-id="@assetEvent.StoryAssetID"
title="@assetEvent.AssetName: @assetEvent.EventTitle">
@assetEvent.MarkerText
</a>
}
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="future-lane-placeholder">
Add story assets and asset events to see asset lanes here.
</div>
}
@if (Model.CharacterLanes.Any() && Model.OrderedScenes.Any())
{
<div class="character-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<span>Character appearances</span>
<small>First twelve project characters</small>
</div>
@foreach (var lane in Model.CharacterLanes)
{
<div class="character-lane-row">
<a class="character-lane-label" asp-controller="Characters" asp-action="Details" asp-route-id="@lane.Character.CharacterID">
<strong>@lane.Character.CharacterName</strong>
<span>@(lane.Character.ShortName ?? "Character")</span>
</a>
<div class="character-lane-slots">
@foreach (var slot in lane.SceneSlots)
{
<div class="character-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
@foreach (var appearance in slot.Appearances)
{
<a class="character-marker"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@slot.Scene.SceneID"
title="@appearance.CharacterName: @appearance.RoleInSceneTypeName / @appearance.PresenceTypeName">
@(!string.IsNullOrWhiteSpace(appearance.PresenceTypeName) ? appearance.PresenceTypeName.Substring(0, 1) : "C")
</a>
}
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="future-lane-placeholder">
Add characters to scenes to see character appearance lanes here.
</div>
}
</section>
<section class="pacing-panel">
<div class="pacing-heading">
<div>
<p class="eyebrow">Pacing</p>
<h2>Metric shape</h2>
</div>
<span class="muted">@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</span>
</div>
@if (!Model.MetricGraphs.Any() || !Model.OrderedScenes.Any())
{
<p class="muted">Add scenes to begin seeing emotional and pacing shape.</p>
}
else
{
<div class="metric-shape-chart" data-metric-shape-chart>
<p class="metric-shape-intro">Compare selected metrics across the same scene timeline. Click a point to focus that scene.</p>
<div class="metric-toggle-row" data-metric-toggles></div>
<div class="metric-chart-frame">
<canvas aria-label="Comparative scene metric line graph"></canvas>
<p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
</div>
<script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(timelineMetricShapePayload))</script>
</div>
<details class="metric-raw-details">
<summary>Show raw metric rows</summary>
<div class="metric-graph-stack mt-2">
@foreach (var graph in Model.MetricGraphs)
{
<div class="metric-graph-row">
<div class="metric-graph-label">@graph.MetricName</div>
<div class="metric-graph-points" style="--scene-count:@Math.Max(graph.Points.Count, 1)">
@foreach (var point in graph.Points)
{
<a asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@point.SceneID"
class="metric-point"
style="--metric-height:@point.Percent%"
title="Scene @point.SceneNumber: @point.SceneTitle - @point.Value">
<span>@point.Value</span>
</a>
}
</div>
</div>
}
</div>
</details>
}
</section>
</main>
<aside class="scene-inspector-panel">
@if (Model.SelectedScene is null)
{
<div class="inspector-empty">
<p class="eyebrow">Scene inspector</p>
<h2>Select a scene</h2>
<p>Click a scene card to edit its summary, timing, purposes, outcomes and metric sliders without leaving the timeline.</p>
</div>
}
else
{
<div class="inspector-header">
<p class="eyebrow">Scene inspector</p>
<h2>@Model.SelectedScene.SceneTitle</h2>
</div>
<partial name="~/Views/Scenes/_SceneInspector.cshtml" model="Model.SelectedScene" />
}
</aside>
</div>
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}