Route completed review modules to canonical pages

This commit is contained in:
Nick Beckley 2026-08-30 22:29:19 +00:00
parent 07113f9b36
commit 0839b8e13d
6 changed files with 238 additions and 27 deletions

View File

@ -282,6 +282,7 @@ var tests = new (string Name, Action Test)[]
("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery), ("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery),
("Story Intelligence detailed review GETs use selected module paths", StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths), ("Story Intelligence detailed review GETs use selected module paths", StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths),
("Story Intelligence completed Character review remains inspectable", StoryIntelligenceCompletedCharacterReviewRemainsInspectable), ("Story Intelligence completed Character review remains inspectable", StoryIntelligenceCompletedCharacterReviewRemainsInspectable),
("Story Intelligence Review Centre completed modules use canonical destinations", StoryIntelligenceReviewCentreCompletedModulesUseCanonicalDestinations),
("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages), ("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages),
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution), ("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution),
("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals), ("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals),
@ -728,7 +729,8 @@ static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories()
Assert(viewModel.Contains("CharacterNeedsAttentionCount => CharacterReview.SummaryNeedsAttentionCount ?? CharacterReview.Candidates.Count", StringComparison.Ordinal), "Character count should prefer lightweight summary counts."); Assert(viewModel.Contains("CharacterNeedsAttentionCount => CharacterReview.SummaryNeedsAttentionCount ?? CharacterReview.Candidates.Count", StringComparison.Ordinal), "Character count should prefer lightweight summary counts.");
Assert(viewModel.Contains("LocationNeedsAttentionCount => LocationReview.SummaryNeedsAttentionCount ?? LocationReview.Candidates.Count", StringComparison.Ordinal), "Location count should prefer lightweight summary counts."); Assert(viewModel.Contains("LocationNeedsAttentionCount => LocationReview.SummaryNeedsAttentionCount ?? LocationReview.Candidates.Count", StringComparison.Ordinal), "Location count should prefer lightweight summary counts.");
Assert(viewModel.Contains("NarrativeNeedsAttentionCount => NarrativeReview.SummaryNeedsAttentionCount ?? NarrativeReview.Candidates.Count", StringComparison.Ordinal), "Narrative count should prefer lightweight summary counts."); Assert(viewModel.Contains("NarrativeNeedsAttentionCount => NarrativeReview.SummaryNeedsAttentionCount ?? NarrativeReview.Candidates.Count", StringComparison.Ordinal), "Narrative count should prefer lightweight summary counts.");
Assert(centre.Contains("Scene Metrics", StringComparison.Ordinal) && centre.Contains("TotalAnalysedScenes", StringComparison.Ordinal), "Metrics should report analysed scene count."); Assert(centre.Contains("Scene Metrics", StringComparison.Ordinal) && centre.Contains("MetricValueCount", StringComparison.Ordinal), "Metrics should report actual metric availability.");
Assert(!centre.Contains("Review Metrics", StringComparison.Ordinal), "Metrics should not present a fake review workflow.");
Assert(!summary.Contains("Locations identified", StringComparison.Ordinal), "Summary should not present total identified rows as review workload."); Assert(!summary.Contains("Locations identified", StringComparison.Ordinal), "Summary should not present total identified rows as review workload.");
} }
@ -757,7 +759,7 @@ static void StoryIntelligenceReviewCentreUsesIndependentTileGrid()
Assert(centre.Contains("can be reviewed in any order", StringComparison.Ordinal), "Header should communicate independent review order."); 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 until Characters are resolved", StringComparison.Ordinal), "Locked dependent tiles should be visible and clearly locked.");
Assert(centre.Contains(">Locked</span>", StringComparison.Ordinal), "Locked tile action should render as non-clickable text."); Assert(centre.Contains(">Locked</span>", 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("No metrics available", StringComparison.Ordinal) && centre.Contains("View on Timeline", StringComparison.Ordinal), "Scene Metrics should distinguish unavailable metrics from Timeline navigation.");
Assert(!centre.Contains("Next", StringComparison.Ordinal) && !centre.Contains("Previous", StringComparison.Ordinal), "Review Centre should not contain wizard navigation."); 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."); Assert(!centre.Contains("Core Import", StringComparison.Ordinal) && !centre.Contains("Analyse Manuscript", StringComparison.Ordinal), "Review Centre should not show phase panels.");
@ -1174,6 +1176,53 @@ static void StoryIntelligenceCompletedCharacterReviewRemainsInspectable()
Assert(view.Contains("AliasTargetOptions", StringComparison.Ordinal), "Completed Character page should use canonical Character data when no pending candidates exist."); Assert(view.Contains("AliasTargetOptions", StringComparison.Ordinal), "Completed Character page should use canonical Character data when no pending candidates exist.");
} }
static void StoryIntelligenceReviewCentreCompletedModulesUseCanonicalDestinations()
{
var root = PlotLineRoot();
var centre = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml"));
var model = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
var persistence = File.ReadAllText(Path.Combine(root, "Models/StoryIntelligencePersistenceModels.cs"));
var service = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
var metricSql = File.ReadAllText(Path.Combine(root, "Sql/178_Phase27E_ReviewCentreMetricSummary.sql"));
Assert(centre.Contains("\"Review Characters\"", StringComparison.Ordinal), "Incomplete Characters should use the Story Intelligence review action label.");
Assert(centre.Contains("\"StoryIntelligenceCharacters\"", StringComparison.Ordinal), "Incomplete Characters should route to Character review.");
Assert(centre.Contains("\"View Characters\"", StringComparison.Ordinal), "Complete Characters should use canonical action label.");
Assert(centre.Contains("\"Characters\"", StringComparison.Ordinal) && centre.Contains("new { projectId = Model.ProjectID }", StringComparison.Ordinal), "Complete Characters should route to Characters/Index with project context.");
Assert(centre.Contains("\"Review Locations\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceLocations\"", StringComparison.Ordinal), "Incomplete Locations should route to Location review.");
Assert(centre.Contains("\"View Locations\"", StringComparison.Ordinal) && centre.Contains("\"Locations\"", StringComparison.Ordinal), "Complete Locations should route to Locations/Index.");
Assert(centre.Contains("\"Review Assets\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceAssets\"", StringComparison.Ordinal), "Incomplete Assets should route to Asset review.");
Assert(centre.Contains("\"View Assets\"", StringComparison.Ordinal) && centre.Contains("\"StoryAssets\"", StringComparison.Ordinal), "Complete Assets should route to StoryAssets/Index.");
Assert(centre.Contains("\"Review Relationships\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceRelationships\"", StringComparison.Ordinal), "Incomplete Relationships should route to Relationship review.");
Assert(centre.Contains("\"View Relationships\"", StringComparison.Ordinal) && centre.Contains("\"RelationshipMap\"", StringComparison.Ordinal), "Complete Relationships should route to RelationshipMap/Index.");
Assert(centre.Contains("new { projectId = Model.ProjectID, bookId = Model.BookID }", StringComparison.Ordinal), "Book-scoped canonical destinations should include book context.");
Assert(centre.Contains("\"Review Knowledge\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceKnowledge\"", StringComparison.Ordinal), "Incomplete Knowledge should route to Knowledge review.");
Assert(centre.Contains("\"View Knowledge\"", StringComparison.Ordinal) && centre.Contains("\"StoryBible\"", StringComparison.Ordinal), "Complete Knowledge should route to StoryBible/Index.");
Assert(centre.Contains("Review Plot Lines & Threads", StringComparison.Ordinal), "Incomplete Plot should route to Plot review.");
Assert(centre.Contains("Url.Action(\"StoryIntelligenceNarrative\", \"Onboarding\"", StringComparison.Ordinal), "Incomplete Plot should use the Story Intelligence Plot review route.");
Assert(centre.Contains("Url.Action(\"Index\", \"Timeline\", new { projectId = Model.ProjectID, bookId = Model.BookID })", StringComparison.Ordinal), "Complete Plot and available Metrics should route to Timeline.");
Assert(centre.Contains("View on Timeline", StringComparison.Ordinal), "Complete Plot should label the canonical Timeline action.");
Assert(centre.Contains("Model.MetricValueCount > 0", StringComparison.Ordinal), "Metrics availability should be based on actual metric values.");
Assert(centre.Contains("No metrics available", StringComparison.Ordinal), "Zero metrics should be clearly identified.");
Assert(centre.Contains("No action", StringComparison.Ordinal), "Zero metrics should not expose a misleading Review Metrics action.");
Assert(!centre.Contains("Review Metrics", StringComparison.Ordinal), "Review Centre should not advertise a non-existent metrics review workflow.");
Assert(model.Contains("public int MetricValueCount", StringComparison.Ordinal), "Review Centre model should expose metric availability.");
Assert(persistence.Contains("public int MetricValueCount", StringComparison.Ordinal), "Persisted summary should expose metric availability.");
Assert(service.Contains("MetricValueCount = summary.MetricValueCount", StringComparison.Ordinal), "Review Centre summary should hydrate metric availability.");
Assert(metricSql.Contains("MetricValueSummary", StringComparison.Ordinal) && metricSql.Contains("SceneMetricValues", StringComparison.Ordinal), "Metric summary SQL should count existing metric values.");
Assert(centre.Contains("var href = complete", StringComparison.Ordinal), "Module helper should switch destination by completion state.");
Assert(centre.Contains("Url.Action(reviewAction, \"Onboarding\", new { batchId = Model.BatchID })", StringComparison.Ordinal), "Incomplete modules should still open Story Intelligence review.");
Assert(centre.Contains("Url.Action(\"Index\", completeController, completeRouteValues)", StringComparison.Ordinal), "Complete modules should open canonical Index routes.");
}
static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages() static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages()
{ {
var root = PlotLineRoot(); var root = PlotLineRoot();

View File

@ -179,6 +179,7 @@ public sealed class StoryIntelligenceReviewCentreSummary
public int NarrativePendingCount { get; init; } public int NarrativePendingCount { get; init; }
public int NarrativeThreadCount { get; init; } public int NarrativeThreadCount { get; init; }
public int NarrativeEventCount { get; init; } public int NarrativeEventCount { get; init; }
public int MetricValueCount { get; init; }
} }
public sealed class StoryIntelligenceCommittedRunSummary public sealed class StoryIntelligenceCommittedRunSummary

View File

@ -846,6 +846,7 @@ public sealed class OnboardingStoryIntelligenceService(
BookID = summary.BookID, BookID = summary.BookID,
ProjectName = summary.ProjectName, ProjectName = summary.ProjectName,
BookTitle = summary.BookDisplayTitle, BookTitle = summary.BookDisplayTitle,
MetricValueCount = summary.MetricValueCount,
CharacterReview = new StoryIntelligenceCharacterReviewViewModel CharacterReview = new StoryIntelligenceCharacterReviewViewModel
{ {
HasCommittedScenes = hasCommittedScenes, HasCommittedScenes = hasCommittedScenes,

View File

@ -0,0 +1,134 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceReviewCentre_GetSummary
@BookID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
;WITH RunSummary AS
(
SELECT
r.BookID,
COUNT_BIG(*) AS RunCount,
SUM(CASE WHEN r.Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END) AS CompletedRunCount,
SUM(CASE WHEN r.Status IN (N'Pending', N'Running') THEN 1 ELSE 0 END) AS ActiveRunCount,
SUM(CASE WHEN r.Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) AS FailedRunCount,
SUM(COALESCE(r.TotalDetectedScenes, 0)) AS TotalDetectedScenes,
SUM(COALESCE(r.CompletedScenes, 0)) AS CompletedScenes,
SUM(COALESCE(r.FailedScenes, 0)) AS FailedScenes
FROM dbo.StoryIntelligenceRuns r
WHERE r.BookID = @BookID
GROUP BY r.BookID
),
NarrativeSummary AS
(
SELECT
nc.BookID,
COUNT_BIG(*) AS NarrativeCandidateCount,
SUM(CASE WHEN nc.Status = N'Pending' THEN 1 ELSE 0 END) AS NarrativePendingCount,
SUM(CASE WHEN nc.CandidateType = N'Thread' THEN 1 ELSE 0 END) AS NarrativeThreadCount
FROM dbo.StoryIntelligenceNarrativeCandidates nc
WHERE nc.BookID = @BookID
AND nc.WholeBookPlotSynthesisRunID IS NOT NULL
GROUP BY nc.BookID
),
NarrativeEventSummary AS
(
SELECT
nc.BookID,
COUNT_BIG(*) AS NarrativeEventCount
FROM dbo.StoryIntelligenceNarrativeCandidateEvents ev
INNER JOIN dbo.StoryIntelligenceNarrativeCandidates nc
ON nc.NarrativeCandidateID = ev.NarrativeCandidateID
WHERE nc.BookID = @BookID
AND nc.WholeBookPlotSynthesisRunID IS NOT NULL
GROUP BY nc.BookID
),
MetricValueSummary AS
(
SELECT
chapter.BookID,
COUNT_BIG(*) AS MetricValueCount
FROM dbo.SceneMetricValues metric
INNER JOIN dbo.Scenes scene
ON scene.SceneID = metric.SceneID
INNER JOIN dbo.Chapters chapter
ON chapter.ChapterID = scene.ChapterID
WHERE chapter.BookID = @BookID
GROUP BY chapter.BookID
),
CurrentPlotRun AS
(
SELECT TOP (1)
run.BookID,
run.WholeBookPlotSynthesisRunID,
run.Status AS WholeBookPlotSynthesisStatus,
run.ErrorMessage AS WholeBookPlotSynthesisErrorMessage
FROM dbo.WholeBookPlotSynthesisRuns run
WHERE run.BookID = @BookID
AND run.IsCurrent = 1
ORDER BY run.WholeBookPlotSynthesisRunID DESC
)
SELECT
p.StoryIntelligenceBookPipelineID,
p.ProjectID,
project.ProjectName,
p.BookID,
book.BookTitle,
book.Subtitle AS BookSubtitle,
p.CurrentStage,
p.LastCompletedStage,
p.CurrentReviewStage,
p.Status,
p.CompletedUtc,
p.CharacterReviewCompletedUtc,
p.LocationReviewCompletedUtc,
p.AssetReviewCompletedUtc,
p.RelationshipReviewCompletedUtc,
p.KnowledgeReviewCompletedUtc,
p.NarrativeReviewCompletedUtc,
p.LastRunID,
CAST(COALESCE(rs.RunCount, 0) AS int) AS RunCount,
CAST(COALESCE(rs.CompletedRunCount, 0) AS int) AS CompletedRunCount,
CAST(COALESCE(rs.ActiveRunCount, 0) AS int) AS ActiveRunCount,
CAST(COALESCE(rs.FailedRunCount, 0) AS int) AS FailedRunCount,
CAST(COALESCE(rs.TotalDetectedScenes, 0) AS int) AS TotalDetectedScenes,
CAST(COALESCE(rs.CompletedScenes, 0) AS int) AS CompletedScenes,
CAST(COALESCE(rs.FailedScenes, 0) AS int) AS FailedScenes,
plot.WholeBookPlotSynthesisRunID,
plot.WholeBookPlotSynthesisStatus,
plot.WholeBookPlotSynthesisErrorMessage,
CAST(COALESCE(ns.NarrativeCandidateCount, 0) AS int) AS NarrativeCandidateCount,
CAST(COALESCE(ns.NarrativePendingCount, 0) AS int) AS NarrativePendingCount,
CAST(COALESCE(ns.NarrativeThreadCount, 0) AS int) AS NarrativeThreadCount,
CAST(COALESCE(nes.NarrativeEventCount, 0) AS int) AS NarrativeEventCount,
CAST(COALESCE(metrics.MetricValueCount, 0) AS int) AS MetricValueCount
FROM dbo.StoryIntelligenceBookPipelines p
INNER JOIN dbo.Projects project
ON project.ProjectID = p.ProjectID
INNER JOIN dbo.Books book
ON book.BookID = p.BookID
INNER JOIN dbo.ProjectUserAccess access
ON access.ProjectID = p.ProjectID
AND access.UserID = @UserID
AND access.IsActive = 1
LEFT JOIN RunSummary rs
ON rs.BookID = p.BookID
LEFT JOIN NarrativeSummary ns
ON ns.BookID = p.BookID
LEFT JOIN NarrativeEventSummary nes
ON nes.BookID = p.BookID
LEFT JOIN MetricValueSummary metrics
ON metrics.BookID = p.BookID
LEFT JOIN CurrentPlotRun plot
ON plot.BookID = p.BookID
WHERE p.BookID = @BookID
AND book.IsArchived = 0
AND project.IsArchived = 0;
END;
GO

View File

@ -184,6 +184,7 @@ public sealed class StoryIntelligenceProgressViewModel
public int TotalDetectedScenes => Chapters.Sum(chapter => chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes ?? 0); public int TotalDetectedScenes => Chapters.Sum(chapter => chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes ?? 0);
public int TotalCompletedScenes => Chapters.Sum(chapter => chapter.CompletedScenes ?? 0); public int TotalCompletedScenes => Chapters.Sum(chapter => chapter.CompletedScenes ?? 0);
public int TotalAnalysedScenes => Math.Max(TotalCompletedScenes, PipelineDashboard.ScenesImported); public int TotalAnalysedScenes => Math.Max(TotalCompletedScenes, PipelineDashboard.ScenesImported);
public int MetricValueCount { get; init; }
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 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 decimal? TotalEstimatedCostUSD => Chapters.Any(chapter => chapter.EstimatedCostUSD.HasValue) ? Chapters.Sum(chapter => chapter.EstimatedCostUSD ?? 0m) : null;

View File

@ -26,11 +26,14 @@
@ReviewModule( @ReviewModule(
"Characters", "Characters",
Model.CharacterNeedsAttentionCount, Model.CharacterNeedsAttentionCount,
Model.CharacterGateSatisfied, Model.PipelineDashboard.CharacterStageComplete,
false, false,
"Resolve character identities before reviewing dependent Story Intelligence.", "Resolve character identities before reviewing dependent Story Intelligence.",
Model.CharacterGateSatisfied ? "Review Characters" : "Resolve Characters", "Review Characters",
"StoryIntelligenceCharacters", "StoryIntelligenceCharacters",
"View Characters",
"Characters",
new { projectId = Model.ProjectID },
true) true)
@ReviewModule( @ReviewModule(
@ -39,8 +42,11 @@
Model.PipelineDashboard.AssetStageComplete, Model.PipelineDashboard.AssetStageComplete,
!Model.CharacterGateSatisfied, !Model.CharacterGateSatisfied,
"Important objects and possessions found in the manuscript.", "Important objects and possessions found in the manuscript.",
ModuleAction("Assets", Model.AssetNeedsAttentionCount, Model.PipelineDashboard.AssetStageComplete), "Review Assets",
"StoryIntelligenceAssets") "StoryIntelligenceAssets",
"View Assets",
"StoryAssets",
new { projectId = Model.ProjectID })
@ReviewModule( @ReviewModule(
"Knowledge", "Knowledge",
@ -48,8 +54,11 @@
Model.PipelineDashboard.KnowledgeStageComplete, Model.PipelineDashboard.KnowledgeStageComplete,
!Model.CharacterGateSatisfied, !Model.CharacterGateSatisfied,
"Facts, discoveries, beliefs, and suspicions found in the manuscript.", "Facts, discoveries, beliefs, and suspicions found in the manuscript.",
ModuleAction("Knowledge", Model.KnowledgeNeedsAttentionCount, Model.PipelineDashboard.KnowledgeStageComplete), "Review Knowledge",
"StoryIntelligenceKnowledge") "StoryIntelligenceKnowledge",
"View Knowledge",
"StoryBible",
new { projectId = Model.ProjectID })
@ReviewModule( @ReviewModule(
"Locations", "Locations",
@ -57,8 +66,11 @@
Model.PipelineDashboard.LocationStageComplete, Model.PipelineDashboard.LocationStageComplete,
!Model.CharacterGateSatisfied, !Model.CharacterGateSatisfied,
"Places found in the manuscript.", "Places found in the manuscript.",
ModuleAction("Locations", Model.LocationNeedsAttentionCount, Model.PipelineDashboard.LocationStageComplete), "Review Locations",
"StoryIntelligenceLocations") "StoryIntelligenceLocations",
"View Locations",
"Locations",
new { projectId = Model.ProjectID })
@ReviewPlotSynthesis() @ReviewPlotSynthesis()
@ -68,8 +80,11 @@
Model.PipelineDashboard.RelationshipStageComplete, Model.PipelineDashboard.RelationshipStageComplete,
!Model.CharacterGateSatisfied, !Model.CharacterGateSatisfied,
"Character relationships found in scene evidence.", "Character relationships found in scene evidence.",
ModuleAction("Relationships", Model.RelationshipNeedsAttentionCount, Model.PipelineDashboard.RelationshipStageComplete), "Review Relationships",
"StoryIntelligenceRelationships") "StoryIntelligenceRelationships",
"View Relationships",
"RelationshipMap",
new { projectId = Model.ProjectID, bookId = Model.BookID })
@ReviewMetrics(!Model.CharacterGateSatisfied) @ReviewMetrics(!Model.CharacterGateSatisfied)
@ -82,7 +97,10 @@
false, false,
"Some chapters or scenes reported analysis issues.", "Some chapters or scenes reported analysis issues.",
"Return to Book", "Return to Book",
null) "StoryIntelligenceReviewCentre",
"Return to Book",
"Books",
new { id = Model.BookID })
} }
</section> </section>
@ -94,9 +112,6 @@
</section> </section>
@functions { @functions {
private string ModuleAction(string title, int? count, bool complete)
=> count.GetValueOrDefault() > 0 || (!complete && !count.HasValue) ? $"Review {title}" : $"View {title}";
private Microsoft.AspNetCore.Html.IHtmlContent ReviewPlotSynthesis() private Microsoft.AspNetCore.Html.IHtmlContent ReviewPlotSynthesis()
{ {
var blocked = !Model.CharacterGateSatisfied; var blocked = !Model.CharacterGateSatisfied;
@ -146,8 +161,8 @@
{ {
status = "Complete"; status = "Complete";
description = "Plot Lines and Threads review has been saved."; description = "Plot Lines and Threads review has been saved.";
var href = Url.Action("StoryIntelligenceNarrative", "Onboarding", new { batchId = Model.BatchID }) ?? "#"; var href = Url.Action("Index", "Timeline", new { projectId = Model.ProjectID, bookId = Model.BookID }) ?? "#";
actionHtml = $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">View Plot Lines & Threads</a>"; actionHtml = $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">View on Timeline</a>";
} }
else else
{ {
@ -177,7 +192,7 @@
if (string.Equals(title, "Scene Metrics", StringComparison.Ordinal)) if (string.Equals(title, "Scene Metrics", StringComparison.Ordinal))
{ {
return "Available"; return Model.MetricValueCount > 0 ? "Available" : "No metrics available";
} }
if (complete) if (complete)
@ -200,13 +215,19 @@
private Microsoft.AspNetCore.Html.IHtmlContent ReviewMetrics(bool blocked) private Microsoft.AspNetCore.Html.IHtmlContent ReviewMetrics(bool blocked)
{ {
var status = StatusText("Scene Metrics", Model.TotalAnalysedScenes, false, blocked); var hasMetrics = Model.MetricValueCount > 0;
var status = StatusText("Scene Metrics", Model.MetricValueCount, false, blocked);
var statusClass = 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--locked"
: "story-review-centre-tile__status story-review-centre-tile__status--ready"; : hasMetrics
? "story-review-centre-tile__status story-review-centre-tile__status--ready"
: "story-review-centre-tile__status story-review-centre-tile__status--locked";
var href = Url.Action("Index", "Timeline", new { projectId = Model.ProjectID, bookId = Model.BookID }) ?? "#";
var actionHtml = blocked var actionHtml = blocked
? "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>" ? "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>"
: "<span class=\"btn btn-outline-primary btn-sm disabled\" aria-disabled=\"true\">View Metrics</span>"; : hasMetrics
? $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">View on Timeline</a>"
: "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">No action</span>";
return new Microsoft.AspNetCore.Html.HtmlString($$""" return new Microsoft.AspNetCore.Html.HtmlString($$"""
<article class="story-review-centre-tile{{(blocked ? " story-review-centre-tile--locked" : string.Empty)}}"> <article class="story-review-centre-tile{{(blocked ? " story-review-centre-tile--locked" : string.Empty)}}">
@ -226,8 +247,11 @@
bool complete, bool complete,
bool blocked, bool blocked,
string description, string description,
string actionLabel, string reviewActionLabel,
string? action, string reviewAction,
string completeActionLabel,
string completeController,
object completeRouteValues,
bool primary = false) bool primary = false)
{ {
var status = StatusText(title, count, complete, blocked); var status = StatusText(title, count, complete, blocked);
@ -239,9 +263,10 @@
var tileClass = "story-review-centre-tile" var tileClass = "story-review-centre-tile"
+ (primary ? " story-review-centre-tile--primary" : string.Empty) + (primary ? " story-review-centre-tile--primary" : string.Empty)
+ (blocked ? " story-review-centre-tile--locked" : string.Empty); + (blocked ? " story-review-centre-tile--locked" : string.Empty);
var href = action is null var href = complete
? Url.Action("Details", "Books", new { id = Model.BookID }) ?? "#" ? Url.Action("Index", completeController, completeRouteValues) ?? "#"
: Url.Action(action, "Onboarding", new { batchId = Model.BatchID }) ?? "#"; : Url.Action(reviewAction, "Onboarding", new { batchId = Model.BatchID }) ?? "#";
var actionLabel = complete ? completeActionLabel : reviewActionLabel;
var actionHtml = blocked var actionHtml = blocked
? "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>" ? "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>"
: $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">{System.Net.WebUtility.HtmlEncode(actionLabel)}</a>"; : $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">{System.Net.WebUtility.HtmlEncode(actionLabel)}</a>";