From fba73186c1b9d541b5f9853fac592f6714415434 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Tue, 7 Jul 2026 21:09:59 +0100 Subject: [PATCH] =?UTF-8?q?Phase=2020AE=20=E2=80=93=20Unify=20Story=20Inte?= =?UTF-8?q?lligence=20Review=20Pipeline=20&=20Remove=20Placeholder=20Scene?= =?UTF-8?q?=20Architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/StoryIntelligenceResultRepository.cs | 11 +-- .../StoryIntelligencePersistenceModels.cs | 2 - .../OnboardingStoryIntelligenceService.cs | 22 +++--- .../StoryIntelligenceImportCommitService.cs | 14 ++-- ...hase20AE_RemoveDefaultScenePlaceholder.sql | 29 ++++++++ .../ViewModels/FeatureRequestViewModels.cs | 2 +- ...onfirmStoryIntelligenceImportCommit.cshtml | 11 +-- .../StoryIntelligenceProgress.cshtml | 2 +- .../Onboarding/StoryIntelligenceReview.cshtml | 74 ++++++++++++++----- 9 files changed, 107 insertions(+), 60 deletions(-) create mode 100644 PlotLine/Sql/125_Phase20AE_RemoveDefaultScenePlaceholder.sql diff --git a/PlotLine/Data/StoryIntelligenceResultRepository.cs b/PlotLine/Data/StoryIntelligenceResultRepository.cs index d50a78d..e2ee7e0 100644 --- a/PlotLine/Data/StoryIntelligenceResultRepository.cs +++ b/PlotLine/Data/StoryIntelligenceResultRepository.cs @@ -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(); - 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))); diff --git a/PlotLine/Models/StoryIntelligencePersistenceModels.cs b/PlotLine/Models/StoryIntelligencePersistenceModels.cs index 275e17c..a6190e1 100644 --- a/PlotLine/Models/StoryIntelligencePersistenceModels.cs +++ b/PlotLine/Models/StoryIntelligencePersistenceModels.cs @@ -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; } } diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs index 1c10537..6197b6e 100644 --- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs +++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs @@ -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 BuildScenePreviews(IReadOnlyList sceneResults) - => sceneResults + private static IReadOnlyList BuildScenePreviews(IReadOnlyList 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(); diff --git a/PlotLine/Services/StoryIntelligenceImportCommitService.cs b/PlotLine/Services/StoryIntelligenceImportCommitService.cs index 402c54c..05b458f 100644 --- a/PlotLine/Services/StoryIntelligenceImportCommitService.cs +++ b/PlotLine/Services/StoryIntelligenceImportCommitService.cs @@ -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, diff --git a/PlotLine/Sql/125_Phase20AE_RemoveDefaultScenePlaceholder.sql b/PlotLine/Sql/125_Phase20AE_RemoveDefaultScenePlaceholder.sql new file mode 100644 index 0000000..4d33893 --- /dev/null +++ b/PlotLine/Sql/125_Phase20AE_RemoveDefaultScenePlaceholder.sql @@ -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 diff --git a/PlotLine/ViewModels/FeatureRequestViewModels.cs b/PlotLine/ViewModels/FeatureRequestViewModels.cs index e94671a..802a1c1 100644 --- a/PlotLine/ViewModels/FeatureRequestViewModels.cs +++ b/PlotLine/ViewModels/FeatureRequestViewModels.cs @@ -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 MetricNames { get; init; } = []; public IReadOnlyList Warnings { get; init; } = []; public IReadOnlyList Blockers { get; init; } = []; + public IReadOnlyList 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; diff --git a/PlotLine/Views/Admin/ConfirmStoryIntelligenceImportCommit.cshtml b/PlotLine/Views/Admin/ConfirmStoryIntelligenceImportCommit.cshtml index 76cc60f..46c415b 100644 --- a/PlotLine/Views/Admin/ConfirmStoryIntelligenceImportCommit.cshtml +++ b/PlotLine/Views/Admin/ConfirmStoryIntelligenceImportCommit.cshtml @@ -54,13 +54,6 @@ } - @if (Model.HasUntouchedDefaultPlaceholder) - { -
- This chapter contains only the default empty scene. It will be replaced by Scene 1 from the import. -
- } -
Project
@Model.ProjectName
@@ -78,10 +71,10 @@
@(Model.MetricNames.Count == 0 ? "None" : string.Join(", ", Model.MetricNames))
- @if (Model.ExistingSceneCount > 0 && !Model.HasUntouchedDefaultPlaceholder) + @if (Model.ExistingSceneCount > 0) {
- 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.
} diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml index 28b74ba..df6556a 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml @@ -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" diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml index c7fb131..e796138 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml @@ -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)); }
@@ -55,7 +55,7 @@ @FormatCost(Model.TotalEstimatedCostUSD)
- Warnings requiring review + Needs review @warningsCount.ToString("N0")
@@ -68,27 +68,27 @@

Chapter @chapter.ChapterNumber

@chapter.ChapterTitle

-

@((chapter.TotalDetectedScenes ?? 0).ToString("N0")) scene@(chapter.TotalDetectedScenes == 1 ? string.Empty : "s") detected / confidence @FormatConfidence(chapter.AverageConfidence)

+

@DetectedCount(chapter).ToString("N0") scene@(DetectedCount(chapter) == 1 ? string.Empty : "s") detected / @ReadyCount(chapter).ToString("N0") ready to create

@FriendlyStatus(chapter)
- Scenes found - @((chapter.TotalDetectedScenes ?? 0).ToString("N0")) + Detected scenes + @DetectedCount(chapter).ToString("N0")
- Scenes ready - @((chapter.CompletedScenes ?? 0).ToString("N0")) + Ready + @ReadyCount(chapter).ToString("N0")
Needs review - @(((chapter.FailedScenes ?? 0) + chapter.Blockers.Count).ToString("N0")) + @NeedsReviewCount(chapter).ToString("N0")
- Confidence - @FormatConfidence(chapter.AverageConfidence) + Rejected + @RejectedCount(chapter).ToString("N0")
@@ -104,7 +104,7 @@
    @foreach (var warning in chapter.Warnings) { -
  • @warning
  • +
  • @AuthorMessage(warning)
  • }
@@ -117,7 +117,7 @@
    @foreach (var blocker in chapter.Blockers) { -
  • @blocker
  • +
  • @AuthorMessage(blocker)
  • }
@@ -125,7 +125,7 @@ @if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage)) { -

@chapter.ErrorMessage

+

@AuthorMessage(chapter.ErrorMessage)

}
@@ -174,7 +174,7 @@
- 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.
@@ -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";