From dc4324f970a2b30b95aed4d216905b80c0ca23f6 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 14 Jun 2026 19:37:26 +0100 Subject: [PATCH] Phase 12B.2: Continuity Explorer Result Context Improvements. --- PlotLine/Services/CoreServices.cs | 103 +++++++++++++++++- PlotLine/ViewModels/CoreViewModels.cs | 20 ++++ .../Views/ContinuityExplorer/Index.cshtml | 88 ++++++++++++++- 3 files changed, 203 insertions(+), 8 deletions(-) diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 1235ee0..4a4966d 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -2280,6 +2280,8 @@ public sealed class StoryStateService( { Project = project, Filter = filter, + ShowBookContext = ShouldShowBookContext(filter, scopedScenes, timeline.Chapters), + ShowChapterContext = ShouldShowChapterContext(filter, scopedScenes, timeline.Chapters), BookOptions = timeline.Books.Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString(), x.BookID == filter.BookID)).ToList(), ChapterOptions = timeline.Chapters.Select(x => new SelectListItem($"Ch {x.ChapterNumber}: {x.ChapterTitle}", x.ChapterID.ToString(), x.ChapterID == filter.ChapterID)).ToList(), SceneOptions = orderedScenes.Select(x => new SelectListItem($"Scene {x.SceneNumber:g}: {x.SceneTitle}", x.SceneID.ToString(), x.SceneID == filter.SelectedSceneID)).ToList(), @@ -2365,12 +2367,19 @@ public sealed class StoryStateService( } previousLocationId = row.LocationID; + var sceneContext = context.SceneContexts.GetValueOrDefault(row.SceneID); result.Add(new CharacterJourneyViewModel { CharacterID = character.CharacterID, CharacterName = character.CharacterName, + BookID = sceneContext?.BookID ?? 0, + BookDisplayName = sceneContext?.BookDisplayName ?? string.Empty, + ChapterID = sceneContext?.ChapterID ?? 0, + ChapterDisplayName = sceneContext?.ChapterDisplayName ?? string.Empty, SceneID = row.SceneID, + SceneTitle = sceneContext?.SceneTitle ?? string.Empty, SceneLabel = context.SceneLabels.GetValueOrDefault(row.SceneID, $"Scene {row.SceneNumber:g}"), + SceneDisplayName = sceneContext?.SceneDisplayName ?? context.SceneLabels.GetValueOrDefault(row.SceneID, $"Scene {row.SceneNumber:g}"), Location = row.LocationPath ?? row.LocationName ?? "Unknown" }); } @@ -2408,12 +2417,19 @@ public sealed class StoryStateService( } previousLabel = row.Label; + var sceneContext = context.SceneContexts.GetValueOrDefault(row.SceneID); result.Add(new AssetJourneyViewModel { StoryAssetID = asset.StoryAssetID, AssetName = asset.AssetName, + BookID = sceneContext?.BookID ?? 0, + BookDisplayName = sceneContext?.BookDisplayName ?? string.Empty, + ChapterID = sceneContext?.ChapterID ?? 0, + ChapterDisplayName = sceneContext?.ChapterDisplayName ?? string.Empty, SceneID = row.SceneID, + SceneTitle = sceneContext?.SceneTitle ?? string.Empty, SceneLabel = context.SceneLabels.GetValueOrDefault(row.SceneID, "Unknown scene"), + SceneDisplayName = sceneContext?.SceneDisplayName ?? context.SceneLabels.GetValueOrDefault(row.SceneID, "Unknown scene"), HolderOrLocation = row.Label }); } @@ -2461,12 +2477,19 @@ public sealed class StoryStateService( continue; } + var sceneContext = context.SceneContexts.GetValueOrDefault(scene.SceneID); result.Add(new LocationActivityViewModel { LocationID = location.LocationID, LocationName = location.LocationPath, + BookID = sceneContext?.BookID ?? 0, + BookDisplayName = sceneContext?.BookDisplayName ?? string.Empty, + ChapterID = sceneContext?.ChapterID ?? 0, + ChapterDisplayName = sceneContext?.ChapterDisplayName ?? string.Empty, SceneID = scene.SceneID, + SceneTitle = sceneContext?.SceneTitle ?? scene.SceneTitle, SceneLabel = SceneLabel(scene.SceneNumber, scene.SceneTitle), + SceneDisplayName = sceneContext?.SceneDisplayName ?? SceneLabel(scene.SceneNumber, scene.SceneTitle), CharactersPresent = sceneCharacters.Any() ? string.Join(", ", sceneCharacters) : "None", AssetsPresent = sceneAssets.Any() ? string.Join(", ", sceneAssets) : "None" }); @@ -2485,7 +2508,8 @@ public sealed class StoryStateService( scopedScenes, scopedScenes.Select(x => x.SceneID).ToHashSet(), BuildSceneIndexes(orderedScenes), - orderedScenes.ToDictionary(x => x.SceneID, x => SceneLabel(x.SceneNumber, x.SceneTitle))); + orderedScenes.ToDictionary(x => x.SceneID, x => SceneLabel(x.SceneNumber, x.SceneTitle)), + BuildSceneContexts(timeline)); } private static IEnumerable OrderScenes(TimelineData timeline) @@ -2709,6 +2733,71 @@ public sealed class StoryStateService( .ToDictionary(x => x.SceneID, x => x.Index); } + private static Dictionary BuildSceneContexts(TimelineData timeline) + { + var chaptersById = timeline.Chapters.ToDictionary(x => x.ChapterID); + var booksById = timeline.Books.ToDictionary(x => x.BookID); + + return timeline.Scenes.ToDictionary( + scene => scene.SceneID, + scene => + { + chaptersById.TryGetValue(scene.ChapterID, out var chapter); + var book = chapter is null ? null : booksById.GetValueOrDefault(chapter.BookID); + + return new SceneContext( + book?.BookID ?? 0, + book is null ? string.Empty : $"Book {book.BookNumber}: {book.BookTitle}", + chapter?.ChapterID ?? 0, + chapter is null ? string.Empty : $"Ch {chapter.ChapterNumber:g}: {chapter.ChapterTitle}", + scene.SceneID, + scene.SceneTitle, + SceneLabel(scene.SceneNumber, scene.SceneTitle)); + }); + } + + private static bool ShouldShowBookContext(ContinuityFilter filter, IReadOnlyList scopedScenes, IReadOnlyList chapters) + { + if (string.Equals(filter.SceneRangeMode, "EntireProject", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (!string.Equals(filter.SceneRangeMode, "Custom", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var chapterBookIds = chapters.ToDictionary(x => x.ChapterID, x => x.BookID); + return scopedScenes + .Select(scene => chapterBookIds.GetValueOrDefault(scene.ChapterID)) + .Where(bookId => bookId > 0) + .Distinct() + .Count() > 1; + } + + private static bool ShouldShowChapterContext(ContinuityFilter filter, IReadOnlyList scopedScenes, IReadOnlyList chapters) + { + if (string.Equals(filter.SceneRangeMode, "EntireProject", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter.SceneRangeMode, "Book", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (string.Equals(filter.SceneRangeMode, "Chapter", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!string.Equals(filter.SceneRangeMode, "Custom", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var showBookContext = ShouldShowBookContext(filter, scopedScenes, chapters); + return showBookContext || scopedScenes.Select(scene => scene.ChapterID).Distinct().Count() > 1; + } + private static void NormaliseFilter(ContinuityFilter filter) { if (filter.ProjectID <= 0) @@ -2751,7 +2840,17 @@ public sealed class StoryStateService( IReadOnlyList ScopedScenes, IReadOnlySet AllowedSceneIds, IReadOnlyDictionary SceneIndexes, - IReadOnlyDictionary SceneLabels); + IReadOnlyDictionary SceneLabels, + IReadOnlyDictionary SceneContexts); + + private sealed record SceneContext( + int BookID, + string BookDisplayName, + int ChapterID, + string ChapterDisplayName, + int SceneID, + string SceneTitle, + string SceneDisplayName); } public sealed class PlotService(IProjectRepository projects, IBookRepository books, IPlotRepository plots, IProjectActivityService activity) : IPlotService diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 21d651a..87d255d 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -307,14 +307,22 @@ public sealed class ContinuityExplorerViewModel public IReadOnlyList CharacterOptions { get; set; } = []; public IReadOnlyList AssetOptions { get; set; } = []; public IReadOnlyList LocationOptions { get; set; } = []; + public bool ShowBookContext { get; set; } + public bool ShowChapterContext { get; set; } } public sealed class CharacterJourneyViewModel { public int CharacterID { get; set; } public string CharacterName { get; set; } = string.Empty; + public int BookID { get; set; } + public string BookDisplayName { get; set; } = string.Empty; + public int ChapterID { get; set; } + public string ChapterDisplayName { get; set; } = string.Empty; public int SceneID { get; set; } + public string SceneTitle { get; set; } = string.Empty; public string SceneLabel { get; set; } = string.Empty; + public string SceneDisplayName { get; set; } = string.Empty; public string Location { get; set; } = string.Empty; } @@ -322,8 +330,14 @@ public sealed class AssetJourneyViewModel { public int StoryAssetID { get; set; } public string AssetName { get; set; } = string.Empty; + public int BookID { get; set; } + public string BookDisplayName { get; set; } = string.Empty; + public int ChapterID { get; set; } + public string ChapterDisplayName { get; set; } = string.Empty; public int SceneID { get; set; } + public string SceneTitle { get; set; } = string.Empty; public string SceneLabel { get; set; } = string.Empty; + public string SceneDisplayName { get; set; } = string.Empty; public string HolderOrLocation { get; set; } = string.Empty; } @@ -331,8 +345,14 @@ public sealed class LocationActivityViewModel { public int LocationID { get; set; } public string LocationName { get; set; } = string.Empty; + public int BookID { get; set; } + public string BookDisplayName { get; set; } = string.Empty; + public int ChapterID { get; set; } + public string ChapterDisplayName { get; set; } = string.Empty; public int SceneID { get; set; } + public string SceneTitle { get; set; } = string.Empty; public string SceneLabel { get; set; } = string.Empty; + public string SceneDisplayName { get; set; } = string.Empty; public string CharactersPresent { get; set; } = string.Empty; public string AssetsPresent { get; set; } = string.Empty; } diff --git a/PlotLine/Views/ContinuityExplorer/Index.cshtml b/PlotLine/Views/ContinuityExplorer/Index.cshtml index 3b393e2..12a982b 100644 --- a/PlotLine/Views/ContinuityExplorer/Index.cshtml +++ b/PlotLine/Views/ContinuityExplorer/Index.cshtml @@ -245,11 +245,35 @@ else if (Model.Filter.DisplayMode == "Journey")

@group.Key.CharacterName

- + + + @if (Model.ShowBookContext) + { + + } + @if (Model.ShowChapterContext) + { + + } + + + + @foreach (var row in group) { - + + @if (Model.ShowBookContext) + { + + } + @if (Model.ShowChapterContext) + { + + } + + + }
SceneLocation
BookChapterSceneLocation
@row.SceneLabel@row.Location
@row.BookDisplayName@row.ChapterDisplayName@row.SceneDisplayName@row.Location
@@ -261,11 +285,35 @@ else if (Model.Filter.DisplayMode == "Journey")

@group.Key.AssetName

- + + + @if (Model.ShowBookContext) + { + + } + @if (Model.ShowChapterContext) + { + + } + + + + @foreach (var row in group) { - + + @if (Model.ShowBookContext) + { + + } + @if (Model.ShowChapterContext) + { + + } + + + }
SceneHolder / Location
BookChapterSceneHolder / Location
@row.SceneLabel@row.HolderOrLocation
@row.BookDisplayName@row.ChapterDisplayName@row.SceneDisplayName@row.HolderOrLocation
@@ -286,11 +334,39 @@ else {
- + + + + @if (Model.ShowBookContext) + { + + } + @if (Model.ShowChapterContext) + { + + } + + + + + @foreach (var row in Model.LocationActivity) { - + + + @if (Model.ShowBookContext) + { + + } + @if (Model.ShowChapterContext) + { + + } + + + + }
LocationSceneCharacters PresentAssets Present
LocationBookChapterSceneCharacters PresentAssets Present
@row.LocationName@row.SceneLabel@row.CharactersPresent@row.AssetsPresent
@row.LocationName@row.BookDisplayName@row.ChapterDisplayName@row.SceneDisplayName@row.CharactersPresent@row.AssetsPresent