Speed up review centre summary
This commit is contained in:
parent
5a7b8c79c7
commit
a1cba8a057
@ -278,6 +278,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Story Intelligence review forms share busy submit guard", StoryIntelligenceReviewFormsShareBusySubmitGuard),
|
("Story Intelligence review forms share busy submit guard", StoryIntelligenceReviewFormsShareBusySubmitGuard),
|
||||||
("Story Intelligence heavy review modules offer bounded rendering", StoryIntelligenceHeavyReviewModulesOfferBoundedRendering),
|
("Story Intelligence heavy review modules offer bounded rendering", StoryIntelligenceHeavyReviewModulesOfferBoundedRendering),
|
||||||
("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls),
|
("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls),
|
||||||
|
("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery),
|
||||||
("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),
|
||||||
("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational),
|
("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational),
|
||||||
@ -720,9 +721,9 @@ static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories()
|
|||||||
var centre = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml"));
|
var centre = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml"));
|
||||||
var summary = File.ReadAllText(Path.Combine(root, "Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml"));
|
var summary = File.ReadAllText(Path.Combine(root, "Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml"));
|
||||||
|
|
||||||
Assert(viewModel.Contains("CharacterNeedsAttentionCount => CharacterReview.Candidates.Count", StringComparison.Ordinal), "Character count should be derived from unresolved candidates.");
|
Assert(viewModel.Contains("CharacterNeedsAttentionCount => CharacterReview.SummaryNeedsAttentionCount ?? CharacterReview.Candidates.Count", StringComparison.Ordinal), "Character count should prefer lightweight summary counts.");
|
||||||
Assert(viewModel.Contains("LocationNeedsAttentionCount => LocationReview.Candidates.Count", StringComparison.Ordinal), "Location count should be derived from unresolved candidates.");
|
Assert(viewModel.Contains("LocationNeedsAttentionCount => LocationReview.SummaryNeedsAttentionCount ?? LocationReview.Candidates.Count", StringComparison.Ordinal), "Location count should prefer lightweight summary counts.");
|
||||||
Assert(viewModel.Contains("NarrativeNeedsAttentionCount => NarrativeReview.Candidates.Count", StringComparison.Ordinal), "Narrative count should be derived from unresolved candidates.");
|
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("TotalAnalysedScenes", StringComparison.Ordinal), "Metrics should report analysed scene count.");
|
||||||
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.");
|
||||||
}
|
}
|
||||||
@ -1048,6 +1049,33 @@ static void StoryIntelligenceReviewCentreAddsNoAiCalls()
|
|||||||
Assert(!centreView.Contains("OpenAI", StringComparison.Ordinal) && !centreView.Contains("prompt", StringComparison.OrdinalIgnoreCase), "Review Centre view should not introduce prompt work.");
|
Assert(!centreView.Contains("OpenAI", StringComparison.Ordinal) && !centreView.Contains("prompt", StringComparison.OrdinalIgnoreCase), "Review Centre view should not introduce prompt work.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceReviewCentreUsesBoundedSummaryQuery()
|
||||||
|
{
|
||||||
|
var root = PlotLineRoot();
|
||||||
|
var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
|
||||||
|
var service = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
|
||||||
|
var pipelineRepository = File.ReadAllText(Path.Combine(root, "Data/StoryIntelligencePipelineRepository.cs"));
|
||||||
|
var pipelineService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligencePipelineStateService.cs"));
|
||||||
|
var viewModel = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
|
||||||
|
var migration = File.ReadAllText(Path.Combine(root, "Sql/176_Phase27C_StoryIntelligenceReviewCentreSummary.sql"));
|
||||||
|
var centreAction = ExtractBetween(controller, "public async Task<IActionResult> StoryIntelligenceReviewCentre", "[HttpGet(\"story-intelligence/characters\")]");
|
||||||
|
var centreSummary = ExtractBetween(service, "public async Task<StoryIntelligenceProgressViewModel?> GetReviewCentreAsync", "public async Task<StoryIntelligenceProgressViewModel?> CancelAsync");
|
||||||
|
|
||||||
|
Assert(centreAction.Contains("GetReviewCentreAsync(batchId)", StringComparison.Ordinal), "Review Centre GET should use the bounded summary path.");
|
||||||
|
Assert(!centreAction.Contains("GetProgressAsync(batchId)", StringComparison.Ordinal), "Review Centre GET should not build the full progress/candidate model.");
|
||||||
|
Assert(centreSummary.Contains("GetReviewCentreSummaryAsync(batch.BookID, batch.UserID)", StringComparison.Ordinal), "Review Centre summary should use one aggregate DB read.");
|
||||||
|
Assert(!centreSummary.Contains("BuildReviewAsync", StringComparison.Ordinal), "Review Centre summary should not call detailed module candidate builders.");
|
||||||
|
Assert(!centreSummary.Contains("EnsureCandidatesAsync", StringComparison.Ordinal), "Review Centre summary should not generate narrative candidates.");
|
||||||
|
Assert(!centreSummary.Contains("ListSceneResultsAsync", StringComparison.Ordinal), "Review Centre summary should not parse scene intelligence results.");
|
||||||
|
Assert(!centreSummary.Contains("ListByChapterAsync", StringComparison.Ordinal), "Review Centre summary should not rebuild scene object graphs.");
|
||||||
|
Assert(pipelineRepository.Contains("StoryIntelligenceReviewCentre_GetSummary", StringComparison.Ordinal), "Repository should call the Review Centre aggregate procedure.");
|
||||||
|
Assert(pipelineService.Contains("GetReviewCentreSummaryAsync(int bookId, int userId)", StringComparison.Ordinal), "Pipeline service should expose summary hydration.");
|
||||||
|
Assert(migration.Contains("CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceReviewCentre_GetSummary", StringComparison.Ordinal), "Migration should create the aggregate summary procedure.");
|
||||||
|
Assert(migration.Contains("IX_StoryIntelligenceNarrativeCandidates_Book_Status_Type", StringComparison.Ordinal), "Narrative candidate counts should have a supporting index.");
|
||||||
|
Assert(!migration.Contains("ParsedJson", StringComparison.Ordinal) && !migration.Contains("OPENJSON", StringComparison.Ordinal), "Summary query should not parse Scene Intelligence JSON.");
|
||||||
|
Assert(viewModel.Contains("SummaryNeedsAttentionCount", StringComparison.Ordinal), "Tile counts should support lightweight summary values without full candidate objects.");
|
||||||
|
}
|
||||||
|
|
||||||
static string PlotLineRoot()
|
static string PlotLineRoot()
|
||||||
=> Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
=> Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using PlotLine.Models;
|
using PlotLine.Models;
|
||||||
using PlotLine.Services;
|
using PlotLine.Services;
|
||||||
using PlotLine.ViewModels;
|
using PlotLine.ViewModels;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace PlotLine.Controllers;
|
namespace PlotLine.Controllers;
|
||||||
|
|
||||||
@ -12,7 +13,8 @@ public sealed class OnboardingController(
|
|||||||
IOnboardingService onboarding,
|
IOnboardingService onboarding,
|
||||||
IOnboardingStoryIntelligenceService storyIntelligence,
|
IOnboardingStoryIntelligenceService storyIntelligence,
|
||||||
IWholeBookPlotIntelligenceService wholeBookPlotIntelligence,
|
IWholeBookPlotIntelligenceService wholeBookPlotIntelligence,
|
||||||
ICurrentUserService currentUser) : Controller
|
ICurrentUserService currentUser,
|
||||||
|
ILogger<OnboardingController> logger) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
@ -289,7 +291,8 @@ public sealed class OnboardingController(
|
|||||||
[HttpGet("story-intelligence/review-centre")]
|
[HttpGet("story-intelligence/review-centre")]
|
||||||
public async Task<IActionResult> StoryIntelligenceReviewCentre(Guid batchId)
|
public async Task<IActionResult> StoryIntelligenceReviewCentre(Guid batchId)
|
||||||
{
|
{
|
||||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var model = await storyIntelligence.GetReviewCentreAsync(batchId);
|
||||||
if (model is null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@ -305,6 +308,15 @@ public sealed class OnboardingController(
|
|||||||
return RedirectToBook(model);
|
return RedirectToBook(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Story Intelligence Review Centre GET rendered. BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} ElapsedMs={ElapsedMs} ActiveRuns={ActiveRuns} SceneAnalysisComplete={SceneAnalysisComplete}",
|
||||||
|
batchId,
|
||||||
|
model.ProjectID,
|
||||||
|
model.BookID,
|
||||||
|
stopwatch.ElapsedMilliseconds,
|
||||||
|
model.HasActiveRuns,
|
||||||
|
model.SceneAnalysisComplete);
|
||||||
|
|
||||||
return View(nameof(StoryIntelligenceReviewCentre), model);
|
return View(nameof(StoryIntelligenceReviewCentre), model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ public interface IStoryIntelligencePipelineRepository
|
|||||||
Task<StoryIntelligenceBookPipelineState?> GetByIdForUserAsync(int importSessionId, int userId);
|
Task<StoryIntelligenceBookPipelineState?> GetByIdForUserAsync(int importSessionId, int userId);
|
||||||
Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId);
|
Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId);
|
||||||
Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId);
|
Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId);
|
||||||
|
Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId);
|
||||||
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
||||||
Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request);
|
Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request);
|
||||||
Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId);
|
Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId);
|
||||||
@ -46,6 +47,15 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceReviewCentreSummary>(
|
||||||
|
"dbo.StoryIntelligenceReviewCentre_GetSummary",
|
||||||
|
new { BookID = bookId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
|
public async Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -144,6 +144,43 @@ public sealed class StoryIntelligenceBookPipelineSaveRequest
|
|||||||
public bool MarkNarrativeReviewComplete { get; init; }
|
public bool MarkNarrativeReviewComplete { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceReviewCentreSummary
|
||||||
|
{
|
||||||
|
public int StoryIntelligenceBookPipelineID { get; init; }
|
||||||
|
public int ProjectID { get; init; }
|
||||||
|
public string ProjectName { get; init; } = string.Empty;
|
||||||
|
public int BookID { get; init; }
|
||||||
|
public string BookTitle { get; init; } = string.Empty;
|
||||||
|
public string? BookSubtitle { get; init; }
|
||||||
|
public string BookDisplayTitle => BookTitleFormatter.DisplayTitle(BookTitle, BookSubtitle);
|
||||||
|
public string CurrentStage { get; init; } = StoryIntelligencePipelineStages.SceneReview;
|
||||||
|
public string? LastCompletedStage { get; init; }
|
||||||
|
public string? CurrentReviewStage { get; init; }
|
||||||
|
public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview;
|
||||||
|
public DateTime? CompletedUtc { get; init; }
|
||||||
|
public DateTime? CharacterReviewCompletedUtc { get; init; }
|
||||||
|
public DateTime? LocationReviewCompletedUtc { get; init; }
|
||||||
|
public DateTime? AssetReviewCompletedUtc { get; init; }
|
||||||
|
public DateTime? RelationshipReviewCompletedUtc { get; init; }
|
||||||
|
public DateTime? KnowledgeReviewCompletedUtc { get; init; }
|
||||||
|
public DateTime? NarrativeReviewCompletedUtc { get; init; }
|
||||||
|
public int? LastRunID { get; init; }
|
||||||
|
public int RunCount { get; init; }
|
||||||
|
public int CompletedRunCount { get; init; }
|
||||||
|
public int ActiveRunCount { get; init; }
|
||||||
|
public int FailedRunCount { get; init; }
|
||||||
|
public int TotalDetectedScenes { get; init; }
|
||||||
|
public int CompletedScenes { get; init; }
|
||||||
|
public int FailedScenes { get; init; }
|
||||||
|
public int WholeBookPlotSynthesisRunID { get; init; }
|
||||||
|
public string? WholeBookPlotSynthesisStatus { get; init; }
|
||||||
|
public string? WholeBookPlotSynthesisErrorMessage { get; init; }
|
||||||
|
public int NarrativeCandidateCount { get; init; }
|
||||||
|
public int NarrativePendingCount { get; init; }
|
||||||
|
public int NarrativeThreadCount { get; init; }
|
||||||
|
public int NarrativeEventCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class StoryIntelligenceCommittedRunSummary
|
public sealed class StoryIntelligenceCommittedRunSummary
|
||||||
{
|
{
|
||||||
public int StoryIntelligenceRunID { get; init; }
|
public int StoryIntelligenceRunID { get; init; }
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using PlotLine.Data;
|
using PlotLine.Data;
|
||||||
using PlotLine.Models;
|
using PlotLine.Models;
|
||||||
@ -11,6 +12,7 @@ public interface IOnboardingStoryIntelligenceService
|
|||||||
Task<StoryIntelligenceOverviewViewModel> GetOverviewAsync();
|
Task<StoryIntelligenceOverviewViewModel> GetOverviewAsync();
|
||||||
Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null, string? primaryAgeGroup = null, string? storyEra = null, string? storyLocation = null);
|
Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null, string? primaryAgeGroup = null, string? storyEra = null, string? storyLocation = null);
|
||||||
Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId);
|
Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId);
|
||||||
|
Task<StoryIntelligenceProgressViewModel?> GetReviewCentreAsync(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<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAllAsync(Guid batchId);
|
||||||
@ -719,6 +721,122 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<StoryIntelligenceProgressViewModel?> GetReviewCentreAsync(Guid batchId)
|
||||||
|
{
|
||||||
|
var totalStopwatch = Stopwatch.StartNew();
|
||||||
|
var batch = await batchStore.GetAsync(RequireUserId(), batchId);
|
||||||
|
if (batch is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summaryStopwatch = Stopwatch.StartNew();
|
||||||
|
var summary = await pipelineState.GetReviewCentreSummaryAsync(batch.BookID, batch.UserID);
|
||||||
|
summaryStopwatch.Stop();
|
||||||
|
if (summary is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var characterComplete = summary.CharacterReviewCompletedUtc is not null;
|
||||||
|
var locationComplete = summary.LocationReviewCompletedUtc is not null;
|
||||||
|
var assetComplete = summary.AssetReviewCompletedUtc is not null;
|
||||||
|
var relationshipComplete = summary.RelationshipReviewCompletedUtc is not null;
|
||||||
|
var knowledgeComplete = summary.KnowledgeReviewCompletedUtc is not null;
|
||||||
|
var narrativeComplete = summary.NarrativeReviewCompletedUtc is not null;
|
||||||
|
var sceneAnalysisComplete = SceneAnalysisComplete(summary);
|
||||||
|
var narrativePendingCount = narrativeComplete ? 0 : summary.NarrativePendingCount;
|
||||||
|
var hasCommittedScenes = summary.CompletedRunCount > 0 || summary.CompletedScenes > 0;
|
||||||
|
|
||||||
|
var model = new StoryIntelligenceProgressViewModel
|
||||||
|
{
|
||||||
|
BatchID = batch.BatchID,
|
||||||
|
PreviewID = batch.PreviewID,
|
||||||
|
ImportSessionID = summary.StoryIntelligenceBookPipelineID,
|
||||||
|
ProjectID = summary.ProjectID,
|
||||||
|
BookID = summary.BookID,
|
||||||
|
ProjectName = summary.ProjectName,
|
||||||
|
BookTitle = summary.BookDisplayTitle,
|
||||||
|
CharacterReview = new StoryIntelligenceCharacterReviewViewModel
|
||||||
|
{
|
||||||
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
|
CanImport = !characterComplete,
|
||||||
|
IsComplete = characterComplete,
|
||||||
|
SummaryNeedsAttentionCount = characterComplete ? 0 : null
|
||||||
|
},
|
||||||
|
LocationReview = new StoryIntelligenceLocationReviewViewModel
|
||||||
|
{
|
||||||
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
|
CanImport = !locationComplete,
|
||||||
|
IsComplete = locationComplete,
|
||||||
|
SummaryNeedsAttentionCount = locationComplete ? 0 : null
|
||||||
|
},
|
||||||
|
AssetReview = new StoryIntelligenceAssetReviewViewModel
|
||||||
|
{
|
||||||
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
|
CanImport = !assetComplete,
|
||||||
|
IsComplete = assetComplete,
|
||||||
|
SummaryNeedsAttentionCount = assetComplete ? 0 : null
|
||||||
|
},
|
||||||
|
RelationshipReview = new StoryIntelligenceRelationshipReviewViewModel
|
||||||
|
{
|
||||||
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
|
CanImport = !relationshipComplete,
|
||||||
|
IsComplete = relationshipComplete,
|
||||||
|
SummaryNeedsAttentionCount = relationshipComplete ? 0 : null
|
||||||
|
},
|
||||||
|
KnowledgeReview = new StoryIntelligenceKnowledgeReviewViewModel
|
||||||
|
{
|
||||||
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
|
CanImport = !knowledgeComplete,
|
||||||
|
IsComplete = knowledgeComplete,
|
||||||
|
SummaryNeedsAttentionCount = knowledgeComplete ? 0 : null
|
||||||
|
},
|
||||||
|
NarrativeReview = new StoryIntelligenceNarrativeReviewViewModel
|
||||||
|
{
|
||||||
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
|
CanImport = narrativePendingCount > 0,
|
||||||
|
IsComplete = narrativeComplete,
|
||||||
|
SummaryNeedsAttentionCount = narrativePendingCount
|
||||||
|
},
|
||||||
|
PlotSynthesis = new WholeBookPlotSynthesisStatusViewModel
|
||||||
|
{
|
||||||
|
WholeBookPlotSynthesisRunID = summary.WholeBookPlotSynthesisRunID == 0 ? null : summary.WholeBookPlotSynthesisRunID,
|
||||||
|
Status = summary.WholeBookPlotSynthesisStatus ?? string.Empty,
|
||||||
|
CandidateCount = summary.NarrativeCandidateCount,
|
||||||
|
ThreadCount = summary.NarrativeThreadCount,
|
||||||
|
EventCount = summary.NarrativeEventCount,
|
||||||
|
ErrorMessage = summary.WholeBookPlotSynthesisErrorMessage
|
||||||
|
},
|
||||||
|
PipelineDashboard = new StoryIntelligencePipelineDashboardViewModel
|
||||||
|
{
|
||||||
|
ChaptersAnalysed = summary.CompletedRunCount,
|
||||||
|
ScenesImported = summary.CompletedScenes,
|
||||||
|
SceneAnalysisComplete = sceneAnalysisComplete,
|
||||||
|
CharacterStageComplete = characterComplete,
|
||||||
|
LocationStageComplete = locationComplete,
|
||||||
|
AssetStageComplete = assetComplete,
|
||||||
|
RelationshipStageComplete = relationshipComplete,
|
||||||
|
KnowledgeStageComplete = knowledgeComplete,
|
||||||
|
NarrativeCandidatesIdentified = summary.NarrativeCandidateCount,
|
||||||
|
NarrativeStageComplete = narrativeComplete
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Story Intelligence Review Centre summary built. BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} SummaryMs={SummaryMs} TotalMs={TotalMs} ActiveRunCount={ActiveRunCount} NarrativeCandidates={NarrativeCandidateCount} NarrativePending={NarrativePendingCount}",
|
||||||
|
batch.BatchID,
|
||||||
|
summary.ProjectID,
|
||||||
|
summary.BookID,
|
||||||
|
summaryStopwatch.ElapsedMilliseconds,
|
||||||
|
totalStopwatch.ElapsedMilliseconds,
|
||||||
|
summary.ActiveRunCount,
|
||||||
|
summary.NarrativeCandidateCount,
|
||||||
|
summary.NarrativePendingCount);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId)
|
public async Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId)
|
||||||
{
|
{
|
||||||
var progress = await GetProgressAsync(batchId);
|
var progress = await GetProgressAsync(batchId);
|
||||||
@ -1086,6 +1204,22 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
or StoryIntelligencePipelineStages.NarrativeImport
|
or StoryIntelligencePipelineStages.NarrativeImport
|
||||||
or StoryIntelligencePipelineStages.Complete;
|
or StoryIntelligencePipelineStages.Complete;
|
||||||
|
|
||||||
|
private static bool SceneAnalysisComplete(StoryIntelligenceReviewCentreSummary state)
|
||||||
|
=> string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.SceneImport, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| state.CurrentStage is StoryIntelligencePipelineStages.CharacterReview
|
||||||
|
or StoryIntelligencePipelineStages.CharacterImport
|
||||||
|
or StoryIntelligencePipelineStages.LocationReview
|
||||||
|
or StoryIntelligencePipelineStages.LocationImport
|
||||||
|
or StoryIntelligencePipelineStages.AssetReview
|
||||||
|
or StoryIntelligencePipelineStages.AssetImport
|
||||||
|
or StoryIntelligencePipelineStages.RelationshipReview
|
||||||
|
or StoryIntelligencePipelineStages.RelationshipImport
|
||||||
|
or StoryIntelligencePipelineStages.KnowledgeReview
|
||||||
|
or StoryIntelligencePipelineStages.KnowledgeImport
|
||||||
|
or StoryIntelligencePipelineStages.NarrativeReview
|
||||||
|
or StoryIntelligencePipelineStages.NarrativeImport
|
||||||
|
or StoryIntelligencePipelineStages.Complete;
|
||||||
|
|
||||||
private async Task<(UserOnboardingState State, ManuscriptScanPreview Preview, ManuscriptScanReviewDecision Review, OnboardingManuscriptBuildResult? Build, OnboardingWizardViewModel Wizard)?> GetContextAsync(int userId)
|
private async Task<(UserOnboardingState State, ManuscriptScanPreview Preview, ManuscriptScanReviewDecision Review, OnboardingManuscriptBuildResult? Build, OnboardingWizardViewModel Wizard)?> GetContextAsync(int userId)
|
||||||
{
|
{
|
||||||
var state = await onboardingRepository.GetAsync(userId);
|
var state = await onboardingRepository.GetAsync(userId);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace PlotLine.Services;
|
|||||||
public interface IStoryIntelligencePipelineStateService
|
public interface IStoryIntelligencePipelineStateService
|
||||||
{
|
{
|
||||||
Task<StoryIntelligenceBookPipelineState?> GetForBookAsync(int bookId, int userId);
|
Task<StoryIntelligenceBookPipelineState?> GetForBookAsync(int bookId, int userId);
|
||||||
|
Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId);
|
||||||
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
||||||
Task<StoryIntelligenceBookPipelineState?> EnsureForBookAsync(int bookId, int userId);
|
Task<StoryIntelligenceBookPipelineState?> EnsureForBookAsync(int bookId, int userId);
|
||||||
Task RecordChapterAnalysisQueuedAsync(int projectId, int bookId, int runId);
|
Task RecordChapterAnalysisQueuedAsync(int projectId, int bookId, int runId);
|
||||||
@ -29,6 +30,9 @@ public sealed class StoryIntelligencePipelineStateService(
|
|||||||
=> await pipelines.GetByBookForUserAsync(bookId, userId)
|
=> await pipelines.GetByBookForUserAsync(bookId, userId)
|
||||||
?? await EnsureForBookAsync(bookId, userId);
|
?? await EnsureForBookAsync(bookId, userId);
|
||||||
|
|
||||||
|
public Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId)
|
||||||
|
=> pipelines.GetReviewCentreSummaryAsync(bookId, userId);
|
||||||
|
|
||||||
public Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
|
public Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
|
||||||
=> pipelines.ListForUserAsync(userId);
|
=> pipelines.ListForUserAsync(userId);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,132 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.indexes
|
||||||
|
WHERE name = N'IX_StoryIntelligenceNarrativeCandidates_Book_Status_Type'
|
||||||
|
AND object_id = OBJECT_ID(N'dbo.StoryIntelligenceNarrativeCandidates')
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_StoryIntelligenceNarrativeCandidates_Book_Status_Type
|
||||||
|
ON dbo.StoryIntelligenceNarrativeCandidates(BookID, Status, CandidateType)
|
||||||
|
INCLUDE (WholeBookPlotSynthesisRunID, AcceptedPlotLineID, AcceptedPlotThreadID);
|
||||||
|
END;
|
||||||
|
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
|
||||||
|
),
|
||||||
|
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
|
||||||
|
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 CurrentPlotRun plot
|
||||||
|
ON plot.BookID = p.BookID
|
||||||
|
WHERE p.BookID = @BookID
|
||||||
|
AND book.IsArchived = 0
|
||||||
|
AND project.IsArchived = 0;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -193,26 +193,26 @@ public sealed class StoryIntelligenceProgressViewModel
|
|||||||
public bool HasReadyScenesToCreate => Chapters.Any(chapter => chapter.CanCommit);
|
public bool HasReadyScenesToCreate => Chapters.Any(chapter => chapter.CanCommit);
|
||||||
public bool SceneAnalysisComplete => PipelineDashboard.SceneAnalysisComplete
|
public bool SceneAnalysisComplete => PipelineDashboard.SceneAnalysisComplete
|
||||||
|| (Chapters.Count > 0 && !HasActiveRuns && Chapters.All(chapter => chapter.IsRunComplete));
|
|| (Chapters.Count > 0 && !HasActiveRuns && Chapters.All(chapter => chapter.IsRunComplete));
|
||||||
public bool HasCharactersToReview => CharacterReview.Candidates.Count > 0;
|
public bool HasCharactersToReview => CharacterNeedsAttentionCount.GetValueOrDefault() > 0;
|
||||||
public bool HasLocationsToReview => LocationReview.Candidates.Count > 0;
|
public bool HasLocationsToReview => LocationNeedsAttentionCount.GetValueOrDefault() > 0;
|
||||||
public bool HasAssetsToReview => AssetReview.Candidates.Count > 0;
|
public bool HasAssetsToReview => AssetNeedsAttentionCount.GetValueOrDefault() > 0;
|
||||||
public bool HasRelationshipsToReview => RelationshipReview.Candidates.Count > 0;
|
public bool HasRelationshipsToReview => RelationshipNeedsAttentionCount.GetValueOrDefault() > 0;
|
||||||
public bool HasKnowledgeToReview => KnowledgeReview.Candidates.Count > 0;
|
public bool HasKnowledgeToReview => KnowledgeNeedsAttentionCount.GetValueOrDefault() > 0;
|
||||||
public bool HasNarrativeToReview => NarrativeReview.Candidates.Count > 0;
|
public bool HasNarrativeToReview => NarrativeNeedsAttentionCount.GetValueOrDefault() > 0;
|
||||||
public int CharacterNeedsAttentionCount => CharacterReview.Candidates.Count;
|
public int? CharacterNeedsAttentionCount => CharacterReview.SummaryNeedsAttentionCount ?? CharacterReview.Candidates.Count;
|
||||||
public int LocationNeedsAttentionCount => LocationReview.Candidates.Count;
|
public int? LocationNeedsAttentionCount => LocationReview.SummaryNeedsAttentionCount ?? LocationReview.Candidates.Count;
|
||||||
public int AssetNeedsAttentionCount => AssetReview.Candidates.Count;
|
public int? AssetNeedsAttentionCount => AssetReview.SummaryNeedsAttentionCount ?? AssetReview.Candidates.Count;
|
||||||
public int RelationshipNeedsAttentionCount => RelationshipReview.Candidates.Count;
|
public int? RelationshipNeedsAttentionCount => RelationshipReview.SummaryNeedsAttentionCount ?? RelationshipReview.Candidates.Count;
|
||||||
public int KnowledgeNeedsAttentionCount => KnowledgeReview.Candidates.Count;
|
public int? KnowledgeNeedsAttentionCount => KnowledgeReview.SummaryNeedsAttentionCount ?? KnowledgeReview.Candidates.Count;
|
||||||
public int NarrativeNeedsAttentionCount => NarrativeReview.Candidates.Count;
|
public int? NarrativeNeedsAttentionCount => NarrativeReview.SummaryNeedsAttentionCount ?? NarrativeReview.Candidates.Count;
|
||||||
public int AnalysisIssueCount => Chapters.Count(chapter => chapter.IsFailed) + Chapters.Sum(chapter => chapter.FailedScenes ?? 0);
|
public int AnalysisIssueCount => Chapters.Count(chapter => chapter.IsFailed) + Chapters.Sum(chapter => chapter.FailedScenes ?? 0);
|
||||||
public bool CharacterGateSatisfied => PipelineDashboard.CharacterStageComplete || CharacterNeedsAttentionCount == 0;
|
public bool CharacterGateSatisfied => PipelineDashboard.CharacterStageComplete || CharacterNeedsAttentionCount == 0;
|
||||||
public int TotalReviewNeedsAttention => CharacterNeedsAttentionCount
|
public int TotalReviewNeedsAttention => (CharacterNeedsAttentionCount ?? 0)
|
||||||
+ LocationNeedsAttentionCount
|
+ (LocationNeedsAttentionCount ?? 0)
|
||||||
+ AssetNeedsAttentionCount
|
+ (AssetNeedsAttentionCount ?? 0)
|
||||||
+ RelationshipNeedsAttentionCount
|
+ (RelationshipNeedsAttentionCount ?? 0)
|
||||||
+ KnowledgeNeedsAttentionCount
|
+ (KnowledgeNeedsAttentionCount ?? 0)
|
||||||
+ NarrativeNeedsAttentionCount
|
+ (NarrativeNeedsAttentionCount ?? 0)
|
||||||
+ AnalysisIssueCount;
|
+ AnalysisIssueCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +260,7 @@ public sealed class StoryIntelligenceNarrativeReviewViewModel
|
|||||||
public bool CanImport { get; init; }
|
public bool CanImport { get; init; }
|
||||||
public bool HasCommittedScenes { get; init; }
|
public bool HasCommittedScenes { get; init; }
|
||||||
public bool IsComplete { get; init; }
|
public bool IsComplete { get; init; }
|
||||||
|
public int? SummaryNeedsAttentionCount { get; init; }
|
||||||
public IReadOnlyList<StoryIntelligenceNarrativeReviewCandidateViewModel> Candidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceNarrativeReviewCandidateViewModel> Candidates { get; init; } = [];
|
||||||
public IReadOnlyList<StoryIntelligenceNarrativeReviewCandidateViewModel> PlotLineCandidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceNarrativeReviewCandidateViewModel> PlotLineCandidates { get; init; } = [];
|
||||||
public IReadOnlyList<StoryIntelligenceNarrativeReviewCandidateViewModel> UnassignedThreadCandidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceNarrativeReviewCandidateViewModel> UnassignedThreadCandidates { get; init; } = [];
|
||||||
@ -362,6 +363,7 @@ public sealed class StoryIntelligenceCharacterReviewViewModel
|
|||||||
public bool HasCommittedScenes { get; init; }
|
public bool HasCommittedScenes { get; init; }
|
||||||
public int AlreadyLinkedCount { get; init; }
|
public int AlreadyLinkedCount { get; init; }
|
||||||
public bool IsComplete { get; init; }
|
public bool IsComplete { get; init; }
|
||||||
|
public int? SummaryNeedsAttentionCount { get; init; }
|
||||||
public int GenericReferenceCount { get; init; }
|
public int GenericReferenceCount { get; init; }
|
||||||
public int ExistingMatchCount { get; init; }
|
public int ExistingMatchCount { get; init; }
|
||||||
public int NewCandidateCount { get; init; }
|
public int NewCandidateCount { get; init; }
|
||||||
@ -437,6 +439,7 @@ public sealed class StoryIntelligenceLocationReviewViewModel
|
|||||||
public bool HasCommittedScenes { get; init; }
|
public bool HasCommittedScenes { get; init; }
|
||||||
public int AlreadyLinkedCount { get; init; }
|
public int AlreadyLinkedCount { get; init; }
|
||||||
public bool IsComplete { get; init; }
|
public bool IsComplete { get; init; }
|
||||||
|
public int? SummaryNeedsAttentionCount { get; init; }
|
||||||
public IReadOnlyList<StoryIntelligenceLocationReviewCandidateViewModel> Candidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceLocationReviewCandidateViewModel> Candidates { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,6 +489,7 @@ public sealed class StoryIntelligenceAssetReviewViewModel
|
|||||||
public bool HasCommittedScenes { get; init; }
|
public bool HasCommittedScenes { get; init; }
|
||||||
public int AlreadyLinkedCount { get; init; }
|
public int AlreadyLinkedCount { get; init; }
|
||||||
public bool IsComplete { get; init; }
|
public bool IsComplete { get; init; }
|
||||||
|
public int? SummaryNeedsAttentionCount { get; init; }
|
||||||
public IReadOnlyList<StoryIntelligenceAssetReviewCandidateViewModel> Candidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceAssetReviewCandidateViewModel> Candidates { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,6 +539,7 @@ public sealed class StoryIntelligenceRelationshipReviewViewModel
|
|||||||
public bool HasCommittedScenes { get; init; }
|
public bool HasCommittedScenes { get; init; }
|
||||||
public int AlreadyLinkedCount { get; init; }
|
public int AlreadyLinkedCount { get; init; }
|
||||||
public bool IsComplete { get; init; }
|
public bool IsComplete { get; init; }
|
||||||
|
public int? SummaryNeedsAttentionCount { get; init; }
|
||||||
public IReadOnlyList<SelectListItem> RelationshipTypeOptions { get; init; } = [];
|
public IReadOnlyList<SelectListItem> RelationshipTypeOptions { get; init; } = [];
|
||||||
public IReadOnlyList<StoryIntelligenceRelationshipReviewCandidateViewModel> Candidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceRelationshipReviewCandidateViewModel> Candidates { get; init; } = [];
|
||||||
}
|
}
|
||||||
@ -586,6 +591,7 @@ public sealed class StoryIntelligenceKnowledgeReviewViewModel
|
|||||||
public bool HasCommittedScenes { get; init; }
|
public bool HasCommittedScenes { get; init; }
|
||||||
public int AlreadyLinkedCount { get; init; }
|
public int AlreadyLinkedCount { get; init; }
|
||||||
public bool IsComplete { get; init; }
|
public bool IsComplete { get; init; }
|
||||||
|
public int? SummaryNeedsAttentionCount { get; init; }
|
||||||
public IReadOnlyList<SelectListItem> KnowledgeStateOptions { get; init; } = [];
|
public IReadOnlyList<SelectListItem> KnowledgeStateOptions { get; init; } = [];
|
||||||
public IReadOnlyList<StoryIntelligenceKnowledgeReviewCandidateViewModel> Candidates { get; init; } = [];
|
public IReadOnlyList<StoryIntelligenceKnowledgeReviewCandidateViewModel> Candidates { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,8 +94,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
private string ModuleAction(string title, int count, bool complete)
|
private string ModuleAction(string title, int? count, bool complete)
|
||||||
=> count > 0 && !complete ? $"Review {title}" : $"View {title}";
|
=> count.GetValueOrDefault() > 0 || (!complete && !count.HasValue) ? $"Review {title}" : $"View {title}";
|
||||||
|
|
||||||
private Microsoft.AspNetCore.Html.IHtmlContent ReviewPlotSynthesis()
|
private Microsoft.AspNetCore.Html.IHtmlContent ReviewPlotSynthesis()
|
||||||
{
|
{
|
||||||
@ -134,7 +134,7 @@
|
|||||||
</form>
|
</form>
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
else if (Model.PlotSynthesis.IsCompleted && Model.NarrativeNeedsAttentionCount > 0)
|
else if (Model.PlotSynthesis.IsCompleted && Model.NarrativeNeedsAttentionCount.GetValueOrDefault() > 0)
|
||||||
{
|
{
|
||||||
status = $"{Model.NarrativeNeedsAttentionCount:N0} need attention";
|
status = $"{Model.NarrativeNeedsAttentionCount:N0} need attention";
|
||||||
statusClass = "story-review-centre-tile__status story-review-centre-tile__status--warning";
|
statusClass = "story-review-centre-tile__status story-review-centre-tile__status--warning";
|
||||||
@ -168,7 +168,7 @@
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string StatusText(string title, int count, bool complete, bool blocked)
|
private string StatusText(string title, int? count, bool complete, bool blocked)
|
||||||
{
|
{
|
||||||
if (blocked)
|
if (blocked)
|
||||||
{
|
{
|
||||||
@ -185,6 +185,11 @@
|
|||||||
return "Complete";
|
return "Complete";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!count.HasValue)
|
||||||
|
{
|
||||||
|
return "Ready";
|
||||||
|
}
|
||||||
|
|
||||||
return count switch
|
return count switch
|
||||||
{
|
{
|
||||||
0 => "No review needed",
|
0 => "No review needed",
|
||||||
@ -217,7 +222,7 @@
|
|||||||
|
|
||||||
private Microsoft.AspNetCore.Html.IHtmlContent ReviewModule(
|
private Microsoft.AspNetCore.Html.IHtmlContent ReviewModule(
|
||||||
string title,
|
string title,
|
||||||
int count,
|
int? count,
|
||||||
bool complete,
|
bool complete,
|
||||||
bool blocked,
|
bool blocked,
|
||||||
string description,
|
string description,
|
||||||
@ -228,7 +233,7 @@
|
|||||||
var status = StatusText(title, count, complete, blocked);
|
var status = StatusText(title, count, complete, 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"
|
||||||
: count > 0 && !complete
|
: (count.GetValueOrDefault() > 0 || (!complete && !count.HasValue))
|
||||||
? "story-review-centre-tile__status story-review-centre-tile__status--warning"
|
? "story-review-centre-tile__status story-review-centre-tile__status--warning"
|
||||||
: "story-review-centre-tile__status story-review-centre-tile__status--ready";
|
: "story-review-centre-tile__status story-review-centre-tile__status--ready";
|
||||||
var tileClass = "story-review-centre-tile"
|
var tileClass = "story-review-centre-tile"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user