Phase 21P.1 - Synchronisation Hotfix
This commit is contained in:
parent
63f427066d
commit
fd37df65c6
@ -53,6 +53,7 @@ var tests = new (string Name, Action Test)[]
|
||||
("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage),
|
||||
("Illustration assignment repair enforces explicit evidence groups and strict subtypes", IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes),
|
||||
("Phase 21O preserves adult defaults and real-mode continuity", Phase21OPreservesAdultDefaultsAndRealModeContinuity),
|
||||
("Phase 21P uses one authoritative current visualisation scene", Phase21PUsesOneAuthoritativeCurrentVisualisationScene),
|
||||
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
||||
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
|
||||
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
|
||||
@ -635,6 +636,23 @@ static void Phase21OPreservesAdultDefaultsAndRealModeContinuity()
|
||||
Assert(view.Contains("Model.Mode == \"simulation\"", StringComparison.Ordinal), "Prototype controls should be gated to simulation mode.");
|
||||
}
|
||||
|
||||
static void Phase21PUsesOneAuthoritativeCurrentVisualisationScene()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..");
|
||||
var viewModel = File.ReadAllText(Path.Combine(root, "PlotLine", "ViewModels", "StoryIntelligenceExperiencePrototypeViewModels.cs"));
|
||||
var snapshotService = File.ReadAllText(Path.Combine(root, "PlotLine", "Services", "StoryIntelligenceVisualisationSnapshotService.cs"));
|
||||
var browser = File.ReadAllText(Path.Combine(root, "PlotLine", "wwwroot", "js", "story-intelligence-experience-prototype.js"));
|
||||
var view = File.ReadAllText(Path.Combine(root, "PlotLine", "Views", "Development", "StoryIntelligenceExperience.cshtml"));
|
||||
|
||||
Assert(viewModel.Contains("public bool IsCurrent", StringComparison.Ordinal), "Scene snapshots must expose an authoritative current-scene marker.");
|
||||
Assert(snapshotService.Contains("IsCurrent = isCurrent", StringComparison.Ordinal), "Snapshot builder must mark the selected current scene.");
|
||||
Assert(snapshotService.Contains("followingCount: 0", StringComparison.Ordinal), "Real import snapshots must not include following scenes that can pull the browser ahead.");
|
||||
Assert(snapshotService.Contains("Story Intelligence snapshot synchronisation", StringComparison.Ordinal), "Snapshot builder must log synchronisation evidence.");
|
||||
Assert(browser.Contains("scene?.isCurrent", StringComparison.Ordinal), "Browser must choose the current scene from the authoritative marker.");
|
||||
Assert(browser.Contains("Story Intelligence browser synchronisation", StringComparison.Ordinal), "Browser must log synchronisation evidence.");
|
||||
Assert(view.Contains("Scenes = Model.Scenes", StringComparison.Ordinal), "Initial real page payload must include the same scene collection as polled snapshots.");
|
||||
}
|
||||
|
||||
static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false)
|
||||
=> new(
|
||||
ageBand,
|
||||
|
||||
@ -8,7 +8,8 @@ namespace PlotLine.Controllers;
|
||||
[Route("api/story-intelligence")]
|
||||
public sealed class StoryIntelligenceVisualisationController(
|
||||
ICurrentUserService currentUser,
|
||||
IStoryIntelligenceVisualisationSnapshotService snapshots) : ControllerBase
|
||||
IStoryIntelligenceVisualisationSnapshotService snapshots,
|
||||
ILogger<StoryIntelligenceVisualisationController> logger) : ControllerBase
|
||||
{
|
||||
[HttpGet("runs/{runId:int}/visualisation-snapshot")]
|
||||
public async Task<IActionResult> GetSnapshot(int runId)
|
||||
@ -19,6 +20,18 @@ public sealed class StoryIntelligenceVisualisationController(
|
||||
}
|
||||
|
||||
var snapshot = await snapshots.BuildSnapshotAsync(runId, userId);
|
||||
if (snapshot is not null)
|
||||
{
|
||||
var current = snapshot.Scenes.FirstOrDefault(scene => scene.IsCurrent) ?? snapshot.Scenes.LastOrDefault();
|
||||
logger.LogInformation(
|
||||
"Story Intelligence browser endpoint synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {CurrentChapter}; current scene {CurrentScene}; scene result {SceneResultId}; snapshot version Endpoint; change token {ChangeToken}.",
|
||||
snapshot.ImportSessionId,
|
||||
snapshot.RunId,
|
||||
current?.ChapterLabel,
|
||||
current?.SceneNumber,
|
||||
snapshot.Diagnostics.SelectedCurrentSceneResultId,
|
||||
snapshot.ChangeToken);
|
||||
}
|
||||
return snapshot is null ? NotFound() : Ok(snapshot);
|
||||
}
|
||||
|
||||
@ -31,6 +44,18 @@ public sealed class StoryIntelligenceVisualisationController(
|
||||
}
|
||||
|
||||
var snapshot = await snapshots.BuildImportSessionSnapshotAsync(importSessionId, userId);
|
||||
if (snapshot is not null)
|
||||
{
|
||||
var current = snapshot.Scenes.FirstOrDefault(scene => scene.IsCurrent) ?? snapshot.Scenes.LastOrDefault();
|
||||
logger.LogInformation(
|
||||
"Story Intelligence browser endpoint synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {CurrentChapter}; current scene {CurrentScene}; scene result {SceneResultId}; snapshot version Endpoint; change token {ChangeToken}.",
|
||||
snapshot.ImportSessionId,
|
||||
snapshot.RunId,
|
||||
current?.ChapterLabel,
|
||||
current?.SceneNumber,
|
||||
snapshot.Diagnostics.SelectedCurrentSceneResultId,
|
||||
snapshot.ChangeToken);
|
||||
}
|
||||
return snapshot is null ? NotFound() : Ok(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,6 +253,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
SourceWordCount = run.SourceWordCount,
|
||||
TotalTokens = run.TotalTokens,
|
||||
TotalDurationMs = run.TotalDurationMs,
|
||||
UpdatedUtc = run.UpdatedUtc,
|
||||
EstimatedCostUSD = run.EstimatedCostUSD,
|
||||
EstimatedCostGBP = run.EstimatedCostGBP,
|
||||
FailureStage = run.FailureStage,
|
||||
|
||||
@ -13,6 +13,7 @@ public interface IPersistedStoryIntelligenceRunner
|
||||
|
||||
public sealed class PersistedStoryIntelligenceRunner(
|
||||
IStoryIntelligenceResultRepository repository,
|
||||
IStoryIntelligencePipelineRepository pipelines,
|
||||
IStoryPromptRepository prompts,
|
||||
IStoryPromptVersionService versions,
|
||||
IStoryPromptBuilder scenePromptBuilder,
|
||||
@ -304,7 +305,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
|
||||
hasWarnings = hasWarnings || validation.Warnings.Count > 0;
|
||||
|
||||
await repository.SaveSceneResultAsync(
|
||||
var sceneResultId = await repository.SaveSceneResultAsync(
|
||||
run.StoryIntelligenceRunID,
|
||||
chapterResultId,
|
||||
new StoryIntelligenceSceneResultSaveRequest
|
||||
@ -329,6 +330,14 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
TotalTokens = SumTokens(sceneAttempt.InputTokens, sceneAttempt.OutputTokens),
|
||||
DurationMs = Convert.ToInt64(sceneAttempt.Duration.TotalMilliseconds)
|
||||
});
|
||||
var importSessionId = run.BookID.HasValue ? (await pipelines.GetByBookAsync(run.BookID.Value))?.StoryIntelligenceBookPipelineID : null;
|
||||
logger.LogInformation(
|
||||
"Story Intelligence importer synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {ChapterNumber}; current scene {SceneNumber}; scene result {SceneResultId}; snapshot version ImportPipeline; change token n/a; status analysed.",
|
||||
importSessionId,
|
||||
run.StoryIntelligenceRunID,
|
||||
run.ChapterNumber,
|
||||
block.TemporarySceneNumber,
|
||||
sceneResultId);
|
||||
|
||||
completedScenes++;
|
||||
await PublishAsync(
|
||||
@ -721,7 +730,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
string message,
|
||||
AiJsonParseException? parseException = null)
|
||||
{
|
||||
await repository.SaveSceneResultAsync(
|
||||
var sceneResultId = await repository.SaveSceneResultAsync(
|
||||
run.StoryIntelligenceRunID,
|
||||
chapterResultId,
|
||||
new StoryIntelligenceSceneResultSaveRequest
|
||||
@ -752,6 +761,14 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
TotalTokens = SumTokens(parseException?.InputTokens, parseException?.OutputTokens),
|
||||
DurationMs = parseException is null ? null : Convert.ToInt64(parseException.Duration.TotalMilliseconds)
|
||||
});
|
||||
var importSessionId = run.BookID.HasValue ? (await pipelines.GetByBookAsync(run.BookID.Value))?.StoryIntelligenceBookPipelineID : null;
|
||||
logger.LogInformation(
|
||||
"Story Intelligence importer synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {ChapterNumber}; current scene {SceneNumber}; scene result {SceneResultId}; snapshot version ImportPipeline; change token n/a; status failed.",
|
||||
importSessionId,
|
||||
run.StoryIntelligenceRunID,
|
||||
run.ChapterNumber,
|
||||
block.TemporarySceneNumber,
|
||||
sceneResultId);
|
||||
}
|
||||
|
||||
private StoryIntelligenceStageModels ResolveStageModels(StoryIntelligenceQueuedRun run)
|
||||
|
||||
@ -188,7 +188,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
}
|
||||
|
||||
var activeSnapshot = PickActiveRunSnapshot(runSnapshots) ?? runSnapshots.Last();
|
||||
var current = activeSnapshot.Current ?? runSnapshots.LastOrDefault(snapshot => snapshot.Completed.Count > 0)?.Current;
|
||||
var current = PickAuthoritativeCurrentScene(runSnapshots);
|
||||
var currentSnapshot = OwnerOf(runSnapshots, current) ?? activeSnapshot;
|
||||
var totalPersistedResults = runSnapshots.Sum(snapshot => snapshot.Results.Count);
|
||||
var parsedScenes = runSnapshots.Sum(snapshot => snapshot.Completed.Count);
|
||||
var totalScenes = Math.Max(
|
||||
@ -215,8 +216,12 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||
var storyMemory = new StoryMemory();
|
||||
var allCompleted = runSnapshots.SelectMany(snapshot => snapshot.Completed).ToList();
|
||||
var visibleSceneIds = VisibleSceneResultIds(allCompleted, current, SceneWindowPrevious, SceneWindowFollowing);
|
||||
var allCompleted = runSnapshots
|
||||
.SelectMany(snapshot => snapshot.Completed)
|
||||
.OrderBy(item => item.Result.CreatedUtc)
|
||||
.ThenBy(item => item.Result.SceneResultID)
|
||||
.ToList();
|
||||
var visibleSceneIds = VisibleSceneResultIds(allCompleted, current, SceneWindowPrevious, followingCount: 0);
|
||||
foreach (var snapshot in runSnapshots)
|
||||
{
|
||||
var runScenes = BuildSceneStates(
|
||||
@ -248,7 +253,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
{
|
||||
Mode = "real",
|
||||
ImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||
RunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||
RunId = currentSnapshot.Run.StoryIntelligenceRunID,
|
||||
BookId = session.BookID,
|
||||
RunStatus = SessionStatus(session, runSnapshots),
|
||||
GeneratedUtc = DateTime.UtcNow,
|
||||
@ -258,7 +263,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
Diagnostics = BuildSessionDiagnostics(
|
||||
session,
|
||||
runSnapshots,
|
||||
activeSnapshot,
|
||||
currentSnapshot,
|
||||
current,
|
||||
parsedScenes,
|
||||
totalPersistedResults,
|
||||
@ -280,6 +285,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
await illustrationMatcher.ApplyCharacterIllustrationsAsync(session.ProjectID, model);
|
||||
ApplyIllustrationDiagnostics(model);
|
||||
StripLiveDiagnostics(model);
|
||||
LogSnapshotSynchronisation(importSessionId, currentSnapshot, current, scenes, model.ChangeToken);
|
||||
LogPayload(model, $"import session {importSessionId}", stopwatch.ElapsedMilliseconds);
|
||||
return model;
|
||||
}
|
||||
@ -322,6 +328,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
yield return new StoryIntelligenceExperienceSceneState
|
||||
{
|
||||
Id = $"scene-result-{item.Result.SceneResultID}",
|
||||
IsCurrent = isCurrent,
|
||||
ChapterLabel = ChapterLabel(run, item),
|
||||
SceneNumber = sceneNumber,
|
||||
Title = SceneTitle(scene, item.Result, sceneNumber),
|
||||
@ -382,9 +389,24 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
return null;
|
||||
}
|
||||
|
||||
return completed.Last();
|
||||
return completed
|
||||
.OrderBy(item => item.Result.CreatedUtc)
|
||||
.ThenBy(item => item.Result.SceneResultID)
|
||||
.Last();
|
||||
}
|
||||
|
||||
private static ParsedSceneResult? PickAuthoritativeCurrentScene(IReadOnlyList<RunSnapshot> snapshots)
|
||||
=> snapshots
|
||||
.SelectMany(snapshot => snapshot.Completed)
|
||||
.OrderBy(item => item.Result.CreatedUtc)
|
||||
.ThenBy(item => item.Result.SceneResultID)
|
||||
.LastOrDefault();
|
||||
|
||||
private static RunSnapshot? OwnerOf(IReadOnlyList<RunSnapshot> snapshots, ParsedSceneResult? scene)
|
||||
=> scene is null
|
||||
? null
|
||||
: snapshots.FirstOrDefault(snapshot => snapshot.Run.StoryIntelligenceRunID == scene.Result.StoryIntelligenceRunID);
|
||||
|
||||
private static IReadOnlyList<StoryIntelligenceExperienceCharacterState> BuildCharacters(SceneIntelligenceScene scene, StoryMemory? storyMemory = null)
|
||||
{
|
||||
var narratorResolution = storyMemory?.ResolvePointOfView(scene) ?? ResolveNarratorName(scene);
|
||||
@ -817,8 +839,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
}
|
||||
|
||||
private static RunSnapshot? PickActiveRunSnapshot(IReadOnlyList<RunSnapshot> snapshots)
|
||||
=> snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status))
|
||||
?? snapshots.LastOrDefault(snapshot => snapshot.Completed.Count > 0)
|
||||
=> snapshots
|
||||
.Where(snapshot => string.Equals(snapshot.Run.Status, StoryIntelligenceRunStatuses.Running, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(snapshot => snapshot.Run.UpdatedUtc)
|
||||
.ThenByDescending(snapshot => snapshot.Run.StoryIntelligenceRunID)
|
||||
.FirstOrDefault()
|
||||
?? snapshots
|
||||
.Where(snapshot => !IsTerminal(snapshot.Run.Status))
|
||||
.OrderBy(snapshot => snapshot.Run.ChapterNumber ?? decimal.MaxValue)
|
||||
.ThenBy(snapshot => snapshot.Run.StoryIntelligenceRunID)
|
||||
.FirstOrDefault()
|
||||
?? snapshots
|
||||
.Where(snapshot => snapshot.Completed.Count > 0)
|
||||
.OrderByDescending(snapshot => snapshot.Run.UpdatedUtc)
|
||||
.ThenByDescending(snapshot => snapshot.Run.StoryIntelligenceRunID)
|
||||
.FirstOrDefault()
|
||||
?? snapshots.LastOrDefault();
|
||||
|
||||
private static string OverallStage(StoryIntelligenceBookPipelineState session, StoryIntelligenceSavedRun activeRun)
|
||||
@ -1290,7 +1325,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
{
|
||||
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
||||
{
|
||||
var scene = Math.Clamp((snapshot.Run.CompletedScenes ?? snapshot.Completed.Count) + (IsTerminal(snapshot.Run.Status) ? 0 : 1), 1, snapshot.Run.TotalDetectedScenes.Value);
|
||||
var scene = Math.Clamp(snapshot.Run.CompletedScenes ?? snapshot.Completed.Count, 0, snapshot.Run.TotalDetectedScenes.Value);
|
||||
return $"Scene {scene:N0} of {snapshot.Run.TotalDetectedScenes.Value:N0}";
|
||||
}
|
||||
|
||||
@ -1613,6 +1648,26 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
largestString.Length);
|
||||
}
|
||||
|
||||
private void LogSnapshotSynchronisation(
|
||||
int importSessionId,
|
||||
RunSnapshot activeSnapshot,
|
||||
ParsedSceneResult? current,
|
||||
IReadOnlyList<StoryIntelligenceExperienceSceneState> scenes,
|
||||
string changeToken)
|
||||
{
|
||||
var displayed = scenes.FirstOrDefault(scene => scene.IsCurrent) ?? scenes.LastOrDefault();
|
||||
logger.LogInformation(
|
||||
"Story Intelligence snapshot synchronisation: import session {ImportSessionId}; active run {RunId}; current chapter {ChapterNumber}; current scene {SceneNumber}; selected scene result {SceneResultId}; displayed scene id {DisplayedSceneId}; change token {ChangeToken}; payload scenes {SceneIds}.",
|
||||
importSessionId,
|
||||
activeSnapshot.Run.StoryIntelligenceRunID,
|
||||
activeSnapshot.Run.ChapterNumber,
|
||||
current is null ? null : SceneSortNumber(current),
|
||||
current?.Result.SceneResultID,
|
||||
displayed?.Id,
|
||||
changeToken,
|
||||
string.Join(",", scenes.Select(scene => scene.Id)));
|
||||
}
|
||||
|
||||
private static (string Path, int Length) LargestStringProperty(StoryIntelligenceExperiencePrototypeViewModel model)
|
||||
{
|
||||
var largest = ("model", 0);
|
||||
|
||||
@ -531,6 +531,7 @@ public sealed class StoryIntelligenceOnboardingChapterViewModel
|
||||
public int? SourceWordCount { get; init; }
|
||||
public int? TotalTokens { get; init; }
|
||||
public long? TotalDurationMs { get; init; }
|
||||
public DateTime? UpdatedUtc { get; init; }
|
||||
public decimal? EstimatedCostUSD { get; init; }
|
||||
public decimal? EstimatedCostGBP { get; init; }
|
||||
public string? FailureStage { get; init; }
|
||||
|
||||
@ -80,6 +80,7 @@ public sealed class StoryIntelligenceExperienceRunOption
|
||||
public sealed class StoryIntelligenceExperienceSceneState
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public bool IsCurrent { get; init; }
|
||||
public string ChapterLabel { get; init; } = string.Empty;
|
||||
public int SceneNumber { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
@ -31,7 +31,9 @@
|
||||
ProjectName = Model.ProjectName,
|
||||
BookTitle = Model.BookTitle,
|
||||
SnapshotMessage = Model.SnapshotMessage,
|
||||
Diagnostics = Model.Diagnostics
|
||||
Diagnostics = Model.Diagnostics,
|
||||
Scenes = Model.Scenes,
|
||||
ChangeToken = Model.ChangeToken
|
||||
}
|
||||
: Model;
|
||||
}
|
||||
@ -54,8 +56,8 @@
|
||||
data-book-id="@Model.BookId"
|
||||
data-import-session-id="@Model.ImportSessionId"
|
||||
data-snapshot-url="@(Model.ImportSessionId.HasValue ? $"/api/story-intelligence/import-sessions/{Model.ImportSessionId.Value}/visualisation-snapshot" : Model.RunId.HasValue ? $"/api/story-intelligence/runs/{Model.RunId.Value}/visualisation-snapshot" : string.Empty)"
|
||||
data-change-token="@(Model.Mode == "real" ? string.Empty : Model.ChangeToken)"
|
||||
data-terminal="@((Model.Mode != "real" && Model.IsTerminal).ToString().ToLowerInvariant())">
|
||||
data-change-token="@Model.ChangeToken"
|
||||
data-terminal="@Model.IsTerminal.ToString().ToLowerInvariant()">
|
||||
<header class="story-exp-header" aria-label="Story Intelligence status">
|
||||
<div class="story-exp-brand">
|
||||
<strong>PlotDirector</strong>
|
||||
|
||||
@ -113,6 +113,10 @@
|
||||
<div data-story-run-id="@chapter.RunID"
|
||||
data-story-chapter-title="@chapter.ChapterTitle"
|
||||
data-story-raw-status="@chapter.Status"
|
||||
data-story-chapter-number="@chapter.ChapterNumber"
|
||||
data-story-current-stage="@(chapter.CurrentStage ?? string.Empty)"
|
||||
data-story-current-scene-number="@(chapter.CompletedScenes ?? 0)"
|
||||
data-story-updated-utc="@(chapter.UpdatedUtc?.ToUniversalTime().ToString("O") ?? string.Empty)"
|
||||
data-story-source-word-count="@(chapter.SourceWordCount ?? 0)"
|
||||
data-story-duration-ms="@(chapter.TotalDurationMs ?? 0)">
|
||||
<span data-story-run-status>@FriendlyStatus(chapter.Status)</span>
|
||||
|
||||
@ -133,7 +133,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
state.index = realMode ? Math.max(0, scenes.length - 1) : Math.min(state.index, Math.max(0, scenes.length - 1));
|
||||
state.index = realMode ? authoritativeSceneIndex(scenes) : Math.min(state.index, Math.max(0, scenes.length - 1));
|
||||
logSnapshotSynchronisation("snapshot-applied", model, loadSceneState(state.index));
|
||||
if (model.projectName) {
|
||||
text(dom.projectName, model.projectName);
|
||||
}
|
||||
@ -1219,7 +1220,25 @@
|
||||
|
||||
function activeScene(snapshot) {
|
||||
const snapshotScenes = snapshot?.scenes || [];
|
||||
return snapshotScenes.length ? snapshotScenes[snapshotScenes.length - 1] : null;
|
||||
return snapshotScenes.find((scene) => scene?.isCurrent) || (snapshotScenes.length ? snapshotScenes[snapshotScenes.length - 1] : null);
|
||||
}
|
||||
|
||||
function authoritativeSceneIndex(snapshotScenes) {
|
||||
const index = (snapshotScenes || []).findIndex((scene) => scene?.isCurrent);
|
||||
return index >= 0 ? index : Math.max(0, (snapshotScenes || []).length - 1);
|
||||
}
|
||||
|
||||
function logSnapshotSynchronisation(eventName, snapshot, displayedScene) {
|
||||
if (!realMode || !window.console?.info) return;
|
||||
const authoritative = activeScene(snapshot);
|
||||
console.info("Story Intelligence browser synchronisation", {
|
||||
event: eventName,
|
||||
changeToken: snapshot?.changeToken || "",
|
||||
snapshotSceneId: authoritative?.id || "",
|
||||
displayedSceneId: displayedScene?.id || "",
|
||||
snapshotSceneNumber: authoritative?.sceneNumber || "",
|
||||
displayedSceneNumber: displayedScene?.sceneNumber || ""
|
||||
});
|
||||
}
|
||||
|
||||
function keyedById(items) {
|
||||
@ -1414,7 +1433,7 @@
|
||||
}
|
||||
|
||||
if (scenes.length) {
|
||||
transitionToNextState(realMode ? Math.max(0, scenes.length - 1) : 0);
|
||||
transitionToNextState(realMode ? authoritativeSceneIndex(scenes) : 0);
|
||||
startTimer();
|
||||
} else {
|
||||
updateDiagnostics();
|
||||
|
||||
@ -243,6 +243,73 @@
|
||||
}
|
||||
};
|
||||
|
||||
const parseNumber = (value) => Number.parseFloat(String(value || "").replace(/,/g, "")) || 0;
|
||||
|
||||
const parseTime = (value) => {
|
||||
const parsed = Date.parse(value || "");
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
const isActiveStatus = (status) => status === "Pending" || status === "Running";
|
||||
|
||||
const authoritativeChapter = (chapters) => {
|
||||
const active = chapters
|
||||
.filter((chapter) => isActiveStatus(chapter.dataset.storyRawStatus || "")
|
||||
&& parseNumber(chapter.dataset.storyCurrentSceneNumber) > 0)
|
||||
.sort((left, right) => {
|
||||
const byUpdated = parseTime(right.dataset.storyUpdatedUtc) - parseTime(left.dataset.storyUpdatedUtc);
|
||||
if (byUpdated !== 0) return byUpdated;
|
||||
return parseNumber(right.dataset.storyChapterNumber) - parseNumber(left.dataset.storyChapterNumber);
|
||||
});
|
||||
if (active.length > 0) return active[0];
|
||||
|
||||
const completed = chapters
|
||||
.filter((chapter) => {
|
||||
const status = chapter.dataset.storyRawStatus || "";
|
||||
return status === "Completed" || status === "CompletedWithWarnings";
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const byUpdated = parseTime(right.dataset.storyUpdatedUtc) - parseTime(left.dataset.storyUpdatedUtc);
|
||||
if (byUpdated !== 0) return byUpdated;
|
||||
return parseNumber(right.dataset.storyChapterNumber) - parseNumber(left.dataset.storyChapterNumber);
|
||||
});
|
||||
return completed[0] || chapters[0] || null;
|
||||
};
|
||||
|
||||
const applyAuthoritativeCursor = (root) => {
|
||||
const chapters = [...root.querySelectorAll("[data-story-run-id]")];
|
||||
const chapter = authoritativeChapter(chapters);
|
||||
if (!chapter) return;
|
||||
|
||||
const status = chapter.dataset.storyRawStatus || "Pending";
|
||||
const stage = chapter.dataset.storyCurrentStage || status;
|
||||
const currentSceneNumber = chapter.dataset.storyCurrentSceneNumber || "";
|
||||
const total = chapter.querySelector("[data-story-run-total]")?.textContent || "";
|
||||
const completed = chapter.querySelector("[data-story-run-completed]")?.textContent || "";
|
||||
const message = chapter.querySelector("[data-story-run-message]")?.textContent || "";
|
||||
|
||||
root.querySelectorAll("[data-story-current-chapter]").forEach((node) => {
|
||||
node.textContent = chapter.dataset.storyChapterTitle || "Current chapter";
|
||||
});
|
||||
root.querySelectorAll("[data-story-current-stage]").forEach((node) => {
|
||||
node.textContent = friendlyStage(stage, status);
|
||||
});
|
||||
root.querySelectorAll("[data-story-current-scene]").forEach((node) => {
|
||||
node.textContent = currentSceneNumber ? numberText(currentSceneNumber) : "Waiting";
|
||||
});
|
||||
const totalNumber = Number.parseInt(total.replace(/,/g, "") || "0", 10) || 0;
|
||||
const completedNumber = Number.parseInt(completed.replace(/,/g, "") || "0", 10) || 0;
|
||||
const currentSceneDisplay = currentSceneNumber ? Number.parseInt(currentSceneNumber, 10) || completedNumber : completedNumber;
|
||||
root.querySelectorAll("[data-story-chapter-progress-text]").forEach((node) => {
|
||||
node.textContent = totalNumber > 0
|
||||
? `Scene ${numberText(Math.min(Math.max(currentSceneDisplay, 0), totalNumber))} of ${numberText(totalNumber)}`
|
||||
: "Finding scenes...";
|
||||
});
|
||||
root.querySelectorAll("[data-story-current-message]").forEach((node) => {
|
||||
node.textContent = message;
|
||||
});
|
||||
};
|
||||
|
||||
const recalculateOnboardingTotals = (root) => {
|
||||
const hadActiveRuns = root.dataset.storyIntelligenceHasActiveRuns === "true";
|
||||
const chapters = [...root.querySelectorAll("[data-story-run-id]")];
|
||||
@ -306,6 +373,7 @@
|
||||
root.querySelectorAll("[data-story-remaining]").forEach((node) => {
|
||||
node.textContent = calculateRemaining(chapters, active);
|
||||
});
|
||||
applyAuthoritativeCursor(root);
|
||||
|
||||
if (hadActiveRuns && !active && root.dataset.reloadScheduled !== "true") {
|
||||
root.dataset.reloadScheduled = "true";
|
||||
@ -335,6 +403,7 @@
|
||||
const totalDurationMs = field(state, "totalDurationMs", "TotalDurationMs");
|
||||
const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec";
|
||||
const currentSceneNumber = field(state, "currentSceneNumber", "CurrentSceneNumber");
|
||||
const updatedUtc = field(state, "updatedUtc", "UpdatedUtc");
|
||||
const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber);
|
||||
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
||||
const discoveries = field(state, "recentDiscoveries", "RecentDiscoveries") || [];
|
||||
@ -344,6 +413,11 @@
|
||||
node.textContent = statusLabel(state);
|
||||
});
|
||||
chapter.dataset.storyRawStatus = status;
|
||||
chapter.dataset.storyCurrentStage = stage;
|
||||
chapter.dataset.storyCurrentSceneNumber = completed || "0";
|
||||
if (updatedUtc) {
|
||||
chapter.dataset.storyUpdatedUtc = updatedUtc;
|
||||
}
|
||||
if (totalDurationMs !== null) {
|
||||
chapter.dataset.storyDurationMs = String(totalDurationMs);
|
||||
}
|
||||
@ -392,30 +466,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-story-current-chapter]").forEach((node) => {
|
||||
node.textContent = chapter.dataset.storyChapterTitle || "Current chapter";
|
||||
});
|
||||
root.querySelectorAll("[data-story-current-stage]").forEach((node) => {
|
||||
node.textContent = friendlyStage(stage, status);
|
||||
});
|
||||
root.querySelectorAll("[data-story-current-scene]").forEach((node) => {
|
||||
node.textContent = currentSceneNumber ? numberText(currentSceneNumber) : "Waiting";
|
||||
});
|
||||
const totalNumber = Number.parseInt(total || "0", 10) || 0;
|
||||
const completedNumber = Number.parseInt(completed || "0", 10) || 0;
|
||||
const currentSceneDisplay = currentSceneNumber ? Number.parseInt(currentSceneNumber, 10) || completedNumber : completedNumber;
|
||||
root.querySelectorAll("[data-story-chapter-progress-text]").forEach((node) => {
|
||||
node.textContent = totalNumber > 0
|
||||
? `Scene ${numberText(Math.min(Math.max(currentSceneDisplay, 0), totalNumber))} of ${numberText(totalNumber)}`
|
||||
: "Finding scenes...";
|
||||
});
|
||||
root.querySelectorAll("[data-story-chapter-progress-bar]").forEach((node) => {
|
||||
const percent = totalNumber > 0 ? Math.floor((Math.min(completedNumber, totalNumber) * 100) / totalNumber) : 0;
|
||||
node.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
||||
});
|
||||
root.querySelectorAll("[data-story-current-message]").forEach((node) => {
|
||||
node.textContent = displayMessage;
|
||||
});
|
||||
root.querySelectorAll("[data-story-elapsed]").forEach((node) => {
|
||||
node.textContent = elapsed;
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user