Phase 21H - Story Intelligence Live Import Stability
This commit is contained in:
parent
2b7a4b599d
commit
fd85473258
@ -156,6 +156,7 @@ public static class StoryIntelligenceExperiencePrototypeData
|
|||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
ChapterLabel = $"Chapter {Math.Max(1, chapters)}",
|
||||||
SceneNumber = number,
|
SceneNumber = number,
|
||||||
Title = title,
|
Title = title,
|
||||||
Summary = summary,
|
Summary = summary,
|
||||||
@ -167,6 +168,7 @@ public static class StoryIntelligenceExperiencePrototypeData
|
|||||||
ChapterTotal = 5,
|
ChapterTotal = 5,
|
||||||
ScenesAnalysed = number,
|
ScenesAnalysed = number,
|
||||||
SceneTotal = sceneTotal,
|
SceneTotal = sceneTotal,
|
||||||
|
ElapsedTime = $"{Math.Max(1, number * 2)}m",
|
||||||
EstimatedRemaining = remaining,
|
EstimatedRemaining = remaining,
|
||||||
PovCharacterId = pov,
|
PovCharacterId = pov,
|
||||||
Location = location,
|
Location = location,
|
||||||
|
|||||||
@ -135,6 +135,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
Scenes = scenes
|
Scenes = scenes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StabiliseCharacterIllustrations(model);
|
||||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||||
logger.LogInformation("Built Story Intelligence visualisation snapshot for run {RunId} in {ElapsedMs}ms.", runId, stopwatch.ElapsedMilliseconds);
|
logger.LogInformation("Built Story Intelligence visualisation snapshot for run {RunId} in {ElapsedMs}ms.", runId, stopwatch.ElapsedMilliseconds);
|
||||||
return model;
|
return model;
|
||||||
@ -183,10 +184,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var sourceChapterCount = (await sources.ListChaptersAsync(session.BookID)).Count;
|
var sourceChapterCount = (await sources.ListChaptersAsync(session.BookID)).Count;
|
||||||
var bookChapterTotal = Math.Max(1, Math.Max(sourceChapterCount, runSnapshots.Count));
|
var bookChapterTotal = Math.Max(1, Math.Max(sourceChapterCount, runSnapshots.Count));
|
||||||
var overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal: false);
|
var overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal: false);
|
||||||
var completedChapterRuns = runSnapshots.Count(snapshot => IsTerminal(snapshot.Run.Status));
|
var completedChapterRuns = runSnapshots.Count(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||||
var isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status))
|
var isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status));
|
||||||
&& string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase);
|
var isSuccessfullyComplete = runSnapshots.All(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||||
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal);
|
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isSuccessfullyComplete);
|
||||||
|
var overallElapsed = SessionElapsed(runSnapshots);
|
||||||
|
var overallRemaining = isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes);
|
||||||
|
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
||||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||||
var nextSceneNumber = 1;
|
var nextSceneNumber = 1;
|
||||||
foreach (var snapshot in runSnapshots)
|
foreach (var snapshot in runSnapshots)
|
||||||
@ -202,8 +206,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
overallChaptersAnalysed: completedChapterRuns,
|
overallChaptersAnalysed: completedChapterRuns,
|
||||||
overallChapterTotal: bookChapterTotal,
|
overallChapterTotal: bookChapterTotal,
|
||||||
overallProgressPercent: overallProgressPercent,
|
overallProgressPercent: overallProgressPercent,
|
||||||
overallStage: OverallStage(session, activeSnapshot.Run),
|
overallStage: overallStage,
|
||||||
overallRemaining: EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes),
|
overallElapsed: overallElapsed,
|
||||||
|
overallRemaining: overallRemaining,
|
||||||
continuousSceneNumbering: true,
|
continuousSceneNumbering: true,
|
||||||
overallTerminal: isTerminal)
|
overallTerminal: isTerminal)
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -226,12 +231,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||||
IsTerminal = isTerminal,
|
IsTerminal = isTerminal,
|
||||||
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
||||||
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, overallProgressPercent),
|
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent),
|
||||||
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
||||||
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
||||||
Scenes = scenes
|
Scenes = scenes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StabiliseCharacterIllustrations(model);
|
||||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||||
logger.LogInformation("Built Story Intelligence import session visualisation snapshot for session {ImportSessionId} in {ElapsedMs}ms.", importSessionId, stopwatch.ElapsedMilliseconds);
|
logger.LogInformation("Built Story Intelligence import session visualisation snapshot for session {ImportSessionId} in {ElapsedMs}ms.", importSessionId, stopwatch.ElapsedMilliseconds);
|
||||||
return model;
|
return model;
|
||||||
@ -249,6 +255,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
int? overallChapterTotal = null,
|
int? overallChapterTotal = null,
|
||||||
int? overallProgressPercent = null,
|
int? overallProgressPercent = null,
|
||||||
string? overallStage = null,
|
string? overallStage = null,
|
||||||
|
string? overallElapsed = null,
|
||||||
string? overallRemaining = null,
|
string? overallRemaining = null,
|
||||||
bool continuousSceneNumbering = false,
|
bool continuousSceneNumbering = false,
|
||||||
bool overallTerminal = false)
|
bool overallTerminal = false)
|
||||||
@ -265,6 +272,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
yield return new StoryIntelligenceExperienceSceneState
|
yield return new StoryIntelligenceExperienceSceneState
|
||||||
{
|
{
|
||||||
Id = $"scene-result-{item.Result.SceneResultID}",
|
Id = $"scene-result-{item.Result.SceneResultID}",
|
||||||
|
ChapterLabel = ChapterLabel(run, item),
|
||||||
SceneNumber = sceneNumber,
|
SceneNumber = sceneNumber,
|
||||||
Title = SceneTitle(scene, item.Result, sceneNumber),
|
Title = SceneTitle(scene, item.Result, sceneNumber),
|
||||||
Summary = Trim(FirstConfigured(scene.Summary?.Short, scene.Summary?.Detailed, run.CurrentMessage, "Scene analysed."), 180),
|
Summary = Trim(FirstConfigured(scene.Summary?.Short, scene.Summary?.Detailed, run.CurrentMessage, "Scene analysed."), 180),
|
||||||
@ -279,8 +287,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ChapterTotal = Math.Max(1, overallChapterTotal ?? run.SourceChapterCount ?? ChapterCount(completed)),
|
ChapterTotal = Math.Max(1, overallChapterTotal ?? run.SourceChapterCount ?? ChapterCount(completed)),
|
||||||
ScenesAnalysed = overallCompletedScenes ?? completedScenes,
|
ScenesAnalysed = overallCompletedScenes ?? completedScenes,
|
||||||
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
||||||
|
ElapsedTime = overallElapsed ?? EstimateElapsed(run),
|
||||||
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
||||||
PovCharacterId = CharacterId(scene.PointOfView?.CharacterName),
|
PovCharacterId = CharacterId(ResolveNarratorName(scene) ?? scene.PointOfView?.CharacterName),
|
||||||
Location = BuildLocation(scene),
|
Location = BuildLocation(scene),
|
||||||
Characters = BuildCharacters(scene),
|
Characters = BuildCharacters(scene),
|
||||||
Assets = BuildAssets(scene),
|
Assets = BuildAssets(scene),
|
||||||
@ -297,6 +306,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
Id = $"run-{run.StoryIntelligenceRunID}-waiting",
|
Id = $"run-{run.StoryIntelligenceRunID}-waiting",
|
||||||
|
ChapterLabel = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter",
|
||||||
SceneNumber = 1,
|
SceneNumber = 1,
|
||||||
Title = run.Status is StoryIntelligenceRunStatuses.Failed ? "Analysis failed" : "Waiting for analysed scenes",
|
Title = run.Status is StoryIntelligenceRunStatuses.Failed ? "Analysis failed" : "Waiting for analysed scenes",
|
||||||
Summary = FirstConfigured(run.ErrorMessage, run.CurrentMessage, "No scene analysis results have been persisted yet."),
|
Summary = FirstConfigured(run.ErrorMessage, run.CurrentMessage, "No scene analysis results have been persisted yet."),
|
||||||
@ -308,6 +318,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ChapterTotal = Math.Max(1, run.SourceChapterCount ?? 1),
|
ChapterTotal = Math.Max(1, run.SourceChapterCount ?? 1),
|
||||||
ScenesAnalysed = run.CompletedScenes ?? 0,
|
ScenesAnalysed = run.CompletedScenes ?? 0,
|
||||||
SceneTotal = Math.Max(1, run.TotalDetectedScenes ?? 1),
|
SceneTotal = Math.Max(1, run.TotalDetectedScenes ?? 1),
|
||||||
|
ElapsedTime = EstimateElapsed(run),
|
||||||
EstimatedRemaining = EstimateRemaining(run, run.CompletedScenes ?? 0, run.TotalDetectedScenes ?? 0),
|
EstimatedRemaining = EstimateRemaining(run, run.CompletedScenes ?? 0, run.TotalDetectedScenes ?? 0),
|
||||||
Location = PendingLocation(),
|
Location = PendingLocation(),
|
||||||
LatestDiscoveries = run.ErrorMessage is null ? [] : [$"Run failed: {Trim(run.ErrorMessage, 90)}"],
|
LatestDiscoveries = run.ErrorMessage is null ? [] : [$"Run failed: {Trim(run.ErrorMessage, 90)}"],
|
||||||
@ -326,7 +337,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
private static IReadOnlyList<StoryIntelligenceExperienceCharacterState> BuildCharacters(SceneIntelligenceScene scene)
|
private static IReadOnlyList<StoryIntelligenceExperienceCharacterState> BuildCharacters(SceneIntelligenceScene scene)
|
||||||
{
|
{
|
||||||
var povName = Clean(scene.PointOfView?.CharacterName);
|
var narratorResolution = ResolveNarratorName(scene);
|
||||||
|
var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName);
|
||||||
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
||||||
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var group in (scene.Characters ?? [])
|
foreach (var group in (scene.Characters ?? [])
|
||||||
@ -334,6 +346,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
||||||
.Take(5))
|
.Take(5))
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(narratorResolution) && IsGenericNarratorName(group.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var character = group.OrderByDescending(item => item.Confidence ?? 0).First();
|
var character = group.OrderByDescending(item => item.Confidence ?? 0).First();
|
||||||
var isPov = !string.IsNullOrWhiteSpace(povName) && string.Equals(group.Key, povName, StringComparison.OrdinalIgnoreCase);
|
var isPov = !string.IsNullOrWhiteSpace(povName) && string.Equals(group.Key, povName, StringComparison.OrdinalIgnoreCase);
|
||||||
var libraryCode = CharacterLibraryCode(group.Key, character, scene, reservedCodes);
|
var libraryCode = CharacterLibraryCode(group.Key, character, scene, reservedCodes);
|
||||||
@ -368,6 +385,62 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
return states.OrderByDescending(character => character.Weight).ToList();
|
return states.OrderByDescending(character => character.Weight).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ResolveNarratorName(SceneIntelligenceScene scene)
|
||||||
|
{
|
||||||
|
var povName = Clean(scene.PointOfView?.CharacterName);
|
||||||
|
if (!IsGenericNarratorName(povName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var evidence = string.Join(' ', new[]
|
||||||
|
{
|
||||||
|
scene.PointOfView?.Evidence,
|
||||||
|
scene.Summary?.Short,
|
||||||
|
scene.Summary?.Detailed,
|
||||||
|
scene.ScenePurpose?.ObservedFunction
|
||||||
|
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
|
||||||
|
var namedCharacters = (scene.Characters ?? [])
|
||||||
|
.Where(character => !string.IsNullOrWhiteSpace(character.Name))
|
||||||
|
.Select(character => Clean(character.Name))
|
||||||
|
.Where(name => !IsGenericNarratorName(name))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var name in namedCharacters)
|
||||||
|
{
|
||||||
|
if (ContainsIdentityEvidence(evidence, name))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return namedCharacters.Count == 1 ? namedCharacters[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsIdentityEvidence(string evidence, string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(evidence) || string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lowerEvidence = evidence.ToLowerInvariant();
|
||||||
|
var lowerName = name.ToLowerInvariant();
|
||||||
|
return lowerEvidence.Contains($"narrated by {lowerName}", StringComparison.Ordinal)
|
||||||
|
|| lowerEvidence.Contains($"narrator is {lowerName}", StringComparison.Ordinal)
|
||||||
|
|| lowerEvidence.Contains($"i am {lowerName}", StringComparison.Ordinal)
|
||||||
|
|| lowerEvidence.Contains($"i'm {lowerName}", StringComparison.Ordinal)
|
||||||
|
|| lowerEvidence.Contains($"my name is {lowerName}", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGenericNarratorName(string? name)
|
||||||
|
{
|
||||||
|
var value = Clean(name).ToLowerInvariant();
|
||||||
|
return value is "narrator" or "i" or "me" or "my" or "myself" or "first person narrator" or "unnamed narrator";
|
||||||
|
}
|
||||||
|
|
||||||
private static StoryIntelligenceExperienceLocationState BuildLocation(SceneIntelligenceScene scene)
|
private static StoryIntelligenceExperienceLocationState BuildLocation(SceneIntelligenceScene scene)
|
||||||
{
|
{
|
||||||
var location = (scene.Locations ?? [])
|
var location = (scene.Locations ?? [])
|
||||||
@ -646,7 +719,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var active = snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status));
|
var active = snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status));
|
||||||
if (active is not null)
|
if (active is not null)
|
||||||
{
|
{
|
||||||
return FirstConfigured(active.Run.CurrentMessage, $"Analysing chapter {active.Run.ChapterNumber:0.##}.");
|
return BackendStageDisplay(active.Run);
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase)
|
return string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase)
|
||||||
@ -658,13 +731,34 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
{
|
{
|
||||||
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
||||||
{
|
{
|
||||||
var completed = Math.Clamp(snapshot.Run.CompletedScenes ?? snapshot.Completed.Count, 0, snapshot.Run.TotalDetectedScenes.Value);
|
var scene = Math.Clamp((snapshot.Run.CompletedScenes ?? snapshot.Completed.Count) + (IsTerminal(snapshot.Run.Status) ? 0 : 1), 1, snapshot.Run.TotalDetectedScenes.Value);
|
||||||
return $"Scene {completed:N0} of {snapshot.Run.TotalDetectedScenes.Value:N0}";
|
return $"Scene {scene:N0} of {snapshot.Run.TotalDetectedScenes.Value:N0}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot.Current is null ? "Waiting" : $"Scene {SceneSortNumber(snapshot.Current):0.##}";
|
return snapshot.Current is null ? "Waiting" : $"Scene {SceneSortNumber(snapshot.Current):0.##}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BackendStageDisplay(StoryIntelligenceSavedRun run)
|
||||||
|
{
|
||||||
|
var stage = FirstConfigured(run.CurrentStage, run.Status);
|
||||||
|
var sceneText = run.TotalDetectedScenes.HasValue
|
||||||
|
? $"Scene {Math.Clamp((run.CompletedScenes ?? 0) + (IsTerminal(run.Status) ? 0 : 1), 1, Math.Max(1, run.TotalDetectedScenes.Value)):N0}"
|
||||||
|
: "current scene";
|
||||||
|
var chapterText = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##} " : string.Empty;
|
||||||
|
return stage switch
|
||||||
|
{
|
||||||
|
StoryIntelligenceFailureStages.SceneIntelligence => $"Analysing {chapterText}{sceneText}",
|
||||||
|
StoryIntelligencePipelineStages.CharacterImport or StoryIntelligencePipelineStages.CharacterReview => "Analysing Characters",
|
||||||
|
StoryIntelligencePipelineStages.RelationshipImport or StoryIntelligencePipelineStages.RelationshipReview => "Building Relationships",
|
||||||
|
StoryIntelligencePipelineStages.AssetImport or StoryIntelligencePipelineStages.AssetReview => "Analysing Assets",
|
||||||
|
StoryIntelligencePipelineStages.LocationImport or StoryIntelligencePipelineStages.LocationReview => "Analysing Locations",
|
||||||
|
StoryIntelligenceFailureStages.DomainImport or StoryIntelligencePipelineStages.KnowledgeImport or StoryIntelligencePipelineStages.KnowledgeReview => "Inferring Knowledge",
|
||||||
|
StoryIntelligenceFailureStages.Persistence => "Building Continuity",
|
||||||
|
_ when stage.Contains("Scene", StringComparison.OrdinalIgnoreCase) => $"Analysing {chapterText}{sceneText}",
|
||||||
|
_ => FirstConfigured(run.CurrentMessage, stage, $"Analysing {chapterText}{sceneText}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static StoryIntelligenceExperienceSnapshotDiagnostics BuildSessionDiagnostics(
|
private static StoryIntelligenceExperienceSnapshotDiagnostics BuildSessionDiagnostics(
|
||||||
StoryIntelligenceBookPipelineState session,
|
StoryIntelligenceBookPipelineState session,
|
||||||
IReadOnlyList<RunSnapshot> snapshots,
|
IReadOnlyList<RunSnapshot> snapshots,
|
||||||
@ -672,6 +766,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ParsedSceneResult? current,
|
ParsedSceneResult? current,
|
||||||
int parsedScenes,
|
int parsedScenes,
|
||||||
int totalPersistedResults,
|
int totalPersistedResults,
|
||||||
|
int totalScenes,
|
||||||
int overallProgressPercent)
|
int overallProgressPercent)
|
||||||
{
|
{
|
||||||
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
||||||
@ -679,6 +774,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
RequestedMode = "real",
|
RequestedMode = "real",
|
||||||
|
CurrentImportId = session.StoryIntelligenceBookPipelineID,
|
||||||
RequestedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
RequestedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||||
ResolvedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
ResolvedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||||
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||||
@ -686,6 +782,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
OverallBookProgress = overallProgressPercent,
|
OverallBookProgress = overallProgressPercent,
|
||||||
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||||
CurrentScene = CurrentSceneText(activeSnapshot),
|
CurrentScene = CurrentSceneText(activeSnapshot),
|
||||||
|
CompletedScenes = parsedScenes,
|
||||||
|
TotalScenes = totalScenes,
|
||||||
|
ProgressSource = "ImportSession",
|
||||||
|
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
||||||
SnapshotSource = "Real",
|
SnapshotSource = "Real",
|
||||||
RunStatus = SessionStatus(session, snapshots),
|
RunStatus = SessionStatus(session, snapshots),
|
||||||
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
||||||
@ -722,8 +822,12 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
OverallBookProgress = Progress(run, run.CompletedScenes ?? completed.Count, Math.Max(run.TotalDetectedScenes ?? 0, results.Count)),
|
OverallBookProgress = Progress(run, run.CompletedScenes ?? completed.Count, Math.Max(run.TotalDetectedScenes ?? 0, results.Count)),
|
||||||
CurrentChapter = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter unknown",
|
CurrentChapter = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||||
CurrentScene = run.TotalDetectedScenes.HasValue
|
CurrentScene = run.TotalDetectedScenes.HasValue
|
||||||
? $"Scene {Math.Clamp(run.CompletedScenes ?? completed.Count, 0, run.TotalDetectedScenes.Value):N0} of {run.TotalDetectedScenes.Value:N0}"
|
? $"Scene {Math.Clamp((run.CompletedScenes ?? completed.Count) + (IsTerminal(run.Status) ? 0 : 1), 1, run.TotalDetectedScenes.Value):N0} of {run.TotalDetectedScenes.Value:N0}"
|
||||||
: (current is null ? "Waiting" : $"Scene {SceneSortNumber(current):0.##}"),
|
: (current is null ? "Waiting" : $"Scene {SceneSortNumber(current):0.##}"),
|
||||||
|
CompletedScenes = run.CompletedScenes ?? completed.Count,
|
||||||
|
TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count),
|
||||||
|
ProgressSource = "ChapterRun",
|
||||||
|
CurrentBackendStage = BackendStageDisplay(run),
|
||||||
SnapshotSource = "Real",
|
SnapshotSource = "Real",
|
||||||
RunStatus = run.Status,
|
RunStatus = run.Status,
|
||||||
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
||||||
@ -750,9 +854,36 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
_ => FirstConfigured(run.CurrentMessage, "Analysis snapshot is updating.")
|
_ => FirstConfigured(run.CurrentMessage, "Analysis snapshot is updating.")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static void StabiliseCharacterIllustrations(StoryIntelligenceExperiencePrototypeViewModel model)
|
||||||
|
{
|
||||||
|
var firstAssignments = new Dictionary<string, (string LibraryCode, string ImagePath)>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var scene in model.Scenes.OrderBy(scene => scene.SceneNumber))
|
||||||
|
{
|
||||||
|
foreach (var character in scene.Characters)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(character.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstAssignments.TryGetValue(character.Id, out var assignment))
|
||||||
|
{
|
||||||
|
character.LibraryCode = assignment.LibraryCode;
|
||||||
|
character.ImagePath = assignment.ImagePath;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstAssignments[character.Id] = (character.LibraryCode, character.ImagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsTerminal(string status)
|
private static bool IsTerminal(string status)
|
||||||
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings or StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled;
|
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings or StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled;
|
||||||
|
|
||||||
|
private static bool IsSuccessfulTerminal(string status)
|
||||||
|
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings;
|
||||||
|
|
||||||
private static int SceneNumber(ParsedSceneResult item)
|
private static int SceneNumber(ParsedSceneResult item)
|
||||||
=> item.Scene?.SceneReference?.SceneNumber.HasValue == true ? Convert.ToInt32(item.Scene.SceneReference.SceneNumber.Value) : item.Result.TemporarySceneNumber;
|
=> item.Scene?.SceneReference?.SceneNumber.HasValue == true ? Convert.ToInt32(item.Scene.SceneReference.SceneNumber.Value) : item.Result.TemporarySceneNumber;
|
||||||
|
|
||||||
@ -764,11 +895,46 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
=> item.Scene?.SceneReference?.SceneNumber ?? item.Result.TemporarySceneNumber;
|
=> item.Scene?.SceneReference?.SceneNumber ?? item.Result.TemporarySceneNumber;
|
||||||
|
|
||||||
private static string SceneTitle(SceneIntelligenceScene scene, StoryIntelligenceSavedSceneResult result, int sceneNumber)
|
private static string SceneTitle(SceneIntelligenceScene scene, StoryIntelligenceSavedSceneResult result, int sceneNumber)
|
||||||
=> FirstConfigured(scene.SceneReference?.SourceLabel, result.SourceLabel, scene.Summary?.Short, $"Scene {sceneNumber:N0}");
|
=> CompactSceneTitle(FirstConfigured(scene.SceneReference?.SourceLabel, result.SourceLabel, scene.Summary?.Short, $"Scene {sceneNumber:N0}"), sceneNumber);
|
||||||
|
|
||||||
|
private static string CompactSceneTitle(string title, int sceneNumber)
|
||||||
|
{
|
||||||
|
var value = Clean(title);
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return $"Scene {sceneNumber:N0}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sceneMarker = $"scene {sceneNumber}".ToLowerInvariant();
|
||||||
|
var lower = value.ToLowerInvariant();
|
||||||
|
var markerIndex = lower.IndexOf(sceneMarker, StringComparison.Ordinal);
|
||||||
|
if (markerIndex > 0)
|
||||||
|
{
|
||||||
|
value = value[markerIndex..].Trim(' ', '-', ':', '|');
|
||||||
|
}
|
||||||
|
|
||||||
|
var separators = new[] { " - ", " | ", ": " };
|
||||||
|
foreach (var separator in separators)
|
||||||
|
{
|
||||||
|
var parts = value.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length >= 3)
|
||||||
|
{
|
||||||
|
return Trim(parts[^1], 72);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Trim(value, 72);
|
||||||
|
}
|
||||||
|
|
||||||
private static string TimelineLabel(SceneIntelligenceScene scene, StoryIntelligenceSavedSceneResult result)
|
private static string TimelineLabel(SceneIntelligenceScene scene, StoryIntelligenceSavedSceneResult result)
|
||||||
=> FirstConfigured(scene.TimelineClues?.FirstOrDefault()?.AbsoluteDate, scene.Setting?.DateOrTimeReference, result.SourceLabel, "Known scene");
|
=> FirstConfigured(scene.TimelineClues?.FirstOrDefault()?.AbsoluteDate, scene.Setting?.DateOrTimeReference, result.SourceLabel, "Known scene");
|
||||||
|
|
||||||
|
private static string ChapterLabel(StoryIntelligenceSavedRun run, ParsedSceneResult item)
|
||||||
|
{
|
||||||
|
var chapter = item.Scene?.SceneReference?.ChapterNumber ?? run.ChapterNumber;
|
||||||
|
return chapter.HasValue ? $"Chapter {chapter:0.##}" : "Chapter";
|
||||||
|
}
|
||||||
|
|
||||||
private static int Progress(StoryIntelligenceSavedRun run, int completedScenes, int totalScenes)
|
private static int Progress(StoryIntelligenceSavedRun run, int completedScenes, int totalScenes)
|
||||||
{
|
{
|
||||||
if (IsTerminal(run.Status) && run.Status is not StoryIntelligenceRunStatuses.Failed)
|
if (IsTerminal(run.Status) && run.Status is not StoryIntelligenceRunStatuses.Failed)
|
||||||
@ -817,7 +983,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
private static decimal ChapterProgress(RunSnapshot snapshot)
|
private static decimal ChapterProgress(RunSnapshot snapshot)
|
||||||
{
|
{
|
||||||
if (IsTerminal(snapshot.Run.Status))
|
if (IsSuccessfulTerminal(snapshot.Run.Status))
|
||||||
{
|
{
|
||||||
return 1m;
|
return 1m;
|
||||||
}
|
}
|
||||||
@ -853,6 +1019,19 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
return FormatDuration(averageMs * (totalScenes - completedScenes));
|
return FormatDuration(averageMs * (totalScenes - completedScenes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string EstimateElapsed(StoryIntelligenceSavedRun run)
|
||||||
|
{
|
||||||
|
var elapsedMs = run.TotalDurationMs ?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - run.StartedUtc).TotalMilliseconds));
|
||||||
|
return FormatDuration(elapsedMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SessionElapsed(IReadOnlyList<RunSnapshot> snapshots)
|
||||||
|
{
|
||||||
|
var elapsedMs = snapshots.Sum(snapshot => snapshot.Run.TotalDurationMs
|
||||||
|
?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - snapshot.Run.StartedUtc).TotalMilliseconds)));
|
||||||
|
return FormatDuration(elapsedMs);
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatDuration(long milliseconds)
|
private static string FormatDuration(long milliseconds)
|
||||||
{
|
{
|
||||||
var duration = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds));
|
var duration = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds));
|
||||||
|
|||||||
@ -20,6 +20,7 @@ public sealed class StoryIntelligenceExperiencePrototypeViewModel
|
|||||||
public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
||||||
{
|
{
|
||||||
public string RequestedMode { get; init; } = string.Empty;
|
public string RequestedMode { get; init; } = string.Empty;
|
||||||
|
public int? CurrentImportId { get; init; }
|
||||||
public int? RequestedImportSessionId { get; init; }
|
public int? RequestedImportSessionId { get; init; }
|
||||||
public int? ResolvedImportSessionId { get; init; }
|
public int? ResolvedImportSessionId { get; init; }
|
||||||
public int? ActiveChapterRunId { get; init; }
|
public int? ActiveChapterRunId { get; init; }
|
||||||
@ -27,6 +28,10 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
|||||||
public int OverallBookProgress { get; init; }
|
public int OverallBookProgress { get; init; }
|
||||||
public string CurrentChapter { get; init; } = string.Empty;
|
public string CurrentChapter { get; init; } = string.Empty;
|
||||||
public string CurrentScene { get; init; } = string.Empty;
|
public string CurrentScene { get; init; } = string.Empty;
|
||||||
|
public int CompletedScenes { get; init; }
|
||||||
|
public int TotalScenes { get; init; }
|
||||||
|
public string ProgressSource { get; init; } = string.Empty;
|
||||||
|
public string CurrentBackendStage { get; init; } = string.Empty;
|
||||||
public string SnapshotSource { get; init; } = string.Empty;
|
public string SnapshotSource { get; init; } = string.Empty;
|
||||||
public string RunStatus { get; init; } = string.Empty;
|
public string RunStatus { get; init; } = string.Empty;
|
||||||
public int PersistedCompletedSceneCount { get; init; }
|
public int PersistedCompletedSceneCount { get; init; }
|
||||||
@ -54,6 +59,7 @@ public sealed class StoryIntelligenceExperienceRunOption
|
|||||||
public sealed class StoryIntelligenceExperienceSceneState
|
public sealed class StoryIntelligenceExperienceSceneState
|
||||||
{
|
{
|
||||||
public string Id { get; init; } = string.Empty;
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string ChapterLabel { get; init; } = string.Empty;
|
||||||
public int SceneNumber { get; init; }
|
public int SceneNumber { get; init; }
|
||||||
public string Title { get; init; } = string.Empty;
|
public string Title { get; init; } = string.Empty;
|
||||||
public string Summary { get; init; } = string.Empty;
|
public string Summary { get; init; } = string.Empty;
|
||||||
@ -65,6 +71,7 @@ public sealed class StoryIntelligenceExperienceSceneState
|
|||||||
public int ChapterTotal { get; init; }
|
public int ChapterTotal { get; init; }
|
||||||
public int ScenesAnalysed { get; init; }
|
public int ScenesAnalysed { get; init; }
|
||||||
public int SceneTotal { get; init; }
|
public int SceneTotal { get; init; }
|
||||||
|
public string ElapsedTime { get; init; } = string.Empty;
|
||||||
public string EstimatedRemaining { get; init; } = string.Empty;
|
public string EstimatedRemaining { get; init; } = string.Empty;
|
||||||
public string PovCharacterId { get; init; } = string.Empty;
|
public string PovCharacterId { get; init; } = string.Empty;
|
||||||
public StoryIntelligenceExperienceLocationState Location { get; init; } = new();
|
public StoryIntelligenceExperienceLocationState Location { get; init; } = new();
|
||||||
|
|||||||
@ -46,9 +46,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<ol class="story-exp-pipeline" data-analysis-stages aria-label="Analysis stages"></ol>
|
<ol class="story-exp-pipeline" data-analysis-stages aria-label="Analysis stages"></ol>
|
||||||
<div class="story-exp-context">
|
<div class="story-exp-context">
|
||||||
<span>@statusLabel</span>
|
<span data-experience-status>@statusLabel</span>
|
||||||
<span>@Model.ProjectName</span>
|
<span data-project-name>@Model.ProjectName</span>
|
||||||
<span>@Model.BookTitle</span>
|
<span data-book-title>@Model.BookTitle</span>
|
||||||
<span data-stage-label>Preparing</span>
|
<span data-stage-label>Preparing</span>
|
||||||
<span>Scene <strong data-current-scene>1</strong></span>
|
<span>Scene <strong data-current-scene>1</strong></span>
|
||||||
</div>
|
</div>
|
||||||
@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.SnapshotMessage))
|
@if (!string.IsNullOrWhiteSpace(Model.SnapshotMessage))
|
||||||
{
|
{
|
||||||
<aside class="story-exp-status-message" role="status">
|
<aside class="story-exp-status-message" role="status" data-snapshot-message>
|
||||||
@Model.SnapshotMessage
|
@Model.SnapshotMessage
|
||||||
</aside>
|
</aside>
|
||||||
}
|
}
|
||||||
@ -185,6 +185,10 @@
|
|||||||
<details class="story-exp-diagnostics" data-diagnostics>
|
<details class="story-exp-diagnostics" data-diagnostics>
|
||||||
<summary>Diagnostics</summary>
|
<summary>Diagnostics</summary>
|
||||||
<dl>
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>Current Import ID</dt>
|
||||||
|
<dd data-diagnostic-key="currentImportId">@Model.Diagnostics.CurrentImportId</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Requested mode</dt>
|
<dt>Requested mode</dt>
|
||||||
<dd data-diagnostic-key="requestedMode">@Model.Diagnostics.RequestedMode</dd>
|
<dd data-diagnostic-key="requestedMode">@Model.Diagnostics.RequestedMode</dd>
|
||||||
@ -217,6 +221,30 @@
|
|||||||
<dt>Current scene</dt>
|
<dt>Current scene</dt>
|
||||||
<dd data-diagnostic-key="currentScene">@Model.Diagnostics.CurrentScene</dd>
|
<dd data-diagnostic-key="currentScene">@Model.Diagnostics.CurrentScene</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Completed scenes</dt>
|
||||||
|
<dd data-diagnostic-key="completedScenes">@Model.Diagnostics.CompletedScenes</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Total scenes</dt>
|
||||||
|
<dd data-diagnostic-key="totalScenes">@Model.Diagnostics.TotalScenes</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Progress source</dt>
|
||||||
|
<dd data-diagnostic-key="progressSource">@Model.Diagnostics.ProgressSource</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Current backend stage</dt>
|
||||||
|
<dd data-diagnostic-key="currentBackendStage">@Model.Diagnostics.CurrentBackendStage</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Current polling interval</dt>
|
||||||
|
<dd data-diagnostic-key="currentPollingInterval">Initial</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Latest snapshot age</dt>
|
||||||
|
<dd data-diagnostic-key="latestSnapshotAge">Initial</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Snapshot source</dt>
|
<dt>Snapshot source</dt>
|
||||||
<dd data-diagnostic-key="snapshotSource">@Model.Diagnostics.SnapshotSource</dd>
|
<dd data-diagnostic-key="snapshotSource">@Model.Diagnostics.SnapshotSource</dd>
|
||||||
|
|||||||
@ -52,6 +52,9 @@
|
|||||||
|
|
||||||
const dom = {
|
const dom = {
|
||||||
stageLabel: root.querySelector("[data-stage-label]"),
|
stageLabel: root.querySelector("[data-stage-label]"),
|
||||||
|
projectName: root.querySelector("[data-project-name]"),
|
||||||
|
bookTitle: root.querySelector("[data-book-title]"),
|
||||||
|
snapshotMessage: root.querySelector("[data-snapshot-message]"),
|
||||||
stageRail: root.querySelector("[data-analysis-stages]"),
|
stageRail: root.querySelector("[data-analysis-stages]"),
|
||||||
currentScene: root.querySelector("[data-current-scene]"),
|
currentScene: root.querySelector("[data-current-scene]"),
|
||||||
progressPercent: root.querySelector("[data-progress-percent]"),
|
progressPercent: root.querySelector("[data-progress-percent]"),
|
||||||
@ -141,12 +144,13 @@
|
|||||||
state.terminal = !!model.isTerminal;
|
state.terminal = !!model.isTerminal;
|
||||||
state.index = realMode ? Math.max(0, scenes.length - 1) : Math.min(state.index, Math.max(0, scenes.length - 1));
|
state.index = realMode ? Math.max(0, scenes.length - 1) : Math.min(state.index, Math.max(0, scenes.length - 1));
|
||||||
if (model.projectName) {
|
if (model.projectName) {
|
||||||
const project = root.querySelector(".story-exp-context span:first-child");
|
text(dom.projectName, model.projectName);
|
||||||
text(project, model.projectName);
|
|
||||||
}
|
}
|
||||||
if (model.bookTitle) {
|
if (model.bookTitle) {
|
||||||
const book = root.querySelector(".story-exp-context span:nth-child(2)");
|
text(dom.bookTitle, model.bookTitle);
|
||||||
text(book, model.bookTitle);
|
}
|
||||||
|
if (model.snapshotMessage) {
|
||||||
|
text(dom.snapshotMessage, model.snapshotMessage);
|
||||||
}
|
}
|
||||||
transitionToNextState(state.index, operations);
|
transitionToNextState(state.index, operations);
|
||||||
if (state.terminal) stopPolling("terminal model received");
|
if (state.terminal) stopPolling("terminal model received");
|
||||||
@ -156,7 +160,7 @@
|
|||||||
text(dom.stageLabel, scene.stage);
|
text(dom.stageLabel, scene.stage);
|
||||||
text(dom.currentScene, scene.sceneNumber);
|
text(dom.currentScene, scene.sceneNumber);
|
||||||
animateNumber(dom.progressPercent, scene.progressPercent, "%");
|
animateNumber(dom.progressPercent, scene.progressPercent, "%");
|
||||||
animateNumber(dom.elapsedTime, Math.max(1, Math.round(scene.progressPercent / 9)), "m");
|
text(dom.elapsedTime, scene.elapsedTime || "Calculating");
|
||||||
text(dom.estimatedRemaining, scene.estimatedRemaining);
|
text(dom.estimatedRemaining, scene.estimatedRemaining);
|
||||||
text(dom.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`);
|
text(dom.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`);
|
||||||
text(dom.sceneCount, `${scene.scenesAnalysed} of ${scene.sceneTotal}`);
|
text(dom.sceneCount, `${scene.scenesAnalysed} of ${scene.sceneTotal}`);
|
||||||
@ -410,7 +414,7 @@
|
|||||||
}
|
}
|
||||||
card.classList.toggle("is-current", item.id === scene.id);
|
card.classList.toggle("is-current", item.id === scene.id);
|
||||||
card.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
card.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
||||||
text(card.querySelector("span"), `Scene ${item.sceneNumber}`);
|
text(card.querySelector("span"), `${item.chapterLabel || "Chapter"} • Scene ${item.sceneNumber}`);
|
||||||
text(card.querySelector("strong"), compactPhrase(item.title, item.id === scene.id ? 42 : 28));
|
text(card.querySelector("strong"), compactPhrase(item.title, item.id === scene.id ? 42 : 28));
|
||||||
text(card.querySelector("small"), item.timelineEvent);
|
text(card.querySelector("small"), item.timelineEvent);
|
||||||
dom.ribbon.append(card);
|
dom.ribbon.append(card);
|
||||||
@ -1267,10 +1271,20 @@
|
|||||||
if (key === "consecutiveFailedPolls") return state.consecutiveFailedPolls;
|
if (key === "consecutiveFailedPolls") return state.consecutiveFailedPolls;
|
||||||
if (key === "pollingActive") return state.pollTimer ? "active" : "stopped";
|
if (key === "pollingActive") return state.pollTimer ? "active" : "stopped";
|
||||||
if (key === "pollingStoppedReason") return state.pollingStoppedReason || "none";
|
if (key === "pollingStoppedReason") return state.pollingStoppedReason || "none";
|
||||||
|
if (key === "currentPollingInterval") return realMode ? `${document.hidden ? 15000 : 7000}ms` : `${state.intervalMs}ms`;
|
||||||
|
if (key === "latestSnapshotAge") return latestSnapshotAge();
|
||||||
const value = diagnostics[key];
|
const value = diagnostics[key];
|
||||||
return value === null || value === undefined || value === "" ? "none" : value;
|
return value === null || value === undefined || value === "" ? "none" : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function latestSnapshotAge() {
|
||||||
|
if (!model.generatedUtc) return "unknown";
|
||||||
|
const generated = Date.parse(model.generatedUtc);
|
||||||
|
if (Number.isNaN(generated)) return "unknown";
|
||||||
|
const seconds = Math.max(0, Math.round((Date.now() - generated) / 1000));
|
||||||
|
return seconds < 60 ? `${seconds}s` : `${Math.round(seconds / 60)}m`;
|
||||||
|
}
|
||||||
|
|
||||||
function animateNumber(node, nextValue, suffix) {
|
function animateNumber(node, nextValue, suffix) {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const previousValue = Number.parseInt(node.textContent, 10);
|
const previousValue = Number.parseInt(node.textContent, 10);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user