From ed4ad314667312e4c693da530ebac7eb47a8cb2b Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Thu, 27 Aug 2026 20:11:24 +0000 Subject: [PATCH] Fix Story Intelligence progress scene context --- PlotLine.Tests/Program.cs | 28 +++++++- PlotLine/Hubs/StoryIntelligenceHub.cs | 69 +++++++++++++++++-- PlotLine/Models/StoryIntelligenceModels.cs | 2 + .../OnboardingStoryIntelligenceService.cs | 63 ++++++++++++++++- .../PersistedStoryIntelligenceRunner.cs | 33 +++++++-- PlotLine/ViewModels/OnboardingViewModels.cs | 3 + .../StoryIntelligenceProgress.cshtml | 21 +++--- PlotLine/wwwroot/css/onboarding.css | 14 +++- .../wwwroot/js/story-intelligence-progress.js | 39 ++++++++++- 9 files changed, 251 insertions(+), 21 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 12c79b3..6761e7a 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -84,6 +84,7 @@ var tests = new (string Name, Action Test)[] ("Scan review separates known matches from unresolved names", ScanReviewSeparatesKnownMatchesFromUnresolvedNames), ("Story Intelligence uses persisted scene source after restart", StoryIntelligenceUsesPersistedSceneSourceAfterRestart), ("Story Intelligence canonical mode bypasses duplicate boundary analysis", StoryIntelligenceCanonicalModeBypassesDuplicateBoundaryAnalysis), + ("Story Intelligence progress uses canonical scene summaries", StoryIntelligenceProgressUsesCanonicalSceneSummaries), ("Story Intelligence downstream imports prefer canonical SceneIDs", StoryIntelligenceDownstreamImportsPreferCanonicalSceneIds), ("Story Intelligence scene prompt no longer asks for duplicate factual summary", StoryIntelligenceScenePromptNoLongerRequestsDuplicateSummary), ("Story Intelligence review uses canonical structural summaries and counts", StoryIntelligenceReviewUsesCanonicalStructuralSummariesAndCounts), @@ -2013,7 +2014,7 @@ static void StoryIntelligenceUsesPersistedSceneSourceAfterRestart() Assert(onboardingStory.Contains("durableSource = await sources.GetChapterSourceAsync", StringComparison.Ordinal), "Onboarding Story Intelligence should read durable chapter text from scene sources."); Assert(onboardingStory.Contains("CoreImportComplete", StringComparison.Ordinal), "Optional onboarding Story Intelligence should require core import completion."); Assert(existingQueue.Contains("sources.GetChapterSourceAsync", StringComparison.Ordinal), "Later Story Intelligence queueing should start from persisted chapter/scene source, not onboarding DTOs."); - Assert(runner.Contains("FirstNonBlank(source.StructuralSummary, scene.StructuralSummary)", StringComparison.Ordinal), "Scene Intelligence context should receive structural summaries from canonical scene sources."); + Assert(runner.Contains("FirstNonBlank(scene.StructuralSummary, scene.Summary, source.StructuralSummary)", StringComparison.Ordinal), "Scene Intelligence context should prefer canonical scene summaries before source-row fallbacks."); } static void StoryIntelligenceCanonicalModeBypassesDuplicateBoundaryAnalysis() @@ -2038,6 +2039,31 @@ static void StoryIntelligenceCanonicalModeBypassesDuplicateBoundaryAnalysis() Assert(progressJs.Contains("Loading scenes...", StringComparison.Ordinal), "Live canonical progress should load scenes rather than claim to find them."); } +static void StoryIntelligenceProgressUsesCanonicalSceneSummaries() +{ + var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); + var onboardingStory = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs")); + var runner = File.ReadAllText(Path.Combine(root, "Services/PersistedStoryIntelligenceRunner.cs")); + var hub = File.ReadAllText(Path.Combine(root, "Hubs/StoryIntelligenceHub.cs")); + var progressView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceProgress.cshtml")); + var progressJs = File.ReadAllText(Path.Combine(root, "wwwroot/js/story-intelligence-progress.js")); + var css = File.ReadAllText(Path.Combine(root, "wwwroot/css/onboarding.css")); + + Assert(onboardingStory.Contains("CurrentCanonicalScene(run, canonicalScenes)", StringComparison.Ordinal), "Progress model should resolve the current canonical scene for each active run."); + Assert(onboardingStory.Contains("CanonicalSceneSummary(currentScene)", StringComparison.Ordinal), "Progress model should use canonical scene summaries."); + Assert(onboardingStory.Contains("FirstConfigured(scene?.StructuralSummary, scene?.Summary)", StringComparison.Ordinal), "Canonical progress summary should prefer Scenes.StructuralSummary before Scenes.Summary."); + Assert(runner.Contains("latestSceneSummary: block.CanonicalSummary", StringComparison.Ordinal), "Live progress events should send the canonical scene summary."); + Assert(runner.Contains("CurrentSceneID = currentSceneId", StringComparison.Ordinal), "Live progress events should carry canonical SceneID."); + Assert(hub.Contains("CurrentCanonicalScene(run, canonicalScenes)", StringComparison.Ordinal), "SignalR reconnect snapshots should also resolve canonical scenes."); + Assert(progressView.Contains("data-story-latest-scene-title", StringComparison.Ordinal), "Progress UI should render the current canonical scene title."); + Assert(progressView.Contains("data-story-current-scene-id", StringComparison.Ordinal), "Rendered chapter state should carry canonical SceneID."); + Assert(!progressView.Contains("Scene summaries will appear here", StringComparison.Ordinal), "Progress UI should not imply Story Intelligence is generating summaries."); + Assert(!progressView.Contains("with summaries, context", StringComparison.Ordinal), "Progress hero should not describe summaries as Story Intelligence work."); + Assert(progressJs.Contains("currentSceneDisplayTitle", StringComparison.Ordinal), "Live browser updates should carry the current canonical scene title."); + Assert(css.Contains("white-space: normal;", StringComparison.Ordinal), "Progress info alert should allow wrapping."); + Assert(!runner.Contains("latestSceneSummary: SceneSummaryText(sceneAttempt.Parsed)", StringComparison.Ordinal), "Live progress should not use the Scene Intelligence parsed summary as the manuscript summary."); +} + static void StoryIntelligenceDownstreamImportsPreferCanonicalSceneIds() { var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); diff --git a/PlotLine/Hubs/StoryIntelligenceHub.cs b/PlotLine/Hubs/StoryIntelligenceHub.cs index fc43d1e..340f219 100644 --- a/PlotLine/Hubs/StoryIntelligenceHub.cs +++ b/PlotLine/Hubs/StoryIntelligenceHub.cs @@ -43,8 +43,12 @@ public sealed class StoryIntelligenceHub( return null; } - var sceneResults = await persistedRuns.ListSceneResultsAsync(runId); - return ToRunProgress(run, "Current status", LatestSceneSummary(sceneResults)); + var canonicalScenes = run.ChapterID.HasValue + ? await persistedRuns.ListChapterScenesAsync(run.ChapterID.Value) + : []; + var currentScene = CurrentCanonicalScene(run, canonicalScenes); + var sceneResults = currentScene is null ? await persistedRuns.ListSceneResultsAsync(runId) : []; + return ToRunProgress(run, "Current status", currentScene, currentScene is null ? LatestSceneSummary(sceneResults) : CanonicalSceneSummary(currentScene)); } public async Task WatchOnboardingStoryIntelligenceBatch(Guid batchId) @@ -56,11 +60,14 @@ public sealed class StoryIntelligenceHub( public static string UserGroup(int userId) => $"story-intelligence:{userId}"; - private static StoryIntelligenceRunProgressEvent ToRunProgress(StoryIntelligenceSavedRun run, string eventType, string? latestSceneSummary) + private static StoryIntelligenceRunProgressEvent ToRunProgress(StoryIntelligenceSavedRun run, string eventType, Scene? currentScene, string? latestSceneSummary) { var completedScenes = run.CompletedScenes ?? 0; var totalScenes = run.TotalDetectedScenes ?? 0; var elapsedMs = run.TotalDurationMs ?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - run.StartedUtc).TotalMilliseconds)); + var currentSceneNumber = currentScene is null + ? CurrentSceneNumber(run) + : Convert.ToInt32(currentScene.SceneNumber); return new StoryIntelligenceRunProgressEvent { @@ -79,7 +86,11 @@ public sealed class StoryIntelligenceHub( FailedScenes = run.FailedScenes, TotalTokens = run.TotalTokens, TotalDurationMs = elapsedMs, - CurrentSceneNumber = run.CompletedScenes, + CurrentSceneID = currentScene?.SceneID, + CurrentSceneNumber = currentSceneNumber, + CurrentSceneDisplayTitle = currentScene is null || !currentSceneNumber.HasValue + ? null + : SceneTitleDisplay.Format(currentScene.SceneNumber, currentScene.SceneTitle), ElapsedTime = FormatDuration(elapsedMs), EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes, totalScenes), LatestSceneSummary = latestSceneSummary, @@ -90,6 +101,56 @@ public sealed class StoryIntelligenceHub( }; } + private static Scene? CurrentCanonicalScene(StoryIntelligenceSavedRun run, IReadOnlyList scenes) + { + if (scenes.Count == 0) + { + return null; + } + + var orderedScenes = scenes.OrderBy(scene => scene.SceneNumber).ToList(); + var sceneNumber = CurrentSceneNumber(run); + if (sceneNumber is not > 0) + { + return orderedScenes[0]; + } + + var byNumber = orderedScenes.FirstOrDefault(scene => Convert.ToInt32(scene.SceneNumber) == sceneNumber.Value); + if (byNumber is not null) + { + return byNumber; + } + + return orderedScenes[Math.Clamp(sceneNumber.Value - 1, 0, orderedScenes.Count - 1)]; + } + + private static int? CurrentSceneNumber(StoryIntelligenceSavedRun run) + { + var completed = Math.Max(0, run.CompletedScenes ?? 0); + var total = Math.Max(0, run.TotalDetectedScenes ?? 0); + if (run.Status == StoryIntelligenceRunStatuses.Running + && string.Equals(run.CurrentStage, StoryIntelligenceFailureStages.SceneIntelligence, StringComparison.OrdinalIgnoreCase) + && total > 0) + { + return Math.Clamp(completed + 1, 1, total); + } + + if (completed > 0) + { + return Math.Clamp(completed, 1, total > 0 ? total : completed); + } + + return run.Status is StoryIntelligenceRunStatuses.Pending or StoryIntelligenceRunStatuses.Running && total > 0 + ? 1 + : null; + } + + private static string? CanonicalSceneSummary(Scene? scene) + => FirstConfigured(scene?.StructuralSummary, scene?.Summary); + + private static string? FirstConfigured(params string?[] values) + => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim(); + private static string FormatDuration(long? milliseconds) { var elapsed = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds ?? 0)); diff --git a/PlotLine/Models/StoryIntelligenceModels.cs b/PlotLine/Models/StoryIntelligenceModels.cs index b345994..c66984e 100644 --- a/PlotLine/Models/StoryIntelligenceModels.cs +++ b/PlotLine/Models/StoryIntelligenceModels.cs @@ -82,7 +82,9 @@ public sealed class StoryIntelligenceRunProgressEvent public int? FailedScenes { get; init; } public int? TotalTokens { get; init; } public long? TotalDurationMs { get; init; } + public int? CurrentSceneID { get; init; } public int? CurrentSceneNumber { get; init; } + public string? CurrentSceneDisplayTitle { get; init; } public string ElapsedTime { get; init; } = "0 sec"; public string? EstimatedRemaining { get; init; } public string? LatestSceneSummary { get; init; } diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs index e07766a..6e58ff2 100644 --- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs +++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs @@ -448,6 +448,17 @@ public sealed class OnboardingStoryIntelligenceService( ? await commits.BuildConfirmationAsync(item.RunID) : null; var sceneResults = await runs.ListSceneResultsAsync(item.RunID); + var canonicalScenes = item.ChapterID > 0 + ? await runs.ListChapterScenesAsync(item.ChapterID) + : []; + var currentScene = CurrentCanonicalScene(run, canonicalScenes); + var currentSceneNumber = currentScene is null + ? CurrentSceneNumber(run) + : Convert.ToInt32(currentScene.SceneNumber); + var currentSceneDisplayTitle = currentScene is null + ? (currentSceneNumber.HasValue ? SceneTitleDisplay.Format(currentSceneNumber.Value, null) : null) + : SceneTitleDisplay.Format(currentScene.SceneNumber, currentScene.SceneTitle); + var canonicalSceneSummary = CanonicalSceneSummary(currentScene); chapters.Add(new StoryIntelligenceOnboardingChapterViewModel { @@ -470,7 +481,10 @@ public sealed class OnboardingStoryIntelligenceService( EstimatedCostGBP = run.EstimatedCostGBP, FailureStage = run.FailureStage, ErrorMessage = run.ErrorMessage, - LatestSceneSummary = LatestSceneSummary(sceneResults), + CurrentSceneID = currentScene?.SceneID, + CurrentSceneNumber = currentSceneNumber, + CurrentSceneDisplayTitle = currentSceneDisplayTitle, + LatestSceneSummary = currentScene is null ? LatestSceneSummary(sceneResults) : canonicalSceneSummary, HasCompletedCommit = commit is not null && string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase), CanCommit = confirmation?.CanCommit == true, ScenesCreated = commit?.ScenesCreated ?? 0, @@ -1003,6 +1017,53 @@ public sealed class OnboardingStoryIntelligenceService( ? 0 : value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length; + private static Scene? CurrentCanonicalScene(StoryIntelligenceSavedRun run, IReadOnlyList scenes) + { + if (scenes.Count == 0) + { + return null; + } + + var orderedScenes = scenes.OrderBy(scene => scene.SceneNumber).ToList(); + var sceneNumber = CurrentSceneNumber(run); + if (sceneNumber is not > 0) + { + return orderedScenes[0]; + } + + var byNumber = orderedScenes.FirstOrDefault(scene => Convert.ToInt32(scene.SceneNumber) == sceneNumber.Value); + if (byNumber is not null) + { + return byNumber; + } + + return orderedScenes[Math.Clamp(sceneNumber.Value - 1, 0, orderedScenes.Count - 1)]; + } + + private static int? CurrentSceneNumber(StoryIntelligenceSavedRun run) + { + var completed = Math.Max(0, run.CompletedScenes ?? 0); + var total = Math.Max(0, run.TotalDetectedScenes ?? 0); + if (run.Status == StoryIntelligenceRunStatuses.Running + && string.Equals(run.CurrentStage, StoryIntelligenceFailureStages.SceneIntelligence, StringComparison.OrdinalIgnoreCase) + && total > 0) + { + return Math.Clamp(completed + 1, 1, total); + } + + if (completed > 0) + { + return Math.Clamp(completed, 1, total > 0 ? total : completed); + } + + return run.Status is StoryIntelligenceRunStatuses.Pending or StoryIntelligenceRunStatuses.Running && total > 0 + ? 1 + : null; + } + + private static string? CanonicalSceneSummary(Scene? scene) + => FirstConfigured(scene?.StructuralSummary, scene?.Summary); + private static string? LatestSceneSummary(IReadOnlyList sceneResults) { foreach (var scene in sceneResults diff --git a/PlotLine/Services/PersistedStoryIntelligenceRunner.cs b/PlotLine/Services/PersistedStoryIntelligenceRunner.cs index 7762607..7d4d2e3 100644 --- a/PlotLine/Services/PersistedStoryIntelligenceRunner.cs +++ b/PlotLine/Services/PersistedStoryIntelligenceRunner.cs @@ -160,7 +160,11 @@ public sealed class PersistedStoryIntelligenceRunner( totalDetectedScenes: sceneBlocks.Count, completedScenes: completedScenes, failedScenes: failedScenes, - totalTokens: totals.TotalTokens); + totalTokens: totals.TotalTokens, + currentSceneId: block.SceneID, + currentSceneNumber: block.TemporarySceneNumber, + currentSceneDisplayTitle: block.DisplayTitle, + latestSceneSummary: block.CanonicalSummary); continue; } @@ -182,7 +186,10 @@ public sealed class PersistedStoryIntelligenceRunner( completedScenes: completedScenes, failedScenes: failedScenes, totalTokens: totals.TotalTokens, - currentSceneNumber: block.TemporarySceneNumber); + currentSceneId: block.SceneID, + currentSceneNumber: block.TemporarySceneNumber, + currentSceneDisplayTitle: block.DisplayTitle, + latestSceneSummary: block.CanonicalSummary); try { @@ -307,7 +314,9 @@ public sealed class PersistedStoryIntelligenceRunner( failedScenes: failedScenes, totalTokens: totals.TotalTokens, currentSceneNumber: block.TemporarySceneNumber, - latestSceneSummary: SceneSummaryText(sceneAttempt.Parsed), + currentSceneId: block.SceneID, + currentSceneDisplayTitle: block.DisplayTitle, + latestSceneSummary: block.CanonicalSummary, recentDiscoveries: BuildDiscoveries(sceneAttempt.Parsed)); if (!validation.IsValid || validation.Warnings.Count > 0) { @@ -330,6 +339,9 @@ public sealed class PersistedStoryIntelligenceRunner( failedScenes: failedScenes, totalTokens: totals.TotalTokens, currentSceneNumber: block.TemporarySceneNumber, + currentSceneId: block.SceneID, + currentSceneDisplayTitle: block.DisplayTitle, + latestSceneSummary: block.CanonicalSummary, errorMessage: ex.Message); logger.LogWarning(ex, "Story Intelligence scene failed for run {StoryIntelligenceRunID}, scene {TemporarySceneNumber}.", run.StoryIntelligenceRunID, block.TemporarySceneNumber); } @@ -690,7 +702,9 @@ public sealed class PersistedStoryIntelligenceRunner( int? completedScenes = null, int? failedScenes = null, int? totalTokens = null, + int? currentSceneId = null, int? currentSceneNumber = null, + string? currentSceneDisplayTitle = null, string? latestSceneSummary = null, IReadOnlyList? recentDiscoveries = null, string? errorMessage = null) @@ -711,7 +725,9 @@ public sealed class PersistedStoryIntelligenceRunner( FailedScenes = failedScenes, TotalTokens = totalTokens, TotalDurationMs = elapsedMs, + CurrentSceneID = currentSceneId, CurrentSceneNumber = currentSceneNumber, + CurrentSceneDisplayTitle = currentSceneDisplayTitle, ElapsedTime = FormatDuration(elapsedMs), EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes ?? 0, totalDetectedScenes ?? 0), LatestSceneSummary = latestSceneSummary, @@ -991,6 +1007,7 @@ public sealed class PersistedStoryIntelligenceRunner( var end = source.SourceEndParagraph ?? source.WordEndParagraphIndex ?? 0; var sourceLabel = $"{SourceLabel(run)}, scene {sceneNumber:N0}"; var sceneText = (source.SourceText ?? string.Empty).Trim(); + var canonicalSummary = FirstNonBlank(scene.StructuralSummary, scene.Summary, source.StructuralSummary); var splitValid = scene.SceneID > 0 && !string.IsNullOrWhiteSpace(sceneText) && (!source.SourceStartParagraph.HasValue || !source.SourceEndParagraph.HasValue || source.SourceStartParagraph <= source.SourceEndParagraph) @@ -1008,11 +1025,13 @@ public sealed class PersistedStoryIntelligenceRunner( run, scene.SceneID, scene.SceneNumber, - FirstNonBlank(source.StructuralSummary, scene.StructuralSummary), + canonicalSummary, sourceLabel, configuredMetrics, existingPlotLines) : string.Empty, + SceneTitleDisplay.Format(scene.SceneNumber, scene.SceneTitle), + canonicalSummary, splitValid); } @@ -1030,7 +1049,7 @@ public sealed class PersistedStoryIntelligenceRunner( if (sceneNumber <= 0 || start <= 0 || end <= 0 || start > end || end > paragraphs.Count) { - return new StorySceneTextBlock(null, sceneNumber, start, end, sourceLabel, string.Empty, string.Empty, false); + return new StorySceneTextBlock(null, sceneNumber, start, end, sourceLabel, string.Empty, string.Empty, null, boundary.StructuralSummary, false); } var sceneText = string.Join(Environment.NewLine + Environment.NewLine, paragraphs.Skip(start - 1).Take(end - start + 1)); @@ -1049,6 +1068,8 @@ public sealed class PersistedStoryIntelligenceRunner( sourceLabel, configuredMetrics, existingPlotLines), + SceneTitleDisplay.Format(sceneNumber, null), + boundary.StructuralSummary, true); } @@ -1363,6 +1384,8 @@ public sealed class PersistedStoryIntelligenceRunner( string SourceLabel, string SceneText, string SceneContextJson, + string? DisplayTitle, + string? CanonicalSummary, bool SplitValid); private sealed record SceneAnalysisModeSelection( diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index dc2aca8..625854c 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -642,6 +642,9 @@ public sealed class StoryIntelligenceOnboardingChapterViewModel public decimal? EstimatedCostGBP { get; init; } public string? FailureStage { get; init; } public string? ErrorMessage { get; init; } + public int? CurrentSceneID { get; init; } + public int? CurrentSceneNumber { get; init; } + public string? CurrentSceneDisplayTitle { get; init; } public string? LatestSceneSummary { get; init; } public bool HasCompletedCommit { get; init; } public bool CanCommit { get; init; } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml index 051135a..6ac0b2f 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml @@ -17,7 +17,7 @@

Analyse manuscript

Reading your manuscript

-

PlotDirector is enriching the imported scenes with summaries, context, relationships, knowledge, timeline details and metrics.

+

PlotDirector is enriching the imported scenes with context, relationships, knowledge, timeline details and metrics.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -91,13 +91,15 @@

Latest from your manuscript

@if (!string.IsNullOrWhiteSpace(currentChapter?.LatestSceneSummary)) { +

@currentChapter.LatestSceneSummary

- + } else { + -

Scene summaries will appear here as PlotDirector reads.

+

Scene context will appear here as PlotDirector analyses each imported scene.

}
@@ -115,7 +117,9 @@ data-story-raw-status="@chapter.Status" data-story-chapter-number="@chapter.ChapterNumber" data-story-current-stage="@(chapter.CurrentStage ?? string.Empty)" - data-story-current-scene-number="@(chapter.CompletedScenes ?? 0)" + data-story-current-scene-id="@(chapter.CurrentSceneID?.ToString() ?? string.Empty)" + data-story-current-scene-number="@(chapter.CurrentSceneNumber?.ToString() ?? string.Empty)" + data-story-current-scene-title="@(chapter.CurrentSceneDisplayTitle ?? string.Empty)" data-story-updated-utc="@(chapter.UpdatedUtc?.ToUniversalTime().ToString("O") ?? string.Empty)" data-story-source-word-count="@(chapter.SourceWordCount ?? 0)" data-story-duration-ms="@(chapter.TotalDurationMs ?? 0)"> @@ -125,6 +129,7 @@ @((chapter.FailedScenes ?? 0).ToString("N0")) @FriendlyMessage(chapter) @(chapter.ErrorMessage ?? string.Empty) + @(chapter.CurrentSceneDisplayTitle ?? string.Empty) @(chapter.LatestSceneSummary ?? string.Empty)
} @@ -334,8 +339,8 @@ private static string CurrentScene(StoryIntelligenceProgressViewModel model) { var chapter = CurrentChapter(model); - return chapter?.CompletedScenes is > 0 - ? chapter.CompletedScenes.Value.ToString("N0") + return chapter?.CurrentSceneNumber is > 0 + ? chapter.CurrentSceneNumber.Value.ToString("N0") : "Waiting"; } @@ -346,8 +351,8 @@ return "Loading scenes..."; } - var completed = Math.Clamp(chapter?.CompletedScenes ?? 0, 0, chapter?.TotalDetectedScenes ?? 0); - return $"Scene {completed:N0} of {chapter?.TotalDetectedScenes:N0}"; + var current = Math.Clamp(chapter?.CurrentSceneNumber ?? chapter?.CompletedScenes ?? 0, 0, chapter?.TotalDetectedScenes ?? 0); + return $"Scene {current:N0} of {chapter?.TotalDetectedScenes:N0}"; } private static int ChapterProgressPercent(StoryIntelligenceOnboardingChapterViewModel? chapter) diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css index 81b658e..9043a19 100644 --- a/PlotLine/wwwroot/css/onboarding.css +++ b/PlotLine/wwwroot/css/onboarding.css @@ -804,6 +804,10 @@ padding: clamp(1.25rem, 2vw, 1.75rem); } +.story-live-progress > * { + min-width: 0; +} + .story-live-hero h1 { white-space: nowrap; font-size: clamp(2.6rem, 4vw, 3.6rem); @@ -824,7 +828,8 @@ } .story-live-progress > .alert-info { - white-space: nowrap; + white-space: normal; + overflow-wrap: break-word; } .story-live-progress-card, @@ -979,6 +984,13 @@ summary.story-review-chapter-heading { margin-bottom: .55rem; } +.story-latest-scene-title { + margin: 0 0 .45rem; + color: #2f6f63; + font-size: 1rem; + font-weight: 800; +} + .story-latest-summary[data-story-latest-summary] { padding: 1.1rem 1.15rem; border-left-width: 6px; diff --git a/PlotLine/wwwroot/js/story-intelligence-progress.js b/PlotLine/wwwroot/js/story-intelligence-progress.js index 9f56dfe..2858f84 100644 --- a/PlotLine/wwwroot/js/story-intelligence-progress.js +++ b/PlotLine/wwwroot/js/story-intelligence-progress.js @@ -284,9 +284,11 @@ const status = chapter.dataset.storyRawStatus || "Pending"; const stage = chapter.dataset.storyCurrentStage || status; const currentSceneNumber = chapter.dataset.storyCurrentSceneNumber || ""; + const currentSceneTitle = chapter.dataset.storyCurrentSceneTitle || ""; const total = chapter.querySelector("[data-story-run-total]")?.textContent || ""; const completed = chapter.querySelector("[data-story-run-completed]")?.textContent || ""; const message = chapter.querySelector("[data-story-run-message]")?.textContent || ""; + const latestSummary = chapter.querySelector("[data-story-run-latest-summary]")?.textContent || ""; root.querySelectorAll("[data-story-current-chapter]").forEach((node) => { node.textContent = chapter.dataset.storyChapterTitle || "Current chapter"; @@ -297,6 +299,19 @@ root.querySelectorAll("[data-story-current-scene]").forEach((node) => { node.textContent = currentSceneNumber ? numberText(currentSceneNumber) : "Waiting"; }); + root.querySelectorAll("[data-story-latest-scene-title]").forEach((node) => { + node.textContent = currentSceneTitle; + node.hidden = !currentSceneTitle; + }); + root.querySelectorAll("[data-story-latest-summary]").forEach((node) => { + if (latestSummary) { + node.textContent = latestSummary; + node.hidden = false; + } + }); + root.querySelectorAll("[data-story-latest-empty]").forEach((node) => { + node.hidden = !!latestSummary; + }); const totalNumber = Number.parseInt(total.replace(/,/g, "") || "0", 10) || 0; const completedNumber = Number.parseInt(completed.replace(/,/g, "") || "0", 10) || 0; const currentSceneDisplay = currentSceneNumber ? Number.parseInt(currentSceneNumber, 10) || completedNumber : completedNumber; @@ -402,7 +417,9 @@ const tokens = field(state, "totalTokens", "TotalTokens"); const totalDurationMs = field(state, "totalDurationMs", "TotalDurationMs"); const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec"; + const currentSceneId = field(state, "currentSceneID", "CurrentSceneID") ?? field(state, "currentSceneId", "CurrentSceneId"); const currentSceneNumber = field(state, "currentSceneNumber", "CurrentSceneNumber"); + const currentSceneDisplayTitle = field(state, "currentSceneDisplayTitle", "CurrentSceneDisplayTitle"); const updatedUtc = field(state, "updatedUtc", "UpdatedUtc"); const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber); const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary"); @@ -414,7 +431,17 @@ }); chapter.dataset.storyRawStatus = status; chapter.dataset.storyCurrentStage = stage; - chapter.dataset.storyCurrentSceneNumber = completed || "0"; + if (currentSceneId !== null) { + chapter.dataset.storyCurrentSceneId = String(currentSceneId); + } + if (currentSceneNumber !== null) { + chapter.dataset.storyCurrentSceneNumber = String(currentSceneNumber); + } else if (completed !== null) { + chapter.dataset.storyCurrentSceneNumber = String(completed); + } + if (currentSceneDisplayTitle) { + chapter.dataset.storyCurrentSceneTitle = currentSceneDisplayTitle; + } if (updatedUtc) { chapter.dataset.storyUpdatedUtc = updatedUtc; } @@ -465,6 +492,11 @@ node.textContent = latestSummary; } }); + chapter.querySelectorAll("[data-story-run-current-scene-title]").forEach((node) => { + if (currentSceneDisplayTitle) { + node.textContent = currentSceneDisplayTitle; + } + }); const totalNumber = Number.parseInt(total || "0", 10) || 0; const completedNumber = Number.parseInt(completed || "0", 10) || 0; @@ -479,6 +511,11 @@ if (summaryNode && latestSummary) { summaryNode.textContent = latestSummary; summaryNode.hidden = false; + const titleNode = root.querySelector("[data-story-latest-scene-title]"); + if (titleNode) { + titleNode.textContent = currentSceneDisplayTitle || chapter.dataset.storyCurrentSceneTitle || ""; + titleNode.hidden = !titleNode.textContent; + } const emptyNode = root.querySelector("[data-story-latest-empty]"); if (emptyNode) { emptyNode.hidden = true;