Phase 20AI – Story Intelligence Review and Scene Creation
This commit is contained in:
parent
37e01f4b5d
commit
058b3d10dc
@ -132,6 +132,22 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
|||||||
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("story-intelligence/commit-all")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> CommitAllStoryIntelligence(Guid batchId)
|
||||||
|
{
|
||||||
|
var (progress, result) = await storyIntelligence.CommitAllAsync(batchId);
|
||||||
|
if (progress is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||||
|
return result.Success && progress.AllCommitted
|
||||||
|
? RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId })
|
||||||
|
: RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("story-intelligence/complete")]
|
[HttpGet("story-intelligence/complete")]
|
||||||
public async Task<IActionResult> StoryIntelligenceComplete(Guid batchId)
|
public async Task<IActionResult> StoryIntelligenceComplete(Guid batchId)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public interface IOnboardingStoryIntelligenceService
|
|||||||
Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId);
|
Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId);
|
||||||
Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId);
|
Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId);
|
||||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAsync(Guid batchId, int runId);
|
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAsync(Guid batchId, int runId);
|
||||||
|
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAllAsync(Guid batchId);
|
||||||
Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId);
|
Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +236,8 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
SourceWordCount = run.SourceWordCount,
|
SourceWordCount = run.SourceWordCount,
|
||||||
TotalTokens = run.TotalTokens,
|
TotalTokens = run.TotalTokens,
|
||||||
TotalDurationMs = run.TotalDurationMs,
|
TotalDurationMs = run.TotalDurationMs,
|
||||||
|
EstimatedCostUSD = run.EstimatedCostUSD,
|
||||||
|
EstimatedCostGBP = run.EstimatedCostGBP,
|
||||||
FailureStage = run.FailureStage,
|
FailureStage = run.FailureStage,
|
||||||
ErrorMessage = run.ErrorMessage,
|
ErrorMessage = run.ErrorMessage,
|
||||||
LatestSceneSummary = LatestSceneSummary(sceneResults),
|
LatestSceneSummary = LatestSceneSummary(sceneResults),
|
||||||
@ -243,7 +246,8 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
ScenesCreated = commit?.ScenesCreated ?? 0,
|
ScenesCreated = commit?.ScenesCreated ?? 0,
|
||||||
CommitMessage = commit?.Notes,
|
CommitMessage = commit?.Notes,
|
||||||
Blockers = confirmation?.Blockers ?? [],
|
Blockers = confirmation?.Blockers ?? [],
|
||||||
Warnings = confirmation?.Warnings ?? []
|
Warnings = confirmation?.Warnings ?? [],
|
||||||
|
Scenes = BuildScenePreviews(sceneResults)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +291,41 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return (await GetProgressAsync(batchId), result);
|
return (await GetProgressAsync(batchId), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAllAsync(Guid batchId)
|
||||||
|
{
|
||||||
|
var progress = await GetProgressAsync(batchId);
|
||||||
|
if (progress is null)
|
||||||
|
{
|
||||||
|
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = RequireUserId();
|
||||||
|
var created = 0;
|
||||||
|
foreach (var chapter in progress.Chapters.OrderBy(chapter => chapter.ChapterNumber).Where(chapter => chapter.CanCommit))
|
||||||
|
{
|
||||||
|
var result = await commits.CommitAsync(chapter.RunID, userId);
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return (await GetProgressAsync(batchId), new StoryIntelligenceImportCommitResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Scene creation stopped. Chapter {chapter.ChapterNumber:N0} could not be imported. Reason: {result.Message}",
|
||||||
|
ScenesCreated = created
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
created += result.ScenesCreated;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onboarding.MarkCompletedAsync();
|
||||||
|
return (await GetProgressAsync(batchId), new StoryIntelligenceImportCommitResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ScenesCreated = created,
|
||||||
|
Message = $"Scenes successfully created. {created:N0} scene(s) added to PlotDirector."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId)
|
public async Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId)
|
||||||
{
|
{
|
||||||
var progress = await GetProgressAsync(batchId);
|
var progress = await GetProgressAsync(batchId);
|
||||||
@ -300,6 +339,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
BatchID = progress.BatchID,
|
BatchID = progress.BatchID,
|
||||||
ProjectID = progress.ProjectID,
|
ProjectID = progress.ProjectID,
|
||||||
BookID = progress.BookID,
|
BookID = progress.BookID,
|
||||||
|
FirstChapterID = progress.Chapters.OrderBy(chapter => chapter.ChapterNumber).FirstOrDefault()?.ChapterID,
|
||||||
ProjectName = progress.ProjectName,
|
ProjectName = progress.ProjectName,
|
||||||
BookTitle = progress.BookTitle,
|
BookTitle = progress.BookTitle,
|
||||||
ChaptersCommitted = progress.CommittedChapterCount,
|
ChaptersCommitted = progress.CommittedChapterCount,
|
||||||
@ -366,6 +406,64 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceOnboardingScenePreviewViewModel> BuildScenePreviews(IReadOnlyList<StoryIntelligenceSavedSceneResult> sceneResults)
|
||||||
|
=> sceneResults
|
||||||
|
.OrderBy(scene => scene.TemporarySceneNumber)
|
||||||
|
.Select(scene => new { Result = scene, Parsed = TryReadScene(scene.ParsedJson) })
|
||||||
|
.Where(scene => scene.Parsed is not null)
|
||||||
|
.Select(scene => new StoryIntelligenceOnboardingScenePreviewViewModel
|
||||||
|
{
|
||||||
|
SceneNumber = scene.Result.TemporarySceneNumber,
|
||||||
|
Summary = Trim(FirstConfigured(scene.Parsed?.Summary?.Short, scene.Parsed?.Summary?.Detailed, $"Scene {scene.Result.TemporarySceneNumber:N0}")!, 180),
|
||||||
|
Pov = FirstConfigured(scene.Parsed?.PointOfView?.CharacterName, scene.Parsed?.PointOfView?.NarrativeMode),
|
||||||
|
Setting = BuildSetting(scene.Parsed?.Setting),
|
||||||
|
Confidence = scene.Parsed?.Summary?.Confidence ?? scene.Parsed?.Setting?.Confidence,
|
||||||
|
HasWarnings = scene.Result.ValidationWarningsCount > 0
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static SceneIntelligenceScene? TryReadScene(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var direct = JsonSerializer.Deserialize<SceneIntelligenceScene>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
if (!string.IsNullOrWhiteSpace(direct?.SchemaVersion))
|
||||||
|
{
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
if (document.RootElement.TryGetProperty("parsedScene", out var parsedScene))
|
||||||
|
{
|
||||||
|
return parsedScene.Deserialize<SceneIntelligenceScene>(new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildSetting(SceneIntelligenceSetting? setting)
|
||||||
|
=> setting is null
|
||||||
|
? null
|
||||||
|
: FirstConfigured(
|
||||||
|
setting.LocationName,
|
||||||
|
setting.GenericRoomType,
|
||||||
|
setting.LocationType,
|
||||||
|
setting.DateOrTimeReference,
|
||||||
|
setting.TimeOfDay);
|
||||||
|
|
||||||
|
private static string? FirstConfigured(params string?[] values)
|
||||||
|
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim();
|
||||||
|
|
||||||
private static string? ExtractShortSummary(string json)
|
private static string? ExtractShortSummary(string json)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@ -144,8 +144,12 @@ public sealed class StoryIntelligenceProgressViewModel
|
|||||||
public int TotalDetectedScenes => Chapters.Sum(chapter => chapter.TotalDetectedScenes ?? 0);
|
public int TotalDetectedScenes => Chapters.Sum(chapter => chapter.TotalDetectedScenes ?? 0);
|
||||||
public int TotalCompletedScenes => Chapters.Sum(chapter => chapter.CompletedScenes ?? 0);
|
public int TotalCompletedScenes => Chapters.Sum(chapter => chapter.CompletedScenes ?? 0);
|
||||||
public int TotalFailedScenes => Chapters.Sum(chapter => chapter.FailedScenes ?? 0);
|
public int TotalFailedScenes => Chapters.Sum(chapter => chapter.FailedScenes ?? 0);
|
||||||
|
public int TotalAnalysedWords => Chapters.Sum(chapter => chapter.SourceWordCount ?? 0);
|
||||||
|
public decimal? TotalEstimatedCostUSD => Chapters.Any(chapter => chapter.EstimatedCostUSD.HasValue) ? Chapters.Sum(chapter => chapter.EstimatedCostUSD ?? 0m) : null;
|
||||||
|
public long TotalDurationMs => Chapters.Sum(chapter => chapter.TotalDurationMs ?? 0);
|
||||||
public bool HasActiveRuns => Chapters.Any(chapter => chapter.IsActive);
|
public bool HasActiveRuns => Chapters.Any(chapter => chapter.IsActive);
|
||||||
public bool AllCommitted => Chapters.Count > 0 && Chapters.All(chapter => chapter.HasCompletedCommit);
|
public bool AllCommitted => Chapters.Count > 0 && Chapters.All(chapter => chapter.HasCompletedCommit);
|
||||||
|
public bool HasReadyScenesToCreate => Chapters.Any(chapter => chapter.CanCommit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StoryIntelligenceCompletionViewModel
|
public sealed class StoryIntelligenceCompletionViewModel
|
||||||
@ -154,6 +158,7 @@ public sealed class StoryIntelligenceCompletionViewModel
|
|||||||
public Guid? BatchID { get; init; }
|
public Guid? BatchID { get; init; }
|
||||||
public int ProjectID { get; init; }
|
public int ProjectID { get; init; }
|
||||||
public int BookID { get; init; }
|
public int BookID { get; init; }
|
||||||
|
public int? FirstChapterID { get; init; }
|
||||||
public string? ProjectName { get; init; }
|
public string? ProjectName { get; init; }
|
||||||
public string? BookTitle { get; init; }
|
public string? BookTitle { get; init; }
|
||||||
public int ChaptersCommitted { get; init; }
|
public int ChaptersCommitted { get; init; }
|
||||||
@ -176,6 +181,8 @@ public sealed class StoryIntelligenceOnboardingChapterViewModel
|
|||||||
public int? SourceWordCount { 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 decimal? EstimatedCostUSD { get; init; }
|
||||||
|
public decimal? EstimatedCostGBP { get; init; }
|
||||||
public string? FailureStage { get; init; }
|
public string? FailureStage { get; init; }
|
||||||
public string? ErrorMessage { get; init; }
|
public string? ErrorMessage { get; init; }
|
||||||
public string? LatestSceneSummary { get; init; }
|
public string? LatestSceneSummary { get; init; }
|
||||||
@ -185,11 +192,23 @@ public sealed class StoryIntelligenceOnboardingChapterViewModel
|
|||||||
public string? CommitMessage { get; init; }
|
public string? CommitMessage { get; init; }
|
||||||
public IReadOnlyList<string> Blockers { get; init; } = [];
|
public IReadOnlyList<string> Blockers { get; init; } = [];
|
||||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceOnboardingScenePreviewViewModel> Scenes { get; init; } = [];
|
||||||
|
public decimal? AverageConfidence => Scenes.Count == 0 ? null : Scenes.Average(scene => scene.Confidence ?? 0m);
|
||||||
public bool IsActive => Status is StoryIntelligenceRunStatuses.Pending or StoryIntelligenceRunStatuses.Running;
|
public bool IsActive => Status is StoryIntelligenceRunStatuses.Pending or StoryIntelligenceRunStatuses.Running;
|
||||||
public bool IsRunComplete => Status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings;
|
public bool IsRunComplete => Status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings;
|
||||||
public bool IsFailed => Status is StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled;
|
public bool IsFailed => Status is StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceOnboardingScenePreviewViewModel
|
||||||
|
{
|
||||||
|
public int SceneNumber { get; init; }
|
||||||
|
public string Summary { get; init; } = string.Empty;
|
||||||
|
public string? Pov { get; init; }
|
||||||
|
public string? Setting { get; init; }
|
||||||
|
public decimal? Confidence { get; init; }
|
||||||
|
public bool HasWarnings { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class StoryIntelligenceDashboardViewModel
|
public sealed class StoryIntelligenceDashboardViewModel
|
||||||
{
|
{
|
||||||
public bool ShouldShow { get; init; }
|
public bool ShouldShow { get; init; }
|
||||||
|
|||||||
@ -36,7 +36,11 @@
|
|||||||
|
|
||||||
<div class="onboarding-actions">
|
<div class="onboarding-actions">
|
||||||
<a class="btn btn-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">View scenes</a>
|
<a class="btn btn-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">View scenes</a>
|
||||||
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">View book</a>
|
<a class="btn btn-outline-primary" asp-controller="Books" asp-action="Details" asp-route-id="@Model.BookID">View book</a>
|
||||||
|
@if (Model.FirstChapterID.HasValue)
|
||||||
|
{
|
||||||
|
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Chapter" asp-route-id="@Model.FirstChapterID">Open first chapter</a>
|
||||||
|
}
|
||||||
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Index">Back to Dashboard</a>
|
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Index">Back to Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,16 +8,16 @@
|
|||||||
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
||||||
&& adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase));
|
&& adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase));
|
||||||
var chaptersNeedingReview = Model.Chapters.Count(NeedsReview);
|
var chaptersNeedingReview = Model.Chapters.Count(NeedsReview);
|
||||||
var readyToCreate = Model.Chapters.Count(chapter => chapter.CanCommit);
|
var warningsCount = Model.Chapters.Sum(chapter => chapter.Warnings.Count + chapter.Blockers.Count + (chapter.FailedScenes ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="onboarding-shell" aria-labelledby="story-review-title">
|
<section class="onboarding-shell" aria-labelledby="story-review-title">
|
||||||
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
||||||
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 8 })" />
|
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 9 })" />
|
||||||
<div class="onboarding-copy">
|
<div class="onboarding-copy">
|
||||||
<p class="eyebrow">Step 8 of 9</p>
|
<p class="eyebrow">Step 9 of 9</p>
|
||||||
<h1 id="story-review-title">Review analysis</h1>
|
<h1 id="story-review-title">Review Story Intelligence Results</h1>
|
||||||
<p>PlotDirector has finished reading your manuscript. Review the chapter results, then create scenes when you are ready.</p>
|
<p>PlotDirector has finished reading your manuscript. Nothing has been added to your project yet. Please review the detected scenes before creating them.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||||
@ -30,39 +30,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<section class="alert alert-info">
|
<section class="alert alert-info">
|
||||||
Nothing is added to your project until you choose Create scenes. This step only creates scenes, summaries and supported scene notes.
|
Only scenes will be created. Characters, locations, assets, relationships and knowledge will be imported during later Story Intelligence phases.
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="story-review-summary" aria-label="Analysis summary">
|
<section class="story-review-summary" aria-label="Analysis summary">
|
||||||
<div>
|
<div>
|
||||||
<span>Chapters analysed</span>
|
<span>Story Intelligence complete</span>
|
||||||
<strong>@Model.CompletedChapterCount of @Model.ChapterCount</strong>
|
<strong>@Model.CompletedChapterCount.ToString("N0") chapter@(Model.CompletedChapterCount == 1 ? string.Empty : "s")</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Scenes found</span>
|
<span>Scenes detected</span>
|
||||||
<strong>@Model.TotalDetectedScenes.ToString("N0")</strong>
|
<strong>@Model.TotalDetectedScenes.ToString("N0")</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Ready to create</span>
|
<span>Words analysed</span>
|
||||||
<strong>@readyToCreate.ToString("N0")</strong>
|
<strong>@Model.TotalAnalysedWords.ToString("N0")</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Needs review</span>
|
<span>Elapsed time</span>
|
||||||
<strong>@chaptersNeedingReview.ToString("N0")</strong>
|
<strong>@FormatDuration(Model.TotalDurationMs)</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Estimated AI cost</span>
|
||||||
|
<strong>@FormatCost(Model.TotalEstimatedCostUSD)</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Warnings requiring review</span>
|
||||||
|
<strong>@warningsCount.ToString("N0")</strong>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="story-review-chapter-list" aria-label="Chapter review">
|
<section class="story-review-chapter-list" aria-label="Chapter review">
|
||||||
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
||||||
{
|
{
|
||||||
<article class="story-review-chapter-card">
|
<details class="story-review-chapter-card">
|
||||||
<div class="story-review-chapter-heading">
|
<summary class="story-review-chapter-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Chapter @chapter.ChapterNumber</p>
|
<p class="eyebrow">Chapter @chapter.ChapterNumber</p>
|
||||||
<h2>@chapter.ChapterTitle</h2>
|
<h2>@chapter.ChapterTitle</h2>
|
||||||
|
<p>@((chapter.TotalDetectedScenes ?? 0).ToString("N0")) scene@(chapter.TotalDetectedScenes == 1 ? string.Empty : "s") detected / confidence @FormatConfidence(chapter.AverageConfidence)</p>
|
||||||
</div>
|
</div>
|
||||||
<strong class="@StatusClass(chapter)">@FriendlyStatus(chapter)</strong>
|
<strong class="@StatusClass(chapter)">@FriendlyStatus(chapter)</strong>
|
||||||
</div>
|
</summary>
|
||||||
|
|
||||||
<div class="story-review-scene-panel">
|
<div class="story-review-scene-panel">
|
||||||
<div>
|
<div>
|
||||||
@ -77,6 +86,10 @@
|
|||||||
<span>Needs review</span>
|
<span>Needs review</span>
|
||||||
<strong>@(((chapter.FailedScenes ?? 0) + chapter.Blockers.Count).ToString("N0"))</strong>
|
<strong>@(((chapter.FailedScenes ?? 0) + chapter.Blockers.Count).ToString("N0"))</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Confidence</span>
|
||||||
|
<strong>@FormatConfidence(chapter.AverageConfidence)</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(chapter.CommitMessage))
|
@if (!string.IsNullOrWhiteSpace(chapter.CommitMessage))
|
||||||
@ -115,26 +128,31 @@
|
|||||||
<p class="story-review-note story-review-note--warning">@chapter.ErrorMessage</p>
|
<p class="story-review-note story-review-note--warning">@chapter.ErrorMessage</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="story-review-card-actions">
|
<div class="story-scene-preview-list">
|
||||||
@if (chapter.HasCompletedCommit)
|
@if (chapter.Scenes.Count == 0)
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-secondary" type="button" disabled>@chapter.ScenesCreated.ToString("N0") scene(s) created</button>
|
<p class="text-muted mb-0">No readable scene previews are available for this chapter.</p>
|
||||||
}
|
|
||||||
else if (chapter.CanCommit && Model.BatchID.HasValue)
|
|
||||||
{
|
|
||||||
<form asp-action="CommitStoryIntelligence" method="post">
|
|
||||||
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
|
||||||
<input type="hidden" name="runId" value="@chapter.RunID" />
|
|
||||||
<button class="btn btn-primary" type="submit">Create scenes</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
else if (chapter.IsActive && Model.BatchID.HasValue)
|
|
||||||
{
|
|
||||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.BatchID">View progress</a>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-secondary" type="button" disabled>Needs attention</button>
|
@foreach (var scene in chapter.Scenes)
|
||||||
|
{
|
||||||
|
<article class="story-scene-preview">
|
||||||
|
<div>
|
||||||
|
<strong>Scene @scene.SceneNumber.ToString("N0")</strong>
|
||||||
|
@if (scene.HasWarnings)
|
||||||
|
{
|
||||||
|
<span class="story-review-status story-review-status--active">Review recommended</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p>@scene.Summary</p>
|
||||||
|
<dl>
|
||||||
|
<div><dt>POV</dt><dd>@Display(scene.Pov)</dd></div>
|
||||||
|
<div><dt>Setting</dt><dd>@Display(scene.Setting)</dd></div>
|
||||||
|
<div><dt>Confidence</dt><dd>@FormatConfidence(scene.Confidence)</dd></div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -151,18 +169,30 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</details>
|
</details>
|
||||||
}
|
}
|
||||||
</article>
|
</details>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="alert alert-info">
|
||||||
|
Scene creation uses the existing PlotDirector import safeguards. Default empty Scene 1 placeholders can be replaced, duplicate imports are blocked, and failures roll back safely.
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="onboarding-actions">
|
<div class="onboarding-actions">
|
||||||
@if (Model.BatchID.HasValue)
|
@if (Model.BatchID.HasValue)
|
||||||
{
|
{
|
||||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.BatchID">View progress</a>
|
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.BatchID">Back</a>
|
||||||
|
@if (Model.HasReadyScenesToCreate)
|
||||||
|
{
|
||||||
|
<form asp-action="CommitAllStoryIntelligence" method="post">
|
||||||
|
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
||||||
|
<button class="btn btn-primary" type="submit">Create PlotDirector Scenes</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else if (Model.AllCommitted)
|
||||||
|
{
|
||||||
|
<a class="btn btn-primary" asp-action="StoryIntelligenceComplete" asp-route-batchId="@Model.BatchID">Continue</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<form asp-action="ScanManuscript" method="post">
|
|
||||||
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -214,4 +244,25 @@
|
|||||||
? "story-review-status story-review-status--active"
|
? "story-review-status story-review-status--active"
|
||||||
: "story-review-status story-review-status--warning";
|
: "story-review-status story-review-status--warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value;
|
||||||
|
|
||||||
|
private static string FormatConfidence(decimal? confidence)
|
||||||
|
=> confidence.HasValue ? $"{Math.Clamp(confidence.Value, 0m, 1m):P0}" : "Not available";
|
||||||
|
|
||||||
|
private static string FormatCost(decimal? cost)
|
||||||
|
=> cost.HasValue ? $"About ${cost.Value:0.00}" : "Not available";
|
||||||
|
|
||||||
|
private static string FormatDuration(long milliseconds)
|
||||||
|
{
|
||||||
|
var elapsed = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds));
|
||||||
|
if (elapsed.TotalHours >= 1)
|
||||||
|
{
|
||||||
|
return $"{Math.Ceiling(elapsed.TotalHours):N0} hr";
|
||||||
|
}
|
||||||
|
|
||||||
|
return elapsed.TotalMinutes >= 1
|
||||||
|
? $"{Math.Max(1, (int)Math.Round(elapsed.TotalMinutes)):N0} min"
|
||||||
|
: $"{Math.Max(0, (int)Math.Round(elapsed.TotalSeconds)):N0} sec";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -649,6 +649,15 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary.story-review-chapter-heading {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-review-chapter-heading p:not(.eyebrow) {
|
||||||
|
margin: .25rem 0 0;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
.story-live-progress-heading strong {
|
.story-live-progress-heading strong {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: clamp(2rem, 3vw, 2.8rem);
|
font-size: clamp(2rem, 3vw, 2.8rem);
|
||||||
@ -856,6 +865,49 @@
|
|||||||
gap: .65rem;
|
gap: .65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-scene-preview-list {
|
||||||
|
display: grid;
|
||||||
|
gap: .65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: .45rem;
|
||||||
|
border: 1px solid rgba(31, 42, 68, .1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: .75rem;
|
||||||
|
background: rgba(255, 255, 255, .58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-preview > div:first-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-preview p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-preview dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: .5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-preview dt {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-preview dd {
|
||||||
|
margin: .1rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.story-review-admin {
|
.story-review-admin {
|
||||||
border-top: 1px solid rgba(31, 42, 68, .1);
|
border-top: 1px solid rgba(31, 42, 68, .1);
|
||||||
padding-top: .75rem;
|
padding-top: .75rem;
|
||||||
@ -1231,6 +1283,7 @@
|
|||||||
.story-live-stats,
|
.story-live-stats,
|
||||||
.story-review-summary,
|
.story-review-summary,
|
||||||
.story-review-scene-panel,
|
.story-review-scene-panel,
|
||||||
|
.story-scene-preview dl,
|
||||||
.story-live-current,
|
.story-live-current,
|
||||||
.story-live-feed-grid,
|
.story-live-feed-grid,
|
||||||
.story-review-admin dl {
|
.story-review-admin dl {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user