Fix core import scene detection flow
This commit is contained in:
parent
24f2120cb2
commit
4214779883
@ -20,6 +20,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Truncated JSON is rejected", RejectsTruncatedJson),
|
("Truncated JSON is rejected", RejectsTruncatedJson),
|
||||||
("Repaired JSON deserialises into SceneIntelligenceScene", RepairedJsonDeserialises),
|
("Repaired JSON deserialises into SceneIntelligenceScene", RepairedJsonDeserialises),
|
||||||
("Chapter Structure confidence 0. repairs and deserialises", ChapterStructureConfidenceRepairs),
|
("Chapter Structure confidence 0. repairs and deserialises", ChapterStructureConfidenceRepairs),
|
||||||
|
("Chapter Structure boundary normaliser repairs gaps and overlaps", ChapterStructureBoundaryNormaliserRepairsGapsAndOverlaps),
|
||||||
("Shared parser reports raw chapter output on unrecoverable JSON", SharedParserReportsRawOutput),
|
("Shared parser reports raw chapter output on unrecoverable JSON", SharedParserReportsRawOutput),
|
||||||
("Character filtering rejects generic groups", CharacterFilteringRejectsGenericGroups),
|
("Character filtering rejects generic groups", CharacterFilteringRejectsGenericGroups),
|
||||||
("Character filtering preserves titled names", CharacterFilteringPreservesTitledNames),
|
("Character filtering preserves titled names", CharacterFilteringPreservesTitledNames),
|
||||||
@ -858,6 +859,54 @@ static void ChapterStructureConfidenceRepairs()
|
|||||||
Assert(result.Warnings.Any(warning => warning.Contains("$.sceneBoundaries[0].confidence", StringComparison.Ordinal)), "Repair warning did not include JSON path.");
|
Assert(result.Warnings.Any(warning => warning.Contains("$.sceneBoundaries[0].confidence", StringComparison.Ordinal)), "Repair warning did not include JSON path.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ChapterStructureBoundaryNormaliserRepairsGapsAndOverlaps()
|
||||||
|
{
|
||||||
|
var chapter = new ChapterStructureModel
|
||||||
|
{
|
||||||
|
SchemaVersion = "1.0",
|
||||||
|
ChapterSummary = "A chapter with several scene movements.",
|
||||||
|
SceneBoundaries =
|
||||||
|
[
|
||||||
|
new ChapterSceneBoundary
|
||||||
|
{
|
||||||
|
SceneNumber = 1,
|
||||||
|
StartParagraph = 1,
|
||||||
|
EndParagraph = 4,
|
||||||
|
StructuralSummary = "Opening scene summary with enough context for validation.",
|
||||||
|
Confidence = 0.8m,
|
||||||
|
Reason = "Opening location."
|
||||||
|
},
|
||||||
|
new ChapterSceneBoundary
|
||||||
|
{
|
||||||
|
SceneNumber = 2,
|
||||||
|
StartParagraph = 4,
|
||||||
|
EndParagraph = 7,
|
||||||
|
StructuralSummary = "Second scene summary with enough context for validation.",
|
||||||
|
Confidence = 0.8m,
|
||||||
|
Reason = "A new exchange begins."
|
||||||
|
},
|
||||||
|
new ChapterSceneBoundary
|
||||||
|
{
|
||||||
|
SceneNumber = 3,
|
||||||
|
StartParagraph = 9,
|
||||||
|
EndParagraph = 10,
|
||||||
|
StructuralSummary = "Final scene summary with enough context for validation.",
|
||||||
|
Confidence = 0.8m,
|
||||||
|
Reason = "Final turn."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = ChapterStructureBoundaryNormaliser.Normalise(chapter, 10);
|
||||||
|
var boundaries = result.ChapterStructure?.SceneBoundaries;
|
||||||
|
|
||||||
|
Assert(boundaries is { Count: 3 }, "Boundary normaliser should preserve the detected scene count.");
|
||||||
|
Assert(boundaries[0].StartParagraph == 1 && boundaries[0].EndParagraph == 4, "First boundary should remain unchanged.");
|
||||||
|
Assert(boundaries[1].StartParagraph == 5 && boundaries[1].EndParagraph == 7, "Overlap should be repaired by moving scene 2 start to paragraph 5.");
|
||||||
|
Assert(boundaries[2].StartParagraph == 8 && boundaries[2].EndParagraph == 10, "Gap should be repaired by moving scene 3 start to paragraph 8.");
|
||||||
|
Assert(result.Issues.Count >= 2, "Boundary repairs should be reported as warnings.");
|
||||||
|
}
|
||||||
|
|
||||||
static void SharedParserReportsRawOutput()
|
static void SharedParserReportsRawOutput()
|
||||||
{
|
{
|
||||||
const string raw = """{"schemaVersion":"1.0","sceneBoundaries":[""";
|
const string raw = """{"schemaVersion":"1.0","sceneBoundaries":[""";
|
||||||
@ -1534,7 +1583,7 @@ static void StructuralImportPersistsCanonicalSceneSourceText()
|
|||||||
Assert(migration.Contains("IX_SceneManuscriptSources_BookChapter", StringComparison.Ordinal), "Scene source text should be indexed for book/chapter retrieval.");
|
Assert(migration.Contains("IX_SceneManuscriptSources_BookChapter", StringComparison.Ordinal), "Scene source text should be indexed for book/chapter retrieval.");
|
||||||
Assert(onboarding.Contains("SaveSceneManuscriptSourcesAsync", StringComparison.Ordinal), "Structural import should persist scene source text before Word sync.");
|
Assert(onboarding.Contains("SaveSceneManuscriptSourcesAsync", StringComparison.Ordinal), "Structural import should persist scene source text before Word sync.");
|
||||||
Assert(onboarding.IndexOf("await SaveSceneManuscriptSourcesAsync(preview, structuralScenes, result)", StringComparison.Ordinal)
|
Assert(onboarding.IndexOf("await SaveSceneManuscriptSourcesAsync(preview, structuralScenes, result)", StringComparison.Ordinal)
|
||||||
< onboarding.IndexOf("await PublishBuildProgress(userId, preview.PreviewID, \"Updating and saving Word markers", StringComparison.Ordinal), "Scene source text should be persisted before marker sync can complete.");
|
< onboarding.IndexOf("await PublishBuildProgress(userId, preview.PreviewID, \"Updating Word document with scene markers", StringComparison.Ordinal), "Scene source text should be persisted before marker sync can complete.");
|
||||||
Assert(analyzer.Contains("string.Join(Environment.NewLine + Environment.NewLine", StringComparison.Ordinal), "Scene source text should preserve paragraph separation.");
|
Assert(analyzer.Contains("string.Join(Environment.NewLine + Environment.NewLine", StringComparison.Ordinal), "Scene source text should preserve paragraph separation.");
|
||||||
Assert(analyzer.Contains("item.IncludeInSourceText", StringComparison.Ordinal), "Scene source text should exclude non-prose structural paragraphs such as hidden markers/separators.");
|
Assert(analyzer.Contains("item.IncludeInSourceText", StringComparison.Ordinal), "Scene source text should exclude non-prose structural paragraphs such as hidden markers/separators.");
|
||||||
}
|
}
|
||||||
@ -1588,16 +1637,24 @@ static void CoreImportWizardOrdersScenesBeforeCharacters()
|
|||||||
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingService.cs"));
|
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingService.cs"));
|
||||||
|
|
||||||
Assert(review.Contains("value=\"structure\"", StringComparison.Ordinal), "Chapter review should submit to structural import, not character review.");
|
Assert(review.Contains("value=\"structure\"", StringComparison.Ordinal), "Chapter review should submit to structural import, not character review.");
|
||||||
|
Assert(review.Contains("Continue to Scene Detection", StringComparison.Ordinal), "Chapter review should move to scene detection with clear wording.");
|
||||||
|
Assert(!review.Contains("Include all scenes", StringComparison.Ordinal), "Chapter review should not show provisional scene bulk controls.");
|
||||||
|
Assert(!review.Contains("Exclude all scenes", StringComparison.Ordinal), "Chapter review should not show provisional scene bulk controls.");
|
||||||
|
Assert(!review.Contains("Include scene", StringComparison.Ordinal), "Chapter review should not show provisional scene selectors.");
|
||||||
|
Assert(!review.Contains("No scene breaks were detected. PlotDirector will treat this chapter as one scene for now.", StringComparison.Ordinal), "Chapter review should not expose one-scene-per-chapter fallback text.");
|
||||||
|
Assert(review.Split("name=\"intent\" value=\"structure\"", StringSplitOptions.None).Length - 1 == 1, "Chapter review should have exactly one structure progression action.");
|
||||||
Assert(controller.Contains("var readyToImport = string.Equals(intent, \"structure\"", StringComparison.Ordinal), "Controller should treat structure intent as the scene-creation gate.");
|
Assert(controller.Contains("var readyToImport = string.Equals(intent, \"structure\"", StringComparison.Ordinal), "Controller should treat structure intent as the scene-creation gate.");
|
||||||
Assert(controller.Contains("Import the approved chapters and scenes before reviewing characters.", StringComparison.Ordinal), "Character review should be guarded until structural import exists.");
|
Assert(controller.Contains("Import the approved chapters and scenes before reviewing characters.", StringComparison.Ordinal), "Character review should be guarded until structural import exists.");
|
||||||
Assert(controller.Contains("continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport", StringComparison.Ordinal), "Structure-save errors should remain on chapter review rather than jumping to characters.");
|
Assert(controller.Contains("continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport", StringComparison.Ordinal), "Structure-save errors should remain on chapter review rather than jumping to characters.");
|
||||||
Assert(controller.Contains("FinaliseCharacterReviewAsync", StringComparison.Ordinal), "Character review should have a distinct post-structure finalisation path.");
|
Assert(controller.Contains("FinaliseCharacterReviewAsync", StringComparison.Ordinal), "Character review should have a distinct post-structure finalisation path.");
|
||||||
Assert(onboarding.Contains("ValidateScanStructure(review, preview);\n review = new ManuscriptScanReviewDecision", StringComparison.Ordinal), "Saving chapter review should validate only structure before scene import.");
|
Assert(onboarding.Contains("ValidateScanStructure(review, preview);\n review = new ManuscriptScanReviewDecision", StringComparison.Ordinal), "Saving chapter review should validate only structure before scene import.");
|
||||||
Assert(onboarding.Contains("ValidateScanStructure(review, preview);\n\n var chapterDecisions", StringComparison.Ordinal), "Building approved structure should not validate unresolved character decisions.");
|
Assert(onboarding.Contains("ValidateScanStructure(review, preview);\n\n var chapterDecisions", StringComparison.Ordinal), "Building approved structure should not validate unresolved character decisions.");
|
||||||
|
Assert(onboarding.Contains("BuildSceneStructureSummary(chapterDecisions, structuralScenes)", StringComparison.Ordinal), "Detected scene results should be preserved for review before characters.");
|
||||||
Assert(onboarding.Contains("ValidateScanReview(new ManuscriptScanReviewDecision", StringComparison.Ordinal), "Character validation should run when finalising character review.");
|
Assert(onboarding.Contains("ValidateScanReview(new ManuscriptScanReviewDecision", StringComparison.Ordinal), "Character validation should run when finalising character review.");
|
||||||
Assert(onboarding.Contains("Import the approved chapters and scenes before reviewing characters.", StringComparison.Ordinal), "Character finalisation should refuse to run before structural scenes exist.");
|
Assert(onboarding.Contains("Import the approved chapters and scenes before reviewing characters.", StringComparison.Ordinal), "Character finalisation should refuse to run before structural scenes exist.");
|
||||||
|
Assert(complete.Contains("Detected scene structure", StringComparison.Ordinal), "Scene detection result page should show detected counts by chapter.");
|
||||||
Assert(complete.Contains("Review characters", StringComparison.Ordinal), "After Word sync, the next onboarding step should be character review.");
|
Assert(complete.Contains("Review characters", StringComparison.Ordinal), "After Word sync, the next onboarding step should be character review.");
|
||||||
Assert(complete.Contains("Word may now be closed; keep this browser workflow open", StringComparison.Ordinal), "Word safe-close must be distinct from browser/core-import completion.");
|
Assert(complete.Contains("Word may now be closed if you wish; keep this browser workflow open", StringComparison.Ordinal), "Word safe-close must be distinct from browser/core-import completion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CoreImportRemapsCharactersAfterReview()
|
static void CoreImportRemapsCharactersAfterReview()
|
||||||
@ -1894,11 +1951,13 @@ static void OnboardingWizardKeepsCharacterPagesInsideOneMainStep()
|
|||||||
var storyService = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/OnboardingStoryIntelligenceService.cs"));
|
var storyService = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/OnboardingStoryIntelligenceService.cs"));
|
||||||
|
|
||||||
Assert(!header.Contains("BuildDynamicSteps", StringComparison.Ordinal), "Onboarding header should not create one top-level step per character page.");
|
Assert(!header.Contains("BuildDynamicSteps", StringComparison.Ordinal), "Onboarding header should not create one top-level step per character page.");
|
||||||
Assert(header.Contains("new(\"Import Scenes\", 8)", StringComparison.Ordinal), "Onboarding header should include scene import as a distinct main step.");
|
Assert(header.Contains("new(\"Detect Scenes\", 8)", StringComparison.Ordinal), "Onboarding header should include scene detection as a distinct main step.");
|
||||||
Assert(header.Contains("new(\"Review Characters\", 9)", StringComparison.Ordinal), "Onboarding header should keep character review as one main step.");
|
Assert(header.Contains("new(\"Review Characters\", 9)", StringComparison.Ordinal), "Onboarding header should keep character review as one main step.");
|
||||||
|
Assert(!header.Contains("Story Summary", StringComparison.Ordinal), "Core onboarding header should not present Story Summary as a mandatory import step.");
|
||||||
|
Assert(!header.Contains("Story Context", StringComparison.Ordinal), "Core onboarding header should not present Story Context as a mandatory import step.");
|
||||||
Assert(review.Contains("var currentStep = isCharacterStep ? 9 : 7;", StringComparison.Ordinal), "Scan review should keep every character page on the same main wizard step.");
|
Assert(review.Contains("var currentStep = isCharacterStep ? 9 : 7;", StringComparison.Ordinal), "Scan review should keep every character page on the same main wizard step.");
|
||||||
Assert(review.Contains("onboarding-subprogress", StringComparison.Ordinal), "Character pagination should be shown as section-level progress.");
|
Assert(review.Contains("onboarding-subprogress", StringComparison.Ordinal), "Character pagination should be shown as section-level progress.");
|
||||||
Assert(storyService.Contains("const int totalWizardSteps = 12;", StringComparison.Ordinal), "Story Intelligence summary/context should use the fixed wizard step count.");
|
Assert(storyService.Contains("const int totalWizardSteps = 10;", StringComparison.Ordinal), "Story Intelligence summary/context should use the fixed core wizard step count.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel()
|
static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel()
|
||||||
|
|||||||
@ -213,9 +213,25 @@ public sealed class WordCompanionFollowHub(
|
|||||||
PreviewID = previewId
|
PreviewID = previewId
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await onboardingService.BuildApprovedStructureAsync(
|
OnboardingManuscriptBuildResult? result;
|
||||||
previewId,
|
try
|
||||||
progress => Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", progress));
|
{
|
||||||
|
result = await onboardingService.BuildApprovedStructureAsync(
|
||||||
|
previewId,
|
||||||
|
progress => Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", progress));
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||||
|
{
|
||||||
|
Status = "Failed",
|
||||||
|
Message = ex.Message,
|
||||||
|
PercentComplete = 25,
|
||||||
|
PreviewID = previewId
|
||||||
|
});
|
||||||
|
throw new HubException(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
throw new HubException("The reviewed scan could not be found.");
|
throw new HubException("The reviewed scan could not be found.");
|
||||||
|
|||||||
@ -199,9 +199,30 @@ public sealed class OnboardingManuscriptBuildResult
|
|||||||
public IReadOnlyList<OnboardingBuildCharacterMap> CharacterMappings { get; init; } = [];
|
public IReadOnlyList<OnboardingBuildCharacterMap> CharacterMappings { get; init; } = [];
|
||||||
public IReadOnlyList<OnboardingBuildMarkerInstruction> ChapterMarkerInstructions { get; init; } = [];
|
public IReadOnlyList<OnboardingBuildMarkerInstruction> ChapterMarkerInstructions { get; init; } = [];
|
||||||
public IReadOnlyList<OnboardingBuildMarkerInstruction> SceneMarkerInstructions { get; init; } = [];
|
public IReadOnlyList<OnboardingBuildMarkerInstruction> SceneMarkerInstructions { get; init; } = [];
|
||||||
|
public IReadOnlyList<OnboardingSceneStructureSummary> SceneStructure { get; init; } = [];
|
||||||
public string? MarkerWarning { get; init; }
|
public string? MarkerWarning { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class OnboardingSceneStructureSummary
|
||||||
|
{
|
||||||
|
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||||
|
public int ChapterNumber { get; init; }
|
||||||
|
public string ChapterTitle { get; init; } = string.Empty;
|
||||||
|
public int SceneCount { get; init; }
|
||||||
|
public int WordCount { get; init; }
|
||||||
|
public IReadOnlyList<OnboardingSceneStructureDetail> Scenes { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class OnboardingSceneStructureDetail
|
||||||
|
{
|
||||||
|
public int SceneNumberWithinChapter { get; init; }
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public int WordCount { get; init; }
|
||||||
|
public string? StructuralSummary { get; init; }
|
||||||
|
public int StartParagraph { get; init; }
|
||||||
|
public int EndParagraph { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class OnboardingBuildChapterMap
|
public sealed class OnboardingBuildChapterMap
|
||||||
{
|
{
|
||||||
public string TemporaryChapterKey { get; init; } = string.Empty;
|
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public static class ChapterStructureBoundaryNormaliser
|
|||||||
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var issues = new List<ValidationIssue>();
|
||||||
var boundaries = chapterStructure.SceneBoundaries;
|
var boundaries = chapterStructure.SceneBoundaries;
|
||||||
var finalIndex = boundaries.Count - 1;
|
var finalIndex = boundaries.Count - 1;
|
||||||
var final = boundaries[finalIndex];
|
var final = boundaries[finalIndex];
|
||||||
@ -22,31 +23,23 @@ public static class ChapterStructureBoundaryNormaliser
|
|||||||
if (IsTrailingEmptyBoundary(final, previous, paragraphCount, phantomParagraph)
|
if (IsTrailingEmptyBoundary(final, previous, paragraphCount, phantomParagraph)
|
||||||
|| IsTrailingInvalidPlaceholder(final, previous, paragraphCount, phantomParagraph))
|
|| IsTrailingInvalidPlaceholder(final, previous, paragraphCount, phantomParagraph))
|
||||||
{
|
{
|
||||||
var normalisedBoundaries = boundaries
|
boundaries = boundaries
|
||||||
.Take(finalIndex)
|
.Take(finalIndex)
|
||||||
.Select((boundary, index) => CopyBoundary(boundary, index + 1))
|
.Select((boundary, index) => CopyBoundary(boundary, index + 1))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var normalised = new ChapterStructureModel
|
issues.Add(new ValidationIssue
|
||||||
{
|
|
||||||
SchemaVersion = chapterStructure.SchemaVersion,
|
|
||||||
ChapterSummary = chapterStructure.ChapterSummary,
|
|
||||||
SceneBoundaries = normalisedBoundaries,
|
|
||||||
ExtensionData = chapterStructure.ExtensionData
|
|
||||||
};
|
|
||||||
|
|
||||||
var issue = new ValidationIssue
|
|
||||||
{
|
{
|
||||||
Severity = "Warning",
|
Severity = "Warning",
|
||||||
Path = $"sceneBoundaries[{finalIndex}]",
|
Path = $"sceneBoundaries[{finalIndex}]",
|
||||||
Message = BuildNormalisationMessage(final, finalIndex, paragraphCount, phantomParagraph),
|
Message = BuildNormalisationMessage(final, finalIndex, paragraphCount, phantomParagraph),
|
||||||
SuggestedFix = "Do not return a final scene boundary unless it references an actual supplied paragraph."
|
SuggestedFix = "Do not return a final scene boundary unless it references an actual supplied paragraph."
|
||||||
};
|
});
|
||||||
|
|
||||||
return new ChapterStructureBoundaryNormalisationResult(normalised, [issue]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
var continuity = RepairContinuity(chapterStructure, boundaries, paragraphCount);
|
||||||
|
issues.AddRange(continuity.Issues);
|
||||||
|
return new ChapterStructureBoundaryNormalisationResult(continuity.ChapterStructure, issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddIssuesTo(ValidationResult validation, ChapterStructureBoundaryNormalisationResult normalisation)
|
public static void AddIssuesTo(ValidationResult validation, ChapterStructureBoundaryNormalisationResult normalisation)
|
||||||
@ -98,6 +91,137 @@ public static class ChapterStructureBoundaryNormaliser
|
|||||||
ExtensionData = boundary.ExtensionData
|
ExtensionData = boundary.ExtensionData
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static ChapterStructureBoundaryNormalisationResult RepairContinuity(
|
||||||
|
ChapterStructureModel chapterStructure,
|
||||||
|
IReadOnlyList<ChapterSceneBoundary> sourceBoundaries,
|
||||||
|
int paragraphCount)
|
||||||
|
{
|
||||||
|
if (sourceBoundaries.Count == 0)
|
||||||
|
{
|
||||||
|
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var issues = new List<ValidationIssue>();
|
||||||
|
var ordered = sourceBoundaries
|
||||||
|
.Select((boundary, index) => new { Boundary = boundary, OriginalIndex = index })
|
||||||
|
.OrderBy(item => item.Boundary.StartParagraph ?? int.MaxValue)
|
||||||
|
.ThenBy(item => item.Boundary.EndParagraph ?? int.MaxValue)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (ordered.Where((item, index) => item.OriginalIndex != index).Any())
|
||||||
|
{
|
||||||
|
issues.Add(new ValidationIssue
|
||||||
|
{
|
||||||
|
Severity = "Warning",
|
||||||
|
Path = "sceneBoundaries",
|
||||||
|
Message = "Reordered scene boundaries by paragraph position before validation.",
|
||||||
|
SuggestedFix = "Return scene boundaries in reading order."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed = issues.Count > 0;
|
||||||
|
var repaired = new List<ChapterSceneBoundary>();
|
||||||
|
var expectedStart = 1;
|
||||||
|
for (var index = 0; index < ordered.Count; index++)
|
||||||
|
{
|
||||||
|
var boundary = ordered[index].Boundary;
|
||||||
|
if (!boundary.StartParagraph.HasValue || !boundary.EndParagraph.HasValue)
|
||||||
|
{
|
||||||
|
repaired.Add(CopyBoundary(boundary, index + 1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = boundary.StartParagraph.Value;
|
||||||
|
var end = boundary.EndParagraph.Value;
|
||||||
|
if (start != expectedStart)
|
||||||
|
{
|
||||||
|
issues.Add(new ValidationIssue
|
||||||
|
{
|
||||||
|
Severity = "Warning",
|
||||||
|
Path = $"sceneBoundaries[{index}].startParagraph",
|
||||||
|
Message = $"Adjusted scene {index + 1:N0} start paragraph from {start:N0} to {expectedStart:N0} to remove an AI boundary gap or overlap.",
|
||||||
|
SuggestedFix = $"Use startParagraph {expectedStart:N0} for this boundary."
|
||||||
|
});
|
||||||
|
start = expectedStart;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < start)
|
||||||
|
{
|
||||||
|
issues.Add(new ValidationIssue
|
||||||
|
{
|
||||||
|
Severity = "Warning",
|
||||||
|
Path = $"sceneBoundaries[{index}].endParagraph",
|
||||||
|
Message = $"Adjusted scene {index + 1:N0} end paragraph from {end:N0} to {start:N0} after boundary continuity repair.",
|
||||||
|
SuggestedFix = "Return an endParagraph greater than or equal to startParagraph."
|
||||||
|
});
|
||||||
|
end = start;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end > paragraphCount)
|
||||||
|
{
|
||||||
|
issues.Add(new ValidationIssue
|
||||||
|
{
|
||||||
|
Severity = "Warning",
|
||||||
|
Path = $"sceneBoundaries[{index}].endParagraph",
|
||||||
|
Message = $"Clamped scene {index + 1:N0} end paragraph from {end:N0} to the chapter paragraph count {paragraphCount:N0}.",
|
||||||
|
SuggestedFix = $"Use an endParagraph from 1 to {paragraphCount:N0}."
|
||||||
|
});
|
||||||
|
end = paragraphCount;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
repaired.Add(new ChapterSceneBoundary
|
||||||
|
{
|
||||||
|
SceneNumber = index + 1,
|
||||||
|
StartParagraph = start,
|
||||||
|
EndParagraph = end,
|
||||||
|
StructuralSummary = boundary.StructuralSummary,
|
||||||
|
Confidence = boundary.Confidence,
|
||||||
|
Reason = boundary.Reason,
|
||||||
|
ExtensionData = boundary.ExtensionData
|
||||||
|
});
|
||||||
|
expectedStart = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedStart <= paragraphCount && repaired.Count > 0)
|
||||||
|
{
|
||||||
|
var final = repaired[^1];
|
||||||
|
issues.Add(new ValidationIssue
|
||||||
|
{
|
||||||
|
Severity = "Warning",
|
||||||
|
Path = $"sceneBoundaries[{repaired.Count - 1}].endParagraph",
|
||||||
|
Message = $"Extended the final scene boundary to paragraph {paragraphCount:N0} so detected scenes cover the full chapter.",
|
||||||
|
SuggestedFix = $"End the final scene at paragraph {paragraphCount:N0}."
|
||||||
|
});
|
||||||
|
repaired[^1] = new ChapterSceneBoundary
|
||||||
|
{
|
||||||
|
SceneNumber = final.SceneNumber,
|
||||||
|
StartParagraph = final.StartParagraph,
|
||||||
|
EndParagraph = paragraphCount,
|
||||||
|
StructuralSummary = final.StructuralSummary,
|
||||||
|
Confidence = final.Confidence,
|
||||||
|
Reason = final.Reason,
|
||||||
|
ExtensionData = final.ExtensionData
|
||||||
|
};
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ChapterStructureBoundaryNormalisationResult(new ChapterStructureModel
|
||||||
|
{
|
||||||
|
SchemaVersion = chapterStructure.SchemaVersion,
|
||||||
|
ChapterSummary = chapterStructure.ChapterSummary,
|
||||||
|
SceneBoundaries = repaired,
|
||||||
|
ExtensionData = chapterStructure.ExtensionData
|
||||||
|
}, issues);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsTrailingEmptyBoundary(
|
private static bool IsTrailingEmptyBoundary(
|
||||||
ChapterSceneBoundary final,
|
ChapterSceneBoundary final,
|
||||||
ChapterSceneBoundary previous,
|
ChapterSceneBoundary previous,
|
||||||
|
|||||||
@ -43,18 +43,40 @@ public sealed class ManuscriptStructureAnalysisService(
|
|||||||
var prompt = BuildChapterPrompt(template, BuildChapterContextJson(request), StoryIntelligenceParagraphs.Number(paragraphs));
|
var prompt = BuildChapterPrompt(template, BuildChapterContextJson(request), StoryIntelligenceParagraphs.Number(paragraphs));
|
||||||
var result = await client.ExecutePromptAsync(prompt, promptVersion, cancellationToken, clientStatus.ChapterStructureModel);
|
var result = await client.ExecutePromptAsync(prompt, promptVersion, cancellationToken, clientStatus.ChapterStructureModel);
|
||||||
var chapterJson = ExtractOutputText(result.RawResponseText);
|
var chapterJson = ExtractOutputText(result.RawResponseText);
|
||||||
var parsed = JsonSerializer.Deserialize<ChapterStructureModel>(chapterJson, JsonOptions);
|
var parseResult = StoryIntelligenceAiJsonParser.Parse<ChapterStructureModel>(chapterJson, JsonOptions);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
{
|
||||||
|
return ManuscriptStructureAnalysisResult.Failed(
|
||||||
|
request.TemporaryChapterKey,
|
||||||
|
parseResult.ErrorMessage ?? "Chapter Structure JSON could not be parsed.",
|
||||||
|
parseResult.RawJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseResult.Warnings.Count > 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Repaired structural scene-boundary JSON. ProjectID={ProjectID} BookID={BookID} TemporaryChapterKey={TemporaryChapterKey} Warnings={Warnings}",
|
||||||
|
request.ProjectID,
|
||||||
|
request.BookID,
|
||||||
|
request.TemporaryChapterKey,
|
||||||
|
string.Join("; ", parseResult.Warnings));
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = parseResult.Parsed;
|
||||||
var normalisation = ChapterStructureBoundaryNormaliser.Normalise(parsed, paragraphs.Count);
|
var normalisation = ChapterStructureBoundaryNormaliser.Normalise(parsed, paragraphs.Count);
|
||||||
var chapter = normalisation.ChapterStructure;
|
var chapter = normalisation.ChapterStructure;
|
||||||
var validation = validator.Validate(chapter, paragraphs.Count);
|
var validation = validator.Validate(chapter, paragraphs.Count);
|
||||||
ChapterStructureBoundaryNormaliser.AddIssuesTo(validation, normalisation);
|
ChapterStructureBoundaryNormaliser.AddIssuesTo(validation, normalisation);
|
||||||
|
var persistedJson = normalisation.Issues.Count > 0
|
||||||
|
? ChapterStructureBoundaryNormaliser.SerialiseParsedModel(chapter, normalisation, JsonOptions)
|
||||||
|
: parseResult.RepairedJson ?? parseResult.RawJson;
|
||||||
|
|
||||||
if (!validation.IsValid || chapter?.SceneBoundaries is null || chapter.SceneBoundaries.Count == 0)
|
if (!validation.IsValid || chapter?.SceneBoundaries is null || chapter.SceneBoundaries.Count == 0)
|
||||||
{
|
{
|
||||||
return ManuscriptStructureAnalysisResult.Failed(
|
return ManuscriptStructureAnalysisResult.Failed(
|
||||||
request.TemporaryChapterKey,
|
request.TemporaryChapterKey,
|
||||||
BuildValidationMessage(validation),
|
BuildValidationMessage(validation),
|
||||||
chapterJson);
|
persistedJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenes = new List<ManuscriptStructureSceneBoundary>();
|
var scenes = new List<ManuscriptStructureSceneBoundary>();
|
||||||
@ -68,7 +90,7 @@ public sealed class ManuscriptStructureAnalysisService(
|
|||||||
return ManuscriptStructureAnalysisResult.Failed(
|
return ManuscriptStructureAnalysisResult.Failed(
|
||||||
request.TemporaryChapterKey,
|
request.TemporaryChapterKey,
|
||||||
$"Scene {sceneNumber:N0} has an invalid paragraph range.",
|
$"Scene {sceneNumber:N0} has an invalid paragraph range.",
|
||||||
chapterJson);
|
persistedJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
var startMap = request.ParagraphMap.FirstOrDefault(item => item.StructuralParagraphOrdinal == start);
|
var startMap = request.ParagraphMap.FirstOrDefault(item => item.StructuralParagraphOrdinal == start);
|
||||||
@ -78,7 +100,7 @@ public sealed class ManuscriptStructureAnalysisService(
|
|||||||
return ManuscriptStructureAnalysisResult.Failed(
|
return ManuscriptStructureAnalysisResult.Failed(
|
||||||
request.TemporaryChapterKey,
|
request.TemporaryChapterKey,
|
||||||
$"Scene {sceneNumber:N0} could not be mapped back to a Word paragraph.",
|
$"Scene {sceneNumber:N0} could not be mapped back to a Word paragraph.",
|
||||||
chapterJson);
|
persistedJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sceneParagraphs = paragraphs.Skip(start - 1).Take(end - start + 1).ToList();
|
var sceneParagraphs = paragraphs.Skip(start - 1).Take(end - start + 1).ToList();
|
||||||
@ -108,7 +130,7 @@ public sealed class ManuscriptStructureAnalysisService(
|
|||||||
request.TemporaryChapterKey,
|
request.TemporaryChapterKey,
|
||||||
scenes.Count);
|
scenes.Count);
|
||||||
|
|
||||||
return ManuscriptStructureAnalysisResult.Succeeded(request.TemporaryChapterKey, scenes, chapterJson);
|
return ManuscriptStructureAnalysisResult.Succeeded(request.TemporaryChapterKey, scenes, persistedJson);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -235,15 +235,16 @@ public sealed class OnboardingService(
|
|||||||
throw new InvalidOperationException("Review the scan and choose Save and continue before preparing chapters.");
|
throw new InvalidOperationException("Review the scan and choose Save and continue before preparing chapters.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await PublishBuildProgress(userId, preview.PreviewID, "Creating chapters...", 10, progress);
|
await PublishBuildProgress(userId, preview.PreviewID, "Preparing approved chapters for scene detection...", 10, progress);
|
||||||
ValidateScanStructure(review, preview);
|
ValidateScanStructure(review, preview);
|
||||||
|
|
||||||
var chapterDecisions = review.Chapters.Where(chapter => chapter.Include).OrderBy(chapter => chapter.ChapterNumber).ToList();
|
var chapterDecisions = review.Chapters.Where(chapter => chapter.Include).OrderBy(chapter => chapter.ChapterNumber).ToList();
|
||||||
|
|
||||||
await PublishBuildProgress(userId, preview.PreviewID, "Detecting scenes...", 25, progress);
|
await PublishBuildProgress(userId, preview.PreviewID, "Detecting scene boundaries...", 20, progress);
|
||||||
var structuralScenes = new List<ManuscriptStructureSceneBoundary>();
|
var structuralScenes = new List<ManuscriptStructureSceneBoundary>();
|
||||||
foreach (var chapterDecision in chapterDecisions)
|
for (var chapterIndex = 0; chapterIndex < chapterDecisions.Count; chapterIndex++)
|
||||||
{
|
{
|
||||||
|
var chapterDecision = chapterDecisions[chapterIndex];
|
||||||
var chapterPreview = preview.Chapters.FirstOrDefault(item => item.TemporaryChapterKey == chapterDecision.TemporaryChapterKey)
|
var chapterPreview = preview.Chapters.FirstOrDefault(item => item.TemporaryChapterKey == chapterDecision.TemporaryChapterKey)
|
||||||
?? throw new InvalidOperationException($"The approved chapter '{chapterDecision.Title}' could not be found in the scan preview.");
|
?? throw new InvalidOperationException($"The approved chapter '{chapterDecision.Title}' could not be found in the scan preview.");
|
||||||
if (string.IsNullOrWhiteSpace(chapterPreview.ChapterText))
|
if (string.IsNullOrWhiteSpace(chapterPreview.ChapterText))
|
||||||
@ -256,6 +257,13 @@ public sealed class OnboardingService(
|
|||||||
throw new InvalidOperationException($"The approved chapter '{chapterDecision.Title}' does not have paragraph mapping. Scan the manuscript again before importing.");
|
throw new InvalidOperationException($"The approved chapter '{chapterDecision.Title}' does not have paragraph mapping. Scan the manuscript again before importing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var analysisPercent = 20 + (chapterIndex * 20 / Math.Max(1, chapterDecisions.Count));
|
||||||
|
await PublishBuildProgress(
|
||||||
|
userId,
|
||||||
|
preview.PreviewID,
|
||||||
|
$"Analysing Chapter {chapterIndex + 1:N0} of {chapterDecisions.Count:N0} - {chapterDecision.Title}",
|
||||||
|
analysisPercent,
|
||||||
|
progress);
|
||||||
var analysis = await structureAnalysis.AnalyseChapterAsync(new ManuscriptStructureAnalysisRequest
|
var analysis = await structureAnalysis.AnalyseChapterAsync(new ManuscriptStructureAnalysisRequest
|
||||||
{
|
{
|
||||||
ProjectID = preview.ProjectID,
|
ProjectID = preview.ProjectID,
|
||||||
@ -276,7 +284,7 @@ public sealed class OnboardingService(
|
|||||||
structuralScenes.AddRange(analysis.Scenes);
|
structuralScenes.AddRange(analysis.Scenes);
|
||||||
}
|
}
|
||||||
|
|
||||||
await PublishBuildProgress(userId, preview.PreviewID, "Preparing chapters and scenes for import...", 45, progress);
|
await PublishBuildProgress(userId, preview.PreviewID, "Validating detected scene structure...", 45, progress);
|
||||||
var request = new OnboardingManuscriptBuildRequest
|
var request = new OnboardingManuscriptBuildRequest
|
||||||
{
|
{
|
||||||
PreviewID = preview.PreviewID,
|
PreviewID = preview.PreviewID,
|
||||||
@ -301,7 +309,7 @@ public sealed class OnboardingService(
|
|||||||
Characters = []
|
Characters = []
|
||||||
};
|
};
|
||||||
|
|
||||||
await PublishBuildProgress(userId, preview.PreviewID, "Creating PlotDirector structure...", 60, progress);
|
await PublishBuildProgress(userId, preview.PreviewID, "Creating PlotDirector scenes...", 60, progress);
|
||||||
var result = await builds.BuildAsync(userId, request)
|
var result = await builds.BuildAsync(userId, request)
|
||||||
?? throw new InvalidOperationException($"The project chapters could not be prepared. Approved chapters: {request.Chapters.Count}. Return to review, save your selections, and try again.");
|
?? throw new InvalidOperationException($"The project chapters could not be prepared. Approved chapters: {request.Chapters.Count}. Return to review, save your selections, and try again.");
|
||||||
result = new OnboardingManuscriptBuildResult
|
result = new OnboardingManuscriptBuildResult
|
||||||
@ -325,13 +333,14 @@ public sealed class OnboardingService(
|
|||||||
CharacterMappings = result.CharacterMappings,
|
CharacterMappings = result.CharacterMappings,
|
||||||
ChapterMarkerInstructions = BuildChapterMarkerInstructions(preview, result.ChapterMappings),
|
ChapterMarkerInstructions = BuildChapterMarkerInstructions(preview, result.ChapterMappings),
|
||||||
SceneMarkerInstructions = BuildSceneMarkerInstructions(structuralScenes, result.SceneMappings),
|
SceneMarkerInstructions = BuildSceneMarkerInstructions(structuralScenes, result.SceneMappings),
|
||||||
|
SceneStructure = BuildSceneStructureSummary(chapterDecisions, structuralScenes),
|
||||||
MarkerWarning = result.MarkerWarning
|
MarkerWarning = result.MarkerWarning
|
||||||
};
|
};
|
||||||
await SaveStructuralChapterSourcesAsync(preview, result);
|
await SaveStructuralChapterSourcesAsync(preview, result);
|
||||||
await SaveSceneManuscriptSourcesAsync(preview, structuralScenes, result);
|
await SaveSceneManuscriptSourcesAsync(preview, structuralScenes, result);
|
||||||
await coreImportCharacterMapping.MapBookAsync(preview.ProjectID, preview.BookID);
|
await coreImportCharacterMapping.MapBookAsync(preview.ProjectID, preview.BookID);
|
||||||
await scanPreviews.SaveBuildResultAsync(userId, result);
|
await scanPreviews.SaveBuildResultAsync(userId, result);
|
||||||
await PublishBuildProgress(userId, preview.PreviewID, "Updating and saving Word markers. Keep Word and this browser page open.", 90, progress);
|
await PublishBuildProgress(userId, preview.PreviewID, "Updating Word document with scene markers. Keep Word open while the document is saved.", 90, progress);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,6 +453,7 @@ public sealed class OnboardingService(
|
|||||||
CharacterMappings = characterMappings,
|
CharacterMappings = characterMappings,
|
||||||
ChapterMarkerInstructions = build.ChapterMarkerInstructions,
|
ChapterMarkerInstructions = build.ChapterMarkerInstructions,
|
||||||
SceneMarkerInstructions = build.SceneMarkerInstructions,
|
SceneMarkerInstructions = build.SceneMarkerInstructions,
|
||||||
|
SceneStructure = build.SceneStructure,
|
||||||
MarkerWarning = build.MarkerWarning
|
MarkerWarning = build.MarkerWarning
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -500,6 +510,37 @@ public sealed class OnboardingService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<OnboardingSceneStructureSummary> BuildSceneStructureSummary(
|
||||||
|
IReadOnlyList<ManuscriptScanChapterReviewDecision> chapters,
|
||||||
|
IReadOnlyList<ManuscriptStructureSceneBoundary> scenes)
|
||||||
|
=> chapters
|
||||||
|
.OrderBy(chapter => chapter.ChapterNumber)
|
||||||
|
.Select(chapter =>
|
||||||
|
{
|
||||||
|
var chapterScenes = scenes
|
||||||
|
.Where(scene => string.Equals(scene.TemporaryChapterKey, chapter.TemporaryChapterKey, StringComparison.Ordinal))
|
||||||
|
.OrderBy(scene => scene.SceneNumberWithinChapter)
|
||||||
|
.ToList();
|
||||||
|
return new OnboardingSceneStructureSummary
|
||||||
|
{
|
||||||
|
TemporaryChapterKey = chapter.TemporaryChapterKey,
|
||||||
|
ChapterNumber = chapter.ChapterNumber,
|
||||||
|
ChapterTitle = chapter.Title,
|
||||||
|
SceneCount = chapterScenes.Count,
|
||||||
|
WordCount = chapterScenes.Sum(scene => scene.WordCount),
|
||||||
|
Scenes = chapterScenes.Select(scene => new OnboardingSceneStructureDetail
|
||||||
|
{
|
||||||
|
SceneNumberWithinChapter = scene.SceneNumberWithinChapter,
|
||||||
|
Title = scene.Title,
|
||||||
|
WordCount = scene.WordCount,
|
||||||
|
StructuralSummary = scene.StructuralSummary,
|
||||||
|
StartParagraph = scene.StartParagraph,
|
||||||
|
EndParagraph = scene.EndParagraph
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
private async Task SaveStructuralChapterSourcesAsync(ManuscriptScanPreview preview, OnboardingManuscriptBuildResult result)
|
private async Task SaveStructuralChapterSourcesAsync(ManuscriptScanPreview preview, OnboardingManuscriptBuildResult result)
|
||||||
{
|
{
|
||||||
var chapterIds = result.ChapterMappings.ToDictionary(item => item.TemporaryChapterKey, item => item.ChapterID, StringComparer.Ordinal);
|
var chapterIds = result.ChapterMappings.ToDictionary(item => item.TemporaryChapterKey, item => item.ChapterID, StringComparer.Ordinal);
|
||||||
@ -703,6 +744,9 @@ public sealed class OnboardingService(
|
|||||||
|
|
||||||
var selectedChapterKeys = chapters.Where(chapter => chapter.Include).Select(chapter => chapter.TemporaryChapterKey).ToHashSet(StringComparer.Ordinal);
|
var selectedChapterKeys = chapters.Where(chapter => chapter.Include).Select(chapter => chapter.TemporaryChapterKey).ToHashSet(StringComparer.Ordinal);
|
||||||
var selectedScenes = chapters.SelectMany(chapter => chapter.Scenes).Where(scene => scene.Include && selectedChapterKeys.Contains(scene.TemporaryChapterKey)).ToList();
|
var selectedScenes = chapters.SelectMany(chapter => chapter.Scenes).Where(scene => scene.Include && selectedChapterKeys.Contains(scene.TemporaryChapterKey)).ToList();
|
||||||
|
var selectedChapterWordCount = chapters
|
||||||
|
.Where(chapter => chapter.Include)
|
||||||
|
.Sum(chapter => chapter.WordCount);
|
||||||
|
|
||||||
return new ManuscriptScanReviewViewModel
|
return new ManuscriptScanReviewViewModel
|
||||||
{
|
{
|
||||||
@ -718,7 +762,7 @@ public sealed class OnboardingService(
|
|||||||
SelectedChapterCount = selectedChapterKeys.Count,
|
SelectedChapterCount = selectedChapterKeys.Count,
|
||||||
SelectedSceneCount = selectedScenes.Count,
|
SelectedSceneCount = selectedScenes.Count,
|
||||||
SelectedCharacterCount = characters.Count(character => character.Include && !character.IsAlias && !string.Equals(character.Category, "Excluded", StringComparison.OrdinalIgnoreCase)),
|
SelectedCharacterCount = characters.Count(character => character.Include && !character.IsAlias && !string.Equals(character.Category, "Excluded", StringComparison.OrdinalIgnoreCase)),
|
||||||
SelectedWordCount = selectedScenes.Sum(scene => scene.WordCount),
|
SelectedWordCount = selectedChapterWordCount,
|
||||||
Chapters = chapters,
|
Chapters = chapters,
|
||||||
CharacterCandidates = characters,
|
CharacterCandidates = characters,
|
||||||
ExistingCharacterOptions = existingOptions
|
ExistingCharacterOptions = existingOptions
|
||||||
@ -764,18 +808,12 @@ public sealed class OnboardingService(
|
|||||||
private static void ValidateScanStructure(ManuscriptScanReviewDecision review, ManuscriptScanPreview preview)
|
private static void ValidateScanStructure(ManuscriptScanReviewDecision review, ManuscriptScanPreview preview)
|
||||||
{
|
{
|
||||||
var previewChapters = preview.Chapters.ToDictionary(chapter => chapter.TemporaryChapterKey, StringComparer.Ordinal);
|
var previewChapters = preview.Chapters.ToDictionary(chapter => chapter.TemporaryChapterKey, StringComparer.Ordinal);
|
||||||
var previewScenes = preview.Scenes.ToDictionary(scene => scene.TemporarySceneKey, StringComparer.Ordinal);
|
|
||||||
var includedChapterKeys = review.Chapters.Where(chapter => chapter.Include).Select(chapter => chapter.TemporaryChapterKey).ToHashSet(StringComparer.Ordinal);
|
var includedChapterKeys = review.Chapters.Where(chapter => chapter.Include).Select(chapter => chapter.TemporaryChapterKey).ToHashSet(StringComparer.Ordinal);
|
||||||
if (includedChapterKeys.Count == 0)
|
if (includedChapterKeys.Count == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Choose at least one chapter before continuing.");
|
throw new InvalidOperationException("Choose at least one chapter before continuing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!review.Scenes.Any(scene => scene.Include && includedChapterKeys.Contains(scene.TemporaryChapterKey)))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Choose at least one scene before continuing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (review.Chapters.Where(chapter => chapter.Include).Any(chapter => string.IsNullOrWhiteSpace(chapter.Title)))
|
if (review.Chapters.Where(chapter => chapter.Include).Any(chapter => string.IsNullOrWhiteSpace(chapter.Title)))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Every included chapter needs a title.");
|
throw new InvalidOperationException("Every included chapter needs a title.");
|
||||||
@ -786,24 +824,6 @@ public sealed class OnboardingService(
|
|||||||
throw new InvalidOperationException("Chapter order is not valid. Restore the scan defaults and try again.");
|
throw new InvalidOperationException("Chapter order is not valid. Restore the scan defaults and try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var scene in review.Scenes.Where(scene => scene.Include))
|
|
||||||
{
|
|
||||||
if (!includedChapterKeys.Contains(scene.TemporaryChapterKey))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Included scenes must belong to included chapters.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!previewScenes.TryGetValue(scene.TemporarySceneKey, out var previewScene))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("One of the selected scenes no longer exists in the scan preview.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewScene.WordCount <= 0 && !scene.AllowZeroWords)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Included scenes with no words must be explicitly allowed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var chapter in review.Chapters)
|
foreach (var chapter in review.Chapters)
|
||||||
{
|
{
|
||||||
if (!previewChapters.ContainsKey(chapter.TemporaryChapterKey))
|
if (!previewChapters.ContainsKey(chapter.TemporaryChapterKey))
|
||||||
|
|||||||
@ -72,8 +72,8 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
var existingBatch = await batchStore.GetLatestForPreviewAsync(userId, preview.PreviewID);
|
var existingBatch = await batchStore.GetLatestForPreviewAsync(userId, preview.PreviewID);
|
||||||
var missingTextCount = included.Count(chapter => string.IsNullOrWhiteSpace(chapter.Preview.ChapterText));
|
var missingTextCount = included.Count(chapter => string.IsNullOrWhiteSpace(chapter.Preview.ChapterText));
|
||||||
const int storySummaryStep = 10;
|
const int storySummaryStep = 10;
|
||||||
const int storyContextStep = 11;
|
const int storyContextStep = 10;
|
||||||
const int totalWizardSteps = 12;
|
const int totalWizardSteps = 10;
|
||||||
return new StoryIntelligenceOverviewViewModel
|
return new StoryIntelligenceOverviewViewModel
|
||||||
{
|
{
|
||||||
PreviewID = preview.PreviewID,
|
PreviewID = preview.PreviewID,
|
||||||
|
|||||||
@ -128,8 +128,8 @@ public sealed class StoryIntelligenceOverviewViewModel
|
|||||||
public int ApprovedWordCount { get; init; }
|
public int ApprovedWordCount { get; init; }
|
||||||
public int MissingChapterTextCount { get; init; }
|
public int MissingChapterTextCount { get; init; }
|
||||||
public int WizardCurrentStep { get; init; } = 10;
|
public int WizardCurrentStep { get; init; } = 10;
|
||||||
public int WizardContextStep { get; init; } = 11;
|
public int WizardContextStep { get; init; } = 10;
|
||||||
public int WizardTotalSteps { get; init; } = 12;
|
public int WizardTotalSteps { get; init; } = 10;
|
||||||
public string? Message { get; init; }
|
public string? Message { get; init; }
|
||||||
public Guid? ExistingBatchID { get; init; }
|
public Guid? ExistingBatchID { get; init; }
|
||||||
public StoryIntelligenceJobProgress? ExistingJob { get; init; }
|
public StoryIntelligenceJobProgress? ExistingJob { get; init; }
|
||||||
|
|||||||
@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
<section class="onboarding-shell" aria-labelledby="build-complete-title">
|
<section class="onboarding-shell" aria-labelledby="build-complete-title">
|
||||||
<div class="onboarding-panel onboarding-review-panel">
|
<div class="onboarding-panel onboarding-review-panel">
|
||||||
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = coreComplete ? 12 : 9, TotalSteps = 12 })" />
|
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = coreComplete ? 10 : 8, TotalSteps = 10 })" />
|
||||||
<div class="onboarding-copy">
|
<div class="onboarding-copy">
|
||||||
<p class="eyebrow">Core Import Complete</p>
|
<p class="eyebrow">@(coreComplete ? "Core Import Complete" : "Scene Detection Complete")</p>
|
||||||
<div class="onboarding-success-heading">
|
<div class="onboarding-success-heading">
|
||||||
<span aria-hidden="true">✓</span>
|
<span aria-hidden="true">✓</span>
|
||||||
<h1 id="build-complete-title">Your manuscript has been imported.</h1>
|
<h1 id="build-complete-title">@(coreComplete ? "Core import is complete." : "Scene structure has been detected.")</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
@if (coreComplete)
|
@if (coreComplete)
|
||||||
@ -22,11 +22,11 @@
|
|||||||
{
|
{
|
||||||
@:This manuscript structure has already been created in PlotDirector, so nothing was duplicated. Review characters to complete the core import.
|
@:This manuscript structure has already been created in PlotDirector, so nothing was duplicated. Review characters to complete the core import.
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@:PlotDirector created the approved chapters and scenes, persisted their manuscript text, updated the Word markers, and saved the manuscript. Word may now be closed; keep this browser workflow open to review characters.
|
@:PlotDirector created the approved chapters and detected scenes, persisted their manuscript text, updated the Word markers, and saved the manuscript. Word may now be closed if you wish; keep this browser workflow open to review characters.
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
|
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
|
||||||
{
|
{
|
||||||
<p class="text-warning">@Model.MarkerWarning</p>
|
<p class="text-warning">@Model.MarkerWarning</p>
|
||||||
@ -52,6 +52,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (Model.SceneStructure.Any())
|
||||||
|
{
|
||||||
|
<section class="onboarding-complete-next" aria-label="Detected scene structure">
|
||||||
|
<h2>Detected scene structure</h2>
|
||||||
|
<div class="onboarding-review-chapters">
|
||||||
|
@foreach (var chapter in Model.SceneStructure)
|
||||||
|
{
|
||||||
|
<details class="onboarding-review-chapter">
|
||||||
|
<summary>
|
||||||
|
<span>Chapter @chapter.ChapterNumber</span>
|
||||||
|
<strong>@chapter.ChapterTitle</strong>
|
||||||
|
<small>@chapter.SceneCount.ToString("N0") scene@(chapter.SceneCount == 1 ? string.Empty : "s") / @chapter.WordCount.ToString("N0") words</small>
|
||||||
|
</summary>
|
||||||
|
@if (chapter.Scenes.Any(scene => !string.IsNullOrWhiteSpace(scene.StructuralSummary)))
|
||||||
|
{
|
||||||
|
<ol>
|
||||||
|
@foreach (var scene in chapter.Scenes)
|
||||||
|
{
|
||||||
|
<li>
|
||||||
|
<strong>@scene.Title</strong>
|
||||||
|
<span>@scene.WordCount.ToString("N0") words / paragraphs @scene.StartParagraph-@scene.EndParagraph</span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(scene.StructuralSummary))
|
||||||
|
{
|
||||||
|
<p>@scene.StructuralSummary</p>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@if (coreComplete)
|
@if (coreComplete)
|
||||||
{
|
{
|
||||||
<section class="onboarding-complete-next">
|
<section class="onboarding-complete-next">
|
||||||
|
|||||||
@ -239,11 +239,11 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="onboarding-copy">
|
<div class="onboarding-copy">
|
||||||
<p class="eyebrow">Step @currentJourneyOrder of 12</p>
|
<p class="eyebrow">Step @currentJourneyOrder of 10</p>
|
||||||
@if (Model.IsMicrosoftWordPath)
|
@if (Model.IsMicrosoftWordPath)
|
||||||
{
|
{
|
||||||
<h1 id="onboarding-title">@(structureReadyToImport ? "Import scenes and Word markers" : currentJourneyOrder >= 6 ? "Scan Manuscript" : "Connect your Word Companion")</h1>
|
<h1 id="onboarding-title">@(structureReadyToImport ? "Detect Scenes" : currentJourneyOrder >= 6 ? "Scan Manuscript" : "Connect your Word Companion")</h1>
|
||||||
<p>@(structureReadyToImport ? "Keep Word and this browser page open while PlotDirector detects scene boundaries, creates the manuscript structure, inserts bookmark IDs, and saves the document." : "Open your manuscript in Word and start the Companion. When it is connected, use the wizard button below to scan and continue.")</p>
|
<p>@(structureReadyToImport ? "PlotDirector will now analyse the approved chapters to determine where each scene begins and ends. Keep Word open while scene markers are added and the document is saved." : "Open your manuscript in Word and start the Companion. When it is connected, use the wizard button below to scan and continue.")</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -293,10 +293,6 @@
|
|||||||
<span>Chapters</span>
|
<span>Chapters</span>
|
||||||
<strong data-onboarding-scan-chapters>@Model.ScanState.ChapterCount</strong>
|
<strong data-onboarding-scan-chapters>@Model.ScanState.ChapterCount</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span>Scenes</span>
|
|
||||||
<strong data-onboarding-scan-scenes>@Model.ScanState.SceneCount</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span>Characters</span>
|
<span>Characters</span>
|
||||||
<strong data-onboarding-scan-characters>@Model.ScanState.CharacterCandidateCount</strong>
|
<strong data-onboarding-scan-characters>@Model.ScanState.CharacterCandidateCount</strong>
|
||||||
@ -310,8 +306,8 @@
|
|||||||
{
|
{
|
||||||
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
||||||
<div>
|
<div>
|
||||||
<p class="onboarding-scan-next">Review saved. PlotDirector can now import the approved manuscript structure.</p>
|
<p class="onboarding-scan-next">Chapter review saved. PlotDirector can now detect the scene structure.</p>
|
||||||
<p class="onboarding-build-message" data-onboarding-build-message>Next: detect scene boundaries, create PlotDirector scenes, then update and save Word markers.</p>
|
<p class="onboarding-build-message" data-onboarding-build-message>Next: analyse the approved chapters and create the detected scenes.</p>
|
||||||
</div>
|
</div>
|
||||||
<strong data-onboarding-build-percent></strong>
|
<strong data-onboarding-build-percent></strong>
|
||||||
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
||||||
@ -340,7 +336,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
data-onboarding-build-start
|
data-onboarding-build-start
|
||||||
data-preview-id="@Model.ScanState.PreviewID">
|
data-preview-id="@Model.ScanState.PreviewID">
|
||||||
Import manuscript
|
Detect Scenes
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
else if (Model.IsMicrosoftWordPath && Model.ScanState.IsComplete)
|
else if (Model.IsMicrosoftWordPath && Model.ScanState.IsComplete)
|
||||||
@ -461,13 +457,13 @@
|
|||||||
OnboardingSteps.WritingSoftware => 3,
|
OnboardingSteps.WritingSoftware => 3,
|
||||||
OnboardingSteps.Project => 4,
|
OnboardingSteps.Project => 4,
|
||||||
OnboardingSteps.Book => 5,
|
OnboardingSteps.Book => 5,
|
||||||
OnboardingSteps.NextPathPreview when !model.IsMicrosoftWordPath => 11,
|
OnboardingSteps.NextPathPreview when !model.IsMicrosoftWordPath => 10,
|
||||||
OnboardingSteps.NextPathPreview when string.Equals(model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal) => 8,
|
OnboardingSteps.NextPathPreview when string.Equals(model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal) => 8,
|
||||||
OnboardingSteps.NextPathPreview when model.ScanState.IsComplete => 7,
|
OnboardingSteps.NextPathPreview when model.ScanState.IsComplete => 7,
|
||||||
OnboardingSteps.NextPathPreview when model.ScanState.IsRunning || model.ScanState.IsFailed || model.ScanState.PercentComplete.HasValue => 6,
|
OnboardingSteps.NextPathPreview when model.ScanState.IsRunning || model.ScanState.IsFailed || model.ScanState.PercentComplete.HasValue => 6,
|
||||||
OnboardingSteps.NextPathPreview when model.CompanionPresence.IsConnected => 6,
|
OnboardingSteps.NextPathPreview when model.CompanionPresence.IsConnected => 6,
|
||||||
OnboardingSteps.NextPathPreview => 6,
|
OnboardingSteps.NextPathPreview => 6,
|
||||||
OnboardingSteps.Complete => 12,
|
OnboardingSteps.Complete => 10,
|
||||||
_ => 1
|
_ => 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
var characterPage = Math.Clamp(requestedCharacterPage, 1, characterReviewPageCount);
|
var characterPage = Math.Clamp(requestedCharacterPage, 1, characterReviewPageCount);
|
||||||
var currentCharacters = primaryCharacters.Skip((characterPage - 1) * characterPageSize).Take(characterPageSize).ToList();
|
var currentCharacters = primaryCharacters.Skip((characterPage - 1) * characterPageSize).Take(characterPageSize).ToList();
|
||||||
var currentStep = isCharacterStep ? 9 : 7;
|
var currentStep = isCharacterStep ? 9 : 7;
|
||||||
const int totalSteps = 12;
|
const int totalSteps = 10;
|
||||||
var characterPageTitle = $"Review Characters {characterPage} of {primaryCharacterPageCount}";
|
var characterPageTitle = $"Review Characters {characterPage} of {primaryCharacterPageCount}";
|
||||||
var duplicateCanonicalNames = primaryCharacters
|
var duplicateCanonicalNames = primaryCharacters
|
||||||
.Where(candidate => candidate.Include && string.IsNullOrWhiteSpace(candidate.AliasTargetTemporaryCharacterKey))
|
.Where(candidate => candidate.Include && string.IsNullOrWhiteSpace(candidate.AliasTargetTemporaryCharacterKey))
|
||||||
@ -67,7 +67,7 @@
|
|||||||
<div class="onboarding-copy">
|
<div class="onboarding-copy">
|
||||||
<p class="eyebrow">Step @currentStep of @totalSteps</p>
|
<p class="eyebrow">Step @currentStep of @totalSteps</p>
|
||||||
<h1 id="scan-review-title">@(isCharacterStep ? "Review Characters" : "Review Chapters")</h1>
|
<h1 id="scan-review-title">@(isCharacterStep ? "Review Characters" : "Review Chapters")</h1>
|
||||||
<p>@(isCharacterStep ? "Review this page of detected names and aliases, then continue through the character section." : "Confirm the chapter structure before PlotDirector detects scenes with AI and updates Word markers.")</p>
|
<p>@(isCharacterStep ? "Review this page of detected names and aliases, then continue through the character section." : "Confirm which chapters should be imported before PlotDirector analyses their scene structure.")</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (TempData["OnboardingReviewMessage"] is string reviewMessage)
|
@if (TempData["OnboardingReviewMessage"] is string reviewMessage)
|
||||||
@ -87,10 +87,6 @@
|
|||||||
<span>Chapters</span>
|
<span>Chapters</span>
|
||||||
<strong>@Model.SelectedChapterCount / @Model.ChapterCount</strong>
|
<strong>@Model.SelectedChapterCount / @Model.ChapterCount</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span>Scenes</span>
|
|
||||||
<strong>@Model.SelectedSceneCount / @Model.SceneCount</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span>Characters</span>
|
<span>Characters</span>
|
||||||
<strong>@Model.SelectedCharacterCount / @visibleCharacterCount</strong>
|
<strong>@Model.SelectedCharacterCount / @visibleCharacterCount</strong>
|
||||||
@ -106,8 +102,6 @@
|
|||||||
<div class="onboarding-review-tools" data-review-tools>
|
<div class="onboarding-review-tools" data-review-tools>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="true">Include all chapters</button>
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="true">Include all chapters</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="false">Exclude all chapters</button>
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="false">Exclude all chapters</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="scene" data-bulk-state="true">Include all scenes</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="scene" data-bulk-state="false">Exclude all scenes</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="reset">Restore scan defaults</button>
|
<button class="btn btn-sm btn-outline-secondary" type="reset">Restore scan defaults</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -124,7 +118,6 @@
|
|||||||
<div class="onboarding-review-chapters">
|
<div class="onboarding-review-chapters">
|
||||||
@{
|
@{
|
||||||
var chapterIndex = 0;
|
var chapterIndex = 0;
|
||||||
var sceneIndex = 0;
|
|
||||||
}
|
}
|
||||||
@foreach (var chapter in Model.Chapters)
|
@foreach (var chapter in Model.Chapters)
|
||||||
{
|
{
|
||||||
@ -132,7 +125,7 @@
|
|||||||
<summary>
|
<summary>
|
||||||
<span>Chapter @chapter.ChapterNumber</span>
|
<span>Chapter @chapter.ChapterNumber</span>
|
||||||
<strong>@chapter.ReviewTitle</strong>
|
<strong>@chapter.ReviewTitle</strong>
|
||||||
<small>@chapter.WordCount.ToString("N0") words / @chapter.Scenes.Count scene@(chapter.Scenes.Count == 1 ? string.Empty : "s")</small>
|
<small>@chapter.WordCount.ToString("N0") words</small>
|
||||||
</summary>
|
</summary>
|
||||||
<input type="hidden" name="Chapters[@chapterIndex].TemporaryChapterKey" value="@chapter.TemporaryChapterKey" />
|
<input type="hidden" name="Chapters[@chapterIndex].TemporaryChapterKey" value="@chapter.TemporaryChapterKey" />
|
||||||
<input type="hidden" name="Chapters[@chapterIndex].ChapterNumber" value="@chapter.ChapterNumber" />
|
<input type="hidden" name="Chapters[@chapterIndex].ChapterNumber" value="@chapter.ChapterNumber" />
|
||||||
@ -145,55 +138,11 @@
|
|||||||
Chapter title
|
Chapter title
|
||||||
<input class="form-control" name="Chapters[@chapterIndex].Title" value="@chapter.ReviewTitle" />
|
<input class="form-control" name="Chapters[@chapterIndex].Title" value="@chapter.ReviewTitle" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if (chapter.Scenes.Any())
|
|
||||||
{
|
|
||||||
<ol>
|
|
||||||
@foreach (var scene in chapter.Scenes)
|
|
||||||
{
|
|
||||||
<li>
|
|
||||||
<input type="hidden" name="Scenes[@sceneIndex].TemporarySceneKey" value="@scene.TemporarySceneKey" />
|
|
||||||
<input type="hidden" name="Scenes[@sceneIndex].TemporaryChapterKey" value="@scene.TemporaryChapterKey" />
|
|
||||||
<input type="hidden" name="Scenes[@sceneIndex].SceneNumberWithinChapter" value="@scene.SceneNumberWithinChapter" />
|
|
||||||
<label class="onboarding-review-toggle">
|
|
||||||
<input type="checkbox" name="Scenes[@sceneIndex].Include" value="true" checked="@scene.Include" data-review-kind="scene" />
|
|
||||||
<input type="hidden" name="Scenes[@sceneIndex].Include" value="false" />
|
|
||||||
Include scene
|
|
||||||
</label>
|
|
||||||
<label class="onboarding-review-field">
|
|
||||||
Scene title
|
|
||||||
<input class="form-control" name="Scenes[@sceneIndex].Title" value="@scene.ReviewTitle" placeholder="Scene @scene.SceneNumberWithinChapter" />
|
|
||||||
</label>
|
|
||||||
<span>@scene.WordCount.ToString("N0") words</span>
|
|
||||||
@if (scene.WordCount == 0)
|
|
||||||
{
|
|
||||||
<label class="onboarding-review-toggle">
|
|
||||||
<input type="checkbox" name="Scenes[@sceneIndex].AllowZeroWords" value="true" />
|
|
||||||
<input type="hidden" name="Scenes[@sceneIndex].AllowZeroWords" value="false" />
|
|
||||||
Allow empty scene
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrWhiteSpace(scene.OpeningTextPreview))
|
|
||||||
{
|
|
||||||
<p>@scene.OpeningTextPreview</p>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
sceneIndex++;
|
|
||||||
}
|
|
||||||
</ol>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>No scene breaks were detected. PlotDirector will treat this chapter as one scene for now.</p>
|
|
||||||
}
|
|
||||||
</details>
|
</details>
|
||||||
chapterIndex++;
|
chapterIndex++;
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="onboarding-actions onboarding-actions--inline">
|
|
||||||
<button class="btn btn-primary" type="submit" name="intent" value="structure">Save chapter review</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="onboarding-review-section" data-scan-review-panel="characters" hidden="@(!isCharacterStep)">
|
<aside class="onboarding-review-section" data-scan-review-panel="characters" hidden="@(!isCharacterStep)">
|
||||||
@ -364,7 +313,7 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a class="btn btn-outline-secondary" asp-controller="Onboarding" asp-action="Index">Back to setup</a>
|
<a class="btn btn-outline-secondary" asp-controller="Onboarding" asp-action="Index">Back to setup</a>
|
||||||
<button class="btn btn-primary" type="submit" name="intent" value="structure">Save chapter review</button>
|
<button class="btn btn-primary" type="submit" name="intent" value="structure">Continue to Scene Detection</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -36,11 +36,9 @@
|
|||||||
new("Book", 5),
|
new("Book", 5),
|
||||||
new("Connect Word", 6),
|
new("Connect Word", 6),
|
||||||
new("Review Chapters", 7),
|
new("Review Chapters", 7),
|
||||||
new("Import Scenes", 8),
|
new("Detect Scenes", 8),
|
||||||
new("Review Characters", 9),
|
new("Review Characters", 9),
|
||||||
new("Story Summary", 10),
|
new("Complete", 10)
|
||||||
new("Story Context", 11),
|
|
||||||
new("Complete", 12)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private static string StepClass(int order, int currentStep)
|
private static string StepClass(int order, int currentStep)
|
||||||
|
|||||||
@ -226,7 +226,7 @@
|
|||||||
node.classList.add("is-running");
|
node.classList.add("is-running");
|
||||||
});
|
});
|
||||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||||
node.textContent = "Preparing the approved chapters...";
|
node.textContent = "Preparing approved chapters for scene detection...";
|
||||||
});
|
});
|
||||||
document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => {
|
document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => {
|
||||||
node.textContent = "0%";
|
node.textContent = "0%";
|
||||||
@ -238,7 +238,7 @@
|
|||||||
const result = await connection.invoke("StartOnboardingProjectBuild", previewId);
|
const result = await connection.invoke("StartOnboardingProjectBuild", previewId);
|
||||||
pendingBuildPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
pendingBuildPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
||||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||||
node.textContent = "Updating and saving Word markers. Keep Word and this browser page open.";
|
node.textContent = "Updating Word document with scene markers. Keep Word open while the document is saved.";
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user