Phase 20AE – Unify Story Intelligence Review Pipeline & Remove Placeholder Scene Architecture
This commit is contained in:
parent
5b4c78c040
commit
fba73186c1
@ -433,7 +433,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
return new StoryIntelligenceImportCommitResult
|
||||
{
|
||||
Success = false,
|
||||
Message = readiness.BlockReason ?? "This chapter contains existing authored scenes."
|
||||
Message = readiness.BlockReason ?? "This chapter already contains scenes."
|
||||
};
|
||||
}
|
||||
|
||||
@ -442,9 +442,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
{
|
||||
var item = orderedScenes[index];
|
||||
var operation = "preparing scene";
|
||||
var replacePlaceholder = readiness.HasUntouchedDefaultPlaceholder
|
||||
&& readiness.PlaceholderSceneID.HasValue
|
||||
&& index == 0;
|
||||
|
||||
try
|
||||
{
|
||||
@ -462,7 +459,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
"dbo.Scene_Save",
|
||||
new
|
||||
{
|
||||
SceneID = replacePlaceholder ? readiness.PlaceholderSceneID : null,
|
||||
SceneID = (int?)null,
|
||||
ChapterID = request.ChapterID,
|
||||
SceneNumber = Convert.ToDecimal(item.TemporarySceneNumber),
|
||||
SceneTitle = sceneTitle,
|
||||
@ -587,9 +584,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
}
|
||||
|
||||
var notes = new List<string>();
|
||||
notes.Add(readiness.HasUntouchedDefaultPlaceholder
|
||||
? "Replaced untouched default placeholder scene."
|
||||
: "Committed into empty chapter.");
|
||||
notes.Add("Committed into empty chapter.");
|
||||
notes.AddRange(request.Warnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
|
||||
notes.AddRange(commitWarnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
|
||||
|
||||
|
||||
@ -417,8 +417,6 @@ public sealed class StoryIntelligenceChapterImportReadiness
|
||||
{
|
||||
public int ActiveSceneCount { get; init; }
|
||||
public bool CanCommit { get; init; }
|
||||
public bool HasUntouchedDefaultPlaceholder { get; init; }
|
||||
public int? PlaceholderSceneID { get; init; }
|
||||
public string CommitMode { get; init; } = string.Empty;
|
||||
public string? BlockReason { get; init; }
|
||||
}
|
||||
|
||||
@ -247,7 +247,9 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
CommitMessage = commit?.Notes,
|
||||
Blockers = confirmation?.Blockers ?? [],
|
||||
Warnings = confirmation?.Warnings ?? [],
|
||||
Scenes = BuildScenePreviews(sceneResults)
|
||||
Scenes = confirmation is null
|
||||
? []
|
||||
: BuildScenePreviews(confirmation.Scenes)
|
||||
});
|
||||
}
|
||||
|
||||
@ -406,19 +408,17 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<StoryIntelligenceOnboardingScenePreviewViewModel> BuildScenePreviews(IReadOnlyList<StoryIntelligenceSavedSceneResult> sceneResults)
|
||||
=> sceneResults
|
||||
private static IReadOnlyList<StoryIntelligenceOnboardingScenePreviewViewModel> BuildScenePreviews(IReadOnlyList<StoryIntelligenceSceneImportItem> importScenes)
|
||||
=> importScenes
|
||||
.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
|
||||
SceneNumber = scene.TemporarySceneNumber,
|
||||
Summary = Trim(FirstConfigured(scene.ParsedScene.Summary?.Short, scene.ParsedScene.Summary?.Detailed, $"Scene {scene.TemporarySceneNumber:N0}")!, 180),
|
||||
Pov = FirstConfigured(scene.ParsedScene.PointOfView?.CharacterName, scene.ParsedScene.PointOfView?.NarrativeMode),
|
||||
Setting = BuildSetting(scene.ParsedScene.Setting),
|
||||
Confidence = scene.ParsedScene.Summary?.Confidence ?? scene.ParsedScene.Setting?.Confidence,
|
||||
HasWarnings = false
|
||||
})
|
||||
.ToList();
|
||||
|
||||
|
||||
@ -129,11 +129,7 @@ public sealed class StoryIntelligenceImportCommitService(
|
||||
: null;
|
||||
if (readiness is not null && !readiness.CanCommit)
|
||||
{
|
||||
blockers.Add(readiness.BlockReason ?? "This chapter contains existing authored scenes.");
|
||||
}
|
||||
else if (readiness?.HasUntouchedDefaultPlaceholder == true)
|
||||
{
|
||||
warnings.Add("This chapter contains only the default empty scene. It will be replaced by Scene 1 from the import.");
|
||||
blockers.Add(readiness.BlockReason ?? "This chapter already contains scenes.");
|
||||
}
|
||||
|
||||
if (existingCommit is not null && string.Equals(existingCommit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
|
||||
@ -169,7 +165,7 @@ public sealed class StoryIntelligenceImportCommitService(
|
||||
var parsed = TryReadScene(sceneResult);
|
||||
if (parsed is null)
|
||||
{
|
||||
warnings.Add($"Suggested scene {sceneResult.TemporarySceneNumber:N0} was skipped because its parsed JSON could not be read.");
|
||||
warnings.Add($"Scene {sceneResult.TemporarySceneNumber:N0} could not be prepared automatically and needs review.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -178,7 +174,7 @@ public sealed class StoryIntelligenceImportCommitService(
|
||||
|
||||
if (importScenes.Count == 0)
|
||||
{
|
||||
blockers.Add("No valid scene intelligence results are available to import.");
|
||||
blockers.Add("No scenes are ready to create for this chapter.");
|
||||
}
|
||||
|
||||
var projectName = run.ProjectID.HasValue ? (await projects.GetAsync(run.ProjectID.Value))?.ProjectName ?? $"Project {run.ProjectID.Value:N0}" : "None";
|
||||
@ -200,13 +196,13 @@ public sealed class StoryIntelligenceImportCommitService(
|
||||
BookTitle = bookTitle,
|
||||
ChapterLabel = chapterLabel,
|
||||
ExistingSceneCount = readiness?.ActiveSceneCount ?? 0,
|
||||
HasUntouchedDefaultPlaceholder = readiness?.HasUntouchedDefaultPlaceholder == true,
|
||||
CommitMode = readiness?.CommitMode ?? string.Empty,
|
||||
ScenesToCreate = importScenes.Count,
|
||||
MetricsToImport = importScenes.Sum(scene => scene.Metrics.Count),
|
||||
MetricNames = importScenes.SelectMany(scene => scene.Metrics.Select(metric => metric.MetricName)).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name).ToList(),
|
||||
Warnings = warnings,
|
||||
Blockers = blockers
|
||||
Blockers = blockers,
|
||||
Scenes = importScenes
|
||||
},
|
||||
importScenes,
|
||||
timeModeId,
|
||||
|
||||
29
PlotLine/Sql/125_Phase20AE_RemoveDefaultScenePlaceholder.sql
Normal file
29
PlotLine/Sql/125_Phase20AE_RemoveDefaultScenePlaceholder.sql
Normal file
@ -0,0 +1,29 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.TR_Chapters_CreateDefaultScene', N'TR') IS NOT NULL
|
||||
DROP TRIGGER dbo.TR_Chapters_CreateDefaultScene;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceImportCommit_GetChapterReadiness
|
||||
@ChapterID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ActiveSceneCount int;
|
||||
|
||||
SELECT @ActiveSceneCount = COUNT(1)
|
||||
FROM dbo.Scenes
|
||||
WHERE ChapterID = @ChapterID
|
||||
AND IsArchived = 0;
|
||||
|
||||
SELECT
|
||||
@ActiveSceneCount AS ActiveSceneCount,
|
||||
CAST(CASE WHEN @ActiveSceneCount = 0 THEN 1 ELSE 0 END AS bit) AS CanCommit,
|
||||
CAST(CASE WHEN @ActiveSceneCount = 0 THEN N'EmptyChapter' ELSE N'Blocked' END AS nvarchar(80)) AS CommitMode,
|
||||
CAST(CASE WHEN @ActiveSceneCount = 0 THEN NULL ELSE N'This chapter already contains scenes.' END AS nvarchar(300)) AS BlockReason;
|
||||
END;
|
||||
GO
|
||||
@ -304,13 +304,13 @@ public sealed class StoryIntelligenceImportConfirmationViewModel
|
||||
public string BookTitle { get; init; } = string.Empty;
|
||||
public string ChapterLabel { get; init; } = string.Empty;
|
||||
public int ExistingSceneCount { get; init; }
|
||||
public bool HasUntouchedDefaultPlaceholder { get; init; }
|
||||
public string CommitMode { get; init; } = string.Empty;
|
||||
public int ScenesToCreate { get; init; }
|
||||
public int MetricsToImport { get; init; }
|
||||
public IReadOnlyList<string> MetricNames { get; init; } = [];
|
||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||
public IReadOnlyList<string> Blockers { get; init; } = [];
|
||||
public IReadOnlyList<StoryIntelligenceSceneImportItem> Scenes { get; init; } = [];
|
||||
public bool HasCompletedCommit => ExistingCommit is not null
|
||||
&& string.Equals(ExistingCommit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase);
|
||||
public bool CanCommit => Blockers.Count == 0 && !HasCompletedCommit;
|
||||
|
||||
@ -54,13 +54,6 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.HasUntouchedDefaultPlaceholder)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
This chapter contains only the default empty scene. It will be replaced by Scene 1 from the import.
|
||||
</div>
|
||||
}
|
||||
|
||||
<dl>
|
||||
<dt>Project</dt>
|
||||
<dd>@Model.ProjectName</dd>
|
||||
@ -78,10 +71,10 @@
|
||||
<dd>@(Model.MetricNames.Count == 0 ? "None" : string.Join(", ", Model.MetricNames))</dd>
|
||||
</dl>
|
||||
|
||||
@if (Model.ExistingSceneCount > 0 && !Model.HasUntouchedDefaultPlaceholder)
|
||||
@if (Model.ExistingSceneCount > 0)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
This chapter already has authored scenes. The current import mode will not merge, replace, or overwrite authored scene content.
|
||||
This chapter already has scenes. The current import mode will not merge, replace, or overwrite scene content.
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
StoryIntelligenceRunStatuses.Pending => "Waiting",
|
||||
StoryIntelligenceRunStatuses.Running => "Reading",
|
||||
StoryIntelligenceRunStatuses.Completed => "Ready to create",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Ready with notes",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Ready",
|
||||
StoryIntelligenceRunStatuses.Failed => "Needs review",
|
||||
StoryIntelligenceRunStatuses.Cancelled => "Cancelled",
|
||||
_ => "Waiting"
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
||||
&& adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase));
|
||||
var chaptersNeedingReview = Model.Chapters.Count(NeedsReview);
|
||||
var warningsCount = Model.Chapters.Sum(chapter => chapter.Warnings.Count + chapter.Blockers.Count + (chapter.FailedScenes ?? 0));
|
||||
var warningsCount = Model.Chapters.Sum(chapter => NeedsReviewCount(chapter));
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-review-title">
|
||||
@ -55,7 +55,7 @@
|
||||
<strong>@FormatCost(Model.TotalEstimatedCostUSD)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Warnings requiring review</span>
|
||||
<span>Needs review</span>
|
||||
<strong>@warningsCount.ToString("N0")</strong>
|
||||
</div>
|
||||
</section>
|
||||
@ -68,27 +68,27 @@
|
||||
<div>
|
||||
<p class="eyebrow">Chapter @chapter.ChapterNumber</p>
|
||||
<h2>@chapter.ChapterTitle</h2>
|
||||
<p>@((chapter.TotalDetectedScenes ?? 0).ToString("N0")) scene@(chapter.TotalDetectedScenes == 1 ? string.Empty : "s") detected / confidence @FormatConfidence(chapter.AverageConfidence)</p>
|
||||
<p>@DetectedCount(chapter).ToString("N0") scene@(DetectedCount(chapter) == 1 ? string.Empty : "s") detected / @ReadyCount(chapter).ToString("N0") ready to create</p>
|
||||
</div>
|
||||
<strong class="@StatusClass(chapter)">@FriendlyStatus(chapter)</strong>
|
||||
</summary>
|
||||
|
||||
<div class="story-review-scene-panel">
|
||||
<div>
|
||||
<span>Scenes found</span>
|
||||
<strong>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</strong>
|
||||
<span>Detected scenes</span>
|
||||
<strong>@DetectedCount(chapter).ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes ready</span>
|
||||
<strong>@((chapter.CompletedScenes ?? 0).ToString("N0"))</strong>
|
||||
<span>Ready</span>
|
||||
<strong>@ReadyCount(chapter).ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Needs review</span>
|
||||
<strong>@(((chapter.FailedScenes ?? 0) + chapter.Blockers.Count).ToString("N0"))</strong>
|
||||
<strong>@NeedsReviewCount(chapter).ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Confidence</span>
|
||||
<strong>@FormatConfidence(chapter.AverageConfidence)</strong>
|
||||
<span>Rejected</span>
|
||||
<strong>@RejectedCount(chapter).ToString("N0")</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
<ul>
|
||||
@foreach (var warning in chapter.Warnings)
|
||||
{
|
||||
<li>@warning</li>
|
||||
<li>@AuthorMessage(warning)</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@ -117,7 +117,7 @@
|
||||
<ul>
|
||||
@foreach (var blocker in chapter.Blockers)
|
||||
{
|
||||
<li>@blocker</li>
|
||||
<li>@AuthorMessage(blocker)</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@ -125,7 +125,7 @@
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage))
|
||||
{
|
||||
<p class="story-review-note story-review-note--warning">@chapter.ErrorMessage</p>
|
||||
<p class="story-review-note story-review-note--warning">@AuthorMessage(chapter.ErrorMessage)</p>
|
||||
}
|
||||
|
||||
<div class="story-scene-preview-list">
|
||||
@ -174,7 +174,7 @@
|
||||
</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.
|
||||
Scene creation uses PlotDirector import safeguards. Duplicate imports are blocked, and failures roll back safely.
|
||||
</section>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
@ -200,10 +200,21 @@
|
||||
@functions {
|
||||
private static bool NeedsReview(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
=> chapter.IsFailed
|
||||
|| (chapter.FailedScenes ?? 0) > 0
|
||||
|| chapter.Blockers.Count > 0
|
||||
|| NeedsReviewCount(chapter) > 0
|
||||
|| (!chapter.CanCommit && !chapter.HasCompletedCommit && chapter.IsRunComplete);
|
||||
|
||||
private static int DetectedCount(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
=> Math.Max(chapter.TotalDetectedScenes ?? 0, ReadyCount(chapter) + Math.Max(chapter.FailedScenes ?? 0, 0));
|
||||
|
||||
private static int ReadyCount(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
=> chapter.HasCompletedCommit ? chapter.ScenesCreated : chapter.Scenes.Count;
|
||||
|
||||
private static int RejectedCount(StoryIntelligenceOnboardingChapterViewModel chapter) => 0;
|
||||
|
||||
private static int NeedsReviewCount(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
=> Math.Max(0, DetectedCount(chapter) - ReadyCount(chapter) - RejectedCount(chapter));
|
||||
|
||||
private static string FriendlyStatus(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
{
|
||||
if (chapter.HasCompletedCommit)
|
||||
@ -213,18 +224,18 @@
|
||||
|
||||
if (chapter.CanCommit)
|
||||
{
|
||||
return chapter.Warnings.Count > 0 ? "Ready with notes" : "Ready to create";
|
||||
return "Ready";
|
||||
}
|
||||
|
||||
return chapter.Status switch
|
||||
{
|
||||
StoryIntelligenceRunStatuses.Pending => "Waiting",
|
||||
StoryIntelligenceRunStatuses.Running => "Reading",
|
||||
StoryIntelligenceRunStatuses.Completed => "Needs attention",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Needs attention",
|
||||
StoryIntelligenceRunStatuses.Completed => "Cannot import",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Cannot import",
|
||||
StoryIntelligenceRunStatuses.Failed => "Needs review",
|
||||
StoryIntelligenceRunStatuses.Cancelled => "Cancelled",
|
||||
_ => "Needs attention"
|
||||
_ => "Needs review"
|
||||
};
|
||||
}
|
||||
|
||||
@ -247,6 +258,31 @@
|
||||
|
||||
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value;
|
||||
|
||||
private static string AuthorMessage(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "This chapter needs review before scenes can be created.";
|
||||
}
|
||||
|
||||
var message = value.Trim();
|
||||
if (message.Contains("JSON", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("parsed", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("parser", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("deserial", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "One or more scene suggestions could not be prepared automatically and need review.";
|
||||
}
|
||||
|
||||
if (message.Contains("existing authored scenes", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("already contains scenes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "This chapter already contains scenes. Choose an empty chapter before creating scenes from this analysis.";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private static string FormatConfidence(decimal? confidence)
|
||||
=> confidence.HasValue ? $"{Math.Clamp(confidence.Value, 0m, 1m):P0}" : "Not available";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user