diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index ea5faf0..3476a12 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -67,6 +67,7 @@ var tests = new (string Name, Action Test)[] ("Character sex hints are conservative and reviewable", CharacterSexHintsAreConservativeAndReviewable), ("Character discovery does not truncate scan review candidates", CharacterDiscoveryDoesNotTruncateScanReviewCandidates), ("Character discovery rejects sentence-start words", CharacterDiscoveryRejectsSentenceStartWords), + ("Manuscript scan preview keeps only auto-selected character candidates", ManuscriptScanPreviewKeepsOnlyAutoSelectedCharacterCandidates), ("Scan review aliases feed Story Intelligence known character context", ScanReviewAliasesFeedStoryIntelligenceKnownCharacterContext), ("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions), ("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel), @@ -1315,6 +1316,23 @@ static void CharacterDiscoveryRejectsSentenceStartWords() Assert(!actionCandidates.Any(candidate => candidate.Text is "Door" or "Glass" or "Window"), "Repeated sentence-start action nouns must not become character candidates."); } +static void ManuscriptScanPreviewKeepsOnlyAutoSelectedCharacterCandidates() +{ + var preview = new ManuscriptScanPreview + { + CharacterCandidates = + [ + new() { TemporaryCharacterKey = "c-colin", Name = "Colin", MentionCount = 4, QualityScore = 7, Category = "ProbableCharacter" }, + new() { TemporaryCharacterKey = "c-door", Name = "Door", MentionCount = 4, QualityScore = 2, Category = "PossibleCharacter" }, + new() { TemporaryCharacterKey = "c-road", Name = "Road", MentionCount = 8, QualityScore = 1, Category = "Excluded" } + ] + }; + + var candidates = WordCompanionService.DiscoverManuscriptScanCharacterCandidates(preview); + Assert(candidates.Count == 1, $"Scan preview should keep only auto-selected character candidates; got {candidates.Count}."); + Assert(candidates[0].Name == "Colin", "Scan preview should keep probable character candidates."); +} + static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel() { var view = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Views/Development/StoryIntelligenceExperience.cshtml")); diff --git a/PlotLine/Services/WordCompanionService.cs b/PlotLine/Services/WordCompanionService.cs index f507b5f..f3ccf41 100644 --- a/PlotLine/Services/WordCompanionService.cs +++ b/PlotLine/Services/WordCompanionService.cs @@ -658,6 +658,7 @@ public sealed class WordCompanionService( } return DiscoverCharacterCandidates(manuscriptText) + .Where(IsAutoSelectedScanCharacterCandidate) .Select((candidate, index) => new ManuscriptScanCharacterCandidatePreview { TemporaryCharacterKey = $"c-{StableCandidateKey(candidate.Text, index)}", @@ -995,7 +996,7 @@ public sealed class WordCompanionService( IReadOnlyList candidates) => candidates .Where(candidate => !string.IsNullOrWhiteSpace(candidate.Name)) - .Where(candidate => !string.Equals(candidate.Category, "Excluded", StringComparison.OrdinalIgnoreCase)) + .Where(IsAutoSelectedScanCharacterCandidate) .Where(candidate => candidate.Name.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length > 1 || candidate.QualityScore >= 4 @@ -1008,6 +1009,12 @@ public sealed class WordCompanionService( .ThenBy(candidate => candidate.Name, StringComparer.OrdinalIgnoreCase) .ToList(); + private static bool IsAutoSelectedScanCharacterCandidate(WordCompanionCharacterCandidateDto candidate) + => string.Equals(candidate.Category, "ProbableCharacter", StringComparison.OrdinalIgnoreCase); + + private static bool IsAutoSelectedScanCharacterCandidate(ManuscriptScanCharacterCandidatePreview candidate) + => string.Equals(candidate.Category, "ProbableCharacter", StringComparison.OrdinalIgnoreCase); + private static string StableCandidateKey(string name, int index) { var slug = Regex.Replace(name.ToLowerInvariant(), @"[^a-z0-9]+", "-").Trim('-'); diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index c17a703..e6b0619 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -2349,12 +2349,11 @@ const discovery = await postJson("/api/word-companion/manuscript/discover-characters", { projectId: preview.projectID, documentText: documentTextForDiscovery, - includeExcluded: true + includeExcluded: false }); - preview.characterCandidates = mapOnboardingCharacterCandidates(discovery?.candidates); - preview.characterCandidateCount = preview.characterCandidates - .filter((candidate) => String(candidate.category || "").toLowerCase() !== "excluded") - .length; + preview.characterCandidates = mapOnboardingCharacterCandidates(discovery?.candidates) + .filter((candidate) => String(candidate.category || "").toLowerCase() === "probablecharacter"); + preview.characterCandidateCount = preview.characterCandidates.length; } catch (discoveryError) { console.warn("Unable to refine onboarding character candidates.", discoveryError); preview.characterCandidates = [];