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()
|
||||
{
|
||||
Id = id,
|
||||
ChapterLabel = $"Chapter {Math.Max(1, chapters)}",
|
||||
SceneNumber = number,
|
||||
Title = title,
|
||||
Summary = summary,
|
||||
@ -167,6 +168,7 @@ public static class StoryIntelligenceExperiencePrototypeData
|
||||
ChapterTotal = 5,
|
||||
ScenesAnalysed = number,
|
||||
SceneTotal = sceneTotal,
|
||||
ElapsedTime = $"{Math.Max(1, number * 2)}m",
|
||||
EstimatedRemaining = remaining,
|
||||
PovCharacterId = pov,
|
||||
Location = location,
|
||||
|
||||
@ -135,6 +135,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
Scenes = scenes
|
||||
};
|
||||
|
||||
StabiliseCharacterIllustrations(model);
|
||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||
logger.LogInformation("Built Story Intelligence visualisation snapshot for run {RunId} in {ElapsedMs}ms.", runId, stopwatch.ElapsedMilliseconds);
|
||||
return model;
|
||||
@ -183,10 +184,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
var sourceChapterCount = (await sources.ListChaptersAsync(session.BookID)).Count;
|
||||
var bookChapterTotal = Math.Max(1, Math.Max(sourceChapterCount, runSnapshots.Count));
|
||||
var overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal: false);
|
||||
var completedChapterRuns = runSnapshots.Count(snapshot => IsTerminal(snapshot.Run.Status));
|
||||
var isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status))
|
||||
&& string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase);
|
||||
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal);
|
||||
var completedChapterRuns = runSnapshots.Count(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||
var isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status));
|
||||
var isSuccessfullyComplete = runSnapshots.All(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||
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 nextSceneNumber = 1;
|
||||
foreach (var snapshot in runSnapshots)
|
||||
@ -202,8 +206,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
overallChaptersAnalysed: completedChapterRuns,
|
||||
overallChapterTotal: bookChapterTotal,
|
||||
overallProgressPercent: overallProgressPercent,
|
||||
overallStage: OverallStage(session, activeSnapshot.Run),
|
||||
overallRemaining: EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes),
|
||||
overallStage: overallStage,
|
||||
overallElapsed: overallElapsed,
|
||||
overallRemaining: overallRemaining,
|
||||
continuousSceneNumbering: true,
|
||||
overallTerminal: isTerminal)
|
||||
.ToList();
|
||||
@ -226,12 +231,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||
IsTerminal = isTerminal,
|
||||
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"),
|
||||
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
||||
Scenes = scenes
|
||||
};
|
||||
|
||||
StabiliseCharacterIllustrations(model);
|
||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||
logger.LogInformation("Built Story Intelligence import session visualisation snapshot for session {ImportSessionId} in {ElapsedMs}ms.", importSessionId, stopwatch.ElapsedMilliseconds);
|
||||
return model;
|
||||
@ -249,6 +255,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
int? overallChapterTotal = null,
|
||||
int? overallProgressPercent = null,
|
||||
string? overallStage = null,
|
||||
string? overallElapsed = null,
|
||||
string? overallRemaining = null,
|
||||
bool continuousSceneNumbering = false,
|
||||
bool overallTerminal = false)
|
||||
@ -265,6 +272,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
yield return new StoryIntelligenceExperienceSceneState
|
||||
{
|
||||
Id = $"scene-result-{item.Result.SceneResultID}",
|
||||
ChapterLabel = ChapterLabel(run, item),
|
||||
SceneNumber = sceneNumber,
|
||||
Title = SceneTitle(scene, item.Result, sceneNumber),
|
||||
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)),
|
||||
ScenesAnalysed = overallCompletedScenes ?? completedScenes,
|
||||
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
||||
ElapsedTime = overallElapsed ?? EstimateElapsed(run),
|
||||
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
||||
PovCharacterId = CharacterId(scene.PointOfView?.CharacterName),
|
||||
PovCharacterId = CharacterId(ResolveNarratorName(scene) ?? scene.PointOfView?.CharacterName),
|
||||
Location = BuildLocation(scene),
|
||||
Characters = BuildCharacters(scene),
|
||||
Assets = BuildAssets(scene),
|
||||
@ -297,6 +306,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
=> new()
|
||||
{
|
||||
Id = $"run-{run.StoryIntelligenceRunID}-waiting",
|
||||
ChapterLabel = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter",
|
||||
SceneNumber = 1,
|
||||
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."),
|
||||
@ -308,6 +318,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
ChapterTotal = Math.Max(1, run.SourceChapterCount ?? 1),
|
||||
ScenesAnalysed = run.CompletedScenes ?? 0,
|
||||
SceneTotal = Math.Max(1, run.TotalDetectedScenes ?? 1),
|
||||
ElapsedTime = EstimateElapsed(run),
|
||||
EstimatedRemaining = EstimateRemaining(run, run.CompletedScenes ?? 0, run.TotalDetectedScenes ?? 0),
|
||||
Location = PendingLocation(),
|
||||
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)
|
||||
{
|
||||
var povName = Clean(scene.PointOfView?.CharacterName);
|
||||
var narratorResolution = ResolveNarratorName(scene);
|
||||
var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName);
|
||||
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
||||
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var group in (scene.Characters ?? [])
|
||||
@ -334,6 +346,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
||||
.Take(5))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(narratorResolution) && IsGenericNarratorName(group.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var character = group.OrderByDescending(item => item.Confidence ?? 0).First();
|
||||
var isPov = !string.IsNullOrWhiteSpace(povName) && string.Equals(group.Key, povName, StringComparison.OrdinalIgnoreCase);
|
||||
var libraryCode = CharacterLibraryCode(group.Key, character, scene, reservedCodes);
|
||||
@ -368,6 +385,62 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
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)
|
||||
{
|
||||
var location = (scene.Locations ?? [])
|
||||
@ -646,7 +719,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
var active = snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status));
|
||||
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)
|
||||
@ -658,13 +731,34 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
{
|
||||
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
||||
{
|
||||
var completed = Math.Clamp(snapshot.Run.CompletedScenes ?? snapshot.Completed.Count, 0, snapshot.Run.TotalDetectedScenes.Value);
|
||||
return $"Scene {completed:N0} of {snapshot.Run.TotalDetectedScenes.Value:N0}";
|
||||
var scene = Math.Clamp((snapshot.Run.CompletedScenes ?? snapshot.Completed.Count) + (IsTerminal(snapshot.Run.Status) ? 0 : 1), 1, snapshot.Run.TotalDetectedScenes.Value);
|
||||
return $"Scene {scene:N0} of {snapshot.Run.TotalDetectedScenes.Value:N0}";
|
||||
}
|
||||
|
||||
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(
|
||||
StoryIntelligenceBookPipelineState session,
|
||||
IReadOnlyList<RunSnapshot> snapshots,
|
||||
@ -672,6 +766,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
ParsedSceneResult? current,
|
||||
int parsedScenes,
|
||||
int totalPersistedResults,
|
||||
int totalScenes,
|
||||
int overallProgressPercent)
|
||||
{
|
||||
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
||||
@ -679,6 +774,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
return new()
|
||||
{
|
||||
RequestedMode = "real",
|
||||
CurrentImportId = session.StoryIntelligenceBookPipelineID,
|
||||
RequestedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||
ResolvedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||
@ -686,6 +782,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
OverallBookProgress = overallProgressPercent,
|
||||
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||
CurrentScene = CurrentSceneText(activeSnapshot),
|
||||
CompletedScenes = parsedScenes,
|
||||
TotalScenes = totalScenes,
|
||||
ProgressSource = "ImportSession",
|
||||
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
||||
SnapshotSource = "Real",
|
||||
RunStatus = SessionStatus(session, snapshots),
|
||||
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)),
|
||||
CurrentChapter = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||
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.##}"),
|
||||
CompletedScenes = run.CompletedScenes ?? completed.Count,
|
||||
TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count),
|
||||
ProgressSource = "ChapterRun",
|
||||
CurrentBackendStage = BackendStageDisplay(run),
|
||||
SnapshotSource = "Real",
|
||||
RunStatus = run.Status,
|
||||
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
||||
@ -750,9 +854,36 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
_ => 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)
|
||||
=> 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)
|
||||
=> 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;
|
||||
|
||||
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)
|
||||
=> 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)
|
||||
{
|
||||
if (IsTerminal(run.Status) && run.Status is not StoryIntelligenceRunStatuses.Failed)
|
||||
@ -817,7 +983,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
|
||||
private static decimal ChapterProgress(RunSnapshot snapshot)
|
||||
{
|
||||
if (IsTerminal(snapshot.Run.Status))
|
||||
if (IsSuccessfulTerminal(snapshot.Run.Status))
|
||||
{
|
||||
return 1m;
|
||||
}
|
||||
@ -853,6 +1019,19 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
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)
|
||||
{
|
||||
var duration = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds));
|
||||
|
||||
@ -20,6 +20,7 @@ public sealed class StoryIntelligenceExperiencePrototypeViewModel
|
||||
public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
{
|
||||
public string RequestedMode { get; init; } = string.Empty;
|
||||
public int? CurrentImportId { get; init; }
|
||||
public int? RequestedImportSessionId { get; init; }
|
||||
public int? ResolvedImportSessionId { get; init; }
|
||||
public int? ActiveChapterRunId { get; init; }
|
||||
@ -27,6 +28,10 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
public int OverallBookProgress { get; init; }
|
||||
public string CurrentChapter { 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 RunStatus { get; init; } = string.Empty;
|
||||
public int PersistedCompletedSceneCount { get; init; }
|
||||
@ -54,6 +59,7 @@ public sealed class StoryIntelligenceExperienceRunOption
|
||||
public sealed class StoryIntelligenceExperienceSceneState
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string ChapterLabel { get; init; } = string.Empty;
|
||||
public int SceneNumber { get; init; }
|
||||
public string Title { 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 ScenesAnalysed { get; init; }
|
||||
public int SceneTotal { get; init; }
|
||||
public string ElapsedTime { get; init; } = string.Empty;
|
||||
public string EstimatedRemaining { get; init; } = string.Empty;
|
||||
public string PovCharacterId { get; init; } = string.Empty;
|
||||
public StoryIntelligenceExperienceLocationState Location { get; init; } = new();
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
</div>
|
||||
<ol class="story-exp-pipeline" data-analysis-stages aria-label="Analysis stages"></ol>
|
||||
<div class="story-exp-context">
|
||||
<span>@statusLabel</span>
|
||||
<span>@Model.ProjectName</span>
|
||||
<span>@Model.BookTitle</span>
|
||||
<span data-experience-status>@statusLabel</span>
|
||||
<span data-project-name>@Model.ProjectName</span>
|
||||
<span data-book-title>@Model.BookTitle</span>
|
||||
<span data-stage-label>Preparing</span>
|
||||
<span>Scene <strong data-current-scene>1</strong></span>
|
||||
</div>
|
||||
@ -68,7 +68,7 @@
|
||||
|
||||
@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
|
||||
</aside>
|
||||
}
|
||||
@ -185,6 +185,10 @@
|
||||
<details class="story-exp-diagnostics" data-diagnostics>
|
||||
<summary>Diagnostics</summary>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Current Import ID</dt>
|
||||
<dd data-diagnostic-key="currentImportId">@Model.Diagnostics.CurrentImportId</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Requested mode</dt>
|
||||
<dd data-diagnostic-key="requestedMode">@Model.Diagnostics.RequestedMode</dd>
|
||||
@ -217,6 +221,30 @@
|
||||
<dt>Current scene</dt>
|
||||
<dd data-diagnostic-key="currentScene">@Model.Diagnostics.CurrentScene</dd>
|
||||
</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>
|
||||
<dt>Snapshot source</dt>
|
||||
<dd data-diagnostic-key="snapshotSource">@Model.Diagnostics.SnapshotSource</dd>
|
||||
|
||||
@ -52,6 +52,9 @@
|
||||
|
||||
const dom = {
|
||||
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]"),
|
||||
currentScene: root.querySelector("[data-current-scene]"),
|
||||
progressPercent: root.querySelector("[data-progress-percent]"),
|
||||
@ -141,12 +144,13 @@
|
||||
state.terminal = !!model.isTerminal;
|
||||
state.index = realMode ? Math.max(0, scenes.length - 1) : Math.min(state.index, Math.max(0, scenes.length - 1));
|
||||
if (model.projectName) {
|
||||
const project = root.querySelector(".story-exp-context span:first-child");
|
||||
text(project, model.projectName);
|
||||
text(dom.projectName, model.projectName);
|
||||
}
|
||||
if (model.bookTitle) {
|
||||
const book = root.querySelector(".story-exp-context span:nth-child(2)");
|
||||
text(book, model.bookTitle);
|
||||
text(dom.bookTitle, model.bookTitle);
|
||||
}
|
||||
if (model.snapshotMessage) {
|
||||
text(dom.snapshotMessage, model.snapshotMessage);
|
||||
}
|
||||
transitionToNextState(state.index, operations);
|
||||
if (state.terminal) stopPolling("terminal model received");
|
||||
@ -156,7 +160,7 @@
|
||||
text(dom.stageLabel, scene.stage);
|
||||
text(dom.currentScene, scene.sceneNumber);
|
||||
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.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`);
|
||||
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-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("small"), item.timelineEvent);
|
||||
dom.ribbon.append(card);
|
||||
@ -1267,10 +1271,20 @@
|
||||
if (key === "consecutiveFailedPolls") return state.consecutiveFailedPolls;
|
||||
if (key === "pollingActive") return state.pollTimer ? "active" : "stopped";
|
||||
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];
|
||||
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) {
|
||||
if (!node) return;
|
||||
const previousValue = Number.parseInt(node.textContent, 10);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user