Phase 12E: Continuity Warnings Engine (Informational).
This commit is contained in:
parent
9cc20ea266
commit
7c7815642a
@ -118,6 +118,7 @@ public interface IStoryStateService
|
|||||||
Task<List<CharacterJourneyViewModel>> GetCharacterJourneyAsync(int projectId, int characterId, ContinuityFilter filter);
|
Task<List<CharacterJourneyViewModel>> GetCharacterJourneyAsync(int projectId, int characterId, ContinuityFilter filter);
|
||||||
Task<List<AssetJourneyViewModel>> GetAssetJourneyAsync(int projectId, int assetId, ContinuityFilter filter);
|
Task<List<AssetJourneyViewModel>> GetAssetJourneyAsync(int projectId, int assetId, ContinuityFilter filter);
|
||||||
Task<List<LocationActivityViewModel>> GetLocationActivityAsync(int projectId, int locationId, ContinuityFilter filter);
|
Task<List<LocationActivityViewModel>> GetLocationActivityAsync(int projectId, int locationId, ContinuityFilter filter);
|
||||||
|
Task<List<ContinuityWarningViewModel>> GetContinuityWarningsAsync(int projectId, ContinuityFilter filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPlotService
|
public interface IPlotService
|
||||||
@ -2336,6 +2337,10 @@ public sealed class StoryStateService(
|
|||||||
.ThenBy(x => x.LocationName)
|
.ThenBy(x => x.LocationName)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(filter.DisplayMode, "ContinuityWarnings", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
model.ContinuityWarnings = await GetContinuityWarningsAsync(filter.ProjectID, filter);
|
||||||
|
}
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
@ -2519,6 +2524,42 @@ public sealed class StoryStateService(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<ContinuityWarningViewModel>> GetContinuityWarningsAsync(int projectId, ContinuityFilter filter)
|
||||||
|
{
|
||||||
|
var context = await GetRangeContextAsync(projectId, filter);
|
||||||
|
var characterTimeline = await characters.GetTimelineAsync(projectId, null);
|
||||||
|
var assetTimeline = await assets.GetTimelineAsync(projectId, null);
|
||||||
|
var allAssetLocations = await GetAssetLocationsThroughCurrentAsync(context.OrderedScenes);
|
||||||
|
var custodyCharacters = await assets.ListCustodyCharactersByProjectAsync(projectId);
|
||||||
|
var enabledCharacterIds = filter.CharacterIDs.ToHashSet();
|
||||||
|
var enabledAssetIds = filter.AssetIDs.ToHashSet();
|
||||||
|
var warnings = new List<ContinuityWarningViewModel>();
|
||||||
|
|
||||||
|
if (filter.IncludeCharacters)
|
||||||
|
{
|
||||||
|
warnings.AddRange(BuildMultipleCharacterLocationWarnings(characterTimeline.Appearances, enabledCharacterIds, context));
|
||||||
|
warnings.AddRange(BuildImpossibleMovementWarnings(characterTimeline.Appearances, enabledCharacterIds, context));
|
||||||
|
warnings.AddRange(BuildUnknownCharacterLocationWarnings(characterTimeline.Characters, characterTimeline.Appearances, enabledCharacterIds, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.IncludeAssets)
|
||||||
|
{
|
||||||
|
var scopedAssetLocations = allAssetLocations.Where(x => context.AllowedSceneIds.Contains(x.SceneID)).ToList();
|
||||||
|
var scopedCustody = custodyCharacters.Where(x => context.AllowedSceneIds.Contains(x.SceneID)).ToList();
|
||||||
|
warnings.AddRange(BuildAssetLocationConflictWarnings(assetTimeline.Assets, scopedAssetLocations, scopedCustody, enabledAssetIds, context));
|
||||||
|
warnings.AddRange(BuildUnknownAssetLocationWarnings(assetTimeline.Assets, assetTimeline.Events, allAssetLocations, custodyCharacters, enabledAssetIds, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
.OrderBy(x => WarningSeveritySort(x.Severity))
|
||||||
|
.ThenBy(x => x.BookDisplayName)
|
||||||
|
.ThenBy(x => x.ChapterDisplayName)
|
||||||
|
.ThenBy(x => x.SceneID ?? int.MaxValue)
|
||||||
|
.ThenBy(x => x.WarningType)
|
||||||
|
.ThenBy(x => x.Message)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<RangeContext> GetRangeContextAsync(int projectId, ContinuityFilter filter)
|
private async Task<RangeContext> GetRangeContextAsync(int projectId, ContinuityFilter filter)
|
||||||
{
|
{
|
||||||
NormaliseFilter(filter);
|
NormaliseFilter(filter);
|
||||||
@ -2737,6 +2778,203 @@ public sealed class StoryStateService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ContinuityWarningViewModel> BuildMultipleCharacterLocationWarnings(
|
||||||
|
IReadOnlyList<SceneCharacter> appearances,
|
||||||
|
IReadOnlySet<int> enabledCharacterIds,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
return appearances
|
||||||
|
.Where(x => context.AllowedSceneIds.Contains(x.SceneID)
|
||||||
|
&& x.LocationID.HasValue
|
||||||
|
&& (!enabledCharacterIds.Any() || enabledCharacterIds.Contains(x.CharacterID)))
|
||||||
|
.GroupBy(x => new { x.SceneID, x.CharacterID, x.CharacterName })
|
||||||
|
.Where(group => group.Select(x => x.LocationID).Distinct().Count() > 1)
|
||||||
|
.Select(group => BuildWarning(
|
||||||
|
"Multiple Character Locations",
|
||||||
|
"Critical",
|
||||||
|
$"{group.Key.CharacterName} appears in multiple locations during {context.SceneLabels.GetValueOrDefault(group.Key.SceneID, "this scene")}.",
|
||||||
|
group.Key.SceneID,
|
||||||
|
context,
|
||||||
|
characterId: group.Key.CharacterID))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ContinuityWarningViewModel> BuildImpossibleMovementWarnings(
|
||||||
|
IReadOnlyList<SceneCharacter> appearances,
|
||||||
|
IReadOnlySet<int> enabledCharacterIds,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
var warnings = new List<ContinuityWarningViewModel>();
|
||||||
|
var rowsByCharacter = appearances
|
||||||
|
.Where(x => x.LocationID.HasValue && (!enabledCharacterIds.Any() || enabledCharacterIds.Contains(x.CharacterID)))
|
||||||
|
.GroupBy(x => x.CharacterID);
|
||||||
|
|
||||||
|
foreach (var group in rowsByCharacter)
|
||||||
|
{
|
||||||
|
var explicitLocations = group
|
||||||
|
.GroupBy(x => x.SceneID)
|
||||||
|
.Select(sceneGroup => sceneGroup.OrderByDescending(x => x.UpdatedDate).First())
|
||||||
|
.OrderBy(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
for (var index = 1; index < explicitLocations.Count; index++)
|
||||||
|
{
|
||||||
|
var previous = explicitLocations[index - 1];
|
||||||
|
var current = explicitLocations[index];
|
||||||
|
if (previous.LocationID == current.LocationID || !context.AllowedSceneIds.Contains(current.SceneID))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousIndex = context.SceneIndexes.GetValueOrDefault(previous.SceneID, -1);
|
||||||
|
var currentIndex = context.SceneIndexes.GetValueOrDefault(current.SceneID, -1);
|
||||||
|
if (currentIndex - previousIndex != 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings.Add(BuildWarning(
|
||||||
|
"Impossible Character Movement",
|
||||||
|
"Warning",
|
||||||
|
$"{current.CharacterName} moved from {previous.LocationPath ?? previous.LocationName ?? "an unknown location"} to {current.LocationPath ?? current.LocationName ?? "an unknown location"} between consecutive scenes.",
|
||||||
|
current.SceneID,
|
||||||
|
context,
|
||||||
|
characterId: current.CharacterID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ContinuityWarningViewModel> BuildUnknownCharacterLocationWarnings(
|
||||||
|
IReadOnlyList<Character> projectCharacters,
|
||||||
|
IReadOnlyList<SceneCharacter> appearances,
|
||||||
|
IReadOnlySet<int> enabledCharacterIds,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
var appearancesByCharacter = appearances
|
||||||
|
.Where(x => context.AllowedSceneIds.Contains(x.SceneID))
|
||||||
|
.GroupBy(x => x.CharacterID)
|
||||||
|
.ToDictionary(x => x.Key, x => x.ToList());
|
||||||
|
var locationCharacterIds = appearances
|
||||||
|
.Where(x => x.LocationID.HasValue)
|
||||||
|
.Select(x => x.CharacterID)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
return projectCharacters
|
||||||
|
.Where(character => (!enabledCharacterIds.Any() || enabledCharacterIds.Contains(character.CharacterID))
|
||||||
|
&& appearancesByCharacter.ContainsKey(character.CharacterID)
|
||||||
|
&& !locationCharacterIds.Contains(character.CharacterID))
|
||||||
|
.Select(character =>
|
||||||
|
{
|
||||||
|
var firstSceneId = appearancesByCharacter[character.CharacterID]
|
||||||
|
.OrderBy(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1))
|
||||||
|
.First()
|
||||||
|
.SceneID;
|
||||||
|
return BuildWarning(
|
||||||
|
"Characters With Unknown Locations",
|
||||||
|
"Information",
|
||||||
|
$"{character.CharacterName} has no known location.",
|
||||||
|
firstSceneId,
|
||||||
|
context,
|
||||||
|
characterId: character.CharacterID);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ContinuityWarningViewModel> BuildAssetLocationConflictWarnings(
|
||||||
|
IReadOnlyList<StoryAsset> projectAssets,
|
||||||
|
IReadOnlyList<SceneAssetLocation> sceneAssetLocations,
|
||||||
|
IReadOnlyList<AssetCustodyCharacter> custodyCharacters,
|
||||||
|
IReadOnlySet<int> enabledAssetIds,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
var assetNames = projectAssets.ToDictionary(x => x.StoryAssetID, x => x.AssetName);
|
||||||
|
var directLocationGroups = sceneAssetLocations
|
||||||
|
.Where(x => !enabledAssetIds.Any() || enabledAssetIds.Contains(x.StoryAssetID))
|
||||||
|
.GroupBy(x => new { x.SceneID, x.StoryAssetID });
|
||||||
|
var custodyGroups = custodyCharacters
|
||||||
|
.Where(x => !enabledAssetIds.Any() || enabledAssetIds.Contains(x.StoryAssetID))
|
||||||
|
.GroupBy(x => new { x.SceneID, x.StoryAssetID })
|
||||||
|
.ToDictionary(x => x.Key, x => x.ToList());
|
||||||
|
var warnings = new List<ContinuityWarningViewModel>();
|
||||||
|
|
||||||
|
foreach (var group in directLocationGroups)
|
||||||
|
{
|
||||||
|
var directLocationCount = group.Select(x => x.LocationID).Distinct().Count();
|
||||||
|
custodyGroups.TryGetValue(group.Key, out var custodyRows);
|
||||||
|
var hasCustody = custodyRows?.Any() == true;
|
||||||
|
if (directLocationCount <= 1 && !hasCustody)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings.Add(BuildWarning(
|
||||||
|
"Asset Location Conflict",
|
||||||
|
"Critical",
|
||||||
|
$"{assetNames.GetValueOrDefault(group.Key.StoryAssetID, "An asset")} has multiple explicit locations in {context.SceneLabels.GetValueOrDefault(group.Key.SceneID, "this scene")}.",
|
||||||
|
group.Key.SceneID,
|
||||||
|
context,
|
||||||
|
assetId: group.Key.StoryAssetID));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in custodyGroups.Where(x => !directLocationGroups.Any(locationGroup => locationGroup.Key.SceneID == x.Key.SceneID && locationGroup.Key.StoryAssetID == x.Key.StoryAssetID)))
|
||||||
|
{
|
||||||
|
var holderCount = group.Value.Select(x => x.CharacterID?.ToString() ?? x.CharacterNameText ?? string.Empty).Where(x => x.Length > 0).Distinct().Count();
|
||||||
|
if (holderCount <= 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings.Add(BuildWarning(
|
||||||
|
"Asset Location Conflict",
|
||||||
|
"Critical",
|
||||||
|
$"{assetNames.GetValueOrDefault(group.Key.StoryAssetID, "An asset")} has multiple explicit holders in {context.SceneLabels.GetValueOrDefault(group.Key.SceneID, "this scene")}.",
|
||||||
|
group.Key.SceneID,
|
||||||
|
context,
|
||||||
|
assetId: group.Key.StoryAssetID));
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ContinuityWarningViewModel> BuildUnknownAssetLocationWarnings(
|
||||||
|
IReadOnlyList<StoryAsset> projectAssets,
|
||||||
|
IReadOnlyList<AssetEvent> assetEvents,
|
||||||
|
IReadOnlyList<SceneAssetLocation> allAssetLocations,
|
||||||
|
IReadOnlyList<AssetCustodyCharacter> custodyCharacters,
|
||||||
|
IReadOnlySet<int> enabledAssetIds,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
var appearedAssetSceneIds = assetEvents
|
||||||
|
.Where(x => context.AllowedSceneIds.Contains(x.SceneID))
|
||||||
|
.Select(x => new { x.StoryAssetID, x.SceneID })
|
||||||
|
.Concat(allAssetLocations.Where(x => context.AllowedSceneIds.Contains(x.SceneID)).Select(x => new { x.StoryAssetID, x.SceneID }))
|
||||||
|
.Concat(custodyCharacters.Where(x => context.AllowedSceneIds.Contains(x.SceneID)).Select(x => new { x.StoryAssetID, x.SceneID }))
|
||||||
|
.GroupBy(x => x.StoryAssetID)
|
||||||
|
.ToDictionary(x => x.Key, x => x.Min(row => context.SceneIndexes.GetValueOrDefault(row.SceneID, int.MaxValue)));
|
||||||
|
var locatedAssetIds = allAssetLocations.Select(x => x.StoryAssetID)
|
||||||
|
.Concat(custodyCharacters.Select(x => x.StoryAssetID))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
return projectAssets
|
||||||
|
.Where(asset => (!enabledAssetIds.Any() || enabledAssetIds.Contains(asset.StoryAssetID))
|
||||||
|
&& appearedAssetSceneIds.ContainsKey(asset.StoryAssetID)
|
||||||
|
&& !locatedAssetIds.Contains(asset.StoryAssetID))
|
||||||
|
.Select(asset =>
|
||||||
|
{
|
||||||
|
var firstSceneId = context.OrderedScenes.FirstOrDefault(scene => context.SceneIndexes.GetValueOrDefault(scene.SceneID, int.MaxValue) == appearedAssetSceneIds[asset.StoryAssetID])?.SceneID;
|
||||||
|
return BuildWarning(
|
||||||
|
"Assets With Unknown Locations",
|
||||||
|
"Information",
|
||||||
|
$"{asset.AssetName} has no known location.",
|
||||||
|
firstSceneId,
|
||||||
|
context,
|
||||||
|
assetId: asset.StoryAssetID);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static LocationActivityEntityViewModel? BuildCurrentCharacterPresence(
|
private static LocationActivityEntityViewModel? BuildCurrentCharacterPresence(
|
||||||
Character character,
|
Character character,
|
||||||
IReadOnlyList<SceneCharacter> assignments,
|
IReadOnlyList<SceneCharacter> assignments,
|
||||||
@ -2890,6 +3128,40 @@ public sealed class StoryStateService(
|
|||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ContinuityWarningViewModel BuildWarning(
|
||||||
|
string warningType,
|
||||||
|
string severity,
|
||||||
|
string message,
|
||||||
|
int? sceneId,
|
||||||
|
RangeContext context,
|
||||||
|
int? characterId = null,
|
||||||
|
int? assetId = null)
|
||||||
|
{
|
||||||
|
var sceneContext = sceneId.HasValue ? context.SceneContexts.GetValueOrDefault(sceneId.Value) : null;
|
||||||
|
return new ContinuityWarningViewModel
|
||||||
|
{
|
||||||
|
WarningType = warningType,
|
||||||
|
Severity = severity,
|
||||||
|
Message = message,
|
||||||
|
CharacterID = characterId,
|
||||||
|
AssetID = assetId,
|
||||||
|
SceneID = sceneId,
|
||||||
|
SceneDisplayName = sceneContext?.SceneDisplayName ?? (sceneId.HasValue ? context.SceneLabels.GetValueOrDefault(sceneId.Value, string.Empty) : string.Empty),
|
||||||
|
BookDisplayName = sceneContext?.BookDisplayName ?? string.Empty,
|
||||||
|
ChapterDisplayName = sceneContext?.ChapterDisplayName ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WarningSeveritySort(string severity)
|
||||||
|
{
|
||||||
|
return severity switch
|
||||||
|
{
|
||||||
|
"Critical" => 0,
|
||||||
|
"Warning" => 1,
|
||||||
|
_ => 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<int, int> BuildSceneIndexes(IReadOnlyList<Scene> orderedScenes)
|
private static Dictionary<int, int> BuildSceneIndexes(IReadOnlyList<Scene> orderedScenes)
|
||||||
{
|
{
|
||||||
return orderedScenes
|
return orderedScenes
|
||||||
|
|||||||
@ -303,6 +303,7 @@ public sealed class ContinuityExplorerViewModel
|
|||||||
public IReadOnlyList<CharacterJourneyViewModel> CharacterJourney { get; set; } = [];
|
public IReadOnlyList<CharacterJourneyViewModel> CharacterJourney { get; set; } = [];
|
||||||
public IReadOnlyList<AssetJourneyViewModel> AssetJourney { get; set; } = [];
|
public IReadOnlyList<AssetJourneyViewModel> AssetJourney { get; set; } = [];
|
||||||
public IReadOnlyList<LocationActivityViewModel> LocationActivity { get; set; } = [];
|
public IReadOnlyList<LocationActivityViewModel> LocationActivity { get; set; } = [];
|
||||||
|
public IReadOnlyList<ContinuityWarningViewModel> ContinuityWarnings { get; set; } = [];
|
||||||
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
|
||||||
public IReadOnlyList<SelectListItem> ChapterOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> ChapterOptions { get; set; } = [];
|
||||||
public IReadOnlyList<SelectListItem> SceneOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> SceneOptions { get; set; } = [];
|
||||||
@ -313,6 +314,19 @@ public sealed class ContinuityExplorerViewModel
|
|||||||
public bool ShowChapterContext { get; set; }
|
public bool ShowChapterContext { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ContinuityWarningViewModel
|
||||||
|
{
|
||||||
|
public string WarningType { get; set; } = string.Empty;
|
||||||
|
public string Severity { get; set; } = "Information";
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public int? CharacterID { get; set; }
|
||||||
|
public int? AssetID { get; set; }
|
||||||
|
public int? SceneID { get; set; }
|
||||||
|
public string SceneDisplayName { get; set; } = string.Empty;
|
||||||
|
public string BookDisplayName { get; set; } = string.Empty;
|
||||||
|
public string ChapterDisplayName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class CharacterJourneyViewModel
|
public sealed class CharacterJourneyViewModel
|
||||||
{
|
{
|
||||||
public int CharacterID { get; set; }
|
public int CharacterID { get; set; }
|
||||||
|
|||||||
@ -12,6 +12,13 @@
|
|||||||
_ => "bg-warning text-dark"
|
_ => "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) =>
|
private bool IsEntitySelected(string entityType) =>
|
||||||
Model.Filter.EntityTypes.Contains(entityType, StringComparer.OrdinalIgnoreCase);
|
Model.Filter.EntityTypes.Contains(entityType, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@ -144,6 +151,10 @@
|
|||||||
<input class="form-check-input" type="radio" name="DisplayMode" value="LocationActivity" checked="@(Model.Filter.DisplayMode == "LocationActivity")" />
|
<input class="form-check-input" type="radio" name="DisplayMode" value="LocationActivity" checked="@(Model.Filter.DisplayMode == "LocationActivity")" />
|
||||||
<span class="form-check-label">Location Activity</span>
|
<span class="form-check-label">Location Activity</span>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
|
||||||
@ -350,6 +361,52 @@ else if (Model.Filter.DisplayMode == "Journey")
|
|||||||
}
|
}
|
||||||
</section>
|
</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
|
else
|
||||||
{
|
{
|
||||||
<section class="story-bible-section">
|
<section class="story-bible-section">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user