Phase 20AC – Story Intelligence Progress Page UX and Status Clarity
This commit is contained in:
parent
8d493afb43
commit
908660db1c
@ -20,8 +20,16 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
|||||||
[HttpGet("scan-review")]
|
[HttpGet("scan-review")]
|
||||||
public async Task<IActionResult> ScanReview(Guid? previewId)
|
public async Task<IActionResult> ScanReview(Guid? previewId)
|
||||||
{
|
{
|
||||||
var model = await onboarding.GetScanReviewAsync(previewId);
|
var model = await onboarding.GetScanReviewAsync(previewId)
|
||||||
return model is null ? NotFound() : View(model);
|
?? (previewId.HasValue ? await onboarding.GetScanReviewAsync() : null);
|
||||||
|
if (model is null)
|
||||||
|
{
|
||||||
|
TempData["ArchiveError"] = "The scan review could not be opened. Please scan the manuscript again.";
|
||||||
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("build-complete")]
|
[HttpGet("build-complete")]
|
||||||
@ -61,6 +69,14 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
|||||||
return RedirectToAction("Index", "Projects");
|
return RedirectToAction("Index", "Projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("scan-manuscript")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ScanManuscript()
|
||||||
|
{
|
||||||
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("story-intelligence/progress")]
|
[HttpGet("story-intelligence/progress")]
|
||||||
public async Task<IActionResult> StoryIntelligenceProgress(Guid batchId)
|
public async Task<IActionResult> StoryIntelligenceProgress(Guid batchId)
|
||||||
{
|
{
|
||||||
@ -126,7 +142,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
|||||||
? "Review saved. Next: prepare chapters and analyse manuscript."
|
? "Review saved. Next: prepare chapters and analyse manuscript."
|
||||||
: "Review choices saved.";
|
: "Review choices saved.";
|
||||||
return readyToImport
|
return readyToImport
|
||||||
? RedirectToAction(nameof(Index))
|
? RedirectToAction(nameof(StoryIntelligence))
|
||||||
: RedirectToAction(nameof(ScanReview), new { previewId = form.PreviewID });
|
: RedirectToAction(nameof(ScanReview), new { previewId = form.PreviewID });
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
|
|||||||
@ -74,6 +74,7 @@ public sealed class StoryIntelligenceHub(
|
|||||||
FailedScenes = run.FailedScenes,
|
FailedScenes = run.FailedScenes,
|
||||||
TotalTokens = run.TotalTokens,
|
TotalTokens = run.TotalTokens,
|
||||||
TotalDurationMs = elapsedMs,
|
TotalDurationMs = elapsedMs,
|
||||||
|
CurrentSceneNumber = run.CompletedScenes,
|
||||||
ElapsedTime = FormatDuration(elapsedMs),
|
ElapsedTime = FormatDuration(elapsedMs),
|
||||||
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes, totalScenes),
|
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes, totalScenes),
|
||||||
ErrorMessage = run.ErrorMessage,
|
ErrorMessage = run.ErrorMessage,
|
||||||
|
|||||||
@ -106,6 +106,11 @@ public sealed class WordCompanionFollowHub(
|
|||||||
{
|
{
|
||||||
var userId = RequireUserId();
|
var userId = RequireUserId();
|
||||||
var state = await onboarding.GetAsync(userId);
|
var state = await onboarding.GetAsync(userId);
|
||||||
|
if (state is not null)
|
||||||
|
{
|
||||||
|
preview = NormalisePreviewContext(preview, userId, state);
|
||||||
|
}
|
||||||
|
|
||||||
if (state is null
|
if (state is null
|
||||||
|| state.UserOnboardingStateID != preview.OnboardingID
|
|| state.UserOnboardingStateID != preview.OnboardingID
|
||||||
|| state.ProjectID != preview.ProjectID
|
|| state.ProjectID != preview.ProjectID
|
||||||
@ -185,4 +190,25 @@ public sealed class WordCompanionFollowHub(
|
|||||||
private static bool IsMicrosoftWordState(UserOnboardingState state)
|
private static bool IsMicrosoftWordState(UserOnboardingState state)
|
||||||
=> string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal)
|
=> string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal)
|
||||||
|| state.UsesWordCompanion == true;
|
|| state.UsesWordCompanion == true;
|
||||||
|
|
||||||
|
private static ManuscriptScanPreview NormalisePreviewContext(ManuscriptScanPreview preview, int userId, UserOnboardingState state)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
PreviewID = preview.PreviewID,
|
||||||
|
UserID = preview.UserID == 0 ? userId : preview.UserID,
|
||||||
|
OnboardingID = preview.OnboardingID == 0 ? state.UserOnboardingStateID : preview.OnboardingID,
|
||||||
|
ProjectID = preview.ProjectID == 0 && state.ProjectID.HasValue ? state.ProjectID.Value : preview.ProjectID,
|
||||||
|
BookID = preview.BookID == 0 && state.BookID.HasValue ? state.BookID.Value : preview.BookID,
|
||||||
|
Source = preview.Source,
|
||||||
|
DocumentTitle = preview.DocumentTitle,
|
||||||
|
CompanionDocumentIdentifier = preview.CompanionDocumentIdentifier,
|
||||||
|
TotalWordCount = preview.TotalWordCount,
|
||||||
|
ChapterCount = preview.ChapterCount,
|
||||||
|
SceneCount = preview.SceneCount,
|
||||||
|
CharacterCandidateCount = preview.CharacterCandidateCount,
|
||||||
|
CreatedUtc = preview.CreatedUtc,
|
||||||
|
Chapters = preview.Chapters,
|
||||||
|
Scenes = preview.Scenes,
|
||||||
|
CharacterCandidates = preview.CharacterCandidates
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,7 @@ public sealed class StoryIntelligenceRunProgressEvent
|
|||||||
public int? FailedScenes { get; init; }
|
public int? FailedScenes { get; init; }
|
||||||
public int? TotalTokens { get; init; }
|
public int? TotalTokens { get; init; }
|
||||||
public long? TotalDurationMs { get; init; }
|
public long? TotalDurationMs { get; init; }
|
||||||
|
public int? CurrentSceneNumber { get; init; }
|
||||||
public string ElapsedTime { get; init; } = "0 sec";
|
public string ElapsedTime { get; init; } = "0 sec";
|
||||||
public string? EstimatedRemaining { get; init; }
|
public string? EstimatedRemaining { get; init; }
|
||||||
public string? LatestSceneSummary { get; init; }
|
public string? LatestSceneSummary { get; init; }
|
||||||
|
|||||||
@ -77,6 +77,7 @@ public sealed class OnboardingService(
|
|||||||
{
|
{
|
||||||
CurrentStep = step,
|
CurrentStep = step,
|
||||||
StepNumber = StepNumber(step),
|
StepNumber = StepNumber(step),
|
||||||
|
TotalSteps = 9,
|
||||||
WritingJourney = state.WritingJourney,
|
WritingJourney = state.WritingJourney,
|
||||||
WritingSoftware = state.WritingSoftware,
|
WritingSoftware = state.WritingSoftware,
|
||||||
ProjectID = state.ProjectID,
|
ProjectID = state.ProjectID,
|
||||||
|
|||||||
@ -268,7 +268,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
totalDetectedScenes: sceneBlocks.Count,
|
totalDetectedScenes: sceneBlocks.Count,
|
||||||
completedScenes: completedScenes,
|
completedScenes: completedScenes,
|
||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens);
|
totalTokens: totals.TotalTokens,
|
||||||
|
currentSceneNumber: block.TemporarySceneNumber);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -341,6 +342,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
completedScenes: completedScenes,
|
completedScenes: completedScenes,
|
||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens,
|
totalTokens: totals.TotalTokens,
|
||||||
|
currentSceneNumber: block.TemporarySceneNumber,
|
||||||
latestSceneSummary: TrimOptional(sceneAttempt.Parsed?.Summary?.Short, 220),
|
latestSceneSummary: TrimOptional(sceneAttempt.Parsed?.Summary?.Short, 220),
|
||||||
recentDiscoveries: BuildDiscoveries(sceneAttempt.Parsed));
|
recentDiscoveries: BuildDiscoveries(sceneAttempt.Parsed));
|
||||||
if (!validation.IsValid || validation.Warnings.Count > 0)
|
if (!validation.IsValid || validation.Warnings.Count > 0)
|
||||||
@ -363,6 +365,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
completedScenes: completedScenes,
|
completedScenes: completedScenes,
|
||||||
failedScenes: failedScenes,
|
failedScenes: failedScenes,
|
||||||
totalTokens: totals.TotalTokens,
|
totalTokens: totals.TotalTokens,
|
||||||
|
currentSceneNumber: block.TemporarySceneNumber,
|
||||||
errorMessage: ex.Message);
|
errorMessage: ex.Message);
|
||||||
logger.LogWarning(ex, "Story Intelligence scene failed for run {StoryIntelligenceRunID}, scene {TemporarySceneNumber}.", run.StoryIntelligenceRunID, block.TemporarySceneNumber);
|
logger.LogWarning(ex, "Story Intelligence scene failed for run {StoryIntelligenceRunID}, scene {TemporarySceneNumber}.", run.StoryIntelligenceRunID, block.TemporarySceneNumber);
|
||||||
}
|
}
|
||||||
@ -487,6 +490,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
int? completedScenes = null,
|
int? completedScenes = null,
|
||||||
int? failedScenes = null,
|
int? failedScenes = null,
|
||||||
int? totalTokens = null,
|
int? totalTokens = null,
|
||||||
|
int? currentSceneNumber = null,
|
||||||
string? latestSceneSummary = null,
|
string? latestSceneSummary = null,
|
||||||
IReadOnlyList<string>? recentDiscoveries = null,
|
IReadOnlyList<string>? recentDiscoveries = null,
|
||||||
string? errorMessage = null)
|
string? errorMessage = null)
|
||||||
@ -507,6 +511,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
FailedScenes = failedScenes,
|
FailedScenes = failedScenes,
|
||||||
TotalTokens = totalTokens,
|
TotalTokens = totalTokens,
|
||||||
TotalDurationMs = elapsedMs,
|
TotalDurationMs = elapsedMs,
|
||||||
|
CurrentSceneNumber = currentSceneNumber,
|
||||||
ElapsedTime = FormatDuration(elapsedMs),
|
ElapsedTime = FormatDuration(elapsedMs),
|
||||||
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes ?? 0, totalDetectedScenes ?? 0),
|
EstimatedRemaining = EstimateRemaining(elapsedMs, completedScenes ?? 0, totalDetectedScenes ?? 0),
|
||||||
LatestSceneSummary = latestSceneSummary,
|
LatestSceneSummary = latestSceneSummary,
|
||||||
@ -558,19 +563,19 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Take(3)
|
.Take(3)
|
||||||
.Select(name => $"Character detected: {name}"));
|
.Select(name => $"Possible character detected: {name}"));
|
||||||
discoveries.AddRange((scene.Locations ?? [])
|
discoveries.AddRange((scene.Locations ?? [])
|
||||||
.Select(location => TrimOptional(location.Name, 120))
|
.Select(location => TrimOptional(location.Name, 120))
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Take(3)
|
.Take(3)
|
||||||
.Select(name => $"Location detected: {name}"));
|
.Select(name => $"Possible location detected: {name}"));
|
||||||
discoveries.AddRange((scene.Assets ?? [])
|
discoveries.AddRange((scene.Assets ?? [])
|
||||||
.Select(asset => TrimOptional(asset.Name, 120))
|
.Select(asset => TrimOptional(asset.Name, 120))
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Take(2)
|
.Take(2)
|
||||||
.Select(name => $"Asset detected: {name}"));
|
.Select(name => $"Possible asset detected: {name}"));
|
||||||
discoveries.AddRange((scene.Relationships ?? [])
|
discoveries.AddRange((scene.Relationships ?? [])
|
||||||
.Select(relationship => BuildRelationshipDiscovery(relationship))
|
.Select(relationship => BuildRelationshipDiscovery(relationship))
|
||||||
.Where(text => !string.IsNullOrWhiteSpace(text))
|
.Where(text => !string.IsNullOrWhiteSpace(text))
|
||||||
@ -580,7 +585,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
.Select(clue => TrimOptional(clue.Clue, 160))
|
.Select(clue => TrimOptional(clue.Clue, 160))
|
||||||
.Where(text => !string.IsNullOrWhiteSpace(text))
|
.Where(text => !string.IsNullOrWhiteSpace(text))
|
||||||
.Take(2)
|
.Take(2)
|
||||||
.Select(text => $"Timeline clue detected: {text}"));
|
.Select(text => $"Timeline clue found: {text}"));
|
||||||
|
|
||||||
return discoveries
|
return discoveries
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
@ -599,8 +604,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(signal)
|
return string.IsNullOrWhiteSpace(signal)
|
||||||
? $"Relationship detected: {characterA} and {characterB}"
|
? $"Relationship signal found: {characterA} and {characterB}"
|
||||||
: $"Relationship detected: {characterA} and {characterB} - {signal}";
|
: $"Relationship signal found: {characterA} and {characterB} - {signal}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TrimOptional(string? value, int maxLength)
|
private static string? TrimOptional(string? value, int maxLength)
|
||||||
|
|||||||
@ -147,30 +147,13 @@ public sealed class StoryIntelligenceService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var state = await onboarding.GetAsync(currentUser.UserId.Value);
|
var state = await onboarding.GetAsync(currentUser.UserId.Value);
|
||||||
var latest = await repository.GetLatestForUserAsync(currentUser.UserId.Value);
|
|
||||||
if (latest is not null)
|
|
||||||
{
|
|
||||||
return new StoryIntelligenceDashboardViewModel
|
|
||||||
{
|
|
||||||
ShouldShow = true,
|
|
||||||
Job = ToProgress(latest),
|
|
||||||
Title = latest.IsCompleted ? "Story Intelligence ready" : latest.IsActive ? "Story Intelligence preparing" : "Story Intelligence available",
|
|
||||||
Description = latest.IsCompleted
|
|
||||||
? "The analysis framework is prepared for your manuscript."
|
|
||||||
: latest.IsActive
|
|
||||||
? latest.CurrentMessage
|
|
||||||
: "You can prepare Story Intelligence for your built manuscript.",
|
|
||||||
ButtonText = latest.IsCompleted ? "View completion" : latest.IsActive ? "View progress" : "Set up Story Intelligence"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state?.ProjectID.HasValue == true && state.BookID.HasValue
|
return state?.ProjectID.HasValue == true && state.BookID.HasValue
|
||||||
? new StoryIntelligenceDashboardViewModel
|
? new StoryIntelligenceDashboardViewModel
|
||||||
{
|
{
|
||||||
ShouldShow = true,
|
ShouldShow = true,
|
||||||
Title = "Story Intelligence available",
|
Title = "Import your manuscript",
|
||||||
Description = "Prepare the optional analysis framework for your manuscript.",
|
Description = "Scan your manuscript with the Word Companion, review the chapters, then let PlotDirector prepare scene suggestions.",
|
||||||
ButtonText = "Set up Story Intelligence"
|
ButtonText = "Continue manuscript import"
|
||||||
}
|
}
|
||||||
: new StoryIntelligenceDashboardViewModel();
|
: new StoryIntelligenceDashboardViewModel();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
new { Label = "Connect Word", Order = 5 },
|
new { Label = "Connect Word", Order = 5 },
|
||||||
new { Label = "Scan", Order = 6 },
|
new { Label = "Scan", Order = 6 },
|
||||||
new { Label = "Review", Order = 7 },
|
new { Label = "Review", Order = 7 },
|
||||||
new { Label = "Prepare Chapters", Order = 8 },
|
new { Label = "Analyse", Order = 8 },
|
||||||
new { Label = "Complete", Order = 9 }
|
new { Label = "Complete", Order = 9 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -294,6 +294,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
<section class="onboarding-scan-panel"
|
<section class="onboarding-scan-panel"
|
||||||
data-onboarding-scan-panel
|
data-onboarding-scan-panel
|
||||||
|
data-onboarding-auto-review
|
||||||
data-scan-status="@Model.ScanState.Status"
|
data-scan-status="@Model.ScanState.Status"
|
||||||
aria-label="Manuscript scan">
|
aria-label="Manuscript scan">
|
||||||
<div class="onboarding-scan-status">
|
<div class="onboarding-scan-status">
|
||||||
@ -339,20 +340,17 @@
|
|||||||
{
|
{
|
||||||
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
||||||
<div>
|
<div>
|
||||||
<p class="onboarding-scan-next">Review saved. PlotDirector can now create the approved structure.</p>
|
<p class="onboarding-scan-next">Review saved. PlotDirector can now analyse the approved chapters.</p>
|
||||||
<p class="onboarding-build-message" data-onboarding-build-message>Ready to prepare approved chapters for Story Intelligence.</p>
|
<p class="onboarding-build-message" data-onboarding-build-message>Next: analyse the manuscript and review suggested scenes.</p>
|
||||||
</div>
|
</div>
|
||||||
<strong data-onboarding-build-percent></strong>
|
<strong data-onboarding-build-percent></strong>
|
||||||
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
||||||
<span data-onboarding-build-progress></span>
|
<span data-onboarding-build-progress></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-success"
|
<a class="btn btn-success" asp-controller="Onboarding" asp-action="StoryIntelligence">
|
||||||
type="button"
|
Analyse manuscript
|
||||||
data-onboarding-build-start
|
</a>
|
||||||
data-preview-id="@Model.ScanState.PreviewID">
|
|
||||||
Prepare Chapters
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -384,9 +382,24 @@
|
|||||||
{
|
{
|
||||||
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Go to Project Overview</a>
|
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Go to Project Overview</a>
|
||||||
}
|
}
|
||||||
<form asp-controller="Onboarding" asp-action="Finish" method="post">
|
@if (Model.IsMicrosoftWordPath && string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
||||||
<button class="btn btn-primary" type="submit">@(Model.IsMicrosoftWordPath ? "Continue" : "Finish setup for now")</button>
|
{
|
||||||
</form>
|
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">Analyse manuscript</a>
|
||||||
|
}
|
||||||
|
else if (Model.IsMicrosoftWordPath && Model.ScanState.IsComplete)
|
||||||
|
{
|
||||||
|
<span class="text-muted">Review the scan above to continue.</span>
|
||||||
|
}
|
||||||
|
else if (Model.IsMicrosoftWordPath)
|
||||||
|
{
|
||||||
|
<span class="text-muted">Scan your manuscript to continue.</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-controller="Onboarding" asp-action="Finish" method="post">
|
||||||
|
<button class="btn btn-primary" type="submit">Finish setup for now</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,7 +25,12 @@
|
|||||||
<section class="onboarding-story-card">
|
<section class="onboarding-story-card">
|
||||||
<h2>Analysis has already started</h2>
|
<h2>Analysis has already started</h2>
|
||||||
<p>You can continue reviewing progress for the approved chapters.</p>
|
<p>You can continue reviewing progress for the approved chapters.</p>
|
||||||
<a class="btn btn-primary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.ExistingBatchID">Review scenes</a>
|
<div class="onboarding-actions">
|
||||||
|
<a class="btn btn-primary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.ExistingBatchID">Review scenes</a>
|
||||||
|
<form asp-action="ScanManuscript" method="post">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -70,6 +75,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="onboarding-actions">
|
<div class="onboarding-actions">
|
||||||
|
<form asp-action="ScanManuscript" method="post">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||||
|
</form>
|
||||||
<form asp-action="SkipStoryIntelligence" method="post">
|
<form asp-action="SkipStoryIntelligence" method="post">
|
||||||
<button class="btn btn-outline-secondary" type="submit">Skip for now</button>
|
<button class="btn btn-outline-secondary" type="submit">Skip for now</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
@model StoryIntelligenceProgressViewModel
|
@model StoryIntelligenceProgressViewModel
|
||||||
|
@using System.Security.Claims
|
||||||
|
@inject IConfiguration Configuration
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Review scenes";
|
ViewData["Title"] = "Review scenes";
|
||||||
var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID));
|
var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID));
|
||||||
var progressPercent = ProgressPercent(Model);
|
var progressPercent = ProgressPercent(Model);
|
||||||
|
var userEmail = User.FindFirstValue(ClaimTypes.Email);
|
||||||
|
var adminEmails = Configuration.GetSection("Admin:AllowedEmails").Get<string[]>() ?? [];
|
||||||
|
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
||||||
|
&& adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="onboarding-shell" aria-labelledby="story-progress-title">
|
<section class="onboarding-shell" aria-labelledby="story-progress-title">
|
||||||
@ -10,11 +16,12 @@
|
|||||||
data-story-intelligence-onboarding
|
data-story-intelligence-onboarding
|
||||||
data-story-intelligence-batch-id="@Model.BatchID"
|
data-story-intelligence-batch-id="@Model.BatchID"
|
||||||
data-story-intelligence-run-ids="@runIds"
|
data-story-intelligence-run-ids="@runIds"
|
||||||
|
data-story-max-progress-percent="@progressPercent"
|
||||||
data-story-intelligence-has-active-runs="@Model.HasActiveRuns.ToString().ToLowerInvariant()">
|
data-story-intelligence-has-active-runs="@Model.HasActiveRuns.ToString().ToLowerInvariant()">
|
||||||
<div class="onboarding-copy">
|
<div class="onboarding-copy">
|
||||||
<p class="eyebrow">@Model.BookTitle</p>
|
<p class="eyebrow">@Model.BookTitle</p>
|
||||||
<h1 id="story-progress-title">Review scenes</h1>
|
<h1 id="story-progress-title">Review scenes</h1>
|
||||||
<p>PlotDirector is reading the approved chapters and turning them into scenes you can review before creating them.</p>
|
<p>PlotDirector is reading the approved chapters and preparing scene suggestions for you to review.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||||
@ -26,24 +33,36 @@
|
|||||||
<div class="alert alert-success">@message</div>
|
<div class="alert alert-success">@message</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<section class="alert alert-info" data-story-reassurance>
|
||||||
|
@if (Model.Chapters.Any(chapter => chapter.CanCommit || chapter.HasCompletedCommit))
|
||||||
|
{
|
||||||
|
<span>Scenes can be created now. Characters, locations, assets and relationships remain suggestions for later review.</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Nothing has been added to your project yet. PlotDirector is preparing suggestions. You will review them before anything is created.</span>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||||
<div>
|
<div>
|
||||||
<span>Overall progress</span>
|
<span>Import progress</span>
|
||||||
<strong data-story-overall-percent>@progressPercent%</strong>
|
<strong data-story-overall-percent>@progressPercent%</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Chapters completed</span>
|
<span>Chapters read</span>
|
||||||
<strong><span data-story-chapters-completed>@Model.CompletedChapterCount</span> / @Model.ChapterCount</strong>
|
<strong><span data-story-chapters-completed>@Model.CompletedChapterCount</span> of @Model.ChapterCount</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Scenes analysed</span>
|
<span>Scenes read</span>
|
||||||
<strong><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> / <span data-story-scenes-total>@Model.TotalDetectedScenes</span></strong>
|
<strong><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> so far</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Failures</span>
|
<span>Needs review</span>
|
||||||
<strong data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
<strong data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-muted">These are chapters or scenes PlotDirector could not fully analyse. You can review them after the run completes.</p>
|
||||||
|
|
||||||
<div class="progress mb-3" role="progressbar" aria-label="Story Intelligence progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="@progressPercent">
|
<div class="progress mb-3" role="progressbar" aria-label="Story Intelligence progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="@progressPercent">
|
||||||
<div class="progress-bar" data-story-overall-progress-bar style="width:@progressPercent%"></div>
|
<div class="progress-bar" data-story-overall-progress-bar style="width:@progressPercent%"></div>
|
||||||
@ -51,15 +70,19 @@
|
|||||||
|
|
||||||
<section class="onboarding-review-grid" aria-label="Story Intelligence progress">
|
<section class="onboarding-review-grid" aria-label="Story Intelligence progress">
|
||||||
<div class="onboarding-review-section">
|
<div class="onboarding-review-section">
|
||||||
<h2>Reading now</h2>
|
<h2>Latest from your manuscript</h2>
|
||||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||||
<div>
|
<div>
|
||||||
<span>Current chapter</span>
|
<span>Current chapter</span>
|
||||||
<strong data-story-current-chapter>@(CurrentChapter(Model)?.ChapterTitle ?? "Waiting")</strong>
|
<strong data-story-current-chapter>@(CurrentChapter(Model)?.ChapterTitle ?? "Waiting")</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Current scene</span>
|
||||||
|
<strong data-story-current-scene>@CurrentScene(Model)</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Current stage</span>
|
<span>Current stage</span>
|
||||||
<strong data-story-current-stage>@(CurrentChapter(Model)?.CurrentStage ?? "Queued")</strong>
|
<strong data-story-current-stage>@FriendlyStage(CurrentChapter(Model)?.CurrentStage, CurrentChapter(Model)?.Status)</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Elapsed</span>
|
<span>Elapsed</span>
|
||||||
@ -67,10 +90,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Estimated remaining</span>
|
<span>Estimated remaining</span>
|
||||||
<strong data-story-remaining>Calculating</strong>
|
<strong data-story-remaining>@EstimateRemaining(Model)</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p data-story-current-message>@(CurrentChapter(Model)?.CurrentMessage ?? "Waiting for analysis to begin.")</p>
|
<p data-story-current-message>@FriendlyMessage(CurrentChapter(Model))</p>
|
||||||
<p class="lead" data-story-latest-summary hidden></p>
|
<p class="lead" data-story-latest-summary hidden></p>
|
||||||
<ul data-story-discovery-feed></ul>
|
<ul data-story-discovery-feed></ul>
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +103,7 @@
|
|||||||
<div class="onboarding-review-chapters">
|
<div class="onboarding-review-chapters">
|
||||||
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
||||||
{
|
{
|
||||||
<details class="onboarding-review-chapter" open data-story-run-id="@chapter.RunID" data-story-chapter-title="@chapter.ChapterTitle">
|
<details class="onboarding-review-chapter" open data-story-run-id="@chapter.RunID" data-story-chapter-title="@chapter.ChapterTitle" data-story-raw-status="@chapter.Status">
|
||||||
<summary>
|
<summary>
|
||||||
<span>@chapter.ChapterNumber. @chapter.ChapterTitle</span>
|
<span>@chapter.ChapterNumber. @chapter.ChapterTitle</span>
|
||||||
<strong data-story-chapter-status-label>@ChapterStatusLabel(chapter)</strong>
|
<strong data-story-chapter-status-label>@ChapterStatusLabel(chapter)</strong>
|
||||||
@ -88,23 +111,27 @@
|
|||||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||||
<div>
|
<div>
|
||||||
<span>Status</span>
|
<span>Status</span>
|
||||||
<strong data-story-run-status>@chapter.Status</strong>
|
<strong data-story-run-status>@FriendlyStatus(chapter.Status)</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Scenes</span>
|
<span>Scenes detected</span>
|
||||||
<strong><span data-story-run-completed>@((chapter.CompletedScenes ?? 0).ToString("N0"))</span> / <span data-story-run-total>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</span></strong>
|
<strong><span data-story-run-completed>@((chapter.CompletedScenes ?? 0).ToString("N0"))</span> / <span data-story-run-total>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</span></strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Failed</span>
|
<span>Needs review</span>
|
||||||
<strong data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</strong>
|
<strong data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span>Tokens</span>
|
|
||||||
<strong data-story-run-tokens>@(chapter.TotalTokens?.ToString("N0") ?? "-")</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p data-story-run-message>@(chapter.CurrentMessage ?? string.Empty)</p>
|
<p data-story-run-message>@FriendlyMessage(chapter)</p>
|
||||||
|
@if ((chapter.FailedScenes ?? 0) > 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted" data-story-run-review-note>Needs review after analysis completes.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="text-muted" data-story-run-review-note hidden></p>
|
||||||
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage))
|
@if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage))
|
||||||
{
|
{
|
||||||
<p class="text-danger" data-story-run-error>@chapter.ErrorMessage</p>
|
<p class="text-danger" data-story-run-error>@chapter.ErrorMessage</p>
|
||||||
@ -150,6 +177,27 @@
|
|||||||
<span class="btn btn-outline-secondary disabled">Review needed</span>
|
<span class="btn btn-outline-secondary disabled">Review needed</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (isAdmin)
|
||||||
|
{
|
||||||
|
<details class="mt-3">
|
||||||
|
<summary>Diagnostics</summary>
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-4">Run ID</dt>
|
||||||
|
<dd class="col-sm-8">@chapter.RunID</dd>
|
||||||
|
<dt class="col-sm-4">Raw status</dt>
|
||||||
|
<dd class="col-sm-8" data-story-run-raw-status>@chapter.Status</dd>
|
||||||
|
<dt class="col-sm-4">Raw stage</dt>
|
||||||
|
<dd class="col-sm-8" data-story-run-raw-stage>@(chapter.CurrentStage ?? "-")</dd>
|
||||||
|
<dt class="col-sm-4">Tokens</dt>
|
||||||
|
<dd class="col-sm-8" data-story-run-tokens>@(chapter.TotalTokens?.ToString("N0") ?? "-")</dd>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(chapter.FailureStage))
|
||||||
|
{
|
||||||
|
<dt class="col-sm-4">Failure stage</dt>
|
||||||
|
<dd class="col-sm-8">@chapter.FailureStage</dd>
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
</details>
|
</details>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -159,6 +207,9 @@
|
|||||||
<div class="onboarding-actions">
|
<div class="onboarding-actions">
|
||||||
@if (Model.BatchID.HasValue)
|
@if (Model.BatchID.HasValue)
|
||||||
{
|
{
|
||||||
|
<form asp-action="ScanManuscript" method="post">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit" disabled="@Model.HasActiveRuns">Scan manuscript again</button>
|
||||||
|
</form>
|
||||||
<form asp-action="CancelStoryIntelligence" method="post">
|
<form asp-action="CancelStoryIntelligence" method="post">
|
||||||
<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>
|
||||||
@ -202,6 +253,80 @@
|
|||||||
return "Reviewing";
|
return "Reviewing";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FriendlyStatus(string? status)
|
||||||
|
=> status switch
|
||||||
|
{
|
||||||
|
StoryIntelligenceRunStatuses.Pending => "Waiting",
|
||||||
|
StoryIntelligenceRunStatuses.Running => "Reading",
|
||||||
|
StoryIntelligenceRunStatuses.Completed => "Ready to review",
|
||||||
|
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Ready to review, with notes",
|
||||||
|
StoryIntelligenceRunStatuses.Failed => "Needs attention",
|
||||||
|
StoryIntelligenceRunStatuses.Cancelled => "Cancelled",
|
||||||
|
_ => "Waiting"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FriendlyStage(string? stage, string? status)
|
||||||
|
{
|
||||||
|
if (status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings)
|
||||||
|
{
|
||||||
|
return "Ready to review";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status is StoryIntelligenceRunStatuses.Failed)
|
||||||
|
{
|
||||||
|
return "Needs attention";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status is StoryIntelligenceRunStatuses.Cancelled)
|
||||||
|
{
|
||||||
|
return "Cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
return stage switch
|
||||||
|
{
|
||||||
|
"Queued" => "Waiting",
|
||||||
|
StoryIntelligenceFailureStages.DocumentRead => "Preparing chapter",
|
||||||
|
StoryIntelligenceFailureStages.ChapterStructure => "Finding scenes",
|
||||||
|
StoryIntelligenceFailureStages.SceneSplit => "Preparing scenes",
|
||||||
|
StoryIntelligenceFailureStages.SceneIntelligence => "Reading scenes",
|
||||||
|
StoryIntelligenceFailureStages.Validation => "Checking results",
|
||||||
|
StoryIntelligenceFailureStages.Persistence => "Saving analysis",
|
||||||
|
StoryIntelligenceRunStatuses.Completed => "Ready to review",
|
||||||
|
_ => "Reading"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FriendlyMessage(StoryIntelligenceOnboardingChapterViewModel? chapter)
|
||||||
|
{
|
||||||
|
if (chapter is null)
|
||||||
|
{
|
||||||
|
return "Waiting for analysis to begin.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapter.Status == StoryIntelligenceRunStatuses.Cancelled)
|
||||||
|
{
|
||||||
|
return "Analysis was cancelled. Any completed chapters remain available for review.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapter.Status == StoryIntelligenceRunStatuses.Failed)
|
||||||
|
{
|
||||||
|
return "This chapter needs attention before scenes can be created.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapter.CurrentStage switch
|
||||||
|
{
|
||||||
|
"Queued" => "Waiting to read this chapter.",
|
||||||
|
StoryIntelligenceFailureStages.DocumentRead => "Preparing this chapter.",
|
||||||
|
StoryIntelligenceFailureStages.ChapterStructure => "Finding scenes in this chapter.",
|
||||||
|
StoryIntelligenceFailureStages.SceneSplit => "Preparing the detected scenes.",
|
||||||
|
StoryIntelligenceFailureStages.SceneIntelligence => "Reading the detected scenes.",
|
||||||
|
StoryIntelligenceFailureStages.Validation => "Checking the results.",
|
||||||
|
_ => chapter.IsRunComplete
|
||||||
|
? "This chapter is ready to review."
|
||||||
|
: "Waiting for analysis to begin."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static StoryIntelligenceOnboardingChapterViewModel? CurrentChapter(StoryIntelligenceProgressViewModel model)
|
private static StoryIntelligenceOnboardingChapterViewModel? CurrentChapter(StoryIntelligenceProgressViewModel model)
|
||||||
=> model.Chapters.FirstOrDefault(chapter => chapter.IsActive)
|
=> model.Chapters.FirstOrDefault(chapter => chapter.IsActive)
|
||||||
?? model.Chapters.LastOrDefault(chapter => chapter.IsRunComplete)
|
?? model.Chapters.LastOrDefault(chapter => chapter.IsRunComplete)
|
||||||
@ -214,12 +339,34 @@
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.TotalDetectedScenes > 0)
|
var chapterProgress = model.Chapters.Sum(ChapterProgress);
|
||||||
|
return Math.Clamp((int)Math.Floor(chapterProgress * 100m / model.ChapterCount), 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal ChapterProgress(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||||
|
{
|
||||||
|
if (chapter.HasCompletedCommit || chapter.IsRunComplete)
|
||||||
{
|
{
|
||||||
return Math.Clamp((int)Math.Round(model.TotalCompletedScenes * 100m / Math.Max(1, model.TotalDetectedScenes)), 0, 100);
|
return 1m;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.Clamp((int)Math.Round(model.CompletedChapterCount * 100m / Math.Max(1, model.ChapterCount)), 0, 100);
|
if (chapter.IsFailed)
|
||||||
|
{
|
||||||
|
return 1m;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapter.Status == StoryIntelligenceRunStatuses.Pending)
|
||||||
|
{
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((chapter.TotalDetectedScenes ?? 0) > 0)
|
||||||
|
{
|
||||||
|
var sceneProgress = Math.Clamp((chapter.CompletedScenes ?? 0) / Math.Max(1m, chapter.TotalDetectedScenes ?? 1), 0m, 1m);
|
||||||
|
return Math.Clamp(0.25m + (sceneProgress * 0.7m), 0m, 0.95m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.15m;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Elapsed(StoryIntelligenceProgressViewModel model)
|
private static string Elapsed(StoryIntelligenceProgressViewModel model)
|
||||||
@ -230,4 +377,37 @@
|
|||||||
? $"{Math.Max(1, (int)Math.Round(elapsed.TotalMinutes)):N0} min"
|
? $"{Math.Max(1, (int)Math.Round(elapsed.TotalMinutes)):N0} min"
|
||||||
: $"{Math.Max(0, (int)Math.Round(elapsed.TotalSeconds)):N0} sec";
|
: $"{Math.Max(0, (int)Math.Round(elapsed.TotalSeconds)):N0} sec";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string EstimateRemaining(StoryIntelligenceProgressViewModel model)
|
||||||
|
{
|
||||||
|
if (!model.HasActiveRuns)
|
||||||
|
{
|
||||||
|
return "Almost done";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.TotalCompletedScenes <= 0)
|
||||||
|
{
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingScenes = Math.Max(0, model.TotalDetectedScenes - model.TotalCompletedScenes);
|
||||||
|
if (remainingScenes == 0)
|
||||||
|
{
|
||||||
|
return "Almost done";
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedMs = model.Chapters.Max(chapter => chapter.TotalDurationMs ?? 0);
|
||||||
|
var averageSceneMinutes = Math.Max(1, (int)Math.Ceiling(TimeSpan.FromMilliseconds(elapsedMs).TotalMinutes / Math.Max(1, model.TotalCompletedScenes)));
|
||||||
|
var low = Math.Max(1, averageSceneMinutes * remainingScenes);
|
||||||
|
var high = Math.Max(low + 1, low * 2);
|
||||||
|
return $"About {low:N0}-{high:N0} minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
||||||
|
{
|
||||||
|
var chapter = CurrentChapter(model);
|
||||||
|
return chapter?.CompletedScenes is > 0
|
||||||
|
? chapter.CompletedScenes.Value.ToString("N0")
|
||||||
|
: "Waiting";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,36 +50,13 @@
|
|||||||
@if (Model.StoryIntelligence.ShouldShow)
|
@if (Model.StoryIntelligence.ShouldShow)
|
||||||
{
|
{
|
||||||
<section class="story-intelligence-dashboard-card"
|
<section class="story-intelligence-dashboard-card"
|
||||||
data-story-intelligence-dashboard
|
|
||||||
data-story-intelligence-job-id="@Model.StoryIntelligence.Job?.JobID"
|
|
||||||
aria-label="Story Intelligence">
|
aria-label="Story Intelligence">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Story Intelligence</p>
|
<p class="eyebrow">Story Intelligence</p>
|
||||||
<h2>@Model.StoryIntelligence.Title</h2>
|
<h2>@Model.StoryIntelligence.Title</h2>
|
||||||
<p data-story-intelligence-message>@Model.StoryIntelligence.Description</p>
|
<p>@Model.StoryIntelligence.Description</p>
|
||||||
@if (Model.StoryIntelligence.Job is not null)
|
|
||||||
{
|
|
||||||
<div class="story-intelligence-dashboard-progress">
|
|
||||||
<span data-story-intelligence-status>@Model.StoryIntelligence.Job.Status</span>
|
|
||||||
<strong data-story-intelligence-percent>@Model.StoryIntelligence.Job.ProgressPercent%</strong>
|
|
||||||
<div class="onboarding-scan-progress" aria-hidden="true">
|
|
||||||
<span data-story-intelligence-progress-bar style="width:@Model.StoryIntelligence.Job.ProgressPercent%"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
@if (Model.StoryIntelligence.Job?.IsActive == true)
|
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="Index">@Model.StoryIntelligence.ButtonText</a>
|
||||||
{
|
|
||||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">@Model.StoryIntelligence.ButtonText</a>
|
|
||||||
}
|
|
||||||
else if (Model.StoryIntelligence.Job?.IsCompleted == true)
|
|
||||||
{
|
|
||||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">@Model.StoryIntelligence.ButtonText</a>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">@Model.StoryIntelligence.ButtonText</a>
|
|
||||||
}
|
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,7 +322,6 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/story-intelligence-progress.js" asp-append-version="true"></script>
|
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const modalElement = document.getElementById("hardDeleteProjectModal");
|
const modalElement = document.getElementById("hardDeleteProjectModal");
|
||||||
|
|||||||
@ -545,6 +545,65 @@
|
|||||||
color: var(--bs-secondary-color);
|
color: var(--bs-secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] .onboarding-review-counts {
|
||||||
|
position: static;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] > .onboarding-review-counts {
|
||||||
|
gap: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] > .onboarding-review-counts div {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] > .onboarding-review-counts strong {
|
||||||
|
font-size: 1.45rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] .onboarding-review-grid {
|
||||||
|
grid-template-columns: minmax(320px, .9fr) minmax(0, 1.35fr);
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] .onboarding-review-section {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] .onboarding-review-chapter {
|
||||||
|
padding: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] .onboarding-review-chapter summary {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: start;
|
||||||
|
gap: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-intelligence-onboarding] .onboarding-review-chapter summary strong {
|
||||||
|
white-space: normal;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-latest-summary] {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
border-left: 4px solid #2f6f63;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(47, 111, 99, .08);
|
||||||
|
color: #1f2a44;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-story-discovery-feed] {
|
||||||
|
display: grid;
|
||||||
|
gap: .45rem;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
.onboarding-character-candidates li {
|
.onboarding-character-candidates li {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
|
|||||||
@ -20,6 +20,109 @@
|
|||||||
return Number.isFinite(number) ? number.toLocaleString() : "0";
|
return Number.isFinite(number) ? number.toLocaleString() : "0";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const friendlyStatus = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case "Pending":
|
||||||
|
return "Waiting";
|
||||||
|
case "Running":
|
||||||
|
return "Reading";
|
||||||
|
case "Completed":
|
||||||
|
return "Ready to review";
|
||||||
|
case "CompletedWithWarnings":
|
||||||
|
return "Ready to review, with notes";
|
||||||
|
case "Failed":
|
||||||
|
return "Needs attention";
|
||||||
|
case "Cancelled":
|
||||||
|
return "Cancelled";
|
||||||
|
default:
|
||||||
|
return "Waiting";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const friendlyStage = (stage, status) => {
|
||||||
|
if (status === "Completed" || status === "CompletedWithWarnings") {
|
||||||
|
return "Ready to review";
|
||||||
|
}
|
||||||
|
if (status === "Failed") {
|
||||||
|
return "Needs attention";
|
||||||
|
}
|
||||||
|
if (status === "Cancelled") {
|
||||||
|
return "Cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (stage) {
|
||||||
|
case "Queued":
|
||||||
|
return "Waiting";
|
||||||
|
case "DocumentRead":
|
||||||
|
return "Preparing chapter";
|
||||||
|
case "ChapterStructure":
|
||||||
|
return "Finding scenes";
|
||||||
|
case "SceneSplit":
|
||||||
|
return "Preparing scenes";
|
||||||
|
case "SceneIntelligence":
|
||||||
|
return "Reading scenes";
|
||||||
|
case "Validation":
|
||||||
|
return "Checking results";
|
||||||
|
case "Persistence":
|
||||||
|
return "Saving analysis";
|
||||||
|
default:
|
||||||
|
return "Reading";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const friendlyRemaining = (value, completedScenes, totalScenes) => {
|
||||||
|
if (totalScenes > 0 && completedScenes >= totalScenes) {
|
||||||
|
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) {
|
||||||
|
return "Estimating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `About ${minutes}-${Math.max(minutes + 1, minutes * 2)} minutes`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const possibleDiscoveryText = (text) => String(text || "")
|
||||||
|
.replace(/^Character detected:/i, "Possible character detected:")
|
||||||
|
.replace(/^Location detected:/i, "Possible location detected:")
|
||||||
|
.replace(/^Asset detected:/i, "Possible asset detected:")
|
||||||
|
.replace(/^Timeline clue detected:/i, "Timeline clue found:")
|
||||||
|
.replace(/^Relationship detected:/i, "Relationship signal found:");
|
||||||
|
|
||||||
|
const friendlyMessage = (message, stage, status, sceneNumber) => {
|
||||||
|
if (status === "Cancelled") {
|
||||||
|
return "Analysis was cancelled. Any completed chapters remain available for review.";
|
||||||
|
}
|
||||||
|
if (status === "Failed") {
|
||||||
|
return "This chapter needs attention before scenes can be created.";
|
||||||
|
}
|
||||||
|
if (status === "Completed" || status === "CompletedWithWarnings") {
|
||||||
|
return "This chapter is ready to review.";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (stage) {
|
||||||
|
case "Queued":
|
||||||
|
return "Waiting to read this chapter.";
|
||||||
|
case "DocumentRead":
|
||||||
|
return "Preparing this chapter.";
|
||||||
|
case "ChapterStructure":
|
||||||
|
return "Finding scenes in this chapter.";
|
||||||
|
case "SceneSplit":
|
||||||
|
return "Preparing the detected scenes.";
|
||||||
|
case "SceneIntelligence":
|
||||||
|
return sceneNumber ? `Reading scene ${numberText(sceneNumber)}.` : "Reading the detected scenes.";
|
||||||
|
case "Validation":
|
||||||
|
return "Checking the results.";
|
||||||
|
default:
|
||||||
|
return message || "Waiting for analysis to begin.";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateLegacyRoot = (root, state) => {
|
const updateLegacyRoot = (root, state) => {
|
||||||
const percent = field(state, "progressPercent", "ProgressPercent") ?? 0;
|
const percent = field(state, "progressPercent", "ProgressPercent") ?? 0;
|
||||||
const status = field(state, "status", "Status") || "Pending";
|
const status = field(state, "status", "Status") || "Pending";
|
||||||
@ -92,7 +195,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const item = document.createElement("li");
|
const item = document.createElement("li");
|
||||||
item.textContent = text;
|
item.textContent = possibleDiscoveryText(text);
|
||||||
list.prepend(item);
|
list.prepend(item);
|
||||||
while (list.children.length > 8) {
|
while (list.children.length > 8) {
|
||||||
list.lastElementChild?.remove();
|
list.lastElementChild?.remove();
|
||||||
@ -103,7 +206,7 @@
|
|||||||
const hadActiveRuns = root.dataset.storyIntelligenceHasActiveRuns === "true";
|
const hadActiveRuns = root.dataset.storyIntelligenceHasActiveRuns === "true";
|
||||||
const chapters = [...root.querySelectorAll("[data-story-run-id]")];
|
const chapters = [...root.querySelectorAll("[data-story-run-id]")];
|
||||||
const totals = chapters.reduce((current, chapter) => {
|
const totals = chapters.reduce((current, chapter) => {
|
||||||
const status = chapter.querySelector("[data-story-run-status]")?.textContent?.trim() || "";
|
const status = chapter.dataset.storyRawStatus || "";
|
||||||
const completed = Number.parseInt(chapter.querySelector("[data-story-run-completed]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
const completed = Number.parseInt(chapter.querySelector("[data-story-run-completed]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
||||||
const total = Number.parseInt(chapter.querySelector("[data-story-run-total]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
const total = Number.parseInt(chapter.querySelector("[data-story-run-total]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
||||||
const failed = Number.parseInt(chapter.querySelector("[data-story-run-failed]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
const failed = Number.parseInt(chapter.querySelector("[data-story-run-failed]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
||||||
@ -115,9 +218,25 @@
|
|||||||
return current;
|
return current;
|
||||||
}, { completedChapters: 0, failedChapters: 0, completedScenes: 0, totalScenes: 0, failedScenes: 0 });
|
}, { completedChapters: 0, failedChapters: 0, completedScenes: 0, totalScenes: 0, failedScenes: 0 });
|
||||||
|
|
||||||
const percent = totals.totalScenes > 0
|
const chapterProgress = chapters.reduce((total, chapter) => {
|
||||||
? Math.round((totals.completedScenes * 100) / Math.max(1, totals.totalScenes))
|
const status = chapter.dataset.storyRawStatus || "";
|
||||||
: Math.round((totals.completedChapters * 100) / Math.max(1, chapters.length));
|
if (status === "Completed" || status === "CompletedWithWarnings" || status === "Failed" || status === "Cancelled") {
|
||||||
|
return total + 1;
|
||||||
|
}
|
||||||
|
if (status === "Pending") {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
const completed = Number.parseInt(chapter.querySelector("[data-story-run-completed]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
||||||
|
const detected = Number.parseInt(chapter.querySelector("[data-story-run-total]")?.textContent?.replace(/,/g, "") || "0", 10) || 0;
|
||||||
|
if (detected > 0) {
|
||||||
|
return total + Math.min(0.95, 0.25 + ((completed / Math.max(1, detected)) * 0.7));
|
||||||
|
}
|
||||||
|
return total + 0.15;
|
||||||
|
}, 0);
|
||||||
|
const calculatedPercent = Math.floor((chapterProgress * 100) / Math.max(1, chapters.length));
|
||||||
|
const previousPercent = Number.parseInt(root.dataset.storyMaxProgressPercent || "0", 10) || 0;
|
||||||
|
const percent = Math.max(previousPercent, Math.max(0, Math.min(100, calculatedPercent)));
|
||||||
|
root.dataset.storyMaxProgressPercent = String(percent);
|
||||||
|
|
||||||
root.querySelectorAll("[data-story-overall-percent]").forEach((node) => {
|
root.querySelectorAll("[data-story-overall-percent]").forEach((node) => {
|
||||||
node.textContent = `${Math.max(0, Math.min(100, percent))}%`;
|
node.textContent = `${Math.max(0, Math.min(100, percent))}%`;
|
||||||
@ -139,7 +258,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const active = chapters.some((chapter) => {
|
const active = chapters.some((chapter) => {
|
||||||
const status = chapter.querySelector("[data-story-run-status]")?.textContent?.trim();
|
const status = chapter.dataset.storyRawStatus || "";
|
||||||
return status === "Pending" || status === "Running";
|
return status === "Pending" || status === "Running";
|
||||||
});
|
});
|
||||||
root.dataset.storyIntelligenceHasActiveRuns = String(active);
|
root.dataset.storyIntelligenceHasActiveRuns = String(active);
|
||||||
@ -171,6 +290,8 @@
|
|||||||
const tokens = field(state, "totalTokens", "TotalTokens");
|
const tokens = field(state, "totalTokens", "TotalTokens");
|
||||||
const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec";
|
const elapsed = field(state, "elapsedTime", "ElapsedTime") || "0 sec";
|
||||||
const remaining = field(state, "estimatedRemaining", "EstimatedRemaining") || "Calculating";
|
const remaining = field(state, "estimatedRemaining", "EstimatedRemaining") || "Calculating";
|
||||||
|
const currentSceneNumber = field(state, "currentSceneNumber", "CurrentSceneNumber");
|
||||||
|
const displayMessage = friendlyMessage(message, stage, status, currentSceneNumber);
|
||||||
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
const latestSummary = field(state, "latestSceneSummary", "LatestSceneSummary");
|
||||||
const discoveries = field(state, "recentDiscoveries", "RecentDiscoveries") || [];
|
const discoveries = field(state, "recentDiscoveries", "RecentDiscoveries") || [];
|
||||||
const error = field(state, "errorMessage", "ErrorMessage");
|
const error = field(state, "errorMessage", "ErrorMessage");
|
||||||
@ -178,11 +299,18 @@
|
|||||||
chapter.querySelectorAll("[data-story-chapter-status-label]").forEach((node) => {
|
chapter.querySelectorAll("[data-story-chapter-status-label]").forEach((node) => {
|
||||||
node.textContent = statusLabel(state);
|
node.textContent = statusLabel(state);
|
||||||
});
|
});
|
||||||
|
chapter.dataset.storyRawStatus = status;
|
||||||
chapter.querySelectorAll("[data-story-run-status]").forEach((node) => {
|
chapter.querySelectorAll("[data-story-run-status]").forEach((node) => {
|
||||||
|
node.textContent = friendlyStatus(status);
|
||||||
|
});
|
||||||
|
chapter.querySelectorAll("[data-story-run-raw-status]").forEach((node) => {
|
||||||
node.textContent = status;
|
node.textContent = status;
|
||||||
});
|
});
|
||||||
|
chapter.querySelectorAll("[data-story-run-raw-stage]").forEach((node) => {
|
||||||
|
node.textContent = stage;
|
||||||
|
});
|
||||||
chapter.querySelectorAll("[data-story-run-message]").forEach((node) => {
|
chapter.querySelectorAll("[data-story-run-message]").forEach((node) => {
|
||||||
node.textContent = message;
|
node.textContent = displayMessage;
|
||||||
});
|
});
|
||||||
chapter.querySelectorAll("[data-story-run-completed]").forEach((node) => {
|
chapter.querySelectorAll("[data-story-run-completed]").forEach((node) => {
|
||||||
if (completed !== null) {
|
if (completed !== null) {
|
||||||
@ -199,6 +327,11 @@
|
|||||||
node.textContent = numberText(failed);
|
node.textContent = numberText(failed);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
chapter.querySelectorAll("[data-story-run-review-note]").forEach((node) => {
|
||||||
|
const failedCount = Number.parseInt(failed || "0", 10) || 0;
|
||||||
|
node.textContent = failedCount > 0 ? "Needs review after analysis completes." : "";
|
||||||
|
node.hidden = failedCount <= 0;
|
||||||
|
});
|
||||||
chapter.querySelectorAll("[data-story-run-tokens]").forEach((node) => {
|
chapter.querySelectorAll("[data-story-run-tokens]").forEach((node) => {
|
||||||
node.textContent = tokens === null ? "-" : numberText(tokens);
|
node.textContent = tokens === null ? "-" : numberText(tokens);
|
||||||
});
|
});
|
||||||
@ -211,16 +344,19 @@
|
|||||||
node.textContent = chapter.dataset.storyChapterTitle || "Current chapter";
|
node.textContent = chapter.dataset.storyChapterTitle || "Current chapter";
|
||||||
});
|
});
|
||||||
root.querySelectorAll("[data-story-current-stage]").forEach((node) => {
|
root.querySelectorAll("[data-story-current-stage]").forEach((node) => {
|
||||||
node.textContent = stage;
|
node.textContent = friendlyStage(stage, status);
|
||||||
|
});
|
||||||
|
root.querySelectorAll("[data-story-current-scene]").forEach((node) => {
|
||||||
|
node.textContent = currentSceneNumber ? numberText(currentSceneNumber) : "Waiting";
|
||||||
});
|
});
|
||||||
root.querySelectorAll("[data-story-current-message]").forEach((node) => {
|
root.querySelectorAll("[data-story-current-message]").forEach((node) => {
|
||||||
node.textContent = message;
|
node.textContent = displayMessage;
|
||||||
});
|
});
|
||||||
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) => {
|
root.querySelectorAll("[data-story-remaining]").forEach((node) => {
|
||||||
node.textContent = remaining;
|
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]");
|
||||||
|
|||||||
@ -2032,10 +2032,10 @@
|
|||||||
const documentTextForDiscovery = documentText.join("\n");
|
const documentTextForDiscovery = documentText.join("\n");
|
||||||
return {
|
return {
|
||||||
previewID: "00000000-0000-0000-0000-000000000000",
|
previewID: "00000000-0000-0000-0000-000000000000",
|
||||||
userID: command.userID || command.UserID || 0,
|
userID: scanCommandValue(command, "userID", "UserID", "userId"),
|
||||||
onboardingID: command.onboardingID || command.OnboardingID || 0,
|
onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID", "onboardingId"),
|
||||||
projectID: command.projectID || command.ProjectID || 0,
|
projectID: scanCommandValue(command, "projectID", "ProjectID", "projectId"),
|
||||||
bookID: command.bookID || command.BookID || 0,
|
bookID: scanCommandValue(command, "bookID", "BookID", "bookId"),
|
||||||
source: "WordCompanion",
|
source: "WordCompanion",
|
||||||
documentTitle: currentDocumentName() || "Word manuscript",
|
documentTitle: currentDocumentName() || "Word manuscript",
|
||||||
companionDocumentIdentifier: currentDocumentGuid(),
|
companionDocumentIdentifier: currentDocumentGuid(),
|
||||||
@ -2051,7 +2051,15 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const scanCommandValue = (command, camel, pascal) => command?.[camel] ?? command?.[pascal] ?? null;
|
const scanCommandValue = (command, ...names) => {
|
||||||
|
for (const name of names) {
|
||||||
|
if (command?.[name] !== undefined && command?.[name] !== null) {
|
||||||
|
return command[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const reportOnboardingScanProgress = async (command, message, percentComplete, counts = {}) => {
|
const reportOnboardingScanProgress = async (command, message, percentComplete, counts = {}) => {
|
||||||
if (!companionPresenceConnection || companionPresenceConnection.state !== signalR.HubConnectionState.Connected) {
|
if (!companionPresenceConnection || companionPresenceConnection.state !== signalR.HubConnectionState.Connected) {
|
||||||
@ -2059,8 +2067,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await companionPresenceConnection.invoke("ReportOnboardingScanProgress", {
|
await companionPresenceConnection.invoke("ReportOnboardingScanProgress", {
|
||||||
userID: scanCommandValue(command, "userID", "UserID"),
|
userID: scanCommandValue(command, "userID", "UserID", "userId"),
|
||||||
onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID"),
|
onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID", "onboardingId"),
|
||||||
message,
|
message,
|
||||||
percentComplete,
|
percentComplete,
|
||||||
chapterCount: counts.chapterCount || 0,
|
chapterCount: counts.chapterCount || 0,
|
||||||
@ -2076,8 +2084,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await companionPresenceConnection.invoke("FailOnboardingScan", {
|
await companionPresenceConnection.invoke("FailOnboardingScan", {
|
||||||
userID: scanCommandValue(command, "userID", "UserID"),
|
userID: scanCommandValue(command, "userID", "UserID", "userId"),
|
||||||
onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID"),
|
onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID", "onboardingId"),
|
||||||
message
|
message
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -10,6 +10,7 @@
|
|||||||
let companionConnected = false;
|
let companionConnected = false;
|
||||||
let companionDocumentOpen = false;
|
let companionDocumentOpen = false;
|
||||||
let currentScanStatus = document.querySelector("[data-onboarding-scan-panel]")?.dataset.scanStatus || "NotStarted";
|
let currentScanStatus = document.querySelector("[data-onboarding-scan-panel]")?.dataset.scanStatus || "NotStarted";
|
||||||
|
let scanStartedInThisPage = currentScanStatus === "Running";
|
||||||
|
|
||||||
const formatTime = (value) => {
|
const formatTime = (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -96,6 +97,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const status = field(state, "status", "Status") || "NotStarted";
|
const status = field(state, "status", "Status") || "NotStarted";
|
||||||
|
if (status === "Running") {
|
||||||
|
scanStartedInThisPage = true;
|
||||||
|
}
|
||||||
currentScanStatus = status;
|
currentScanStatus = status;
|
||||||
const message = field(state, "message", "Message") || (status === "Complete" ? "Scan complete. Review what PlotDirector found." : "Ready to scan");
|
const message = field(state, "message", "Message") || (status === "Complete" ? "Scan complete. Review what PlotDirector found." : "Ready to scan");
|
||||||
const percent = field(state, "percentComplete", "PercentComplete");
|
const percent = field(state, "percentComplete", "PercentComplete");
|
||||||
@ -136,6 +140,14 @@
|
|||||||
node.href = ready ? `/onboarding/scan-review?previewId=${encodeURIComponent(previewId)}` : "#";
|
node.href = ready ? `/onboarding/scan-review?previewId=${encodeURIComponent(previewId)}` : "#";
|
||||||
});
|
});
|
||||||
updateScanButton();
|
updateScanButton();
|
||||||
|
if (status === "Complete"
|
||||||
|
&& previewId
|
||||||
|
&& document.querySelector("[data-onboarding-auto-review]")
|
||||||
|
&& scanStartedInThisPage
|
||||||
|
&& !document.body.dataset.onboardingReviewRedirected) {
|
||||||
|
document.body.dataset.onboardingReviewRedirected = "true";
|
||||||
|
window.location.href = `/onboarding/scan-review?previewId=${encodeURIComponent(previewId)}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user