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();
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
var model = StoryIntelligenceExperiencePrototypeData.Build();
|
||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||
if (!string.Equals(mode, "simulation", StringComparison.OrdinalIgnoreCase) && currentUser.UserId is int selectorUserId)
|
||||
if (simulationRequested)
|
||||
{
|
||||
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."
|
||||
};
|
||||
var model = StoryIntelligenceExperiencePrototypeData.Build();
|
||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||
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()
|
||||
=> new()
|
||||
{
|
||||
Mode = "simulation",
|
||||
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
{
|
||||
RequestedMode = "simulation",
|
||||
SnapshotSource = "Simulation"
|
||||
},
|
||||
ProjectName = "Alpha Flame",
|
||||
BookTitle = "The Doweries Manuscript",
|
||||
Scenes =
|
||||
|
||||
@ -19,6 +19,7 @@ public interface IStoryIntelligenceVisualisationSnapshotService
|
||||
public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
IStoryIntelligenceResultRepository repository,
|
||||
IStoryIntelligencePipelineRepository pipelines,
|
||||
IStoryIntelligenceSourceRepository sources,
|
||||
IIllustrationLibraryService illustrationLibrary,
|
||||
ILogger<StoryIntelligenceVisualisationSnapshotService> logger) : IStoryIntelligenceVisualisationSnapshotService
|
||||
{
|
||||
@ -48,6 +49,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
|
||||
public async Task<IReadOnlyList<StoryIntelligenceExperienceRunOption>> ListImportSessionOptionsAsync(int userId)
|
||||
{
|
||||
await EnsureActiveImportSessionsAsync(userId);
|
||||
var sessions = await pipelines.ListForUserAsync(userId);
|
||||
return sessions
|
||||
.OrderByDescending(session => session.UpdatedUtc)
|
||||
@ -63,6 +65,34 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
.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)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
@ -150,9 +180,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
var totalScenes = Math.Max(
|
||||
runSnapshots.Sum(snapshot => Math.Max(snapshot.Run.TotalDetectedScenes ?? 0, snapshot.Results.Count)),
|
||||
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 isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status))
|
||||
&& string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase);
|
||||
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal);
|
||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||
var nextSceneNumber = 1;
|
||||
foreach (var snapshot in runSnapshots)
|
||||
@ -166,7 +200,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
overallCompletedScenes: parsedScenes,
|
||||
overallTotalScenes: totalScenes,
|
||||
overallChaptersAnalysed: completedChapterRuns,
|
||||
overallChapterTotal: runs.Count,
|
||||
overallChapterTotal: bookChapterTotal,
|
||||
overallProgressPercent: overallProgressPercent,
|
||||
overallStage: OverallStage(session, activeSnapshot.Run),
|
||||
overallRemaining: EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes),
|
||||
continuousSceneNumbering: true,
|
||||
@ -191,7 +226,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||
IsTerminal = isTerminal,
|
||||
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"),
|
||||
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
||||
Scenes = scenes
|
||||
@ -212,6 +247,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
int? overallTotalScenes = null,
|
||||
int? overallChaptersAnalysed = null,
|
||||
int? overallChapterTotal = null,
|
||||
int? overallProgressPercent = null,
|
||||
string? overallStage = null,
|
||||
string? overallRemaining = null,
|
||||
bool continuousSceneNumbering = false,
|
||||
@ -235,9 +271,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
Stage = overallStage ?? StageDisplay(run),
|
||||
TimelineLabel = TimelineLabel(scene, item.Result),
|
||||
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)
|
||||
: Progress(run, completedScenes, totalScenes),
|
||||
: Progress(run, completedScenes, totalScenes)),
|
||||
ChaptersAnalysed = overallChaptersAnalysed ?? ChapterCount(completed),
|
||||
ChapterTotal = Math.Max(1, overallChapterTotal ?? run.SourceChapterCount ?? ChapterCount(completed)),
|
||||
ScenesAnalysed = overallCompletedScenes ?? completedScenes,
|
||||
@ -617,18 +654,39 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
: "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(
|
||||
StoryIntelligenceBookPipelineState session,
|
||||
IReadOnlyList<RunSnapshot> snapshots,
|
||||
RunSnapshot activeSnapshot,
|
||||
ParsedSceneResult? current,
|
||||
int parsedScenes,
|
||||
int totalPersistedResults)
|
||||
int totalPersistedResults,
|
||||
int overallProgressPercent)
|
||||
{
|
||||
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.";
|
||||
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),
|
||||
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
||||
TotalPersistedSceneResultCount = totalPersistedResults,
|
||||
@ -658,6 +716,15 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
|
||||
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,
|
||||
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
||||
TotalPersistedSceneResultCount = results.Count,
|
||||
@ -732,6 +799,43 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
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)
|
||||
{
|
||||
if (IsTerminal(run.Status))
|
||||
|
||||
@ -19,6 +19,15 @@ public sealed class StoryIntelligenceExperiencePrototypeViewModel
|
||||
|
||||
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 int PersistedCompletedSceneCount { get; init; }
|
||||
public int TotalPersistedSceneResultCount { get; init; }
|
||||
|
||||
@ -3,6 +3,21 @@
|
||||
@{
|
||||
Layout = null;
|
||||
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>
|
||||
<html lang="en" data-theme="dark" data-bs-theme="dark">
|
||||
@ -31,6 +46,7 @@
|
||||
</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-stage-label>Preparing</span>
|
||||
@ -50,6 +66,13 @@
|
||||
</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">
|
||||
<aside class="story-exp-left" aria-label="Analysis progress">
|
||||
<div class="story-exp-progress">
|
||||
@ -147,7 +170,7 @@
|
||||
<aside class="story-exp-controls" aria-label="Prototype controls">
|
||||
<div>
|
||||
<span>Prototype controls</span>
|
||||
<small>@(Model.Mode == "real" ? "Real snapshot" : "Simulation only")</small>
|
||||
<small>@sourceLabel</small>
|
||||
</div>
|
||||
<button type="button" data-control="restart">Restart</button>
|
||||
<button type="button" data-control="previous">Previous</button>
|
||||
@ -162,6 +185,42 @@
|
||||
<details class="story-exp-diagnostics" data-diagnostics>
|
||||
<summary>Diagnostics</summary>
|
||||
<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>
|
||||
<dt>Current Snapshot Version</dt>
|
||||
<dd data-diagnostics-current>0</dd>
|
||||
|
||||
@ -327,6 +327,23 @@ input:focus-visible {
|
||||
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 {
|
||||
display: grid;
|
||||
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 scenes = model.scenes || [];
|
||||
if (!scenes.length) return;
|
||||
|
||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const realMode = root.dataset.mode === "real";
|
||||
const simulationMode = root.dataset.mode === "simulation";
|
||||
const snapshotUrl = root.dataset.snapshotUrl || "";
|
||||
const failedImageUrls = new Set();
|
||||
const preloadedImageUrls = new Set();
|
||||
@ -88,6 +88,11 @@
|
||||
diagnosticValues: root.querySelectorAll("[data-diagnostic-key]")
|
||||
};
|
||||
|
||||
if (!scenes.length) {
|
||||
updateDiagnostics();
|
||||
return;
|
||||
}
|
||||
|
||||
function loadSceneState(index) {
|
||||
return scenes[Math.max(0, Math.min(scenes.length - 1, index))];
|
||||
}
|
||||
@ -1070,7 +1075,7 @@
|
||||
|
||||
function startTimer() {
|
||||
stopTimer();
|
||||
if (!state.playing || realMode) return;
|
||||
if (!state.playing || !simulationMode) return;
|
||||
state.timer = window.setInterval(() => {
|
||||
const next = state.index >= scenes.length - 1 ? 0 : state.index + 1;
|
||||
transitionToNextState(next);
|
||||
@ -1341,32 +1346,32 @@
|
||||
}
|
||||
|
||||
root.querySelector("[data-control='restart']")?.addEventListener("click", () => {
|
||||
if (realMode) return;
|
||||
if (!simulationMode) return;
|
||||
transitionToNextState(0);
|
||||
startTimer();
|
||||
});
|
||||
|
||||
root.querySelector("[data-control='previous']")?.addEventListener("click", () => {
|
||||
if (realMode) return;
|
||||
if (!simulationMode) return;
|
||||
transitionToNextState(state.index <= 0 ? scenes.length - 1 : state.index - 1);
|
||||
startTimer();
|
||||
});
|
||||
|
||||
root.querySelector("[data-control='next']")?.addEventListener("click", () => {
|
||||
if (realMode) return;
|
||||
if (!simulationMode) return;
|
||||
transitionToNextState(state.index >= scenes.length - 1 ? 0 : state.index + 1);
|
||||
startTimer();
|
||||
});
|
||||
|
||||
dom.play?.addEventListener("click", () => {
|
||||
if (realMode) return;
|
||||
if (!simulationMode) return;
|
||||
state.playing = !state.playing;
|
||||
text(dom.play, state.playing ? "Pause" : "Play");
|
||||
if (state.playing) startTimer(); else stopTimer();
|
||||
});
|
||||
|
||||
dom.speed?.addEventListener("input", () => {
|
||||
if (realMode) return;
|
||||
if (!simulationMode) return;
|
||||
state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200;
|
||||
startTimer();
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user