Fix Story Intelligence progress scene context
This commit is contained in:
parent
a2cf6a96a8
commit
ed4ad31466
@ -84,6 +84,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Scan review separates known matches from unresolved names", ScanReviewSeparatesKnownMatchesFromUnresolvedNames),
|
("Scan review separates known matches from unresolved names", ScanReviewSeparatesKnownMatchesFromUnresolvedNames),
|
||||||
("Story Intelligence uses persisted scene source after restart", StoryIntelligenceUsesPersistedSceneSourceAfterRestart),
|
("Story Intelligence uses persisted scene source after restart", StoryIntelligenceUsesPersistedSceneSourceAfterRestart),
|
||||||
("Story Intelligence canonical mode bypasses duplicate boundary analysis", StoryIntelligenceCanonicalModeBypassesDuplicateBoundaryAnalysis),
|
("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 downstream imports prefer canonical SceneIDs", StoryIntelligenceDownstreamImportsPreferCanonicalSceneIds),
|
||||||
("Story Intelligence scene prompt no longer asks for duplicate factual summary", StoryIntelligenceScenePromptNoLongerRequestsDuplicateSummary),
|
("Story Intelligence scene prompt no longer asks for duplicate factual summary", StoryIntelligenceScenePromptNoLongerRequestsDuplicateSummary),
|
||||||
("Story Intelligence review uses canonical structural summaries and counts", StoryIntelligenceReviewUsesCanonicalStructuralSummariesAndCounts),
|
("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("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(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(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()
|
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.");
|
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()
|
static void StoryIntelligenceDownstreamImportsPreferCanonicalSceneIds()
|
||||||
{
|
{
|
||||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
|||||||
@ -43,8 +43,12 @@ public sealed class StoryIntelligenceHub(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sceneResults = await persistedRuns.ListSceneResultsAsync(runId);
|
var canonicalScenes = run.ChapterID.HasValue
|
||||||
return ToRunProgress(run, "Current status", LatestSceneSummary(sceneResults));
|
? 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<StoryIntelligenceProgressViewModel?> WatchOnboardingStoryIntelligenceBatch(Guid batchId)
|
public async Task<StoryIntelligenceProgressViewModel?> WatchOnboardingStoryIntelligenceBatch(Guid batchId)
|
||||||
@ -56,11 +60,14 @@ public sealed class StoryIntelligenceHub(
|
|||||||
|
|
||||||
public static string UserGroup(int userId) => $"story-intelligence:{userId}";
|
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 completedScenes = run.CompletedScenes ?? 0;
|
||||||
var totalScenes = run.TotalDetectedScenes ?? 0;
|
var totalScenes = run.TotalDetectedScenes ?? 0;
|
||||||
var elapsedMs = run.TotalDurationMs ?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - run.StartedUtc).TotalMilliseconds));
|
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
|
return new StoryIntelligenceRunProgressEvent
|
||||||
{
|
{
|
||||||
@ -79,7 +86,11 @@ public sealed class StoryIntelligenceHub(
|
|||||||
FailedScenes = run.FailedScenes,
|
FailedScenes = run.FailedScenes,
|
||||||
TotalTokens = run.TotalTokens,
|
TotalTokens = run.TotalTokens,
|
||||||
TotalDurationMs = elapsedMs,
|
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),
|
ElapsedTime = FormatDuration(elapsedMs),
|
||||||
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes, totalScenes),
|
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes, totalScenes),
|
||||||
LatestSceneSummary = latestSceneSummary,
|
LatestSceneSummary = latestSceneSummary,
|
||||||
@ -90,6 +101,56 @@ public sealed class StoryIntelligenceHub(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Scene? CurrentCanonicalScene(StoryIntelligenceSavedRun run, IReadOnlyList<Scene> 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)
|
private static string FormatDuration(long? milliseconds)
|
||||||
{
|
{
|
||||||
var elapsed = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds ?? 0));
|
var elapsed = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds ?? 0));
|
||||||
|
|||||||
@ -82,7 +82,9 @@ public sealed class StoryIntelligenceRunProgressEvent
|
|||||||
public int? FailedScenes { get; init; }
|
public int? FailedScenes { get; init; }
|
||||||
public int? TotalTokens { get; init; }
|
public int? TotalTokens { get; init; }
|
||||||
public long? TotalDurationMs { get; init; }
|
public long? TotalDurationMs { get; init; }
|
||||||
|
public int? CurrentSceneID { get; init; }
|
||||||
public int? CurrentSceneNumber { get; init; }
|
public int? CurrentSceneNumber { get; init; }
|
||||||
|
public string? CurrentSceneDisplayTitle { get; init; }
|
||||||
public string ElapsedTime { get; init; } = "0 sec";
|
public string ElapsedTime { get; init; } = "0 sec";
|
||||||
public string? EstimatedRemaining { get; init; }
|
public string? EstimatedRemaining { get; init; }
|
||||||
public string? LatestSceneSummary { get; init; }
|
public string? LatestSceneSummary { get; init; }
|
||||||
|
|||||||
@ -448,6 +448,17 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
? await commits.BuildConfirmationAsync(item.RunID)
|
? await commits.BuildConfirmationAsync(item.RunID)
|
||||||
: null;
|
: null;
|
||||||
var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
|
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
|
chapters.Add(new StoryIntelligenceOnboardingChapterViewModel
|
||||||
{
|
{
|
||||||
@ -470,7 +481,10 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
EstimatedCostGBP = run.EstimatedCostGBP,
|
EstimatedCostGBP = run.EstimatedCostGBP,
|
||||||
FailureStage = run.FailureStage,
|
FailureStage = run.FailureStage,
|
||||||
ErrorMessage = run.ErrorMessage,
|
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),
|
HasCompletedCommit = commit is not null && string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase),
|
||||||
CanCommit = confirmation?.CanCommit == true,
|
CanCommit = confirmation?.CanCommit == true,
|
||||||
ScenesCreated = commit?.ScenesCreated ?? 0,
|
ScenesCreated = commit?.ScenesCreated ?? 0,
|
||||||
@ -1003,6 +1017,53 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
? 0
|
? 0
|
||||||
: value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
|
: value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
|
||||||
|
|
||||||
|
private static Scene? CurrentCanonicalScene(StoryIntelligenceSavedRun run, IReadOnlyList<Scene> 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<StoryIntelligenceSavedSceneResult> sceneResults)
|
private static string? LatestSceneSummary(IReadOnlyList<StoryIntelligenceSavedSceneResult> sceneResults)
|
||||||
{
|
{
|
||||||
foreach (var scene in sceneResults
|
foreach (var scene in sceneResults
|
||||||
|
|||||||
@ -160,7 +160,11 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
totalDetectedScenes: sceneBlocks.Count,
|
totalDetectedScenes: sceneBlocks.Count,
|
||||||
completedScenes: completedScenes,
|
completedScenes: completedScenes,
|
||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens);
|
totalTokens: totals.TotalTokens,
|
||||||
|
currentSceneId: block.SceneID,
|
||||||
|
currentSceneNumber: block.TemporarySceneNumber,
|
||||||
|
currentSceneDisplayTitle: block.DisplayTitle,
|
||||||
|
latestSceneSummary: block.CanonicalSummary);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +186,10 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
completedScenes: completedScenes,
|
completedScenes: completedScenes,
|
||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens,
|
totalTokens: totals.TotalTokens,
|
||||||
currentSceneNumber: block.TemporarySceneNumber);
|
currentSceneId: block.SceneID,
|
||||||
|
currentSceneNumber: block.TemporarySceneNumber,
|
||||||
|
currentSceneDisplayTitle: block.DisplayTitle,
|
||||||
|
latestSceneSummary: block.CanonicalSummary);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -307,7 +314,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens,
|
totalTokens: totals.TotalTokens,
|
||||||
currentSceneNumber: block.TemporarySceneNumber,
|
currentSceneNumber: block.TemporarySceneNumber,
|
||||||
latestSceneSummary: SceneSummaryText(sceneAttempt.Parsed),
|
currentSceneId: block.SceneID,
|
||||||
|
currentSceneDisplayTitle: block.DisplayTitle,
|
||||||
|
latestSceneSummary: block.CanonicalSummary,
|
||||||
recentDiscoveries: BuildDiscoveries(sceneAttempt.Parsed));
|
recentDiscoveries: BuildDiscoveries(sceneAttempt.Parsed));
|
||||||
if (!validation.IsValid || validation.Warnings.Count > 0)
|
if (!validation.IsValid || validation.Warnings.Count > 0)
|
||||||
{
|
{
|
||||||
@ -330,6 +339,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens,
|
totalTokens: totals.TotalTokens,
|
||||||
currentSceneNumber: block.TemporarySceneNumber,
|
currentSceneNumber: block.TemporarySceneNumber,
|
||||||
|
currentSceneId: block.SceneID,
|
||||||
|
currentSceneDisplayTitle: block.DisplayTitle,
|
||||||
|
latestSceneSummary: block.CanonicalSummary,
|
||||||
errorMessage: ex.Message);
|
errorMessage: ex.Message);
|
||||||
logger.LogWarning(ex, "Story Intelligence scene failed for run {StoryIntelligenceRunID}, scene {TemporarySceneNumber}.", run.StoryIntelligenceRunID, block.TemporarySceneNumber);
|
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? completedScenes = null,
|
||||||
int? failedScenes = null,
|
int? failedScenes = null,
|
||||||
int? totalTokens = null,
|
int? totalTokens = null,
|
||||||
|
int? currentSceneId = null,
|
||||||
int? currentSceneNumber = null,
|
int? currentSceneNumber = null,
|
||||||
|
string? currentSceneDisplayTitle = null,
|
||||||
string? latestSceneSummary = null,
|
string? latestSceneSummary = null,
|
||||||
IReadOnlyList<string>? recentDiscoveries = null,
|
IReadOnlyList<string>? recentDiscoveries = null,
|
||||||
string? errorMessage = null)
|
string? errorMessage = null)
|
||||||
@ -711,7 +725,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
FailedScenes = failedScenes,
|
FailedScenes = failedScenes,
|
||||||
TotalTokens = totalTokens,
|
TotalTokens = totalTokens,
|
||||||
TotalDurationMs = elapsedMs,
|
TotalDurationMs = elapsedMs,
|
||||||
|
CurrentSceneID = currentSceneId,
|
||||||
CurrentSceneNumber = currentSceneNumber,
|
CurrentSceneNumber = currentSceneNumber,
|
||||||
|
CurrentSceneDisplayTitle = currentSceneDisplayTitle,
|
||||||
ElapsedTime = FormatDuration(elapsedMs),
|
ElapsedTime = FormatDuration(elapsedMs),
|
||||||
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes ?? 0, totalDetectedScenes ?? 0),
|
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes ?? 0, totalDetectedScenes ?? 0),
|
||||||
LatestSceneSummary = latestSceneSummary,
|
LatestSceneSummary = latestSceneSummary,
|
||||||
@ -991,6 +1007,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
var end = source.SourceEndParagraph ?? source.WordEndParagraphIndex ?? 0;
|
var end = source.SourceEndParagraph ?? source.WordEndParagraphIndex ?? 0;
|
||||||
var sourceLabel = $"{SourceLabel(run)}, scene {sceneNumber:N0}";
|
var sourceLabel = $"{SourceLabel(run)}, scene {sceneNumber:N0}";
|
||||||
var sceneText = (source.SourceText ?? string.Empty).Trim();
|
var sceneText = (source.SourceText ?? string.Empty).Trim();
|
||||||
|
var canonicalSummary = FirstNonBlank(scene.StructuralSummary, scene.Summary, source.StructuralSummary);
|
||||||
var splitValid = scene.SceneID > 0
|
var splitValid = scene.SceneID > 0
|
||||||
&& !string.IsNullOrWhiteSpace(sceneText)
|
&& !string.IsNullOrWhiteSpace(sceneText)
|
||||||
&& (!source.SourceStartParagraph.HasValue || !source.SourceEndParagraph.HasValue || source.SourceStartParagraph <= source.SourceEndParagraph)
|
&& (!source.SourceStartParagraph.HasValue || !source.SourceEndParagraph.HasValue || source.SourceStartParagraph <= source.SourceEndParagraph)
|
||||||
@ -1008,11 +1025,13 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
run,
|
run,
|
||||||
scene.SceneID,
|
scene.SceneID,
|
||||||
scene.SceneNumber,
|
scene.SceneNumber,
|
||||||
FirstNonBlank(source.StructuralSummary, scene.StructuralSummary),
|
canonicalSummary,
|
||||||
sourceLabel,
|
sourceLabel,
|
||||||
configuredMetrics,
|
configuredMetrics,
|
||||||
existingPlotLines)
|
existingPlotLines)
|
||||||
: string.Empty,
|
: string.Empty,
|
||||||
|
SceneTitleDisplay.Format(scene.SceneNumber, scene.SceneTitle),
|
||||||
|
canonicalSummary,
|
||||||
splitValid);
|
splitValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1030,7 +1049,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
|
|
||||||
if (sceneNumber <= 0 || start <= 0 || end <= 0 || start > end || end > paragraphs.Count)
|
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));
|
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,
|
sourceLabel,
|
||||||
configuredMetrics,
|
configuredMetrics,
|
||||||
existingPlotLines),
|
existingPlotLines),
|
||||||
|
SceneTitleDisplay.Format(sceneNumber, null),
|
||||||
|
boundary.StructuralSummary,
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1363,6 +1384,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
string SourceLabel,
|
string SourceLabel,
|
||||||
string SceneText,
|
string SceneText,
|
||||||
string SceneContextJson,
|
string SceneContextJson,
|
||||||
|
string? DisplayTitle,
|
||||||
|
string? CanonicalSummary,
|
||||||
bool SplitValid);
|
bool SplitValid);
|
||||||
|
|
||||||
private sealed record SceneAnalysisModeSelection(
|
private sealed record SceneAnalysisModeSelection(
|
||||||
|
|||||||
@ -642,6 +642,9 @@ public sealed class StoryIntelligenceOnboardingChapterViewModel
|
|||||||
public decimal? EstimatedCostGBP { get; init; }
|
public decimal? EstimatedCostGBP { get; init; }
|
||||||
public string? FailureStage { get; init; }
|
public string? FailureStage { get; init; }
|
||||||
public string? ErrorMessage { 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 string? LatestSceneSummary { get; init; }
|
||||||
public bool HasCompletedCommit { get; init; }
|
public bool HasCompletedCommit { get; init; }
|
||||||
public bool CanCommit { get; init; }
|
public bool CanCommit { get; init; }
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<div class="onboarding-copy story-live-hero">
|
<div class="onboarding-copy story-live-hero">
|
||||||
<p class="eyebrow">Analyse manuscript</p>
|
<p class="eyebrow">Analyse manuscript</p>
|
||||||
<h1 id="story-progress-title">Reading your manuscript</h1>
|
<h1 id="story-progress-title">Reading your manuscript</h1>
|
||||||
<p>PlotDirector is enriching the imported scenes with summaries, context, relationships, knowledge, timeline details and metrics.</p>
|
<p>PlotDirector is enriching the imported scenes with context, relationships, knowledge, timeline details and metrics.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||||
@ -91,13 +91,15 @@
|
|||||||
<h2>Latest from your manuscript</h2>
|
<h2>Latest from your manuscript</h2>
|
||||||
@if (!string.IsNullOrWhiteSpace(currentChapter?.LatestSceneSummary))
|
@if (!string.IsNullOrWhiteSpace(currentChapter?.LatestSceneSummary))
|
||||||
{
|
{
|
||||||
|
<h3 class="story-latest-scene-title" data-story-latest-scene-title hidden="@(string.IsNullOrWhiteSpace(currentChapter.CurrentSceneDisplayTitle))">@(currentChapter.CurrentSceneDisplayTitle ?? string.Empty)</h3>
|
||||||
<p class="lead story-latest-summary" data-story-latest-summary>@currentChapter.LatestSceneSummary</p>
|
<p class="lead story-latest-summary" data-story-latest-summary>@currentChapter.LatestSceneSummary</p>
|
||||||
<p class="text-muted" data-story-latest-empty hidden>Scene summaries will appear here as PlotDirector reads.</p>
|
<p class="text-muted" data-story-latest-empty hidden>Scene context will appear here as PlotDirector analyses each imported scene.</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<h3 class="story-latest-scene-title" data-story-latest-scene-title hidden></h3>
|
||||||
<p class="lead story-latest-summary" data-story-latest-summary hidden></p>
|
<p class="lead story-latest-summary" data-story-latest-summary hidden></p>
|
||||||
<p class="text-muted" data-story-latest-empty>Scene summaries will appear here as PlotDirector reads.</p>
|
<p class="text-muted" data-story-latest-empty>Scene context will appear here as PlotDirector analyses each imported scene.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="story-live-discoveries">
|
<div class="story-live-discoveries">
|
||||||
@ -115,7 +117,9 @@
|
|||||||
data-story-raw-status="@chapter.Status"
|
data-story-raw-status="@chapter.Status"
|
||||||
data-story-chapter-number="@chapter.ChapterNumber"
|
data-story-chapter-number="@chapter.ChapterNumber"
|
||||||
data-story-current-stage="@(chapter.CurrentStage ?? string.Empty)"
|
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-updated-utc="@(chapter.UpdatedUtc?.ToUniversalTime().ToString("O") ?? string.Empty)"
|
||||||
data-story-source-word-count="@(chapter.SourceWordCount ?? 0)"
|
data-story-source-word-count="@(chapter.SourceWordCount ?? 0)"
|
||||||
data-story-duration-ms="@(chapter.TotalDurationMs ?? 0)">
|
data-story-duration-ms="@(chapter.TotalDurationMs ?? 0)">
|
||||||
@ -125,6 +129,7 @@
|
|||||||
<span data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</span>
|
<span data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</span>
|
||||||
<span data-story-run-message>@FriendlyMessage(chapter)</span>
|
<span data-story-run-message>@FriendlyMessage(chapter)</span>
|
||||||
<span data-story-run-error>@(chapter.ErrorMessage ?? string.Empty)</span>
|
<span data-story-run-error>@(chapter.ErrorMessage ?? string.Empty)</span>
|
||||||
|
<span data-story-run-current-scene-title>@(chapter.CurrentSceneDisplayTitle ?? string.Empty)</span>
|
||||||
<span data-story-run-latest-summary>@(chapter.LatestSceneSummary ?? string.Empty)</span>
|
<span data-story-run-latest-summary>@(chapter.LatestSceneSummary ?? string.Empty)</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -334,8 +339,8 @@
|
|||||||
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
||||||
{
|
{
|
||||||
var chapter = CurrentChapter(model);
|
var chapter = CurrentChapter(model);
|
||||||
return chapter?.CompletedScenes is > 0
|
return chapter?.CurrentSceneNumber is > 0
|
||||||
? chapter.CompletedScenes.Value.ToString("N0")
|
? chapter.CurrentSceneNumber.Value.ToString("N0")
|
||||||
: "Waiting";
|
: "Waiting";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,8 +351,8 @@
|
|||||||
return "Loading scenes...";
|
return "Loading scenes...";
|
||||||
}
|
}
|
||||||
|
|
||||||
var completed = Math.Clamp(chapter?.CompletedScenes ?? 0, 0, chapter?.TotalDetectedScenes ?? 0);
|
var current = Math.Clamp(chapter?.CurrentSceneNumber ?? chapter?.CompletedScenes ?? 0, 0, chapter?.TotalDetectedScenes ?? 0);
|
||||||
return $"Scene {completed:N0} of {chapter?.TotalDetectedScenes:N0}";
|
return $"Scene {current:N0} of {chapter?.TotalDetectedScenes:N0}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ChapterProgressPercent(StoryIntelligenceOnboardingChapterViewModel? chapter)
|
private static int ChapterProgressPercent(StoryIntelligenceOnboardingChapterViewModel? chapter)
|
||||||
|
|||||||
@ -804,6 +804,10 @@
|
|||||||
padding: clamp(1.25rem, 2vw, 1.75rem);
|
padding: clamp(1.25rem, 2vw, 1.75rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-live-progress > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.story-live-hero h1 {
|
.story-live-hero h1 {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: clamp(2.6rem, 4vw, 3.6rem);
|
font-size: clamp(2.6rem, 4vw, 3.6rem);
|
||||||
@ -824,7 +828,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-live-progress > .alert-info {
|
.story-live-progress > .alert-info {
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-live-progress-card,
|
.story-live-progress-card,
|
||||||
@ -979,6 +984,13 @@ summary.story-review-chapter-heading {
|
|||||||
margin-bottom: .55rem;
|
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] {
|
.story-latest-summary[data-story-latest-summary] {
|
||||||
padding: 1.1rem 1.15rem;
|
padding: 1.1rem 1.15rem;
|
||||||
border-left-width: 6px;
|
border-left-width: 6px;
|
||||||
|
|||||||
@ -284,9 +284,11 @@
|
|||||||
const status = chapter.dataset.storyRawStatus || "Pending";
|
const status = chapter.dataset.storyRawStatus || "Pending";
|
||||||
const stage = chapter.dataset.storyCurrentStage || status;
|
const stage = chapter.dataset.storyCurrentStage || status;
|
||||||
const currentSceneNumber = chapter.dataset.storyCurrentSceneNumber || "";
|
const currentSceneNumber = chapter.dataset.storyCurrentSceneNumber || "";
|
||||||
|
const currentSceneTitle = chapter.dataset.storyCurrentSceneTitle || "";
|
||||||
const total = chapter.querySelector("[data-story-run-total]")?.textContent || "";
|
const total = chapter.querySelector("[data-story-run-total]")?.textContent || "";
|
||||||
const completed = chapter.querySelector("[data-story-run-completed]")?.textContent || "";
|
const completed = chapter.querySelector("[data-story-run-completed]")?.textContent || "";
|
||||||
const message = chapter.querySelector("[data-story-run-message]")?.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) => {
|
root.querySelectorAll("[data-story-current-chapter]").forEach((node) => {
|
||||||
node.textContent = chapter.dataset.storyChapterTitle || "Current chapter";
|
node.textContent = chapter.dataset.storyChapterTitle || "Current chapter";
|
||||||
@ -297,6 +299,19 @@
|
|||||||
root.querySelectorAll("[data-story-current-scene]").forEach((node) => {
|
root.querySelectorAll("[data-story-current-scene]").forEach((node) => {
|
||||||
node.textContent = currentSceneNumber ? numberText(currentSceneNumber) : "Waiting";
|
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 totalNumber = Number.parseInt(total.replace(/,/g, "") || "0", 10) || 0;
|
||||||
const completedNumber = Number.parseInt(completed.replace(/,/g, "") || "0", 10) || 0;
|
const completedNumber = Number.parseInt(completed.replace(/,/g, "") || "0", 10) || 0;
|
||||||
const currentSceneDisplay = currentSceneNumber ? Number.parseInt(currentSceneNumber, 10) || completedNumber : completedNumber;
|
const currentSceneDisplay = currentSceneNumber ? Number.parseInt(currentSceneNumber, 10) || completedNumber : completedNumber;
|
||||||
@ -402,7 +417,9 @@
|
|||||||
const tokens = field(state, "totalTokens", "TotalTokens");
|
const tokens = field(state, "totalTokens", "TotalTokens");
|
||||||
const totalDurationMs = field(state, "totalDurationMs", "TotalDurationMs");
|
const totalDurationMs = field(state, "totalDurationMs", "TotalDurationMs");
|
||||||
const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec";
|
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 currentSceneNumber = field(state, "currentSceneNumber", "CurrentSceneNumber");
|
||||||
|
const currentSceneDisplayTitle = field(state, "currentSceneDisplayTitle", "CurrentSceneDisplayTitle");
|
||||||
const updatedUtc = field(state, "updatedUtc", "UpdatedUtc");
|
const updatedUtc = field(state, "updatedUtc", "UpdatedUtc");
|
||||||
const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber);
|
const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber);
|
||||||
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
||||||
@ -414,7 +431,17 @@
|
|||||||
});
|
});
|
||||||
chapter.dataset.storyRawStatus = status;
|
chapter.dataset.storyRawStatus = status;
|
||||||
chapter.dataset.storyCurrentStage = stage;
|
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) {
|
if (updatedUtc) {
|
||||||
chapter.dataset.storyUpdatedUtc = updatedUtc;
|
chapter.dataset.storyUpdatedUtc = updatedUtc;
|
||||||
}
|
}
|
||||||
@ -465,6 +492,11 @@
|
|||||||
node.textContent = latestSummary;
|
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 totalNumber = Number.parseInt(total || "0", 10) || 0;
|
||||||
const completedNumber = Number.parseInt(completed || "0", 10) || 0;
|
const completedNumber = Number.parseInt(completed || "0", 10) || 0;
|
||||||
@ -479,6 +511,11 @@
|
|||||||
if (summaryNode && latestSummary) {
|
if (summaryNode && latestSummary) {
|
||||||
summaryNode.textContent = latestSummary;
|
summaryNode.textContent = latestSummary;
|
||||||
summaryNode.hidden = false;
|
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]");
|
const emptyNode = root.querySelector("[data-story-latest-empty]");
|
||||||
if (emptyNode) {
|
if (emptyNode) {
|
||||||
emptyNode.hidden = true;
|
emptyNode.hidden = true;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user