Phase 12B.2: Continuity Explorer Result Context Improvements.

This commit is contained in:
Nick Beckley 2026-06-14 19:37:26 +01:00
parent 1420c43310
commit dc4324f970
3 changed files with 203 additions and 8 deletions

View File

@ -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<Scene> OrderScenes(TimelineData timeline)
@ -2709,6 +2733,71 @@ public sealed class StoryStateService(
.ToDictionary(x => x.SceneID, x => x.Index);
}
private static Dictionary<int, SceneContext> 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<Scene> scopedScenes, IReadOnlyList<Chapter> 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<Scene> scopedScenes, IReadOnlyList<Chapter> 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<Scene> ScopedScenes,
IReadOnlySet<int> AllowedSceneIds,
IReadOnlyDictionary<int, int> SceneIndexes,
IReadOnlyDictionary<int, string> SceneLabels);
IReadOnlyDictionary<int, string> SceneLabels,
IReadOnlyDictionary<int, SceneContext> 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

View File

@ -307,14 +307,22 @@ public sealed class ContinuityExplorerViewModel
public IReadOnlyList<SelectListItem> CharacterOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> 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;
}

View File

@ -245,11 +245,35 @@ else if (Model.Filter.DisplayMode == "Journey")
<h3>@group.Key.CharacterName</h3>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle">
<thead><tr><th>Scene</th><th>Location</th></tr></thead>
<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><td>@row.SceneLabel</td><td>@row.Location</td></tr>
<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>
@ -261,11 +285,35 @@ else if (Model.Filter.DisplayMode == "Journey")
<h3>@group.Key.AssetName</h3>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle">
<thead><tr><th>Scene</th><th>Holder / Location</th></tr></thead>
<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><td>@row.SceneLabel</td><td>@row.HolderOrLocation</td></tr>
<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>
@ -286,11 +334,39 @@ else
{
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>Location</th><th>Scene</th><th>Characters Present</th><th>Assets Present</th></tr></thead>
<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><td>@row.SceneLabel</td><td>@row.CharactersPresent</td><td>@row.AssetsPresent</td></tr>
<tr>
<td>@row.LocationName</td>
@if (Model.ShowBookContext)
{
<td>@row.BookDisplayName</td>
}
@if (Model.ShowChapterContext)
{
<td>@row.ChapterDisplayName</td>
}
<td>@row.SceneDisplayName</td>
<td>@row.CharactersPresent</td>
<td>@row.AssetsPresent</td>
</tr>
}
</tbody>
</table>