Phase 20AF – Story Intelligence Progress Page Layout and Estimate Fix
This commit is contained in:
parent
2c4849a302
commit
766ad28325
@ -230,6 +230,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
TotalDetectedScenes = run.TotalDetectedScenes,
|
TotalDetectedScenes = run.TotalDetectedScenes,
|
||||||
CompletedScenes = run.CompletedScenes,
|
CompletedScenes = run.CompletedScenes,
|
||||||
FailedScenes = run.FailedScenes,
|
FailedScenes = run.FailedScenes,
|
||||||
|
SourceWordCount = run.SourceWordCount,
|
||||||
TotalTokens = run.TotalTokens,
|
TotalTokens = run.TotalTokens,
|
||||||
TotalDurationMs = run.TotalDurationMs,
|
TotalDurationMs = run.TotalDurationMs,
|
||||||
FailureStage = run.FailureStage,
|
FailureStage = run.FailureStage,
|
||||||
|
|||||||
@ -166,6 +166,7 @@ public sealed class StoryIntelligenceOnboardingChapterViewModel
|
|||||||
public int? TotalDetectedScenes { get; init; }
|
public int? TotalDetectedScenes { get; init; }
|
||||||
public int? CompletedScenes { get; init; }
|
public int? CompletedScenes { get; init; }
|
||||||
public int? FailedScenes { get; init; }
|
public int? FailedScenes { get; init; }
|
||||||
|
public int? SourceWordCount { get; init; }
|
||||||
public int? TotalTokens { get; init; }
|
public int? TotalTokens { get; init; }
|
||||||
public long? TotalDurationMs { get; init; }
|
public long? TotalDurationMs { get; init; }
|
||||||
public string? FailureStage { get; init; }
|
public string? FailureStage { get; init; }
|
||||||
|
|||||||
@ -46,23 +46,23 @@
|
|||||||
<div class="story-live-stats">
|
<div class="story-live-stats">
|
||||||
<div>
|
<div>
|
||||||
<span>Chapters read</span>
|
<span>Chapters read</span>
|
||||||
<strong><span data-story-chapters-completed>@Model.CompletedChapterCount</span> of @Model.ChapterCount</strong>
|
<strong class="story-stat-value"><span data-story-chapters-completed>@Model.CompletedChapterCount</span> of @Model.ChapterCount</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Scenes read</span>
|
<span>Scenes read</span>
|
||||||
<strong><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> so far</strong>
|
<strong class="story-stat-value"><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> so far</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Needs review</span>
|
<span>Needs review</span>
|
||||||
<strong data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
<strong class="story-stat-value" data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Elapsed</span>
|
<span>Elapsed</span>
|
||||||
<strong data-story-elapsed>@Elapsed(Model)</strong>
|
<strong class="story-stat-value" data-story-elapsed>@Elapsed(Model)</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Estimated remaining</span>
|
<span>Estimated remaining</span>
|
||||||
<strong data-story-remaining>@EstimateRemaining(Model)</strong>
|
<strong class="story-stat-value" data-story-remaining>@EstimateRemaining(Model)</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -85,8 +85,9 @@
|
|||||||
<p class="lead story-latest-summary" data-story-latest-summary hidden></p>
|
<p class="lead story-latest-summary" data-story-latest-summary hidden></p>
|
||||||
<p class="text-muted" data-story-latest-empty>Scene summaries will appear here as PlotDirector reads.</p>
|
<p class="text-muted" data-story-latest-empty>Scene summaries will appear here as PlotDirector reads.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="story-live-discoveries">
|
||||||
<h2>Recent discoveries</h2>
|
<h2>Recent discoveries</h2>
|
||||||
|
<p class="text-muted story-discovery-placeholder" data-story-discovery-empty>Possible characters, locations and story clues will appear here as PlotDirector reads.</p>
|
||||||
<ul data-story-discovery-feed></ul>
|
<ul data-story-discovery-feed></ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -94,7 +95,11 @@
|
|||||||
<div class="story-run-state" aria-hidden="true" hidden>
|
<div class="story-run-state" aria-hidden="true" hidden>
|
||||||
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
||||||
{
|
{
|
||||||
<div data-story-run-id="@chapter.RunID" data-story-chapter-title="@chapter.ChapterTitle" data-story-raw-status="@chapter.Status">
|
<div data-story-run-id="@chapter.RunID"
|
||||||
|
data-story-chapter-title="@chapter.ChapterTitle"
|
||||||
|
data-story-raw-status="@chapter.Status"
|
||||||
|
data-story-source-word-count="@(chapter.SourceWordCount ?? 0)"
|
||||||
|
data-story-duration-ms="@(chapter.TotalDurationMs ?? 0)">
|
||||||
<span data-story-run-status>@FriendlyStatus(chapter.Status)</span>
|
<span data-story-run-status>@FriendlyStatus(chapter.Status)</span>
|
||||||
<span data-story-run-completed>@((chapter.CompletedScenes ?? 0).ToString("N0"))</span>
|
<span data-story-run-completed>@((chapter.CompletedScenes ?? 0).ToString("N0"))</span>
|
||||||
<span data-story-run-total>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</span>
|
<span data-story-run-total>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</span>
|
||||||
@ -112,11 +117,13 @@
|
|||||||
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
||||||
<button class="btn btn-outline-secondary" type="submit" disabled="@(!Model.HasActiveRuns)">Cancel analysis</button>
|
<button class="btn btn-outline-secondary" type="submit" disabled="@(!Model.HasActiveRuns)">Cancel analysis</button>
|
||||||
</form>
|
</form>
|
||||||
<a class="btn btn-primary @(Model.HasActiveRuns ? "disabled" : string.Empty)"
|
@if (!Model.HasActiveRuns && Model.Chapters.Any(chapter => chapter.IsRunComplete || chapter.IsFailed || chapter.HasCompletedCommit))
|
||||||
data-story-review-link
|
{
|
||||||
asp-action="StoryIntelligenceReview"
|
<a class="btn btn-primary"
|
||||||
asp-route-batchId="@Model.BatchID"
|
data-story-review-link
|
||||||
aria-disabled="@Model.HasActiveRuns">Review analysis</a>
|
asp-action="StoryIntelligenceReview"
|
||||||
|
asp-route-batchId="@Model.BatchID">Review analysis</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -245,7 +252,60 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string EstimateRemaining(StoryIntelligenceProgressViewModel model)
|
private static string EstimateRemaining(StoryIntelligenceProgressViewModel model)
|
||||||
=> model.HasActiveRuns && model.TotalCompletedScenes > 0 ? "Estimating..." : model.HasActiveRuns ? "Estimating..." : "Ready to review";
|
{
|
||||||
|
if (!model.HasActiveRuns)
|
||||||
|
{
|
||||||
|
return "Ready to review";
|
||||||
|
}
|
||||||
|
|
||||||
|
var completed = model.Chapters
|
||||||
|
.Where(chapter => chapter.IsRunComplete && (chapter.SourceWordCount ?? 0) > 0 && (chapter.TotalDurationMs ?? 0) > 0)
|
||||||
|
.ToList();
|
||||||
|
if (completed.Count == 0)
|
||||||
|
{
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedWords = completed.Sum(chapter => chapter.SourceWordCount ?? 0);
|
||||||
|
var completedSeconds = completed.Sum(chapter => chapter.TotalDurationMs ?? 0) / 1000m;
|
||||||
|
var remainingWords = model.Chapters
|
||||||
|
.Where(chapter => !chapter.IsRunComplete && !chapter.IsFailed)
|
||||||
|
.Sum(chapter => chapter.SourceWordCount ?? 0);
|
||||||
|
if (completedWords <= 0 || completedSeconds <= 0 || remainingWords <= 0)
|
||||||
|
{
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondsPerWord = completedSeconds / completedWords;
|
||||||
|
var bufferedSeconds = secondsPerWord * remainingWords * 1.35m;
|
||||||
|
return FriendlyRemaining(bufferedSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FriendlyRemaining(decimal seconds)
|
||||||
|
{
|
||||||
|
var minutes = (int)Math.Ceiling(seconds / 60m);
|
||||||
|
if (minutes <= 0)
|
||||||
|
{
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes <= 20)
|
||||||
|
{
|
||||||
|
return "About 20-30 minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes <= 45)
|
||||||
|
{
|
||||||
|
return "About 45-60 minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes <= 120)
|
||||||
|
{
|
||||||
|
return "About 1-2 hours";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "More than 2 hours";
|
||||||
|
}
|
||||||
|
|
||||||
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -613,7 +613,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-live-hero p:not(.eyebrow) {
|
.story-live-hero p:not(.eyebrow) {
|
||||||
max-width: 780px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-live-progress > .alert {
|
.story-live-progress > .alert {
|
||||||
@ -710,6 +710,14 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-stat-value {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: .25rem;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.story-live-current,
|
.story-live-current,
|
||||||
.story-live-feed-grid {
|
.story-live-feed-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -754,6 +762,16 @@
|
|||||||
background: rgba(47, 111, 99, .08);
|
background: rgba(47, 111, 99, .08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-live-discoveries {
|
||||||
|
display: grid;
|
||||||
|
gap: .45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-discovery-placeholder {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.story-review-page {
|
.story-review-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@ -70,21 +70,55 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const friendlyRemaining = (value, completedScenes, totalScenes) => {
|
const friendlyRemainingFromSeconds = (seconds) => {
|
||||||
if (totalScenes > 0 && completedScenes >= totalScenes) {
|
const minutes = Math.ceil(seconds / 60);
|
||||||
return "Almost done";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value || value === "Calculating") {
|
|
||||||
return "Estimating...";
|
|
||||||
}
|
|
||||||
|
|
||||||
const minutes = Number.parseInt(String(value).replace(/[^0-9]/g, ""), 10);
|
|
||||||
if (!Number.isFinite(minutes) || minutes <= 0) {
|
if (!Number.isFinite(minutes) || minutes <= 0) {
|
||||||
return "Estimating...";
|
return "Estimating...";
|
||||||
}
|
}
|
||||||
|
|
||||||
return `About ${minutes}-${Math.max(minutes + 1, minutes * 2)} minutes`;
|
if (minutes <= 20) {
|
||||||
|
return "About 20-30 minutes";
|
||||||
|
}
|
||||||
|
if (minutes <= 45) {
|
||||||
|
return "About 45-60 minutes";
|
||||||
|
}
|
||||||
|
if (minutes <= 120) {
|
||||||
|
return "About 1-2 hours";
|
||||||
|
}
|
||||||
|
return "More than 2 hours";
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRemaining = (chapters, hasActiveRuns) => {
|
||||||
|
if (!hasActiveRuns) {
|
||||||
|
return "Ready to review";
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = chapters.filter((chapter) => {
|
||||||
|
const status = chapter.dataset.storyRawStatus || "";
|
||||||
|
const words = Number.parseInt(chapter.dataset.storySourceWordCount || "0", 10) || 0;
|
||||||
|
const duration = Number.parseInt(chapter.dataset.storyDurationMs || "0", 10) || 0;
|
||||||
|
return (status === "Completed" || status === "CompletedWithWarnings") && words > 0 && duration > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (completed.length === 0) {
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedWords = completed.reduce((total, chapter) => total + (Number.parseInt(chapter.dataset.storySourceWordCount || "0", 10) || 0), 0);
|
||||||
|
const completedSeconds = completed.reduce((total, chapter) => total + ((Number.parseInt(chapter.dataset.storyDurationMs || "0", 10) || 0) / 1000), 0);
|
||||||
|
const remainingWords = chapters.reduce((total, chapter) => {
|
||||||
|
const status = chapter.dataset.storyRawStatus || "";
|
||||||
|
if (status === "Completed" || status === "CompletedWithWarnings" || status === "Failed" || status === "Cancelled") {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
return total + (Number.parseInt(chapter.dataset.storySourceWordCount || "0", 10) || 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (completedWords <= 0 || completedSeconds <= 0 || remainingWords <= 0) {
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return friendlyRemainingFromSeconds((completedSeconds / completedWords) * remainingWords * 1.35);
|
||||||
};
|
};
|
||||||
|
|
||||||
const possibleDiscoveryText = (text) => String(text || "")
|
const possibleDiscoveryText = (text) => String(text || "")
|
||||||
@ -200,6 +234,10 @@
|
|||||||
const item = document.createElement("li");
|
const item = document.createElement("li");
|
||||||
item.textContent = possibleDiscoveryText(text);
|
item.textContent = possibleDiscoveryText(text);
|
||||||
list.prepend(item);
|
list.prepend(item);
|
||||||
|
const placeholder = list.closest(".story-live-discoveries")?.querySelector("[data-story-discovery-empty]");
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.hidden = true;
|
||||||
|
}
|
||||||
while (list.children.length > 8) {
|
while (list.children.length > 8) {
|
||||||
list.lastElementChild?.remove();
|
list.lastElementChild?.remove();
|
||||||
}
|
}
|
||||||
@ -265,6 +303,9 @@
|
|||||||
return status === "Pending" || status === "Running";
|
return status === "Pending" || status === "Running";
|
||||||
});
|
});
|
||||||
root.dataset.storyIntelligenceHasActiveRuns = String(active);
|
root.dataset.storyIntelligenceHasActiveRuns = String(active);
|
||||||
|
root.querySelectorAll("[data-story-remaining]").forEach((node) => {
|
||||||
|
node.textContent = calculateRemaining(chapters, active);
|
||||||
|
});
|
||||||
|
|
||||||
if (hadActiveRuns && !active && root.dataset.reloadScheduled !== "true") {
|
if (hadActiveRuns && !active && root.dataset.reloadScheduled !== "true") {
|
||||||
root.dataset.reloadScheduled = "true";
|
root.dataset.reloadScheduled = "true";
|
||||||
@ -291,8 +332,8 @@
|
|||||||
const total = field(state, "totalDetectedScenes", "TotalDetectedScenes");
|
const total = field(state, "totalDetectedScenes", "TotalDetectedScenes");
|
||||||
const failed = field(state, "failedScenes", "FailedScenes");
|
const failed = field(state, "failedScenes", "FailedScenes");
|
||||||
const tokens = field(state, "totalTokens", "TotalTokens");
|
const tokens = field(state, "totalTokens", "TotalTokens");
|
||||||
|
const totalDurationMs = field(state, "totalDurationMs", "TotalDurationMs");
|
||||||
const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec";
|
const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec";
|
||||||
const remaining = field(state, "estimatedRemaining", "EstimatedRemaining") || "Calculating";
|
|
||||||
const currentSceneNumber = field(state, "currentSceneNumber", "CurrentSceneNumber");
|
const currentSceneNumber = field(state, "currentSceneNumber", "CurrentSceneNumber");
|
||||||
const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber);
|
const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber);
|
||||||
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
||||||
@ -303,6 +344,9 @@
|
|||||||
node.textContent = statusLabel(state);
|
node.textContent = statusLabel(state);
|
||||||
});
|
});
|
||||||
chapter.dataset.storyRawStatus = status;
|
chapter.dataset.storyRawStatus = status;
|
||||||
|
if (totalDurationMs !== null) {
|
||||||
|
chapter.dataset.storyDurationMs = String(totalDurationMs);
|
||||||
|
}
|
||||||
chapter.querySelectorAll("[data-story-run-status]").forEach((node) => {
|
chapter.querySelectorAll("[data-story-run-status]").forEach((node) => {
|
||||||
node.textContent = friendlyStatus(status);
|
node.textContent = friendlyStatus(status);
|
||||||
});
|
});
|
||||||
@ -358,10 +402,6 @@
|
|||||||
root.querySelectorAll("[data-story-elapsed]").forEach((node) => {
|
root.querySelectorAll("[data-story-elapsed]").forEach((node) => {
|
||||||
node.textContent = elapsed;
|
node.textContent = elapsed;
|
||||||
});
|
});
|
||||||
root.querySelectorAll("[data-story-remaining]").forEach((node) => {
|
|
||||||
node.textContent = friendlyRemaining(remaining, Number.parseInt(completed || "0", 10) || 0, Number.parseInt(total || "0", 10) || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
const summaryNode = root.querySelector("[data-story-latest-summary]");
|
const summaryNode = root.querySelector("[data-story-latest-summary]");
|
||||||
if (summaryNode && latestSummary) {
|
if (summaryNode && latestSummary) {
|
||||||
summaryNode.textContent = latestSummary;
|
summaryNode.textContent = latestSummary;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user