Consolidate story intelligence review module loads
This commit is contained in:
parent
a1cba8a057
commit
d6d52b09e3
@ -279,6 +279,7 @@ var tests = new (string Name, Action Test)[]
|
||||
("Story Intelligence heavy review modules offer bounded rendering", StoryIntelligenceHeavyReviewModulesOfferBoundedRendering),
|
||||
("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls),
|
||||
("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery),
|
||||
("Story Intelligence detailed review GETs use selected module paths", StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths),
|
||||
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution),
|
||||
("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals),
|
||||
("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational),
|
||||
@ -1059,11 +1060,11 @@ static void StoryIntelligenceReviewCentreUsesBoundedSummaryQuery()
|
||||
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");
|
||||
var centreSummary = ExtractBetween(service, "public async Task<StoryIntelligenceProgressViewModel?> GetReviewCentreAsync", "public Task<StoryIntelligenceProgressViewModel?> GetCharacterReviewAsync");
|
||||
|
||||
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("GetReviewFrameAsync(batchId)", StringComparison.Ordinal), "Review Centre summary should use the shared aggregate review frame.");
|
||||
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.");
|
||||
@ -1076,6 +1077,61 @@ static void StoryIntelligenceReviewCentreUsesBoundedSummaryQuery()
|
||||
Assert(viewModel.Contains("SummaryNeedsAttentionCount", StringComparison.Ordinal), "Tile counts should support lightweight summary values without full candidate objects.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths()
|
||||
{
|
||||
var root = PlotLineRoot();
|
||||
var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
|
||||
var service = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
|
||||
|
||||
var expectedActions = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["StoryIntelligenceCharacters"] = "GetCharacterReviewAsync(batchId)",
|
||||
["StoryIntelligenceLocations"] = "GetLocationReviewAsync(batchId)",
|
||||
["StoryIntelligenceAssets"] = "GetAssetReviewAsync(batchId)",
|
||||
["StoryIntelligenceRelationships"] = "GetRelationshipReviewAsync(batchId)",
|
||||
["StoryIntelligenceKnowledge"] = "GetKnowledgeReviewAsync(batchId)",
|
||||
["StoryIntelligenceNarrative"] = "GetNarrativeReviewAsync(batchId)"
|
||||
};
|
||||
|
||||
foreach (var (actionName, serviceCall) in expectedActions)
|
||||
{
|
||||
var action = ExtractAction(controller, $"public async Task<IActionResult> {actionName}");
|
||||
Assert(action.Contains(serviceCall, StringComparison.Ordinal), $"{actionName} should load only its selected review module.");
|
||||
Assert(!action.Contains("GetProgressAsync(batchId)", StringComparison.Ordinal), $"{actionName} should not build full Story Intelligence progress.");
|
||||
Assert(!action.Contains("ResumeBookAsync", StringComparison.Ordinal), $"{actionName} should not resume/rebuild book analysis.");
|
||||
}
|
||||
|
||||
var selectedReview = ExtractBetween(service, "private async Task<StoryIntelligenceProgressViewModel?> GetSelectedReviewAsync", "private async Task<StoryIntelligenceReviewFrame?> GetReviewFrameAsync");
|
||||
Assert(selectedReview.Contains("GetReviewFrameAsync(batchId)", StringComparison.Ordinal), "Selected module loads should start from the compact persisted review frame.");
|
||||
Assert(!selectedReview.Contains("GetProgressAsync", StringComparison.Ordinal), "Selected module loads should not call full progress.");
|
||||
Assert(!selectedReview.Contains("ListSceneResultsAsync", StringComparison.Ordinal), "Selected module orchestration should not read all retained scene JSON.");
|
||||
Assert(!selectedReview.Contains("EnsureCandidatesAsync", StringComparison.Ordinal), "Selected module orchestration should not regenerate narrative candidates.");
|
||||
Assert(!selectedReview.Contains("ResumeBookAsync", StringComparison.Ordinal), "Selected module orchestration should not resume book analysis.");
|
||||
|
||||
var frame = ExtractBetween(service, "private async Task<StoryIntelligenceReviewFrame?> GetReviewFrameAsync", "private sealed record StoryIntelligenceReviewFrame");
|
||||
Assert(frame.Contains("GetReviewCentreSummaryAsync(batch.BookID, batch.UserID)", StringComparison.Ordinal), "Selected module frame should use the persisted Review Centre summary.");
|
||||
Assert(!frame.Contains("BuildReviewAsync", StringComparison.Ordinal), "Selected module frame should not build unrelated review candidates.");
|
||||
Assert(!frame.Contains("ListSceneResultsAsync", StringComparison.Ordinal), "Selected module frame should not parse retained scene JSON.");
|
||||
Assert(!frame.Contains("EnsureCandidatesAsync", StringComparison.Ordinal), "Selected module frame should not generate candidates.");
|
||||
|
||||
var narrativeMethod = ExtractBetween(service, "public Task<StoryIntelligenceProgressViewModel?> GetNarrativeReviewAsync", "private async Task<StoryIntelligenceProgressViewModel?> GetSelectedReviewAsync");
|
||||
Assert(narrativeMethod.Contains("narrativeReview.GetReviewAsync(frame.Batch)", StringComparison.Ordinal), "Plot Lines & Threads should use persisted narrative candidates.");
|
||||
Assert(!narrativeMethod.Contains("EnsureCandidatesAsync", StringComparison.Ordinal), "Plot Lines & Threads GET should not generate narrative candidates.");
|
||||
}
|
||||
|
||||
static string ExtractAction(string value, string signature)
|
||||
{
|
||||
var start = value.IndexOf(signature, StringComparison.Ordinal);
|
||||
Assert(start >= 0, $"Could not find action signature {signature}.");
|
||||
var next = value.IndexOf(" [Http", start + signature.Length, StringComparison.Ordinal);
|
||||
if (next < 0)
|
||||
{
|
||||
next = value.IndexOf(" private ", start + signature.Length, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return next < 0 ? value[start..] : value[start..next];
|
||||
}
|
||||
|
||||
static string PlotLineRoot()
|
||||
=> Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||
|
||||
|
||||
@ -323,7 +323,7 @@ public sealed class OnboardingController(
|
||||
[HttpGet("story-intelligence/characters")]
|
||||
public async Task<IActionResult> StoryIntelligenceCharacters(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
var model = await storyIntelligence.GetCharacterReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -347,7 +347,7 @@ public sealed class OnboardingController(
|
||||
[HttpGet("story-intelligence/locations")]
|
||||
public async Task<IActionResult> StoryIntelligenceLocations(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
var model = await storyIntelligence.GetLocationReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -371,7 +371,7 @@ public sealed class OnboardingController(
|
||||
[HttpGet("story-intelligence/assets")]
|
||||
public async Task<IActionResult> StoryIntelligenceAssets(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
var model = await storyIntelligence.GetAssetReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -398,7 +398,7 @@ public sealed class OnboardingController(
|
||||
[HttpGet("story-intelligence/relationships")]
|
||||
public async Task<IActionResult> StoryIntelligenceRelationships(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
var model = await storyIntelligence.GetRelationshipReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -555,7 +555,7 @@ public sealed class OnboardingController(
|
||||
[HttpGet("story-intelligence/knowledge")]
|
||||
public async Task<IActionResult> StoryIntelligenceKnowledge(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
var model = await storyIntelligence.GetKnowledgeReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -604,7 +604,7 @@ public sealed class OnboardingController(
|
||||
[HttpGet("story-intelligence/narrative")]
|
||||
public async Task<IActionResult> StoryIntelligenceNarrative(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
var model = await storyIntelligence.GetNarrativeReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
|
||||
@ -13,6 +13,12 @@ public interface IOnboardingStoryIntelligenceService
|
||||
Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null, string? primaryAgeGroup = null, string? storyEra = null, string? storyLocation = null);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetReviewCentreAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetCharacterReviewAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetLocationReviewAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetAssetReviewAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetRelationshipReviewAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetKnowledgeReviewAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetNarrativeReviewAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId);
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAsync(Guid batchId, int runId);
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAllAsync(Guid batchId);
|
||||
@ -724,6 +730,88 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
public async Task<StoryIntelligenceProgressViewModel?> GetReviewCentreAsync(Guid batchId)
|
||||
{
|
||||
var totalStopwatch = Stopwatch.StartNew();
|
||||
var frame = await GetReviewFrameAsync(batchId);
|
||||
if (frame is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Story Intelligence Review Centre summary built. BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} SummaryMs={SummaryMs} TotalMs={TotalMs} ActiveRunCount={ActiveRunCount} NarrativeCandidates={NarrativeCandidateCount} NarrativePending={NarrativePendingCount}",
|
||||
frame.Batch.BatchID,
|
||||
frame.Summary.ProjectID,
|
||||
frame.Summary.BookID,
|
||||
frame.SummaryElapsedMs,
|
||||
totalStopwatch.ElapsedMilliseconds,
|
||||
frame.Summary.ActiveRunCount,
|
||||
frame.Summary.NarrativeCandidateCount,
|
||||
frame.Summary.NarrativePendingCount);
|
||||
|
||||
return frame.Model;
|
||||
}
|
||||
|
||||
public Task<StoryIntelligenceProgressViewModel?> GetCharacterReviewAsync(Guid batchId)
|
||||
=> GetSelectedReviewAsync(batchId, "Characters", async frame => frame.Model.CharacterReview = await characterImport.BuildReviewAsync(frame.Batch));
|
||||
|
||||
public Task<StoryIntelligenceProgressViewModel?> GetLocationReviewAsync(Guid batchId)
|
||||
=> GetSelectedReviewAsync(batchId, "Locations", async frame => frame.Model.LocationReview = await locationImport.BuildReviewAsync(frame.Batch));
|
||||
|
||||
public Task<StoryIntelligenceProgressViewModel?> GetAssetReviewAsync(Guid batchId)
|
||||
=> GetSelectedReviewAsync(batchId, "Assets", async frame => frame.Model.AssetReview = await assetImport.BuildReviewAsync(frame.Batch));
|
||||
|
||||
public Task<StoryIntelligenceProgressViewModel?> GetRelationshipReviewAsync(Guid batchId)
|
||||
=> GetSelectedReviewAsync(batchId, "Relationships", async frame => frame.Model.RelationshipReview = await relationshipImport.BuildReviewAsync(frame.Batch));
|
||||
|
||||
public Task<StoryIntelligenceProgressViewModel?> GetKnowledgeReviewAsync(Guid batchId)
|
||||
=> GetSelectedReviewAsync(batchId, "Knowledge", async frame => frame.Model.KnowledgeReview = await knowledgeImport.BuildReviewAsync(frame.Batch));
|
||||
|
||||
public Task<StoryIntelligenceProgressViewModel?> GetNarrativeReviewAsync(Guid batchId)
|
||||
=> GetSelectedReviewAsync(batchId, "PlotLinesThreads", async frame =>
|
||||
{
|
||||
frame.Model.NarrativeReview = await narrativeReview.GetReviewAsync(frame.Batch);
|
||||
frame.Model.PlotSynthesis = new WholeBookPlotSynthesisStatusViewModel
|
||||
{
|
||||
WholeBookPlotSynthesisRunID = frame.Model.PlotSynthesis.WholeBookPlotSynthesisRunID,
|
||||
Status = frame.Model.PlotSynthesis.Status,
|
||||
CandidateCount = frame.Model.NarrativeReview.Candidates.Count,
|
||||
ThreadCount = frame.Model.NarrativeReview.Candidates.Count(candidate =>
|
||||
string.Equals(candidate.CandidateType, StoryIntelligenceNarrativeCandidateTypes.Thread, StringComparison.OrdinalIgnoreCase)),
|
||||
EventCount = frame.Model.NarrativeReview.Candidates.Sum(candidate => candidate.Events.Count),
|
||||
ErrorMessage = frame.Model.PlotSynthesis.ErrorMessage
|
||||
};
|
||||
});
|
||||
|
||||
private async Task<StoryIntelligenceProgressViewModel?> GetSelectedReviewAsync(
|
||||
Guid batchId,
|
||||
string module,
|
||||
Func<StoryIntelligenceReviewFrame, Task> loadModule)
|
||||
{
|
||||
var totalStopwatch = Stopwatch.StartNew();
|
||||
var frame = await GetReviewFrameAsync(batchId);
|
||||
if (frame is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var moduleStopwatch = Stopwatch.StartNew();
|
||||
await loadModule(frame);
|
||||
moduleStopwatch.Stop();
|
||||
|
||||
logger.LogInformation(
|
||||
"Story Intelligence selected review module built. Module={Module} BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} SummaryMs={SummaryMs} ModuleMs={ModuleMs} TotalMs={TotalMs}",
|
||||
module,
|
||||
frame.Batch.BatchID,
|
||||
frame.Summary.ProjectID,
|
||||
frame.Summary.BookID,
|
||||
frame.SummaryElapsedMs,
|
||||
moduleStopwatch.ElapsedMilliseconds,
|
||||
totalStopwatch.ElapsedMilliseconds);
|
||||
|
||||
return frame.Model;
|
||||
}
|
||||
|
||||
private async Task<StoryIntelligenceReviewFrame?> GetReviewFrameAsync(Guid batchId)
|
||||
{
|
||||
var batch = await batchStore.GetAsync(RequireUserId(), batchId);
|
||||
if (batch is null)
|
||||
{
|
||||
@ -823,20 +911,15 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
return new StoryIntelligenceReviewFrame(batch, summary, model, summaryStopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
private sealed record StoryIntelligenceReviewFrame(
|
||||
OnboardingStoryIntelligenceBatch Batch,
|
||||
StoryIntelligenceReviewCentreSummary Summary,
|
||||
StoryIntelligenceProgressViewModel Model,
|
||||
long SummaryElapsedMs);
|
||||
|
||||
public async Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId)
|
||||
{
|
||||
var progress = await GetProgressAsync(batchId);
|
||||
@ -862,7 +945,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
|
||||
var result = await commits.CommitAsync(runId, RequireUserId());
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAllAsync(Guid batchId)
|
||||
@ -909,7 +992,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
|
||||
var result = await characterImport.ImportAsync(batch, form);
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceCharacterImportResultViewModel?> GetCharacterImportResultAsync(Guid batchId)
|
||||
@ -943,7 +1026,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
|
||||
var result = await locationImport.ImportAsync(batch, form);
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceLocationImportResultViewModel?> GetLocationImportResultAsync(Guid batchId)
|
||||
@ -976,7 +1059,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
|
||||
var result = await assetImport.ImportAsync(batch, form);
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceAssetImportResultViewModel?> GetAssetImportResultAsync(Guid batchId)
|
||||
@ -1010,7 +1093,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
|
||||
var result = await relationshipImport.ImportAsync(batch, form);
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceRelationshipImportResultViewModel?> GetRelationshipImportResultAsync(Guid batchId)
|
||||
@ -1043,7 +1126,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
}
|
||||
|
||||
var result = await knowledgeImport.ImportAsync(batch, form);
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceKnowledgeImportResultViewModel?> GetKnowledgeImportResultAsync(Guid batchId)
|
||||
@ -1076,7 +1159,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
|
||||
var result = await narrativeReview.ImportAsync(batch, form, RequireUserId());
|
||||
await batchStore.SaveAsync(batch);
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
return (await GetReviewCentreAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceNarrativeImportResultViewModel?> GetNarrativeImportResultAsync(Guid batchId)
|
||||
|
||||
@ -55,20 +55,19 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
|
||||
public async Task<StoryIntelligenceAssetReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.AssetStageComplete || pipeline?.AssetReviewCompletedUtc is not null)
|
||||
{
|
||||
return new StoryIntelligenceAssetReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
HasCommittedScenes = true,
|
||||
CanImport = false,
|
||||
AlreadyLinkedCount = data.AlreadyLinkedCount,
|
||||
IsComplete = data.HasCommittedScenes,
|
||||
IsComplete = true,
|
||||
Candidates = []
|
||||
};
|
||||
}
|
||||
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var decidedKeys = batch.AssetDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
return new StoryIntelligenceAssetReviewViewModel
|
||||
|
||||
@ -71,20 +71,19 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
|
||||
public async Task<StoryIntelligenceCharacterReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.CharacterStageComplete || pipeline?.CharacterReviewCompletedUtc is not null)
|
||||
{
|
||||
return new StoryIntelligenceCharacterReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
HasCommittedScenes = true,
|
||||
CanImport = false,
|
||||
AlreadyLinkedCount = data.AlreadyLinkedCount,
|
||||
IsComplete = data.HasCommittedScenes,
|
||||
IsComplete = true,
|
||||
Candidates = []
|
||||
};
|
||||
}
|
||||
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
var existingIndex = await BuildCharacterIndexAsync(batch.ProjectID);
|
||||
|
||||
@ -29,23 +29,31 @@ public sealed class StoryIntelligenceKnowledgeImportService(
|
||||
|
||||
public async Task<StoryIntelligenceKnowledgeReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.KnowledgeStageComplete || pipeline?.KnowledgeReviewCompletedUtc is not null)
|
||||
{
|
||||
return new StoryIntelligenceKnowledgeReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = true,
|
||||
CanImport = false,
|
||||
IsComplete = true,
|
||||
KnowledgeStateOptions = [],
|
||||
Candidates = []
|
||||
};
|
||||
}
|
||||
|
||||
var lookupData = await characters.GetLookupsAsync(batch.ProjectID);
|
||||
var data = await BuildCandidateDataAsync(batch, lookupData.KnowledgeStates);
|
||||
var decidedKeys = batch.KnowledgeDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var selectedStateIds = batch.KnowledgeSelections.ToDictionary(selection => selection.Key, selection => selection.KnowledgeStateID, StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.KnowledgeStageComplete || pipeline?.KnowledgeReviewCompletedUtc is not null)
|
||||
{
|
||||
visibleCandidates = [];
|
||||
}
|
||||
|
||||
return new StoryIntelligenceKnowledgeReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
CanImport = visibleCandidates.Count > 0,
|
||||
AlreadyLinkedCount = data.AlreadyLinkedCount,
|
||||
IsComplete = data.HasCommittedScenes && (visibleCandidates.Count == 0 || batch.KnowledgeStageComplete || pipeline?.KnowledgeReviewCompletedUtc is not null),
|
||||
IsComplete = data.HasCommittedScenes && visibleCandidates.Count == 0,
|
||||
KnowledgeStateOptions = lookupData.KnowledgeStates
|
||||
.OrderBy(state => state.SortOrder)
|
||||
.ThenBy(state => state.StateName)
|
||||
|
||||
@ -69,20 +69,19 @@ public sealed class StoryIntelligenceLocationImportService(
|
||||
|
||||
public async Task<StoryIntelligenceLocationReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.LocationStageComplete || pipeline?.LocationReviewCompletedUtc is not null)
|
||||
{
|
||||
return new StoryIntelligenceLocationReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
HasCommittedScenes = true,
|
||||
CanImport = false,
|
||||
AlreadyLinkedCount = data.AlreadyLinkedCount,
|
||||
IsComplete = data.HasCommittedScenes,
|
||||
IsComplete = true,
|
||||
Candidates = []
|
||||
};
|
||||
}
|
||||
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var decidedKeys = batch.LocationDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
return new StoryIntelligenceLocationReviewViewModel
|
||||
|
||||
@ -28,24 +28,32 @@ public sealed class StoryIntelligenceRelationshipImportService(
|
||||
|
||||
public async Task<StoryIntelligenceRelationshipReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.RelationshipStageComplete || pipeline?.RelationshipReviewCompletedUtc is not null)
|
||||
{
|
||||
return new StoryIntelligenceRelationshipReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = true,
|
||||
CanImport = false,
|
||||
IsComplete = true,
|
||||
RelationshipTypeOptions = [],
|
||||
Candidates = []
|
||||
};
|
||||
}
|
||||
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var lookupData = await characters.GetLookupsAsync(batch.ProjectID);
|
||||
var typeOptions = ToRelationshipTypeOptions(lookupData.RelationshipTypes);
|
||||
var selectedTypeIds = batch.RelationshipSelections.ToDictionary(selection => selection.Key, selection => selection.RelationshipTypeID, StringComparer.OrdinalIgnoreCase);
|
||||
var decidedKeys = batch.RelationshipDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
|
||||
if (batch.RelationshipStageComplete || pipeline?.RelationshipReviewCompletedUtc is not null)
|
||||
{
|
||||
visibleCandidates = [];
|
||||
}
|
||||
|
||||
return new StoryIntelligenceRelationshipReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
CanImport = visibleCandidates.Count > 0,
|
||||
AlreadyLinkedCount = data.AlreadyLinkedCount,
|
||||
IsComplete = data.HasCommittedScenes && (visibleCandidates.Count == 0 || batch.RelationshipStageComplete || pipeline?.RelationshipReviewCompletedUtc is not null),
|
||||
IsComplete = data.HasCommittedScenes && visibleCandidates.Count == 0,
|
||||
RelationshipTypeOptions = typeOptions,
|
||||
Candidates = visibleCandidates.Select(candidate => new StoryIntelligenceRelationshipReviewCandidateViewModel
|
||||
{
|
||||
|
||||
@ -169,13 +169,13 @@ public sealed class StoryIntelligenceProgressViewModel
|
||||
public string? ProjectName { get; init; }
|
||||
public string? BookTitle { get; init; }
|
||||
public IReadOnlyList<StoryIntelligenceOnboardingChapterViewModel> Chapters { get; init; } = [];
|
||||
public StoryIntelligenceCharacterReviewViewModel CharacterReview { get; init; } = new();
|
||||
public StoryIntelligenceLocationReviewViewModel LocationReview { get; init; } = new();
|
||||
public StoryIntelligenceAssetReviewViewModel AssetReview { get; init; } = new();
|
||||
public StoryIntelligenceRelationshipReviewViewModel RelationshipReview { get; init; } = new();
|
||||
public StoryIntelligenceKnowledgeReviewViewModel KnowledgeReview { get; init; } = new();
|
||||
public StoryIntelligenceNarrativeReviewViewModel NarrativeReview { get; init; } = new();
|
||||
public WholeBookPlotSynthesisStatusViewModel PlotSynthesis { get; init; } = new();
|
||||
public StoryIntelligenceCharacterReviewViewModel CharacterReview { get; set; } = new();
|
||||
public StoryIntelligenceLocationReviewViewModel LocationReview { get; set; } = new();
|
||||
public StoryIntelligenceAssetReviewViewModel AssetReview { get; set; } = new();
|
||||
public StoryIntelligenceRelationshipReviewViewModel RelationshipReview { get; set; } = new();
|
||||
public StoryIntelligenceKnowledgeReviewViewModel KnowledgeReview { get; set; } = new();
|
||||
public StoryIntelligenceNarrativeReviewViewModel NarrativeReview { get; set; } = new();
|
||||
public WholeBookPlotSynthesisStatusViewModel PlotSynthesis { get; set; } = new();
|
||||
public StoryIntelligencePipelineDashboardViewModel PipelineDashboard { get; init; } = new();
|
||||
public int ChapterCount => Chapters.Count;
|
||||
public int CompletedChapterCount => Chapters.Count(chapter => chapter.IsRunComplete);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user