Phase 21H.1 - Fix Active Import Session Selection
This commit is contained in:
parent
8985efafbc
commit
2b7a4b599d
@ -21,43 +21,81 @@ public sealed class DevelopmentController(
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(mode, "simulation", StringComparison.OrdinalIgnoreCase) && importSessionId.HasValue && currentUser.UserId is int userId)
|
var requestedMode = string.IsNullOrWhiteSpace(mode) ? "real" : mode.Trim();
|
||||||
|
var simulationRequested = string.Equals(requestedMode, "simulation", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!simulationRequested && importSessionId.HasValue && currentUser.UserId is int userId)
|
||||||
{
|
{
|
||||||
var realModel = await snapshots.BuildImportSessionSnapshotAsync(importSessionId.Value, userId);
|
var realModel = await snapshots.BuildImportSessionSnapshotAsync(importSessionId.Value, userId);
|
||||||
if (realModel is null)
|
if (realModel is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return View(ErrorModel(
|
||||||
|
requestedMode,
|
||||||
|
importSessionId,
|
||||||
|
$"Import Session {importSessionId.Value:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(realModel);
|
return View(realModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(mode, "simulation", StringComparison.OrdinalIgnoreCase) && runId.HasValue && currentUser.UserId is int legacyUserId)
|
if (!simulationRequested && runId.HasValue && currentUser.UserId is int legacyUserId)
|
||||||
{
|
{
|
||||||
var realModel = await snapshots.BuildSnapshotAsync(runId.Value, legacyUserId);
|
var realModel = await snapshots.BuildSnapshotAsync(runId.Value, legacyUserId);
|
||||||
if (realModel is null)
|
if (realModel is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return View(ErrorModel(
|
||||||
|
requestedMode,
|
||||||
|
null,
|
||||||
|
$"Run {runId.Value:N0} could not be found for the current user. Live visualisation URLs should use importSessionId for book imports."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(realModel);
|
return View(realModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (simulationRequested)
|
||||||
|
{
|
||||||
var model = StoryIntelligenceExperiencePrototypeData.Build();
|
var model = StoryIntelligenceExperiencePrototypeData.Build();
|
||||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||||
if (!string.Equals(mode, "simulation", StringComparison.OrdinalIgnoreCase) && currentUser.UserId is int selectorUserId)
|
|
||||||
{
|
|
||||||
model = new StoryIntelligenceExperiencePrototypeViewModel
|
|
||||||
{
|
|
||||||
Mode = "selector",
|
|
||||||
AvailableRuns = await snapshots.ListImportSessionOptionsAsync(selectorUserId),
|
|
||||||
ProjectName = model.ProjectName,
|
|
||||||
BookTitle = model.BookTitle,
|
|
||||||
Scenes = model.Scenes,
|
|
||||||
SnapshotMessage = "Choose a Story Intelligence import session, or open simulation mode."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentUser.UserId is int selectorUserId)
|
||||||
|
{
|
||||||
|
var sessions = await snapshots.ListImportSessionOptionsAsync(selectorUserId);
|
||||||
|
return View(new StoryIntelligenceExperiencePrototypeViewModel
|
||||||
|
{
|
||||||
|
Mode = "selector",
|
||||||
|
AvailableRuns = sessions,
|
||||||
|
ProjectName = "Development",
|
||||||
|
BookTitle = "Story Intelligence",
|
||||||
|
SnapshotMessage = sessions.Count == 0
|
||||||
|
? "No persisted Story Intelligence import sessions were found for the current user. Start a real import, then refresh this page."
|
||||||
|
: "Choose a Story Intelligence import session, or explicitly open simulation mode.",
|
||||||
|
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||||
|
{
|
||||||
|
RequestedMode = requestedMode,
|
||||||
|
RequestedImportSessionId = importSessionId,
|
||||||
|
SnapshotSource = "Real"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(ErrorModel(requestedMode, importSessionId, "No development user is signed in."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoryIntelligenceExperiencePrototypeViewModel ErrorModel(string requestedMode, int? importSessionId, string message)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Mode = "error",
|
||||||
|
ProjectName = "Development",
|
||||||
|
BookTitle = "Story Intelligence",
|
||||||
|
SnapshotMessage = message,
|
||||||
|
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||||
|
{
|
||||||
|
RequestedMode = requestedMode,
|
||||||
|
RequestedImportSessionId = importSessionId,
|
||||||
|
SnapshotSource = "Real",
|
||||||
|
Warning = message
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,12 @@ public static class StoryIntelligenceExperiencePrototypeData
|
|||||||
public static StoryIntelligenceExperiencePrototypeViewModel Build()
|
public static StoryIntelligenceExperiencePrototypeViewModel Build()
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
|
Mode = "simulation",
|
||||||
|
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||||
|
{
|
||||||
|
RequestedMode = "simulation",
|
||||||
|
SnapshotSource = "Simulation"
|
||||||
|
},
|
||||||
ProjectName = "Alpha Flame",
|
ProjectName = "Alpha Flame",
|
||||||
BookTitle = "The Doweries Manuscript",
|
BookTitle = "The Doweries Manuscript",
|
||||||
Scenes =
|
Scenes =
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public interface IStoryIntelligenceVisualisationSnapshotService
|
|||||||
public sealed class StoryIntelligenceVisualisationSnapshotService(
|
public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||||
IStoryIntelligenceResultRepository repository,
|
IStoryIntelligenceResultRepository repository,
|
||||||
IStoryIntelligencePipelineRepository pipelines,
|
IStoryIntelligencePipelineRepository pipelines,
|
||||||
|
IStoryIntelligenceSourceRepository sources,
|
||||||
IIllustrationLibraryService illustrationLibrary,
|
IIllustrationLibraryService illustrationLibrary,
|
||||||
ILogger<StoryIntelligenceVisualisationSnapshotService> logger) : IStoryIntelligenceVisualisationSnapshotService
|
ILogger<StoryIntelligenceVisualisationSnapshotService> logger) : IStoryIntelligenceVisualisationSnapshotService
|
||||||
{
|
{
|
||||||
@ -48,6 +49,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
public async Task<IReadOnlyList<StoryIntelligenceExperienceRunOption>> ListImportSessionOptionsAsync(int userId)
|
public async Task<IReadOnlyList<StoryIntelligenceExperienceRunOption>> ListImportSessionOptionsAsync(int userId)
|
||||||
{
|
{
|
||||||
|
await EnsureActiveImportSessionsAsync(userId);
|
||||||
var sessions = await pipelines.ListForUserAsync(userId);
|
var sessions = await pipelines.ListForUserAsync(userId);
|
||||||
return sessions
|
return sessions
|
||||||
.OrderByDescending(session => session.UpdatedUtc)
|
.OrderByDescending(session => session.UpdatedUtc)
|
||||||
@ -63,6 +65,34 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsureActiveImportSessionsAsync(int userId)
|
||||||
|
{
|
||||||
|
var runs = await repository.ListRunsAsync();
|
||||||
|
var activeBookRuns = runs
|
||||||
|
.Where(run => run.UserID == userId
|
||||||
|
&& run.ProjectID.HasValue
|
||||||
|
&& run.BookID.HasValue
|
||||||
|
&& !IsTerminal(run.Status))
|
||||||
|
.GroupBy(run => new { ProjectID = run.ProjectID!.Value, BookID = run.BookID!.Value })
|
||||||
|
.Select(group => group.OrderByDescending(run => run.CreatedUtc).First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var run in activeBookRuns)
|
||||||
|
{
|
||||||
|
await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest
|
||||||
|
{
|
||||||
|
ProjectID = run.ProjectID!.Value,
|
||||||
|
BookID = run.BookID!.Value,
|
||||||
|
CurrentStage = StoryIntelligencePipelineStages.Chapters,
|
||||||
|
LastCompletedStage = null,
|
||||||
|
CurrentReviewStage = null,
|
||||||
|
Status = StoryIntelligencePipelineStatuses.InProgress,
|
||||||
|
CompletedUtc = null,
|
||||||
|
LastRunID = run.StoryIntelligenceRunID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<StoryIntelligenceExperiencePrototypeViewModel?> BuildSnapshotAsync(int runId, int userId)
|
public async Task<StoryIntelligenceExperiencePrototypeViewModel?> BuildSnapshotAsync(int runId, int userId)
|
||||||
{
|
{
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
@ -150,9 +180,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var totalScenes = Math.Max(
|
var totalScenes = Math.Max(
|
||||||
runSnapshots.Sum(snapshot => Math.Max(snapshot.Run.TotalDetectedScenes ?? 0, snapshot.Results.Count)),
|
runSnapshots.Sum(snapshot => Math.Max(snapshot.Run.TotalDetectedScenes ?? 0, snapshot.Results.Count)),
|
||||||
parsedScenes);
|
parsedScenes);
|
||||||
|
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 completedChapterRuns = runSnapshots.Count(snapshot => IsTerminal(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);
|
&& string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase);
|
||||||
|
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal);
|
||||||
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)
|
||||||
@ -166,7 +200,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
overallCompletedScenes: parsedScenes,
|
overallCompletedScenes: parsedScenes,
|
||||||
overallTotalScenes: totalScenes,
|
overallTotalScenes: totalScenes,
|
||||||
overallChaptersAnalysed: completedChapterRuns,
|
overallChaptersAnalysed: completedChapterRuns,
|
||||||
overallChapterTotal: runs.Count,
|
overallChapterTotal: bookChapterTotal,
|
||||||
|
overallProgressPercent: overallProgressPercent,
|
||||||
overallStage: OverallStage(session, activeSnapshot.Run),
|
overallStage: OverallStage(session, activeSnapshot.Run),
|
||||||
overallRemaining: EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes),
|
overallRemaining: EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes),
|
||||||
continuousSceneNumbering: true,
|
continuousSceneNumbering: true,
|
||||||
@ -191,7 +226,7 @@ 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),
|
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, 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
|
||||||
@ -212,6 +247,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
int? overallTotalScenes = null,
|
int? overallTotalScenes = null,
|
||||||
int? overallChaptersAnalysed = null,
|
int? overallChaptersAnalysed = null,
|
||||||
int? overallChapterTotal = null,
|
int? overallChapterTotal = null,
|
||||||
|
int? overallProgressPercent = null,
|
||||||
string? overallStage = null,
|
string? overallStage = null,
|
||||||
string? overallRemaining = null,
|
string? overallRemaining = null,
|
||||||
bool continuousSceneNumbering = false,
|
bool continuousSceneNumbering = false,
|
||||||
@ -235,9 +271,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
Stage = overallStage ?? StageDisplay(run),
|
Stage = overallStage ?? StageDisplay(run),
|
||||||
TimelineLabel = TimelineLabel(scene, item.Result),
|
TimelineLabel = TimelineLabel(scene, item.Result),
|
||||||
TimelineEvent = Trim(FirstConfigured(scene.Summary?.Short, scene.ScenePurpose?.ObservedFunction, "Scene analysed"), 42),
|
TimelineEvent = Trim(FirstConfigured(scene.Summary?.Short, scene.ScenePurpose?.ObservedFunction, "Scene analysed"), 42),
|
||||||
ProgressPercent = overallTotalScenes.HasValue
|
ProgressPercent = overallProgressPercent
|
||||||
|
?? (overallTotalScenes.HasValue
|
||||||
? OverallProgress(overallCompletedScenes ?? completedScenes, overallTotalScenes.Value, overallTerminal)
|
? OverallProgress(overallCompletedScenes ?? completedScenes, overallTotalScenes.Value, overallTerminal)
|
||||||
: Progress(run, completedScenes, totalScenes),
|
: Progress(run, completedScenes, totalScenes)),
|
||||||
ChaptersAnalysed = overallChaptersAnalysed ?? ChapterCount(completed),
|
ChaptersAnalysed = overallChaptersAnalysed ?? ChapterCount(completed),
|
||||||
ChapterTotal = Math.Max(1, overallChapterTotal ?? run.SourceChapterCount ?? ChapterCount(completed)),
|
ChapterTotal = Math.Max(1, overallChapterTotal ?? run.SourceChapterCount ?? ChapterCount(completed)),
|
||||||
ScenesAnalysed = overallCompletedScenes ?? completedScenes,
|
ScenesAnalysed = overallCompletedScenes ?? completedScenes,
|
||||||
@ -617,18 +654,39 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
: "All known chapter runs are complete. Awaiting the next book import stage.";
|
: "All known chapter runs are complete. Awaiting the next book import stage.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string CurrentSceneText(RunSnapshot snapshot)
|
||||||
|
{
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Current is null ? "Waiting" : $"Scene {SceneSortNumber(snapshot.Current):0.##}";
|
||||||
|
}
|
||||||
|
|
||||||
private static StoryIntelligenceExperienceSnapshotDiagnostics BuildSessionDiagnostics(
|
private static StoryIntelligenceExperienceSnapshotDiagnostics BuildSessionDiagnostics(
|
||||||
StoryIntelligenceBookPipelineState session,
|
StoryIntelligenceBookPipelineState session,
|
||||||
IReadOnlyList<RunSnapshot> snapshots,
|
IReadOnlyList<RunSnapshot> snapshots,
|
||||||
RunSnapshot activeSnapshot,
|
RunSnapshot activeSnapshot,
|
||||||
ParsedSceneResult? current,
|
ParsedSceneResult? current,
|
||||||
int parsedScenes,
|
int parsedScenes,
|
||||||
int totalPersistedResults)
|
int totalPersistedResults,
|
||||||
|
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));
|
||||||
var warning = failed == 0 ? null : $"{failed:N0} scene result(s) skipped because they could not be parsed.";
|
var warning = failed == 0 ? null : $"{failed:N0} scene result(s) skipped because they could not be parsed.";
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
|
RequestedMode = "real",
|
||||||
|
RequestedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||||
|
ResolvedImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||||
|
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||||
|
SessionStatus = session.Status,
|
||||||
|
OverallBookProgress = overallProgressPercent,
|
||||||
|
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||||
|
CurrentScene = CurrentSceneText(activeSnapshot),
|
||||||
|
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),
|
||||||
TotalPersistedSceneResultCount = totalPersistedResults,
|
TotalPersistedSceneResultCount = totalPersistedResults,
|
||||||
@ -658,6 +716,15 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
|
RequestedMode = "real",
|
||||||
|
ActiveChapterRunId = run.StoryIntelligenceRunID,
|
||||||
|
SessionStatus = run.Status,
|
||||||
|
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}"
|
||||||
|
: (current is null ? "Waiting" : $"Scene {SceneSortNumber(current):0.##}"),
|
||||||
|
SnapshotSource = "Real",
|
||||||
RunStatus = run.Status,
|
RunStatus = run.Status,
|
||||||
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
||||||
TotalPersistedSceneResultCount = results.Count,
|
TotalPersistedSceneResultCount = results.Count,
|
||||||
@ -732,6 +799,43 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int BookProgress(IReadOnlyList<RunSnapshot> snapshots, int chapterTotal, bool isTerminal)
|
||||||
|
{
|
||||||
|
if (isTerminal)
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapterTotal <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chapterProgress = snapshots.Sum(ChapterProgress);
|
||||||
|
return Math.Clamp((int)Math.Floor(chapterProgress * 100m / chapterTotal), 0, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal ChapterProgress(RunSnapshot snapshot)
|
||||||
|
{
|
||||||
|
if (IsTerminal(snapshot.Run.Status))
|
||||||
|
{
|
||||||
|
return 1m;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.Run.Status == StoryIntelligenceRunStatuses.Pending)
|
||||||
|
{
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((snapshot.Run.TotalDetectedScenes ?? 0) > 0)
|
||||||
|
{
|
||||||
|
var sceneProgress = Math.Clamp((snapshot.Run.CompletedScenes ?? snapshot.Completed.Count) / Math.Max(1m, snapshot.Run.TotalDetectedScenes ?? 1), 0m, 1m);
|
||||||
|
return Math.Clamp(0.25m + (sceneProgress * 0.7m), 0m, 0.95m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.15m;
|
||||||
|
}
|
||||||
|
|
||||||
private static string EstimateRemaining(StoryIntelligenceSavedRun run, int completedScenes, int totalScenes)
|
private static string EstimateRemaining(StoryIntelligenceSavedRun run, int completedScenes, int totalScenes)
|
||||||
{
|
{
|
||||||
if (IsTerminal(run.Status))
|
if (IsTerminal(run.Status))
|
||||||
|
|||||||
@ -19,6 +19,15 @@ public sealed class StoryIntelligenceExperiencePrototypeViewModel
|
|||||||
|
|
||||||
public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
||||||
{
|
{
|
||||||
|
public string RequestedMode { get; init; } = string.Empty;
|
||||||
|
public int? RequestedImportSessionId { get; init; }
|
||||||
|
public int? ResolvedImportSessionId { get; init; }
|
||||||
|
public int? ActiveChapterRunId { get; init; }
|
||||||
|
public string SessionStatus { get; init; } = string.Empty;
|
||||||
|
public int OverallBookProgress { get; init; }
|
||||||
|
public string CurrentChapter { get; init; } = string.Empty;
|
||||||
|
public string CurrentScene { 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; }
|
||||||
public int TotalPersistedSceneResultCount { get; init; }
|
public int TotalPersistedSceneResultCount { get; init; }
|
||||||
|
|||||||
@ -3,6 +3,21 @@
|
|||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||||
|
var statusLabel = Model.Mode switch
|
||||||
|
{
|
||||||
|
"simulation" => "Simulation",
|
||||||
|
"real" when Model.IsTerminal => "Analysis Complete",
|
||||||
|
"real" => "Live Analysis",
|
||||||
|
"error" => "Import Session Error",
|
||||||
|
_ => "Select Import Session"
|
||||||
|
};
|
||||||
|
var sourceLabel = Model.Mode switch
|
||||||
|
{
|
||||||
|
"simulation" => "Simulation only",
|
||||||
|
"real" => "Real snapshot",
|
||||||
|
"error" => "No live snapshot",
|
||||||
|
_ => "Choose a session"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="dark" data-bs-theme="dark">
|
<html lang="en" data-theme="dark" data-bs-theme="dark">
|
||||||
@ -31,6 +46,7 @@
|
|||||||
</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>@Model.ProjectName</span>
|
<span>@Model.ProjectName</span>
|
||||||
<span>@Model.BookTitle</span>
|
<span>@Model.BookTitle</span>
|
||||||
<span data-stage-label>Preparing</span>
|
<span data-stage-label>Preparing</span>
|
||||||
@ -50,6 +66,13 @@
|
|||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.SnapshotMessage))
|
||||||
|
{
|
||||||
|
<aside class="story-exp-status-message" role="status">
|
||||||
|
@Model.SnapshotMessage
|
||||||
|
</aside>
|
||||||
|
}
|
||||||
|
|
||||||
<section class="story-exp-shell" aria-label="Story Intelligence visual prototype">
|
<section class="story-exp-shell" aria-label="Story Intelligence visual prototype">
|
||||||
<aside class="story-exp-left" aria-label="Analysis progress">
|
<aside class="story-exp-left" aria-label="Analysis progress">
|
||||||
<div class="story-exp-progress">
|
<div class="story-exp-progress">
|
||||||
@ -147,7 +170,7 @@
|
|||||||
<aside class="story-exp-controls" aria-label="Prototype controls">
|
<aside class="story-exp-controls" aria-label="Prototype controls">
|
||||||
<div>
|
<div>
|
||||||
<span>Prototype controls</span>
|
<span>Prototype controls</span>
|
||||||
<small>@(Model.Mode == "real" ? "Real snapshot" : "Simulation only")</small>
|
<small>@sourceLabel</small>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" data-control="restart">Restart</button>
|
<button type="button" data-control="restart">Restart</button>
|
||||||
<button type="button" data-control="previous">Previous</button>
|
<button type="button" data-control="previous">Previous</button>
|
||||||
@ -162,6 +185,42 @@
|
|||||||
<details class="story-exp-diagnostics" data-diagnostics>
|
<details class="story-exp-diagnostics" data-diagnostics>
|
||||||
<summary>Diagnostics</summary>
|
<summary>Diagnostics</summary>
|
||||||
<dl>
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>Requested mode</dt>
|
||||||
|
<dd data-diagnostic-key="requestedMode">@Model.Diagnostics.RequestedMode</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Requested ImportSessionID</dt>
|
||||||
|
<dd data-diagnostic-key="requestedImportSessionId">@Model.Diagnostics.RequestedImportSessionId</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Resolved ImportSessionID</dt>
|
||||||
|
<dd data-diagnostic-key="resolvedImportSessionId">@Model.Diagnostics.ResolvedImportSessionId</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Active chapter RunID</dt>
|
||||||
|
<dd data-diagnostic-key="activeChapterRunId">@Model.Diagnostics.ActiveChapterRunId</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Session status</dt>
|
||||||
|
<dd data-diagnostic-key="sessionStatus">@Model.Diagnostics.SessionStatus</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Overall book progress</dt>
|
||||||
|
<dd data-diagnostic-key="overallBookProgress">@Model.Diagnostics.OverallBookProgress</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Current chapter</dt>
|
||||||
|
<dd data-diagnostic-key="currentChapter">@Model.Diagnostics.CurrentChapter</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Current scene</dt>
|
||||||
|
<dd data-diagnostic-key="currentScene">@Model.Diagnostics.CurrentScene</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Snapshot source</dt>
|
||||||
|
<dd data-diagnostic-key="snapshotSource">@Model.Diagnostics.SnapshotSource</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Current Snapshot Version</dt>
|
<dt>Current Snapshot Version</dt>
|
||||||
<dd data-diagnostics-current>0</dd>
|
<dd data-diagnostics-current>0</dd>
|
||||||
|
|||||||
@ -327,6 +327,23 @@ input:focus-visible {
|
|||||||
color: var(--story-text);
|
color: var(--story-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-exp-status-message {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 86px;
|
||||||
|
z-index: 19;
|
||||||
|
width: min(720px, calc(100vw - 48px));
|
||||||
|
border: 1px solid rgba(215, 168, 93, 0.32);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: rgba(6, 14, 26, 0.9);
|
||||||
|
box-shadow: var(--story-shadow);
|
||||||
|
color: var(--story-text);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
.story-exp-shell {
|
.story-exp-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(206px, 0.54fr) minmax(760px, 2.44fr) minmax(214px, 0.56fr);
|
grid-template-columns: minmax(206px, 0.54fr) minmax(760px, 2.44fr) minmax(214px, 0.56fr);
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
let model = JSON.parse(dataNode.textContent || "{}");
|
let model = JSON.parse(dataNode.textContent || "{}");
|
||||||
let scenes = model.scenes || [];
|
let scenes = model.scenes || [];
|
||||||
if (!scenes.length) return;
|
|
||||||
|
|
||||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
const realMode = root.dataset.mode === "real";
|
const realMode = root.dataset.mode === "real";
|
||||||
|
const simulationMode = root.dataset.mode === "simulation";
|
||||||
const snapshotUrl = root.dataset.snapshotUrl || "";
|
const snapshotUrl = root.dataset.snapshotUrl || "";
|
||||||
const failedImageUrls = new Set();
|
const failedImageUrls = new Set();
|
||||||
const preloadedImageUrls = new Set();
|
const preloadedImageUrls = new Set();
|
||||||
@ -88,6 +88,11 @@
|
|||||||
diagnosticValues: root.querySelectorAll("[data-diagnostic-key]")
|
diagnosticValues: root.querySelectorAll("[data-diagnostic-key]")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!scenes.length) {
|
||||||
|
updateDiagnostics();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function loadSceneState(index) {
|
function loadSceneState(index) {
|
||||||
return scenes[Math.max(0, Math.min(scenes.length - 1, index))];
|
return scenes[Math.max(0, Math.min(scenes.length - 1, index))];
|
||||||
}
|
}
|
||||||
@ -1070,7 +1075,7 @@
|
|||||||
|
|
||||||
function startTimer() {
|
function startTimer() {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
if (!state.playing || realMode) return;
|
if (!state.playing || !simulationMode) return;
|
||||||
state.timer = window.setInterval(() => {
|
state.timer = window.setInterval(() => {
|
||||||
const next = state.index >= scenes.length - 1 ? 0 : state.index + 1;
|
const next = state.index >= scenes.length - 1 ? 0 : state.index + 1;
|
||||||
transitionToNextState(next);
|
transitionToNextState(next);
|
||||||
@ -1341,32 +1346,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
root.querySelector("[data-control='restart']")?.addEventListener("click", () => {
|
root.querySelector("[data-control='restart']")?.addEventListener("click", () => {
|
||||||
if (realMode) return;
|
if (!simulationMode) return;
|
||||||
transitionToNextState(0);
|
transitionToNextState(0);
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
root.querySelector("[data-control='previous']")?.addEventListener("click", () => {
|
root.querySelector("[data-control='previous']")?.addEventListener("click", () => {
|
||||||
if (realMode) return;
|
if (!simulationMode) return;
|
||||||
transitionToNextState(state.index <= 0 ? scenes.length - 1 : state.index - 1);
|
transitionToNextState(state.index <= 0 ? scenes.length - 1 : state.index - 1);
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
root.querySelector("[data-control='next']")?.addEventListener("click", () => {
|
root.querySelector("[data-control='next']")?.addEventListener("click", () => {
|
||||||
if (realMode) return;
|
if (!simulationMode) return;
|
||||||
transitionToNextState(state.index >= scenes.length - 1 ? 0 : state.index + 1);
|
transitionToNextState(state.index >= scenes.length - 1 ? 0 : state.index + 1);
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.play?.addEventListener("click", () => {
|
dom.play?.addEventListener("click", () => {
|
||||||
if (realMode) return;
|
if (!simulationMode) return;
|
||||||
state.playing = !state.playing;
|
state.playing = !state.playing;
|
||||||
text(dom.play, state.playing ? "Pause" : "Play");
|
text(dom.play, state.playing ? "Pause" : "Play");
|
||||||
if (state.playing) startTimer(); else stopTimer();
|
if (state.playing) startTimer(); else stopTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.speed?.addEventListener("input", () => {
|
dom.speed?.addEventListener("input", () => {
|
||||||
if (realMode) return;
|
if (!simulationMode) return;
|
||||||
state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200;
|
state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200;
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user