From dd4b0badb62fb8f8c7773c12cc7341f75b957eac Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sat, 29 Aug 2026 09:56:32 +0000 Subject: [PATCH] Redesign Story Intelligence Review Centre as tile grid --- PlotLine.Tests/Program.cs | 46 ++++ .../StoryIntelligenceReviewCentre.cshtml | 221 ++++++++++-------- PlotLine/wwwroot/css/onboarding.css | 102 ++++++-- 3 files changed, 260 insertions(+), 109 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index b0ae185..fc2adce 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -233,6 +233,7 @@ var tests = new (string Name, Action Test)[] ("Story Intelligence Review Centre route replaces linear review resume", StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume), ("Story Intelligence obsolete create scene workflow is removed", StoryIntelligenceObsoleteCreateSceneWorkflowIsRemoved), ("Story Intelligence Review Centre counts only needs-attention categories", StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories), + ("Story Intelligence Review Centre uses independent tile grid", StoryIntelligenceReviewCentreUsesIndependentTileGrid), ("Story Intelligence Review Centre opens with character gate", StoryIntelligenceReviewCentreOpensWithCharacterGate), ("Story Intelligence review modules are independent after character gate", StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate), ("Story Intelligence downstream review uses completed scene analysis", StoryIntelligenceDownstreamReviewUsesCompletedSceneAnalysis), @@ -665,6 +666,51 @@ static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories() Assert(!summary.Contains("Locations identified", StringComparison.Ordinal), "Summary should not present total identified rows as review workload."); } +static void StoryIntelligenceReviewCentreUsesIndependentTileGrid() +{ + var root = PlotLineRoot(); + var centre = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml")); + var css = File.ReadAllText(Path.Combine(root, "wwwroot/css/onboarding.css")); + var tileOrder = new[] + { + "\"Characters\"", + "\"Assets\"", + "\"Knowledge\"", + "\"Locations\"", + "\"Plot Lines & Threads\"", + "\"Relationships\"", + "ReviewMetrics" + }; + + Assert(!centre.Contains("_StoryIntelligencePipelineHeader", StringComparison.Ordinal), "Review Centre should not render wizard progress header."); + Assert(!centre.Contains("_StoryIntelligencePipelineSummary", StringComparison.Ordinal), "Review Centre should not render separate summary statistics."); + Assert(!centre.Contains("story-review-centre-list", StringComparison.Ordinal), "Review Centre should no longer use the sequential row list."); + Assert(centre.Contains("story-review-centre-grid", StringComparison.Ordinal), "Review Centre should render a category tile grid."); + Assert(centre.Contains("story-review-centre-tile--primary", StringComparison.Ordinal), "Characters tile should have subtle first-action emphasis."); + Assert(centre.Contains("Resolve Characters first.", StringComparison.Ordinal), "Header should explain character-first gating compactly."); + Assert(centre.Contains("can be reviewed in any order", StringComparison.Ordinal), "Header should communicate independent review order."); + Assert(centre.Contains("Locked until Characters are resolved", StringComparison.Ordinal), "Locked dependent tiles should be visible and clearly locked."); + Assert(centre.Contains(">Locked", StringComparison.Ordinal), "Locked tile action should render as non-clickable text."); + Assert(centre.Contains("Available", StringComparison.Ordinal) && centre.Contains("View Metrics", StringComparison.Ordinal), "Scene Metrics should be available, not a mandatory review workload."); + Assert(!centre.Contains("Next", StringComparison.Ordinal) && !centre.Contains("Previous", StringComparison.Ordinal), "Review Centre should not contain wizard navigation."); + Assert(!centre.Contains("Core Import", StringComparison.Ordinal) && !centre.Contains("Analyse Manuscript", StringComparison.Ordinal), "Review Centre should not show phase panels."); + + var lastIndex = -1; + foreach (var marker in tileOrder) + { + var index = centre.IndexOf(marker, StringComparison.Ordinal); + Assert(index > lastIndex, $"{marker} should appear in the expected tile order."); + lastIndex = index; + } + + Assert(css.Contains(".story-review-centre-grid", StringComparison.Ordinal), "Review Centre grid CSS should exist."); + Assert(css.Contains("grid-template-columns: repeat(3, minmax(0, 1fr))", StringComparison.Ordinal), "Desktop Review Centre should use three columns."); + Assert(css.Contains("@media (max-width: 980px)", StringComparison.Ordinal) + && css.Contains("grid-template-columns: repeat(2, minmax(0, 1fr))", StringComparison.Ordinal), "Tablet Review Centre should use two columns."); + Assert(css.Contains("@media (max-width: 720px)", StringComparison.Ordinal) + && css.Contains(".story-review-centre-grid {\n grid-template-columns: 1fr;", StringComparison.Ordinal), "Mobile Review Centre should use one column."); +} + static void StoryIntelligenceReviewCentreOpensWithCharacterGate() { var root = PlotLineRoot(); diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml index 7787e0a..4253c5b 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml @@ -3,14 +3,14 @@ ViewData["Title"] = "Story Intelligence Review Centre"; } -
-
- - -
+
+
+

Story Intelligence

Review Centre

-

Your manuscript analysis is complete. Review only the story intelligence that needs an author decision; everything here is saved and can be picked up later.

+

@(Model.CharacterGateSatisfied + ? "Your manuscript analysis is complete. Choose any category to inspect or review." + : "Resolve Characters first. Once character identities are confirmed, the remaining Story Intelligence categories can be reviewed in any order.")

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -22,138 +22,169 @@
@message
} - - -
+
@ReviewModule( "Characters", Model.CharacterNeedsAttentionCount, - Model.CharacterGateSatisfied ? "Resolved" : "Needs attention", - "Resolve identity questions before reviewing dependent story intelligence.", - "Resolve Characters", + Model.CharacterGateSatisfied, + false, + "Resolve character identities before reviewing dependent Story Intelligence.", + Model.CharacterGateSatisfied ? "Review Characters" : "Resolve Characters", "StoryIntelligenceCharacters", - false) - - @ReviewModule( - "Locations", - Model.LocationNeedsAttentionCount, - Model.PipelineDashboard.LocationStageComplete ? "Reviewed" : AttentionLabel(Model.LocationNeedsAttentionCount), - "Places found in the manuscript that may be worth creating, linking, or ignoring.", - "Review locations", - "StoryIntelligenceLocations", - !Model.CharacterGateSatisfied) + true) @ReviewModule( "Assets", Model.AssetNeedsAttentionCount, - Model.PipelineDashboard.AssetStageComplete ? "Reviewed" : AttentionLabel(Model.AssetNeedsAttentionCount), - "Important objects and possessions detected from generic evidence such as recurrence, ownership, and scene prominence.", - "Review assets", - "StoryIntelligenceAssets", - !Model.CharacterGateSatisfied) - - @ReviewModule( - "Relationships", - Model.RelationshipNeedsAttentionCount, - Model.PipelineDashboard.RelationshipStageComplete ? "Reviewed" : AttentionLabel(Model.RelationshipNeedsAttentionCount), - "Relationship candidates grouped by the canonical character identities involved.", - "Review relationships", - "StoryIntelligenceRelationships", - !Model.CharacterGateSatisfied) + Model.PipelineDashboard.AssetStageComplete, + !Model.CharacterGateSatisfied, + "Important objects and possessions found in the manuscript.", + ModuleAction("Assets", Model.AssetNeedsAttentionCount, Model.PipelineDashboard.AssetStageComplete), + "StoryIntelligenceAssets") @ReviewModule( "Knowledge", Model.KnowledgeNeedsAttentionCount, - Model.PipelineDashboard.KnowledgeStageComplete ? "Reviewed" : AttentionLabel(Model.KnowledgeNeedsAttentionCount), - "Facts, discoveries, beliefs, and suspicions that need a create, link, merge, or ignore decision.", - "Review knowledge", - "StoryIntelligenceKnowledge", - !Model.CharacterGateSatisfied) + Model.PipelineDashboard.KnowledgeStageComplete, + !Model.CharacterGateSatisfied, + "Facts, discoveries, beliefs, and suspicions found in the manuscript.", + ModuleAction("Knowledge", Model.KnowledgeNeedsAttentionCount, Model.PipelineDashboard.KnowledgeStageComplete), + "StoryIntelligenceKnowledge") + + @ReviewModule( + "Locations", + Model.LocationNeedsAttentionCount, + Model.PipelineDashboard.LocationStageComplete, + !Model.CharacterGateSatisfied, + "Places found in the manuscript.", + ModuleAction("Locations", Model.LocationNeedsAttentionCount, Model.PipelineDashboard.LocationStageComplete), + "StoryIntelligenceLocations") @ReviewModule( "Plot Lines & Threads", Model.NarrativeNeedsAttentionCount, - Model.PipelineDashboard.NarrativeStageComplete ? "Reviewed" : AttentionLabel(Model.NarrativeNeedsAttentionCount), - "Recurring arcs and threads organised by the narrative hierarchy discovered from scene evidence.", - "Review plot lines", - "StoryIntelligenceNarrative", - !Model.CharacterGateSatisfied) + Model.PipelineDashboard.NarrativeStageComplete, + !Model.CharacterGateSatisfied, + "Recurring arcs and threads discovered from scene evidence.", + ModuleAction("Plot Lines & Threads", Model.NarrativeNeedsAttentionCount, Model.PipelineDashboard.NarrativeStageComplete), + "StoryIntelligenceNarrative") -
-
-

Scene Metrics

-

Available from the imported scene analysis. Metrics are auto-applied where configured and do not require a mandatory review step.

-
- @if (Model.CharacterGateSatisfied) - { - Available - @Model.TotalAnalysedScenes.ToString("N0") scenes - } - else - { - Resolve Characters first - Locked until Characters are resolved - } -
+ @ReviewModule( + "Relationships", + Model.RelationshipNeedsAttentionCount, + Model.PipelineDashboard.RelationshipStageComplete, + !Model.CharacterGateSatisfied, + "Character relationships found in scene evidence.", + ModuleAction("Relationships", Model.RelationshipNeedsAttentionCount, Model.PipelineDashboard.RelationshipStageComplete), + "StoryIntelligenceRelationships") + + @ReviewMetrics(!Model.CharacterGateSatisfied) @if (Model.AnalysisIssueCount > 0) { -
-
-

Analysis Issues

-

Some chapters or scenes reported analysis issues. Canonical chapters and scenes remain intact.

-
- Needs attention - Return to book - @Model.AnalysisIssueCount.ToString("N0") need attention -
+ @ReviewModule( + "Analysis Issues", + Model.AnalysisIssueCount, + false, + false, + "Some chapters or scenes reported analysis issues.", + "Return to Book", + null) }
-
@functions { - private string AttentionLabel(int count) => count == 0 ? "No outstanding decisions" : "Needs attention"; + private string ModuleAction(string title, int count, bool complete) + => count > 0 && !complete ? $"Review {title}" : $"View {title}"; + + private string StatusText(string title, int count, bool complete, bool blocked) + { + if (blocked) + { + return "Locked until Characters are resolved"; + } + + if (string.Equals(title, "Scene Metrics", StringComparison.Ordinal)) + { + return "Available"; + } + + if (complete) + { + return "Complete"; + } + + return count switch + { + 0 => "No review needed", + 1 => "1 needs attention", + _ => $"{count:N0} need attention" + }; + } + + private Microsoft.AspNetCore.Html.IHtmlContent ReviewMetrics(bool blocked) + { + var status = StatusText("Scene Metrics", Model.TotalAnalysedScenes, false, blocked); + var statusClass = blocked + ? "story-review-centre-tile__status story-review-centre-tile__status--locked" + : "story-review-centre-tile__status story-review-centre-tile__status--ready"; + var actionHtml = blocked + ? "Locked" + : "View Metrics"; + + return new Microsoft.AspNetCore.Html.HtmlString($$""" +
+
+

Scene Metrics

+

Auto-applied metrics from analysed scenes.

+
+ {{System.Net.WebUtility.HtmlEncode(status)}} + {{actionHtml}} +
+ """); + } private Microsoft.AspNetCore.Html.IHtmlContent ReviewModule( string title, int count, - string status, + bool complete, + bool blocked, string description, string actionLabel, - string action, - bool blocked) + string? action, + bool primary = false) { + var status = StatusText(title, count, complete, blocked); var statusClass = blocked - ? "story-review-status story-review-status--active" - : count > 0 - ? "story-review-status story-review-status--warning" - : "story-review-status story-review-status--ready"; - var statusText = blocked ? "Resolve Characters first" : status; - var countText = blocked - ? "Locked until Characters are resolved" - : count == 1 - ? "1 needs attention" - : $"{count:N0} need attention"; - var disabled = blocked ? " disabled aria-disabled=\"true\"" : string.Empty; - var href = blocked - ? "#" + ? "story-review-centre-tile__status story-review-centre-tile__status--locked" + : count > 0 && !complete + ? "story-review-centre-tile__status story-review-centre-tile__status--warning" + : "story-review-centre-tile__status story-review-centre-tile__status--ready"; + var tileClass = "story-review-centre-tile" + + (primary ? " story-review-centre-tile--primary" : string.Empty) + + (blocked ? " story-review-centre-tile--locked" : string.Empty); + var href = action is null + ? Url.Action("Details", "Books", new { id = Model.BookID }) ?? "#" : Url.Action(action, "Onboarding", new { batchId = Model.BatchID }) ?? "#"; + var actionHtml = blocked + ? "Locked" + : $"{System.Net.WebUtility.HtmlEncode(actionLabel)}"; return new Microsoft.AspNetCore.Html.HtmlString($$""" -
+

{{System.Net.WebUtility.HtmlEncode(title)}}

{{System.Net.WebUtility.HtmlEncode(description)}}

- {{System.Net.WebUtility.HtmlEncode(statusText)}} - {{System.Net.WebUtility.HtmlEncode(actionLabel)}} - {{System.Net.WebUtility.HtmlEncode(countText)}} + {{System.Net.WebUtility.HtmlEncode(status)}} + {{actionHtml}}
"""); } diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css index 62aa7f9..bcba8b0 100644 --- a/PlotLine/wwwroot/css/onboarding.css +++ b/PlotLine/wwwroot/css/onboarding.css @@ -1384,36 +1384,101 @@ summary.story-review-chapter-heading { margin-top: .2rem; } -.story-review-centre-list { - display: grid; - gap: .65rem; +.story-review-centre-shell { + max-width: 1100px; + margin: 0 auto; + padding: 2.25rem 1rem 3rem; } -.story-review-centre-row { +.story-review-centre-workspace { display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - gap: .75rem 1rem; - align-items: center; + gap: 1rem; +} + +.story-review-centre-heading { + max-width: 760px; +} + +.story-review-centre-heading h1 { + margin: .15rem 0 .35rem; +} + +.story-review-centre-heading p:not(.eyebrow) { + margin: 0; + color: var(--bs-secondary-color); + font-size: 1.02rem; +} + +.story-review-centre-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: .9rem; + align-items: stretch; +} + +.story-review-centre-tile { + min-height: 12rem; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: .85rem; border: 1px solid rgba(31, 42, 68, .1); border-radius: 8px; - padding: .85rem 1rem; + padding: 1rem; background: rgba(255, 255, 255, .64); } -.story-review-centre-row h2 { +.story-review-centre-tile--primary { + border-color: rgba(122, 74, 39, .32); + box-shadow: 0 10px 24px rgba(122, 74, 39, .08); +} + +.story-review-centre-tile--locked { + background: rgba(255, 255, 255, .46); +} + +.story-review-centre-tile h2 { margin: 0; font-size: 1.05rem; } -.story-review-centre-row p { +.story-review-centre-tile p { margin: .2rem 0 0; color: var(--bs-secondary-color); + font-size: .94rem; +} + +.story-review-centre-tile__status { + display: block; + font-weight: 700; + color: var(--bs-body-color); +} + +.story-review-centre-tile__status--ready { + color: #146c43; +} + +.story-review-centre-tile__status--warning { + color: #7a4a27; +} + +.story-review-centre-tile__status--locked { + color: var(--bs-secondary-color); } -.story-review-centre-row__count { - grid-column: 1 / -1; +.story-review-centre-tile .btn { + align-self: flex-start; +} + +.story-review-centre-tile .btn.disabled { + pointer-events: none; color: var(--bs-secondary-color); - font-weight: 700; + border-color: rgba(31, 42, 68, .16); + background: rgba(31, 42, 68, .04); +} + +.story-review-centre-actions { + justify-content: flex-start; } .story-scene-preview-list { @@ -1972,6 +2037,12 @@ summary.story-review-chapter-heading { } } +@media (max-width: 980px) { + .story-review-centre-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + @media (max-width: 720px) { .story-pipeline-phase-list, .story-database-build ul { @@ -2042,7 +2113,6 @@ summary.story-review-chapter-heading { .story-review-summary, .story-review-scene-panel, .story-character-card__body dl, - .story-review-centre-row, .story-future-stage-grid, .story-scene-preview dl, .story-live-current, @@ -2051,6 +2121,10 @@ summary.story-review-chapter-heading { grid-template-columns: 1fr; } + .story-review-centre-grid { + grid-template-columns: 1fr; + } + .story-live-progress-heading, .story-review-chapter-heading { flex-direction: column;