From 4ebbfdab92d521334354ad77533c7767a7349a65 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 12 Jul 2026 20:37:49 +0000 Subject: [PATCH] Phase 21J - Story Memory Visualisation Refinement --- ...ntelligenceVisualisationSnapshotService.cs | 277 +++++++++++++++++- ...telligenceExperiencePrototypeViewModels.cs | 6 + .../StoryIntelligenceExperience.cshtml | 24 ++ ...tory-intelligence-experience-prototype.css | 14 +- ...story-intelligence-experience-prototype.js | 2 + 5 files changed, 304 insertions(+), 19 deletions(-) diff --git a/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs b/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs index 4f05533..7a12e33 100644 --- a/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs +++ b/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs @@ -115,7 +115,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( .ToList(); var current = PickCurrentScene(run, completed); - var scenes = BuildSceneStates(run, parsed, completed, current).ToList(); + var storyMemory = new StoryMemory(); + var scenes = BuildSceneStates(run, parsed, completed, current, storyMemory: storyMemory).ToList(); if (scenes.Count == 0) { scenes.Add(BuildEmptyScene(run)); @@ -130,7 +131,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( ChangeToken = ChangeToken(run, results), IsTerminal = IsTerminal(run.Status), SnapshotMessage = SnapshotMessage(run, completed.Count), - Diagnostics = BuildDiagnostics(run, results, parsed, completed, current), + Diagnostics = BuildDiagnostics(run, results, parsed, completed, current, storyMemory), ProjectName = DisplayTitle(run.ProjectTitle, "Untitled project"), BookTitle = DisplayTitle(run.BookTitle, "Untitled book"), Scenes = scenes @@ -141,6 +142,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( if (run.ProjectID.HasValue) { await illustrationMatcher.ApplyCharacterIllustrationsAsync(run.ProjectID.Value, model); + ApplyIllustrationDiagnostics(model); } logger.LogInformation("Built Story Intelligence visualisation snapshot for run {RunId} in {ElapsedMs}ms.", runId, stopwatch.ElapsedMilliseconds); return model; @@ -198,6 +200,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run); var scenes = new List(); var nextSceneNumber = 1; + var storyMemory = new StoryMemory(); foreach (var snapshot in runSnapshots) { var runScenes = BuildSceneStates( @@ -215,7 +218,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( overallElapsed: overallElapsed, overallRemaining: overallRemaining, continuousSceneNumbering: true, - overallTerminal: isTerminal) + overallTerminal: isTerminal, + storyMemory: storyMemory) .ToList(); scenes.AddRange(runScenes); nextSceneNumber += snapshot.Completed.Count; @@ -236,7 +240,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( ChangeToken = SessionChangeToken(session, runSnapshots), IsTerminal = isTerminal, SnapshotMessage = SessionSnapshotMessage(session, runSnapshots), - Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent), + Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent, storyMemory), ProjectName = DisplayTitle(session.ProjectName, "Untitled project"), BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"), Scenes = scenes @@ -245,6 +249,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( StabiliseCharacterIllustrations(model); await illustrationLibrary.ApplyApprovedIllustrationsAsync(model); await illustrationMatcher.ApplyCharacterIllustrationsAsync(session.ProjectID, model); + ApplyIllustrationDiagnostics(model); logger.LogInformation("Built Story Intelligence import session visualisation snapshot for session {ImportSessionId} in {ElapsedMs}ms.", importSessionId, stopwatch.ElapsedMilliseconds); return model; } @@ -264,14 +269,17 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( string? overallElapsed = null, string? overallRemaining = null, bool continuousSceneNumbering = false, - bool overallTerminal = false) + bool overallTerminal = false, + StoryMemory? storyMemory = null) { + storyMemory ??= new StoryMemory(); var totalScenes = Math.Max(run.TotalDetectedScenes ?? 0, allResults.Count); var completedScenes = run.CompletedScenes ?? completed.Count; var displayIndex = 0; foreach (var item in completed) { var scene = item.Scene!; + storyMemory.Update(scene); var sourceSceneNumber = SceneNumber(item); var sceneNumber = continuousSceneNumbering ? continuousSceneNumberStart + displayIndex++ : sourceSceneNumber; var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID; @@ -295,11 +303,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes), ElapsedTime = overallElapsed ?? EstimateElapsed(run), EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes), - PovCharacterId = CharacterId(ResolveNarratorName(scene) ?? scene.PointOfView?.CharacterName), + PovCharacterId = CharacterId(storyMemory.ResolvePointOfView(scene)), Location = BuildLocation(scene), - Characters = BuildCharacters(scene), + Characters = BuildCharacters(scene, storyMemory), Assets = BuildAssets(scene), - Relationships = BuildRelationships(scene), + Relationships = BuildRelationships(scene, storyMemory), KnowledgeThreads = BuildKnowledge(scene), Observations = BuildObservations(scene), LatestDiscoveries = isCurrent ? BuildDiscoveries(scene).ToList() : [], @@ -341,12 +349,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( return completed.Last(); } - private static IReadOnlyList BuildCharacters(SceneIntelligenceScene scene) + private static IReadOnlyList BuildCharacters(SceneIntelligenceScene scene, StoryMemory? storyMemory = null) { - var narratorResolution = ResolveNarratorName(scene); + var narratorResolution = storyMemory?.ResolvePointOfView(scene) ?? ResolveNarratorName(scene); var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName); var states = new List(); var reservedCodes = new HashSet(StringComparer.OrdinalIgnoreCase); + var sceneCharacterNames = new HashSet((scene.Characters ?? []).Select(character => Clean(character.Name)), StringComparer.OrdinalIgnoreCase); foreach (var group in (scene.Characters ?? []) .Where(character => !string.IsNullOrWhiteSpace(character.Name)) .GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase) @@ -388,6 +397,28 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( }); } + if (storyMemory is not null) + { + foreach (var memoryCharacter in storyMemory.Characters + .Where(character => !sceneCharacterNames.Contains(character.Name)) + .OrderByDescending(character => character.Weight) + .Take(Math.Max(0, 5 - states.Count))) + { + var libraryCode = CharacterLibraryCode(memoryCharacter.Name, null, scene, reservedCodes); + reservedCodes.Add(libraryCode); + states.Add(new StoryIntelligenceExperienceCharacterState + { + Id = CharacterId(memoryCharacter.Name), + LibraryCode = libraryCode, + Name = memoryCharacter.Name, + Role = memoryCharacter.Role, + Relevance = memoryCharacter.Relevance, + Weight = Math.Max(38, memoryCharacter.Weight - 18), + ImagePath = CharacterFallback(memoryCharacter.Name) + }); + } + } + return states.OrderByDescending(character => character.Weight).ToList(); } @@ -493,8 +524,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( .Take(5) .ToList(); - private static IReadOnlyList BuildRelationships(SceneIntelligenceScene scene) - => (scene.Relationships ?? []) + private static IReadOnlyList BuildRelationships(SceneIntelligenceScene scene, StoryMemory? storyMemory = null) + { + var current = (scene.Relationships ?? []) .Where(item => !string.IsNullOrWhiteSpace(item.CharacterA) && !string.IsNullOrWhiteSpace(item.CharacterB)) .Select(item => new StoryIntelligenceExperienceRelationshipState { @@ -505,9 +537,26 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( State = Trim(FirstConfigured(item.Evidence, item.RelationshipSignal, "Emerging"), 58), Weight = ConfidenceWeight(item.Confidence, 70) }) + .OrderByDescending(item => item.Weight) + .ToList(); + var seen = new HashSet(current.Select(item => $"{item.SourceId}|{item.TargetId}"), StringComparer.OrdinalIgnoreCase); + var memoryRelationships = (storyMemory?.Relationships ?? []) + .Where(item => seen.Add($"{item.SourceId}|{item.TargetId}")) + .Select(item => new StoryIntelligenceExperienceRelationshipState + { + Id = item.Id, + SourceId = item.SourceId, + TargetId = item.TargetId, + Label = item.Label, + State = item.State, + Weight = Math.Max(44, item.Weight - 16) + }); + + return current.Concat(memoryRelationships) .OrderByDescending(item => item.Weight) .Take(4) .ToList(); + } private static IReadOnlyList BuildKnowledge(SceneIntelligenceScene scene) { @@ -733,6 +782,179 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( : "All known chapter runs are complete. Awaiting the next book import stage."; } + private sealed class StoryMemory + { + private readonly Dictionary characters = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary relationships = new(StringComparer.OrdinalIgnoreCase); + private string? narratorResolvedName; + + public IReadOnlyCollection Characters => characters.Values; + public IReadOnlyCollection Relationships => relationships.Values; + public decimal NarratorConfidence { get; private set; } + public int ResolvedCharacterCount => characters.Values.Count(character => character.IsResolved); + public int UnresolvedCharacterCount => characters.Values.Count(character => !character.IsResolved); + + public void Update(SceneIntelligenceScene scene) + { + var resolvedNarrator = ResolveNarratorName(scene); + if (!string.IsNullOrWhiteSpace(resolvedNarrator)) + { + narratorResolvedName = resolvedNarrator; + NarratorConfidence = Math.Max(NarratorConfidence, 0.82m); + } + else if (IsGenericNarratorName(scene.PointOfView?.CharacterName)) + { + NarratorConfidence = Math.Max(NarratorConfidence, 0.35m); + } + + foreach (var character in scene.Characters ?? []) + { + var name = Clean(character.Name); + if (string.IsNullOrWhiteSpace(name) || IsGenericNarratorName(name)) + { + if (!string.IsNullOrWhiteSpace(narratorResolvedName)) + { + name = narratorResolvedName; + } + else + { + name = "Narrator"; + } + } + + var memory = GetOrAddCharacter(name); + memory.Weight = Math.Max(memory.Weight, CharacterWeight(character)); + memory.Role = FirstConfigured(memory.Role, character.RoleInScene, character.MentionedOnly == true ? "Referenced" : "Scene character"); + memory.Relevance = Trim(FirstConfigured(character.Notes, memory.Relevance, string.Join(", ", character.Actions ?? []), "Remembered from earlier scenes"), 60); + ApplyInference(memory, $"{name} {character.RoleInScene} {character.Notes} {string.Join(" ", character.Actions ?? [])}"); + } + + var povName = ResolvePointOfView(scene); + if (!string.IsNullOrWhiteSpace(povName)) + { + var pov = GetOrAddCharacter(povName); + pov.PovCount += 1; + pov.Weight = Math.Max(pov.Weight, 100); + pov.Role = "POV character"; + pov.Relevance = pov.PovCount > 1 ? "Recurring point of view" : "Point of view"; + } + + foreach (var relationship in scene.Relationships ?? []) + { + var a = ResolveCharacterName(relationship.CharacterA); + var b = ResolveCharacterName(relationship.CharacterB); + if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) + { + continue; + } + + ApplyInference(GetOrAddCharacter(a), $"{relationship.RelationshipSignal} {relationship.Evidence}"); + ApplyInference(GetOrAddCharacter(b), $"{relationship.RelationshipSignal} {relationship.Evidence}"); + + var key = $"{CharacterId(a)}|{CharacterId(b)}"; + relationships[key] = new StoryIntelligenceExperienceRelationshipState + { + Id = $"memory-relationship-{StableKey(a)}-{StableKey(b)}", + SourceId = CharacterId(a), + TargetId = CharacterId(b), + Label = Trim(FirstConfigured(relationship.RelationshipSignal, "Relationship remembered"), 44), + State = Trim(FirstConfigured(relationship.Evidence, relationship.RelationshipSignal, "Carried forward from earlier scenes"), 58), + Weight = ConfidenceWeight(relationship.Confidence, 64) + }; + } + } + + public string? ResolvePointOfView(SceneIntelligenceScene scene) + { + var raw = Clean(scene.PointOfView?.CharacterName); + if (IsGenericNarratorName(raw)) + { + return narratorResolvedName ?? raw; + } + + return string.IsNullOrWhiteSpace(raw) ? narratorResolvedName : raw; + } + + private string ResolveCharacterName(string? raw) + { + var name = Clean(raw); + return IsGenericNarratorName(name) && !string.IsNullOrWhiteSpace(narratorResolvedName) ? narratorResolvedName! : name; + } + + private MemoryCharacter GetOrAddCharacter(string name) + { + var key = CharacterId(name); + if (!characters.TryGetValue(key, out var character)) + { + character = new MemoryCharacter(name); + characters[key] = character; + } + + return character; + } + + private static void ApplyInference(MemoryCharacter character, string evidence) + { + var text = evidence.ToLowerInvariant(); + if (ContainsAny(text, "mother", "daughter", "sister", "wife", "she ", " her ")) + { + character.Presentation = "Feminine"; + } + else if (ContainsAny(text, "father", "son", "brother", "husband", " he ", " him ")) + { + character.Presentation = "Masculine"; + } + else + { + character.Presentation ??= NamePresentation(character.Name); + } + + if (ContainsAny(text, "grandfather", "grandmother", "elderly")) + { + character.AgeBand = "Elderly"; + } + else if (ContainsAny(text, "mother", "father", "parent", "middle aged", "middle-aged")) + { + character.AgeBand = "Middle Aged"; + } + else if (ContainsAny(text, "teenage", "teenager", "fifteen", "sixteen", "seventeen")) + { + character.AgeBand = "Older Teen"; + } + } + + private static string? NamePresentation(string name) + { + var key = name.ToLowerInvariant(); + if (key is "beth" or "rosie" or "maggie" or "annie" or "anna" or "anne") + { + return "Feminine"; + } + + if (key is "graham" or "simon" or "kevin" or "colin" or "john" or "david") + { + return "Masculine"; + } + + return null; + } + + private static bool ContainsAny(string text, params string[] needles) + => needles.Any(needle => text.Contains(needle, StringComparison.OrdinalIgnoreCase)); + } + + private sealed class MemoryCharacter(string name) + { + public string Name { get; } = name; + public string Role { get; set; } = "Remembered character"; + public string Relevance { get; set; } = "Known from earlier scenes"; + public int Weight { get; set; } = 48; + public int PovCount { get; set; } + public string? Presentation { get; set; } + public string? AgeBand { get; set; } + public bool IsResolved => !IsGenericNarratorName(Name); + } + private static string CurrentSceneText(RunSnapshot snapshot) { if (snapshot.Run.TotalDetectedScenes.HasValue) @@ -773,7 +995,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( int parsedScenes, int totalPersistedResults, int totalScenes, - int overallProgressPercent) + int overallProgressPercent, + StoryMemory storyMemory) { var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null)); var warning = failed == 0 ? null : $"{failed:N0} scene result(s) skipped because they could not be parsed."; @@ -792,6 +1015,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( TotalScenes = totalScenes, ProgressSource = "ImportSession", CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run), + StoryMemoryCharacterCount = storyMemory.Characters.Count, + ResolvedCharacterCount = storyMemory.ResolvedCharacterCount, + UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount, + NarratorConfidence = storyMemory.NarratorConfidence, SnapshotSource = "Real", RunStatus = SessionStatus(session, snapshots), PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count), @@ -813,7 +1040,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( IReadOnlyList results, IReadOnlyList parsed, IReadOnlyList completed, - ParsedSceneResult? current) + ParsedSceneResult? current, + StoryMemory storyMemory) { var failed = parsed.Where(item => item.Scene is null).ToList(); var warning = failed.Count == 0 @@ -834,6 +1062,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count), ProgressSource = "ChapterRun", CurrentBackendStage = BackendStageDisplay(run), + StoryMemoryCharacterCount = storyMemory.Characters.Count, + ResolvedCharacterCount = storyMemory.ResolvedCharacterCount, + UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount, + NarratorConfidence = storyMemory.NarratorConfidence, SnapshotSource = "Real", RunStatus = run.Status, PersistedCompletedSceneCount = run.CompletedScenes ?? 0, @@ -884,6 +1116,20 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( } } + private static void ApplyIllustrationDiagnostics(StoryIntelligenceExperiencePrototypeViewModel model) + { + var characterResolutions = model.Scenes + .SelectMany(scene => scene.Characters) + .Select(character => character.ImageResolution) + .Where(resolution => resolution.IllustrationLibraryItemId.HasValue) + .ToList(); + model.Diagnostics.IllustrationAssignmentCount = characterResolutions + .Select(resolution => resolution.IllustrationLibraryItemId!.Value) + .Distinct() + .Count(); + model.Diagnostics.IllustrationChangeCount = characterResolutions.Count(resolution => !string.IsNullOrWhiteSpace(resolution.ReasonReassigned)); + } + private static bool IsTerminal(string status) => status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings or StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled; @@ -1102,6 +1348,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( private static string LocationLibraryCode(string name, SceneIntelligenceLocation? location, SceneIntelligenceSetting? setting) { var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant(); + if (value.Contains("bathroom") || value.Contains("toilet") || value.Contains("washroom")) return "loc-laundry-utility-room"; + if (value.Contains("kitchen") || value.Contains("desk") || value.Contains("office") || value.Contains("study") || value.Contains("library")) return "loc-domestic-desk"; + if (value.Contains("waiting room") || value.Contains("bedroom") || value.Contains("sitting room") || value.Contains("living room")) return "loc-small-flat-interior"; if (value.Contains("laundry") || value.Contains("utility")) return "loc-laundry-utility-room"; if (value.Contains("stair")) return "loc-narrow-stairwell"; if (value.Contains("car park") || value.Contains("parking")) return "loc-wet-car-park"; diff --git a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs index eac16c4..be15f02 100644 --- a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs +++ b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs @@ -32,6 +32,12 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics public int TotalScenes { get; init; } public string ProgressSource { get; init; } = string.Empty; public string CurrentBackendStage { get; init; } = string.Empty; + public int StoryMemoryCharacterCount { get; init; } + public int ResolvedCharacterCount { get; init; } + public int UnresolvedCharacterCount { get; init; } + public decimal NarratorConfidence { get; init; } + public int IllustrationAssignmentCount { get; set; } + public int IllustrationChangeCount { get; set; } public string SnapshotSource { get; init; } = string.Empty; public string RunStatus { get; init; } = string.Empty; public int PersistedCompletedSceneCount { get; init; } diff --git a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml index 868f5cc..278b321 100644 --- a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml +++ b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml @@ -237,6 +237,30 @@
Current backend stage
@Model.Diagnostics.CurrentBackendStage
+
+
Story Memory character count
+
@Model.Diagnostics.StoryMemoryCharacterCount
+
+
+
Resolved characters
+
@Model.Diagnostics.ResolvedCharacterCount
+
+
+
Unresolved characters
+
@Model.Diagnostics.UnresolvedCharacterCount
+
+
+
Narrator confidence
+
@Model.Diagnostics.NarratorConfidence
+
+
+
Illustration assignments
+
@Model.Diagnostics.IllustrationAssignmentCount
+
+
+
Illustration changes
+
@Model.Diagnostics.IllustrationChangeCount
+
Current polling interval
Initial
diff --git a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css index 5462e80..3588686 100644 --- a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css +++ b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css @@ -781,7 +781,7 @@ input:focus-visible { display: grid; justify-items: center; gap: 2px; - width: min(176px, calc(var(--node-size, 150px) + 44px)); + width: min(188px, calc(var(--node-size, 150px) + 58px)); max-width: calc(100vw - 32px); margin-left: 0; border: 1px solid rgba(151, 185, 218, 0.1); @@ -831,23 +831,27 @@ input:focus-visible { } .story-character__text strong { + display: -webkit-box; overflow: hidden; max-width: 100%; font-size: clamp(0.82rem, 0.9vw, 0.98rem); line-height: 1.05; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } .story-character__text span { + display: -webkit-box; overflow: hidden; max-width: 100%; color: var(--story-muted); font-size: 0.68rem; font-weight: 700; line-height: 1.1; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; } .story-character.is-context .story-character__text { diff --git a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js index feacf10..ae66b25 100644 --- a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js +++ b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js @@ -193,6 +193,7 @@ function stageIndex(scene) { const stage = String(scene.stage || "").toLowerCase(); if (scene.progressPercent >= 100 || stage.includes("review")) return analysisStages.length - 1; + if (stage.includes("final")) return 10; if (stage.includes("continuity")) return 9; if (stage.includes("knowledge")) return 8; if (stage.includes("relationship")) return 7; @@ -202,6 +203,7 @@ if (stage.includes("content") || stage.includes("inferring")) return 3; if (stage.includes("scene")) return 2; if (stage.includes("chapter")) return 1; + if (stage.includes("reading") || stage.includes("read ")) return 0; return Math.max(0, Math.min(analysisStages.length - 1, Math.floor(scene.progressPercent / 10))); }