Fix story intelligence progress totals
This commit is contained in:
parent
96343138ea
commit
0cc25c570c
@ -231,7 +231,8 @@ var tests = new (string Name, Action Test)[]
|
||||
("Story Intelligence review forms share busy submit guard", StoryIntelligenceReviewFormsShareBusySubmitGuard),
|
||||
("Story Intelligence heavy review modules offer bounded rendering", StoryIntelligenceHeavyReviewModulesOfferBoundedRendering),
|
||||
("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls),
|
||||
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution)
|
||||
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution),
|
||||
("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals)
|
||||
};
|
||||
|
||||
foreach (var test in tests)
|
||||
@ -270,6 +271,77 @@ static void StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution()
|
||||
Assert(openAiClient.Contains("RetryConditionHeaderValue", StringComparison.Ordinal), "OpenAI retry should honour Retry-After metadata.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals()
|
||||
{
|
||||
var root = PlotLineRoot();
|
||||
var viewModel = File.ReadAllText(Path.Combine(root, "ViewModels/StoryIntelligenceGlobalProgressViewModels.cs"));
|
||||
var service = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceGlobalProgressService.cs"));
|
||||
var hub = File.ReadAllText(Path.Combine(root, "Hubs/StoryIntelligenceHub.cs"));
|
||||
var layout = File.ReadAllText(Path.Combine(root, "Views/Shared/_Layout.cshtml"));
|
||||
var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/story-intelligence-global-progress.js"));
|
||||
var migration = File.ReadAllText(Path.Combine(root, "Sql/170_Phase26F_StoryIntelligenceWholeRunProgress.sql"));
|
||||
|
||||
var book = new StoryIntelligenceGlobalProgressBookViewModel
|
||||
{
|
||||
BookTitle = "Test Book",
|
||||
PendingRunCount = 2,
|
||||
RunningRunCount = 3,
|
||||
TotalExpectedScenes = 47,
|
||||
CompletedScenes = 5,
|
||||
FailedScenes = 0,
|
||||
ActiveScenes = 15,
|
||||
TotalDurationMs = TimeSpan.FromMinutes(2).Ticks / TimeSpan.TicksPerMillisecond,
|
||||
Status = StoryIntelligenceRunStatuses.Running
|
||||
};
|
||||
var model = new StoryIntelligenceGlobalProgressViewModel { Books = [book] };
|
||||
|
||||
Assert(model.TotalExpectedScenes == 47, "Global progress denominator should be the expected whole-run scene total.");
|
||||
Assert(model.TotalDetectedScenes == 47, "Legacy detected-scenes accessor should mirror the expected scene total for display.");
|
||||
Assert(model.CompletedScenes == 5, "Completed count should count successful completed scene analyses.");
|
||||
Assert(model.ActiveScenes == 15, "Active scene count should remain distinct from the denominator.");
|
||||
Assert(model.ProgressPercent == 11, "5 of 47 scenes should round to 11%, not 33% from a 15-scene active subset.");
|
||||
Assert(book.SummaryText.Contains("5/47", StringComparison.Ordinal), "Book summary should display 5/47.");
|
||||
|
||||
var finalisingBook = new StoryIntelligenceGlobalProgressBookViewModel
|
||||
{
|
||||
BookTitle = "Test Book",
|
||||
PendingRunCount = 0,
|
||||
RunningRunCount = 1,
|
||||
TotalExpectedScenes = 47,
|
||||
CompletedScenes = 47,
|
||||
FailedScenes = 0,
|
||||
ActiveScenes = 0,
|
||||
TotalDurationMs = TimeSpan.FromMinutes(8).Ticks / TimeSpan.TicksPerMillisecond,
|
||||
Status = "Finalising"
|
||||
};
|
||||
Assert(finalisingBook.ProgressPercent == 100, "Scene progress can be visually full during finalisation.");
|
||||
Assert(finalisingBook.EstimatedRemaining == "Finalising", "All scene analyses complete should show finalising until pipeline state is persisted.");
|
||||
|
||||
var failedBook = new StoryIntelligenceGlobalProgressBookViewModel
|
||||
{
|
||||
BookTitle = "Test Book",
|
||||
PendingRunCount = 0,
|
||||
RunningRunCount = 0,
|
||||
TotalExpectedScenes = 47,
|
||||
CompletedScenes = 46,
|
||||
FailedScenes = 1,
|
||||
ActiveScenes = 0,
|
||||
TotalDurationMs = TimeSpan.FromMinutes(8).Ticks / TimeSpan.TicksPerMillisecond,
|
||||
Status = StoryIntelligenceRunStatuses.CompletedWithWarnings
|
||||
};
|
||||
Assert(failedBook.ProgressPercent == 98, "46 success plus 1 failed must not display as 47 successful scenes.");
|
||||
Assert(failedBook.EstimatedRemaining == "Finalising", "Terminal failures should be excluded from ETA while remaining visible.");
|
||||
|
||||
Assert(service.Contains("TotalExpectedScenes = summary.TotalExpectedScenes", StringComparison.Ordinal), "Global progress service should map total expected scenes.");
|
||||
Assert(hub.Contains("WatchGlobalStoryIntelligenceProgress", StringComparison.Ordinal), "SignalR should expose the aggregate progress calculation.");
|
||||
Assert(layout.Contains("TotalExpectedScenes", StringComparison.Ordinal), "Initial server-rendered strip should use total expected scenes.");
|
||||
Assert(script.Contains("WatchGlobalStoryIntelligenceProgress", StringComparison.Ordinal), "SignalR updates should refresh aggregate progress instead of trusting chapter totals.");
|
||||
Assert(!script.Contains("progress.totalDetectedScenes", StringComparison.Ordinal), "Client should not use per-run TotalDetectedScenes as the denominator.");
|
||||
Assert(migration.Contains("CanonicalSceneScope", StringComparison.Ordinal), "Progress migration should derive expected scope from canonical scenes.");
|
||||
Assert(migration.Contains("ActiveScenes", StringComparison.Ordinal), "Progress migration should keep active scene count distinct.");
|
||||
Assert(migration.Contains("N'Finalising'", StringComparison.Ordinal), "Progress migration should expose a finalising state.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume()
|
||||
{
|
||||
var root = PlotLineRoot();
|
||||
|
||||
@ -13,7 +13,8 @@ namespace PlotLine.Hubs;
|
||||
public sealed class StoryIntelligenceHub(
|
||||
IStoryIntelligenceService storyIntelligence,
|
||||
IStoryIntelligenceResultRepository persistedRuns,
|
||||
IOnboardingStoryIntelligenceService onboardingStoryIntelligence) : Hub
|
||||
IOnboardingStoryIntelligenceService onboardingStoryIntelligence,
|
||||
IStoryIntelligenceGlobalProgressService globalProgress) : Hub
|
||||
{
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
@ -58,6 +59,13 @@ public sealed class StoryIntelligenceHub(
|
||||
return await onboardingStoryIntelligence.GetProgressAsync(batchId);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceGlobalProgressViewModel> WatchGlobalStoryIntelligenceProgress()
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, UserGroup(userId));
|
||||
return await globalProgress.GetForCurrentUserAsync();
|
||||
}
|
||||
|
||||
public static string UserGroup(int userId) => $"story-intelligence:{userId}";
|
||||
|
||||
private static StoryIntelligenceRunProgressEvent ToRunProgress(StoryIntelligenceSavedRun run, string eventType, Scene? currentScene, string? latestSceneSummary)
|
||||
|
||||
@ -354,9 +354,13 @@ public sealed class StoryIntelligenceActiveBookRunSummary
|
||||
public int RunningRunCount { get; init; }
|
||||
public int CompletedRunCount { get; init; }
|
||||
public int FailedRunCount { get; init; }
|
||||
public int TotalExpectedScenes { get; init; }
|
||||
public int TotalDetectedScenes { get; init; }
|
||||
public int CompletedScenes { get; init; }
|
||||
public int FailedScenes { get; init; }
|
||||
public int ActiveScenes { get; init; }
|
||||
public int ProgressPercent { get; init; }
|
||||
public string Status { get; init; } = StoryIntelligenceRunStatuses.Pending;
|
||||
public long TotalDurationMs { get; init; }
|
||||
public DateTime FirstCreatedUtc { get; init; }
|
||||
public DateTime UpdatedUtc { get; init; }
|
||||
|
||||
@ -32,9 +32,11 @@ public sealed class StoryIntelligenceGlobalProgressService(
|
||||
RunningRunCount = summary.RunningRunCount,
|
||||
CompletedRunCount = summary.CompletedRunCount,
|
||||
FailedRunCount = summary.FailedRunCount,
|
||||
TotalDetectedScenes = summary.TotalDetectedScenes,
|
||||
TotalExpectedScenes = summary.TotalExpectedScenes,
|
||||
CompletedScenes = summary.CompletedScenes,
|
||||
FailedScenes = summary.FailedScenes,
|
||||
ActiveScenes = summary.ActiveScenes,
|
||||
Status = summary.Status,
|
||||
TotalDurationMs = summary.TotalDurationMs,
|
||||
UpdatedUtc = summary.UpdatedUtc
|
||||
})
|
||||
|
||||
128
PlotLine/Sql/170_Phase26F_StoryIntelligenceWholeRunProgress.sql
Normal file
128
PlotLine/Sql/170_Phase26F_StoryIntelligenceWholeRunProgress.sql
Normal file
@ -0,0 +1,128 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ListActiveBookSummaryForUser
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
;WITH ActiveScopes AS
|
||||
(
|
||||
SELECT
|
||||
r.UserID,
|
||||
r.ProjectID,
|
||||
r.BookID,
|
||||
LatestCreatedUtc = MAX(r.CreatedUtc)
|
||||
FROM dbo.StoryIntelligenceRuns r
|
||||
WHERE r.UserID = @UserID
|
||||
AND r.Status IN (N'Pending', N'Running')
|
||||
GROUP BY r.UserID, r.ProjectID, r.BookID
|
||||
),
|
||||
ScopedRuns AS
|
||||
(
|
||||
SELECT r.*
|
||||
FROM dbo.StoryIntelligenceRuns r
|
||||
INNER JOIN ActiveScopes scope
|
||||
ON scope.UserID = r.UserID
|
||||
AND ISNULL(scope.ProjectID, -1) = ISNULL(r.ProjectID, -1)
|
||||
AND ISNULL(scope.BookID, -1) = ISNULL(r.BookID, -1)
|
||||
AND r.CreatedUtc >= DATEADD(minute, -30, scope.LatestCreatedUtc)
|
||||
WHERE r.Status IN (N'Pending', N'Running', N'Completed', N'CompletedWithWarnings', N'Failed', N'Cancelled')
|
||||
),
|
||||
CanonicalSceneScope AS
|
||||
(
|
||||
SELECT DISTINCT
|
||||
r.UserID,
|
||||
r.ProjectID,
|
||||
r.BookID,
|
||||
s.SceneID
|
||||
FROM ScopedRuns r
|
||||
INNER JOIN dbo.Chapters c
|
||||
ON c.ChapterID = r.ChapterID
|
||||
AND c.IsArchived = 0
|
||||
INNER JOIN dbo.Scenes s
|
||||
ON s.ChapterID = c.ChapterID
|
||||
AND s.IsArchived = 0
|
||||
),
|
||||
RunSummary AS
|
||||
(
|
||||
SELECT
|
||||
r.UserID,
|
||||
r.ProjectID,
|
||||
r.BookID,
|
||||
COUNT(1) AS RunCount,
|
||||
SUM(CASE WHEN r.Status = N'Pending' THEN 1 ELSE 0 END) AS PendingRunCount,
|
||||
SUM(CASE WHEN r.Status = N'Running' THEN 1 ELSE 0 END) AS RunningRunCount,
|
||||
SUM(CASE WHEN r.Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END) AS CompletedRunCount,
|
||||
SUM(CASE WHEN r.Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) AS FailedRunCount,
|
||||
SUM(ISNULL(r.TotalDetectedScenes, 0)) AS TotalDetectedScenes,
|
||||
SUM(ISNULL(r.CompletedScenes, 0)) AS CompletedScenes,
|
||||
SUM(ISNULL(r.FailedScenes, 0)) AS FailedScenes,
|
||||
SUM(CASE
|
||||
WHEN r.Status = N'Running'
|
||||
THEN CASE
|
||||
WHEN ISNULL(r.TotalDetectedScenes, 0) - ISNULL(r.CompletedScenes, 0) - ISNULL(r.FailedScenes, 0) > 0
|
||||
THEN ISNULL(r.TotalDetectedScenes, 0) - ISNULL(r.CompletedScenes, 0) - ISNULL(r.FailedScenes, 0)
|
||||
ELSE 0
|
||||
END
|
||||
ELSE 0
|
||||
END) AS ActiveScenes,
|
||||
SUM(ISNULL(r.TotalDurationMs, 0)) AS TotalDurationMs,
|
||||
MIN(r.CreatedUtc) AS FirstCreatedUtc,
|
||||
MAX(r.UpdatedUtc) AS UpdatedUtc
|
||||
FROM ScopedRuns r
|
||||
GROUP BY r.UserID, r.ProjectID, r.BookID
|
||||
),
|
||||
ExpectedSceneSummary AS
|
||||
(
|
||||
SELECT
|
||||
scope.UserID,
|
||||
scope.ProjectID,
|
||||
scope.BookID,
|
||||
COUNT(scope.SceneID) AS TotalExpectedScenes
|
||||
FROM CanonicalSceneScope scope
|
||||
GROUP BY scope.UserID, scope.ProjectID, scope.BookID
|
||||
)
|
||||
SELECT
|
||||
summary.UserID,
|
||||
summary.ProjectID,
|
||||
p.ProjectName AS ProjectTitle,
|
||||
summary.BookID,
|
||||
b.BookTitle,
|
||||
b.Subtitle AS BookSubtitle,
|
||||
summary.RunCount,
|
||||
summary.PendingRunCount,
|
||||
summary.RunningRunCount,
|
||||
summary.CompletedRunCount,
|
||||
summary.FailedRunCount,
|
||||
COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) AS TotalExpectedScenes,
|
||||
summary.TotalDetectedScenes,
|
||||
summary.CompletedScenes,
|
||||
summary.FailedScenes,
|
||||
summary.ActiveScenes,
|
||||
CASE
|
||||
WHEN COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) <= 0 THEN 0
|
||||
ELSE CONVERT(int, ROUND(summary.CompletedScenes * 100.0 / COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 1), 0))
|
||||
END AS ProgressPercent,
|
||||
CASE
|
||||
WHEN summary.RunningRunCount > 0
|
||||
AND COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) > 0
|
||||
AND summary.CompletedScenes + summary.FailedScenes >= COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0)
|
||||
THEN N'Finalising'
|
||||
WHEN summary.FailedRunCount > 0 OR summary.FailedScenes > 0 THEN N'CompletedWithWarnings'
|
||||
WHEN summary.RunningRunCount > 0 THEN N'Running'
|
||||
WHEN summary.PendingRunCount > 0 THEN N'Pending'
|
||||
ELSE N'Completed'
|
||||
END AS Status,
|
||||
summary.TotalDurationMs,
|
||||
summary.FirstCreatedUtc,
|
||||
summary.UpdatedUtc
|
||||
FROM RunSummary summary
|
||||
LEFT JOIN ExpectedSceneSummary expected
|
||||
ON expected.UserID = summary.UserID
|
||||
AND ISNULL(expected.ProjectID, -1) = ISNULL(summary.ProjectID, -1)
|
||||
AND ISNULL(expected.BookID, -1) = ISNULL(summary.BookID, -1)
|
||||
LEFT JOIN dbo.Projects p ON p.ProjectID = summary.ProjectID
|
||||
LEFT JOIN dbo.Books b ON b.BookID = summary.BookID
|
||||
WHERE summary.PendingRunCount > 0 OR summary.RunningRunCount > 0
|
||||
ORDER BY summary.UpdatedUtc DESC, summary.FirstCreatedUtc;
|
||||
END;
|
||||
GO
|
||||
@ -1,3 +1,5 @@
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.ViewModels;
|
||||
|
||||
public sealed class StoryIntelligenceGlobalProgressViewModel
|
||||
@ -6,12 +8,21 @@ public sealed class StoryIntelligenceGlobalProgressViewModel
|
||||
public bool HasActiveWork => Books.Any(book => book.HasActiveRuns);
|
||||
public int ActiveBookCount => Books.Count(book => book.HasActiveRuns);
|
||||
public int ActiveRunCount => Books.Sum(book => book.PendingRunCount + book.RunningRunCount);
|
||||
public int TotalDetectedScenes => Books.Sum(book => book.TotalDetectedScenes);
|
||||
public int TotalExpectedScenes => Books.Sum(book => book.TotalExpectedScenes);
|
||||
public int TotalDetectedScenes => TotalExpectedScenes;
|
||||
public int CompletedScenes => Books.Sum(book => book.CompletedScenes);
|
||||
public int FailedScenes => Books.Sum(book => book.FailedScenes);
|
||||
public int ProgressPercent => TotalDetectedScenes <= 0
|
||||
public int ActiveScenes => Books.Sum(book => book.ActiveScenes);
|
||||
public int ProgressPercent => TotalExpectedScenes <= 0
|
||||
? 0
|
||||
: Math.Clamp(Convert.ToInt32(Math.Round(CompletedScenes * 100m / TotalDetectedScenes)), 0, 100);
|
||||
: Math.Clamp(Convert.ToInt32(Math.Round(CompletedScenes * 100m / TotalExpectedScenes)), 0, 100);
|
||||
public string Status => Books.Any(book => book.Status == "Finalising")
|
||||
? "Finalising"
|
||||
: Books.Any(book => book.HasFailures)
|
||||
? StoryIntelligenceRunStatuses.CompletedWithWarnings
|
||||
: Books.Any(book => book.HasActiveRuns)
|
||||
? StoryIntelligenceRunStatuses.Running
|
||||
: StoryIntelligenceRunStatuses.Completed;
|
||||
public string SummaryText => ActiveBookCount switch
|
||||
{
|
||||
0 => string.Empty,
|
||||
@ -30,34 +41,40 @@ public sealed class StoryIntelligenceGlobalProgressBookViewModel
|
||||
public int RunningRunCount { get; init; }
|
||||
public int CompletedRunCount { get; init; }
|
||||
public int FailedRunCount { get; init; }
|
||||
public int TotalDetectedScenes { get; init; }
|
||||
public int TotalExpectedScenes { get; init; }
|
||||
public int TotalDetectedScenes => TotalExpectedScenes;
|
||||
public int CompletedScenes { get; init; }
|
||||
public int FailedScenes { get; init; }
|
||||
public int ActiveScenes { get; init; }
|
||||
public string Status { get; init; } = StoryIntelligenceRunStatuses.Pending;
|
||||
public long TotalDurationMs { get; init; }
|
||||
public DateTime UpdatedUtc { get; init; }
|
||||
|
||||
public bool HasActiveRuns => PendingRunCount > 0 || RunningRunCount > 0;
|
||||
public bool HasFailures => FailedRunCount > 0 || FailedScenes > 0;
|
||||
public int ProgressPercent => TotalDetectedScenes <= 0
|
||||
public int ProgressPercent => TotalExpectedScenes <= 0
|
||||
? 0
|
||||
: Math.Clamp(Convert.ToInt32(Math.Round(CompletedScenes * 100m / TotalDetectedScenes)), 0, 100);
|
||||
public string SummaryText => $"{BookTitle}: {CompletedScenes:N0}/{Math.Max(TotalDetectedScenes, CompletedScenes):N0} scenes";
|
||||
public string EstimatedRemaining => EstimateRemaining(TotalDurationMs, CompletedScenes, TotalDetectedScenes);
|
||||
: Math.Clamp(Convert.ToInt32(Math.Round(CompletedScenes * 100m / TotalExpectedScenes)), 0, 100);
|
||||
public string SummaryText => $"{BookTitle}: {CompletedScenes:N0}/{Math.Max(TotalExpectedScenes, CompletedScenes):N0} scenes";
|
||||
public string EstimatedRemaining => Status == "Finalising"
|
||||
? "Finalising"
|
||||
: EstimateRemaining(TotalDurationMs, CompletedScenes, FailedScenes, TotalExpectedScenes);
|
||||
|
||||
private static string EstimateRemaining(long elapsedMs, int completedScenes, int totalScenes)
|
||||
private static string EstimateRemaining(long elapsedMs, int completedScenes, int failedScenes, int totalScenes)
|
||||
{
|
||||
if (totalScenes <= 0 || completedScenes <= 0)
|
||||
{
|
||||
return "Calculating";
|
||||
}
|
||||
|
||||
if (completedScenes >= totalScenes)
|
||||
var remainingScenes = Math.Max(0, totalScenes - completedScenes - failedScenes);
|
||||
if (remainingScenes == 0)
|
||||
{
|
||||
return "Finalising";
|
||||
}
|
||||
|
||||
var averageSceneMs = elapsedMs / Math.Max(1, completedScenes);
|
||||
var remaining = TimeSpan.FromMilliseconds(averageSceneMs * (totalScenes - completedScenes));
|
||||
var remaining = TimeSpan.FromMilliseconds(averageSceneMs * remainingScenes);
|
||||
if (remaining.TotalMinutes >= 1)
|
||||
{
|
||||
return $"{Math.Max(1, (int)Math.Round(remaining.TotalMinutes)):N0} min";
|
||||
|
||||
@ -234,6 +234,7 @@
|
||||
<div class="story-intelligence-global-strip"
|
||||
data-story-intelligence-global-strip
|
||||
data-progress-percent="@storyIntelligenceProgress.ProgressPercent"
|
||||
data-total-expected-scenes="@storyIntelligenceProgress.TotalExpectedScenes"
|
||||
data-active-books="@storyIntelligenceProgress.ActiveBookCount"
|
||||
data-book-id="@primaryBook?.BookID"
|
||||
data-project-id="@primaryBook?.ProjectID"
|
||||
@ -244,7 +245,11 @@
|
||||
<span class="story-intelligence-global-strip__dot" aria-hidden="true"></span>
|
||||
<strong data-story-intelligence-global-title>@storyIntelligenceProgress.SummaryText</strong>
|
||||
<span data-story-intelligence-global-detail>
|
||||
@storyIntelligenceProgress.CompletedScenes.ToString("N0") of @storyIntelligenceProgress.TotalDetectedScenes.ToString("N0") scenes analysed
|
||||
@storyIntelligenceProgress.CompletedScenes.ToString("N0") of @storyIntelligenceProgress.TotalExpectedScenes.ToString("N0") scenes analysed
|
||||
@if (storyIntelligenceProgress.FailedScenes > 0)
|
||||
{
|
||||
<text> · @storyIntelligenceProgress.FailedScenes.ToString("N0") failed</text>
|
||||
}
|
||||
@if (primaryBook is not null)
|
||||
{
|
||||
<text> · @primaryBook.EstimatedRemaining remaining</text>
|
||||
|
||||
@ -7,24 +7,28 @@
|
||||
const title = strip.querySelector("[data-story-intelligence-global-title]");
|
||||
const detail = strip.querySelector("[data-story-intelligence-global-detail]");
|
||||
const bar = strip.querySelector("[data-story-intelligence-global-bar]");
|
||||
let refreshTimer = null;
|
||||
const readNumber = (value) => {
|
||||
const parsed = Number.parseInt(value || "", 10);
|
||||
return Number.isInteger(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const trackedBookId = readNumber(strip.dataset.bookId);
|
||||
const trackedProjectId = readNumber(strip.dataset.projectId);
|
||||
const formatCount = (value) => new Intl.NumberFormat().format(Math.max(0, value || 0));
|
||||
const estimateRemaining = (elapsedMs, completed, total) => {
|
||||
const estimateRemaining = (elapsedMs, completed, failed, total, status) => {
|
||||
if (status === "Finalising") {
|
||||
return "Finalising";
|
||||
}
|
||||
|
||||
if (!total || !completed) {
|
||||
return "Calculating";
|
||||
}
|
||||
|
||||
if (completed >= total) {
|
||||
const remainingScenes = Math.max(0, total - completed - failed);
|
||||
if (remainingScenes === 0) {
|
||||
return "Finalising";
|
||||
}
|
||||
|
||||
const remainingMs = (elapsedMs / completed) * (total - completed);
|
||||
const remainingMs = (elapsedMs / completed) * remainingScenes;
|
||||
const remainingSeconds = Math.max(1, Math.round(remainingMs / 1000));
|
||||
if (remainingSeconds >= 60) {
|
||||
return `${Math.max(1, Math.round(remainingSeconds / 60))} min`;
|
||||
@ -33,26 +37,22 @@
|
||||
return `${remainingSeconds} sec`;
|
||||
};
|
||||
|
||||
const update = (progress) => {
|
||||
if (!progress) {
|
||||
const render = (progress) => {
|
||||
const books = progress?.books || progress?.Books || [];
|
||||
if (!books.length) {
|
||||
strip.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const bookId = readNumber(progress.bookID ?? progress.bookId);
|
||||
const projectId = readNumber(progress.projectID ?? progress.projectId);
|
||||
if (trackedBookId && bookId && trackedBookId !== bookId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackedBookId && trackedProjectId && projectId && trackedProjectId !== projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = progress.status || progress.Status || "";
|
||||
const total = readNumber(progress.totalDetectedScenes ?? progress.TotalDetectedScenes) || 0;
|
||||
strip.hidden = false;
|
||||
const primary = books.find((book) => book.hasActiveRuns || book.HasActiveRuns) || books[0];
|
||||
const activeBookCount = readNumber(progress.activeBookCount ?? progress.ActiveBookCount) || books.length;
|
||||
const status = progress.status || progress.Status || primary.status || primary.Status || "";
|
||||
const total = readNumber(progress.totalExpectedScenes ?? progress.TotalExpectedScenes) || 0;
|
||||
const completed = readNumber(progress.completedScenes ?? progress.CompletedScenes) || 0;
|
||||
const failed = readNumber(progress.failedScenes ?? progress.FailedScenes) || 0;
|
||||
const elapsedMs = readNumber(progress.totalDurationMs ?? progress.TotalDurationMs) || 0;
|
||||
const active = readNumber(progress.activeScenes ?? progress.ActiveScenes) || 0;
|
||||
const elapsedMs = readNumber(primary.totalDurationMs ?? primary.TotalDurationMs) || 0;
|
||||
const percent = total > 0 ? Math.max(0, Math.min(100, Math.round((completed / total) * 100))) : 0;
|
||||
|
||||
if (bar) {
|
||||
@ -60,12 +60,16 @@
|
||||
}
|
||||
|
||||
if (detail) {
|
||||
const remaining = estimateRemaining(elapsedMs, completed, total);
|
||||
detail.textContent = `${formatCount(completed)} of ${formatCount(total)} scenes analysed \u00b7 ${remaining} remaining`;
|
||||
const remaining = estimateRemaining(elapsedMs, completed, failed, total, status);
|
||||
const failureText = failed > 0 ? ` \u00b7 ${formatCount(failed)} failed` : "";
|
||||
const activeText = active > 0 ? ` \u00b7 ${formatCount(active)} active` : "";
|
||||
detail.textContent = `${formatCount(completed)} of ${formatCount(total)} scenes analysed${failureText}${activeText} \u00b7 ${remaining} remaining`;
|
||||
}
|
||||
|
||||
if (title && status) {
|
||||
if (status === "Failed") {
|
||||
if (status === "Finalising") {
|
||||
title.textContent = "Finalising Story Intelligence";
|
||||
} else if (status === "Failed") {
|
||||
title.textContent = "Story Intelligence needs attention";
|
||||
} else if (status === "Cancelled") {
|
||||
title.textContent = "Story Intelligence cancelled";
|
||||
@ -74,17 +78,36 @@
|
||||
window.setTimeout(() => {
|
||||
strip.hidden = true;
|
||||
}, 10000);
|
||||
} else if (activeBookCount > 1) {
|
||||
title.textContent = `${formatCount(activeBookCount)} books analysing`;
|
||||
} else {
|
||||
title.textContent = primary.summaryText || primary.SummaryText || title.textContent;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
connection.invoke("WatchGlobalStoryIntelligenceProgress")
|
||||
.then(render)
|
||||
.catch(() => {
|
||||
strip.dataset.connection = "offline";
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleRefresh = () => {
|
||||
window.clearTimeout(refreshTimer);
|
||||
refreshTimer = window.setTimeout(refresh, 350);
|
||||
};
|
||||
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl("/hubs/story-intelligence")
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
connection.on("StoryIntelligenceRunProgressChanged", update);
|
||||
connection.start().catch(() => {
|
||||
strip.dataset.connection = "offline";
|
||||
});
|
||||
connection.on("StoryIntelligenceRunProgressChanged", scheduleRefresh);
|
||||
connection.start()
|
||||
.then(refresh)
|
||||
.catch(() => {
|
||||
strip.dataset.connection = "offline";
|
||||
});
|
||||
})();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user