diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 38c7831..898c8d2 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -52,6 +52,7 @@ var tests = new (string Name, Action Test)[] ("Illustration demand archetypes stay broad and use active template", IllustrationDemandArchetypesStayBroadAndUseActiveTemplate), ("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage), ("Illustration assignment repair enforces explicit evidence groups and strict subtypes", IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes), + ("Phase 21O preserves adult defaults and real-mode continuity", Phase21OPreservesAdultDefaultsAndRealModeContinuity), ("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions), ("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel), ("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics), @@ -581,7 +582,7 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS Assert(femaleInstructor.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"female instructor should be Feminine, got {femaleInstructor.Presentation}."); Assert(maleExaminers.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"male examiners should be Masculine, got {maleExaminers.Presentation}."); Assert(lad.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"lad should be Masculine, got {lad.Presentation}."); - Assert(lad.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"lad should infer a young age band, got {lad.AgeBand}."); + Assert(lad.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown, $"lad should not default to Child without explicit child-age evidence, got {lad.AgeBand}."); Assert(girl.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"girl should be Feminine, got {girl.Presentation}."); Assert(girl.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"schoolgirl should infer Child, got {girl.AgeBand}."); Assert(kevin.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"Kevin should not inherit feminine presentation from his instructor, got {kevin.Presentation}."); @@ -597,7 +598,7 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Bathroom", "LaundryUtilityRoom"), "Bathroom must not match laundry room."); Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Road", "CarPark"), "Road must not match car park."); Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("WaitingRoom", "FlatInterior"), "Waiting room must not match flat interior."); - Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Ford Fiesta") == "Car", "Ford Fiesta should classify as Car."); + Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Ford Fiesta") == "Hatchback", "Ford Fiesta should classify as Hatchback."); Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Car") == "Car", "Car should classify as Car."); Assert(StoryIntelligenceIllustrationCompatibility.AssetType("supercharger") == "CarPart", "Supercharger should not classify as full car."); Assert(StoryIntelligenceIllustrationCompatibility.AssetType("biscuit tin") == "Tin", "Biscuit tin should not classify as Photograph."); @@ -612,6 +613,28 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS Assert(Rejected(beth, adult).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Existing adult assignment must be discarded when Beth evidence says YoungTeen."); } +static void Phase21OPreservesAdultDefaultsAndRealModeContinuity() +{ + var unknownNamed = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Sarah", ["Sarah watches."], []); + var explicitGirl = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Mia", ["schoolgirl"], []); + var teacher = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Mrs Patterson", ["Mrs Patterson is a teacher."], []); + var examiner = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("driving examiner", ["female driving examiner"], []); + + Assert(unknownNamed.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown, $"Unknown age should remain Unknown before illustration fallback, got {unknownNamed.AgeBand}."); + Assert(explicitGirl.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"Schoolgirl should remain explicit child evidence, got {explicitGirl.AgeBand}."); + Assert(teacher.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Teacher title/role should infer Adult, got {teacher.AgeBand}."); + Assert(examiner.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Female examiner should infer Feminine, got {examiner.Presentation}."); + Assert(examiner.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Driving examiner should infer Adult, got {examiner.AgeBand}."); + Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Bristol Road") == "Road", "Road names should classify as Road."); + Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Cock Hill Lane") == "Road", "Lane names should classify as Road."); + Assert(!StoryIntelligenceIllustrationCompatibility.AssetCompatible("Hatchback", "Car"), "Hatchbacks must not borrow generic car artwork."); + Assert(!StoryIntelligenceIllustrationCompatibility.AssetCompatible("SportsCar", "Hatchback"), "Vehicle subtypes should remain strict."); + + var view = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PlotLine", "Views", "Development", "StoryIntelligenceExperience.cshtml")); + Assert(view.Contains("Return to Import", StringComparison.Ordinal), "Real visualisation should expose a return-to-import link."); + Assert(view.Contains("Model.Mode == \"simulation\"", StringComparison.Ordinal), "Prototype controls should be gated to simulation mode."); +} + static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false) => new( ageBand, diff --git a/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs b/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs index 6bf791a..9d51d05 100644 --- a/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs +++ b/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs @@ -77,9 +77,13 @@ public static class StoryIntelligenceIllustrationCompatibility presentation = guardedPresentation.Value; presentationConfidence = guardedPresentation.Confidence; var explicitAge = ExplicitAgeBand(scopedText); + if (explicitAge.Value == AgeBands.Unknown) + { + explicitAge = EntityIdentityAgeBand(name, identityText); + } var relationshipAge = RelationshipAgeBand(name, scopedText); var namedAge = NameAgeBand(name); - var age = FirstKnown(relationshipAge.Value, explicitAge.Value, namedAge.Value, AgeBands.Unknown); + var age = FirstKnown(explicitAge.Value, relationshipAge.Value, namedAge.Value, AgeBands.Unknown); var ageConfidence = new[] { explicitAge.Confidence, relationshipAge.Confidence, namedAge.Confidence }.Max(); var attributeText = EntityAttributeText(entityText, name); var hairColour = HairColour(attributeText); @@ -171,7 +175,9 @@ public static class StoryIntelligenceIllustrationCompatibility rejected.Add($"presentation hard mismatch: wanted {evidence.Presentation}, candidate {candidate.Presentation}"); } - if (ExplicitKnown(evidence.HairColour, evidence.AgeConfidence) && KnownMismatch(evidence.HairColour, candidate.HairColour)) + if (!string.Equals(evidence.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase) + && !string.Equals(candidate.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase) + && KnownMismatch(evidence.HairColour, candidate.HairColour)) { rejected.Add($"hair colour hard mismatch: wanted {evidence.HairColour}, candidate {candidate.HairColour}"); } @@ -189,7 +195,7 @@ public static class StoryIntelligenceIllustrationCompatibility var text = (value ?? string.Empty).Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase).ToLowerInvariant(); if (text.Contains("youngteen") || text.Contains("13") || text.Contains("14") || text.Contains("15") || text.Contains("fifteen")) return AgeBands.YoungTeen; if (text.Contains("olderteen") || text.Contains("teen") || text.Contains("16") || text.Contains("17") || text.Contains("18") || text.Contains("19")) return AgeBands.OlderTeen; - if (text.Contains("child") || text.Contains("boy") || text.Contains("girl")) return AgeBands.Child; + if (text.Contains("child")) return AgeBands.Child; if (text.Contains("youngadult")) return AgeBands.YoungAdult; if (text.Contains("middleaged") || text.Contains("matureadult") || text.Contains("parent") || text.Contains("mother") || text.Contains("father") || text.Contains("aunt") || text.Contains("uncle")) return AgeBands.MatureAdult; if (text.Contains("adult")) return AgeBands.Adult; @@ -251,7 +257,7 @@ public static class StoryIntelligenceIllustrationCompatibility if (ContainsAny(text, "office", "desk", "study", "library")) return "Office"; if (text.Contains("school", StringComparison.OrdinalIgnoreCase)) return "School"; if (text.Contains("waiting room", StringComparison.OrdinalIgnoreCase)) return "WaitingRoom"; - if (ContainsAny(text, "road", "street", "lane", "drive", "motorway", "bridge")) return "Road"; + if (ContainsAny(text, "road", "street", "lane", "drive", "close", "avenue", "boulevard", "motorway", "bridge", "roundabout", "footpath", "track")) return "Road"; if (ContainsAny(text, "car park", "parking")) return "CarPark"; if (text.Contains("shop", StringComparison.OrdinalIgnoreCase)) return "Shop"; if (text.Contains("hospital", StringComparison.OrdinalIgnoreCase)) return "Hospital"; @@ -272,7 +278,11 @@ public static class StoryIntelligenceIllustrationCompatibility if (ContainsAny(text, "driving licence", "driver licence", "provisional licence", "license", "licence", "pass certificate", "certificate", "paperwork")) return "Document"; if (ContainsAny(text, "ring", "necklace", "jewellery", "jewelry")) return "Jewellery"; if (ContainsAny(text, "gun", "pistol", "rifle", "weapon")) return "Weapon"; - if (ContainsAny(padded, " ford fiesta ", " fiesta ", " motor car ", " car ", " vehicle ")) return "Car"; + if (ContainsAny(padded, " hatchback ", " vw golf ", " volkswagen golf ", " golf gti ", " ford fiesta ", " fiesta ")) return "Hatchback"; + if (ContainsAny(padded, " family car ", " estate car ", " saloon ")) return "FamilyCar"; + if (ContainsAny(padded, " sports car ", " tr6 ", " roadster ")) return "SportsCar"; + if (ContainsAny(padded, " van ", " transit ")) return "Van"; + if (ContainsAny(padded, " motor car ", " car ", " vehicle ")) return "Car"; if (ContainsAny(text, "supercharger", "engine part")) return "CarPart"; if (ContainsAny(text, "biscuit tin", "tin")) return "Tin"; if (ContainsAny(text, "parcel", "package")) return "Parcel"; @@ -326,22 +336,40 @@ public static class StoryIntelligenceIllustrationCompatibility if (ContainsAny(text, "13", "thirteen", "14", "fourteen")) return new(AgeBands.YoungTeen, 0.98m); if (ContainsAny(text, "16", "sixteen", "17", "seventeen", "18", "eighteen", "19", "nineteen")) return new(AgeBands.OlderTeen, 0.98m); if (ContainsAny(text, "toddler", "baby", "infant", "newborn")) return new(AgeBands.Child, 0.98m); - if (ContainsAny(text, "child", "young girl", "young boy", "schoolboy", "schoolgirl", " boy ", " girl ", " lad ")) return new(AgeBands.Child, 0.86m); + if (ContainsAny(text, "child", "young child", "schoolboy", "schoolgirl", "ten-year-old", "ten year old", "aged 10", "aged 11", "aged 12")) return new(AgeBands.Child, 0.9m); if (ContainsAny(text, "teenager", "teenage")) return new(AgeBands.OlderTeen, 0.86m); if (ContainsAny(text, "young adult", "young woman", "young man")) return new(AgeBands.YoungAdult, 0.82m); if (ContainsAny(text, "middle-aged", "middle aged", "mature adult")) return new(AgeBands.MatureAdult, 0.86m); + if (ContainsAny(text, " mr ", " mr. ", " mrs ", " mrs. ", " miss ", " miss. ", " ms ", " ms. ", " doctor", " dr ", " dr. ", " professor", " driving examiner", " teacher", " detective", " police officer", " nurse", " receptionist", " solicitor", " landlord", " manager")) return new(AgeBands.Adult, 0.88m); if (ContainsAny(text, "elderly", "senior", "old woman", "old man")) return new(AgeBands.Senior, 0.9m); return EvidenceSignal.Unknown; } + private static EvidenceSignal EntityIdentityAgeBand(string name, string identityText) + { + var cleanName = (name ?? string.Empty).Trim().ToLowerInvariant(); + if (ContainsAny(cleanName, "mother", " mum", "father", " dad", "parent", "aunt", "uncle", "grandmother", "grandfather")) + { + return EvidenceSignal.Unknown; + } + + return ExplicitAgeBand(identityText); + } + private static EvidenceSignal RelationshipAgeBand(string name, string text) { if (ContainsAny(text, "toddler", "baby", "newborn")) return new(AgeBands.Child, 0.96m); var identity = $"{name} {text}"; - if (ContainsAny(identity, "mother", " mum", "father", " dad", "parent", "aunt", "uncle", "teacher") + if (ContainsAny(identity, "mother", " mum", "father", " dad", "parent", "aunt", "uncle") && !PossessiveOfOtherIdentity(name, text)) { - return new(AgeBands.MatureAdult, 0.74m); + return new(AgeBands.MatureAdult, 0.82m); + } + + if (ContainsAny(identity, "teacher", "driving examiner", "detective", "police officer", "nurse", "receptionist", "solicitor", "landlord", "manager", "professor", "doctor") + && !PossessiveOfOtherIdentity(name, text)) + { + return new(AgeBands.Adult, 0.82m); } if (ContainsAny(text, "grandmother", "grandfather")) return new(AgeBands.Senior, 0.82m); @@ -351,8 +379,7 @@ public static class StoryIntelligenceIllustrationCompatibility private static EvidenceSignal NameAgeBand(string name) { - var clean = name.Trim().ToLowerInvariant(); - return clean is "beth" ? new(AgeBands.YoungTeen, 0.86m) : EvidenceSignal.Unknown; + return EvidenceSignal.Unknown; } private static EvidenceSignal TitlePresentation(string text) @@ -378,8 +405,8 @@ public static class StoryIntelligenceIllustrationCompatibility private static EvidenceSignal NamePresentation(string text) { - if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " annie ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m); - if (ContainsAny(text, " graham ", " george ", " john ", " david ", " simon ", " kevin ", " colin ")) return new(Presentations.Masculine, 0.82m); + if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " rebecca ", " annie ", " mary ", " susan ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m); + if (ContainsAny(text, " adam ", " colin ", " david ", " kevin ", " simon ", " graham ", " gareth ", " john ", " michael ", " peter ", " george ")) return new(Presentations.Masculine, 0.82m); return EvidenceSignal.Unknown; } @@ -415,13 +442,13 @@ public static class StoryIntelligenceIllustrationCompatibility } if (StartsWithAny(clean, "mr", "father", "dad", "uncle", "brother", "son", "boy", "man", "male") - || clean is "graham" or "george" or "john" or "david" or "simon" or "kevin" or "colin") + || clean is "adam" or "colin" or "david" or "kevin" or "simon" or "graham" or "gareth" or "john" or "michael" or "peter" or "george") { return new(Presentations.Masculine, 0.98m); } if (StartsWithAny(clean, "mrs", "miss", "ms", "mother", "mum", "mom", "aunt", "sister", "daughter", "girl", "woman", "female") - || clean is "beth" or "maggie" or "rosie" or "grace" or "annie" or "helen" or "elen") + || clean is "beth" or "maggie" or "grace" or "rosie" or "rebecca" or "annie" or "mary" or "susan" or "helen" or "elen") { return new(Presentations.Feminine, 0.98m); } diff --git a/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs b/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs index fe584ba..2ac344f 100644 --- a/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs +++ b/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs @@ -443,6 +443,14 @@ public sealed class StoryIntelligenceIllustrationMatchingService( candidate.Metadata.Profile, isNamedCharacter: !IsUnresolvedName(observation.Name), alreadyAssignedToAnotherSignificantCharacter: alreadyAssigned && observation.IsSignificant)); + if (observation.Evidence.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown + && candidate.Metadata.AgeBand is StoryIntelligenceIllustrationCompatibility.AgeBands.Child + or StoryIntelligenceIllustrationCompatibility.AgeBands.YoungTeen + or StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen) + { + rejected.Add("unknown age should use an adult-compatible illustration until explicit child or teen evidence appears"); + } + AddScore(observation.Evidence.AgeBand, candidate.Metadata.AgeBand, 25, "age band"); AddScore(observation.Evidence.Presentation, candidate.Metadata.Presentation, observation.Evidence.HasStrongPresentationEvidence ? 42 : 25, "presentation"); AddScore(observation.Evidence.HairColour, candidate.Metadata.HairColour, 20, "hair colour"); diff --git a/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs b/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs index 63b465e..3ba112a 100644 --- a/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs +++ b/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs @@ -130,6 +130,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( { Mode = "real", RunId = run.StoryIntelligenceRunID, + BookId = run.BookID, RunStatus = run.Status, GeneratedUtc = DateTime.UtcNow, ChangeToken = ChangeToken(run, results), @@ -208,11 +209,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( ? "All known chapter runs completed successfully" : "Weighted chapter-run progress"; var overallProgressPercent = authoritativeImportProgress; - var overallElapsed = SessionElapsed(runSnapshots); - var overallRemaining = isSuccessfullyComplete ? "Ready to review" : isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes); + var overallElapsedMs = SessionElapsedMilliseconds(runSnapshots, isTerminal); + var overallElapsed = FormatDuration(overallElapsedMs); + var overallRemaining = isSuccessfullyComplete ? "Ready to review" : isTerminal ? "Ready to review" : EstimateSessionRemaining(overallElapsedMs, parsedScenes, totalScenes); var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run); var scenes = new List(); - var nextSceneNumber = 1; var storyMemory = new StoryMemory(); var allCompleted = runSnapshots.SelectMany(snapshot => snapshot.Completed).ToList(); var visibleSceneIds = VisibleSceneResultIds(allCompleted, current, SceneWindowPrevious, SceneWindowFollowing); @@ -223,7 +224,6 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( snapshot.Parsed, snapshot.Completed, current, - continuousSceneNumberStart: nextSceneNumber, overallCompletedScenes: parsedScenes, overallTotalScenes: totalScenes, overallChaptersAnalysed: completedChapterRuns, @@ -232,13 +232,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( overallStage: overallStage, overallElapsed: overallElapsed, overallRemaining: overallRemaining, - continuousSceneNumbering: true, overallTerminal: isTerminal, storyMemory: storyMemory, includedSceneResultIds: visibleSceneIds) .ToList(); scenes.AddRange(runScenes); - nextSceneNumber += snapshot.Completed.Count; } if (scenes.Count == 0) @@ -251,6 +249,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( Mode = "real", ImportSessionId = session.StoryIntelligenceBookPipelineID, RunId = activeSnapshot.Run.StoryIntelligenceRunID, + BookId = session.BookID, RunStatus = SessionStatus(session, runSnapshots), GeneratedUtc = DateTime.UtcNow, ChangeToken = SessionChangeToken(session, runSnapshots), @@ -837,19 +836,18 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( }; } - private static string EstimateSessionRemaining(IReadOnlyList snapshots, int completedScenes, int totalScenes) + private static string EstimateSessionRemaining(long elapsedMs, int completedScenes, int totalScenes) { if (totalScenes <= 0 || completedScenes <= 0) { - return snapshots.All(snapshot => IsTerminal(snapshot.Run.Status)) ? "Ready to review" : "Calculating"; + return "Calculating"; } - if (completedScenes >= totalScenes && snapshots.All(snapshot => IsTerminal(snapshot.Run.Status))) + if (completedScenes >= totalScenes) { return "Ready to review"; } - var elapsedMs = snapshots.Sum(snapshot => snapshot.Run.TotalDurationMs ?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - snapshot.Run.StartedUtc).TotalMilliseconds))); var averageMs = elapsedMs / Math.Max(1, completedScenes); return FormatDuration(averageMs * Math.Max(0, totalScenes - completedScenes)); } @@ -1798,11 +1796,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( return FormatDuration(elapsedMs); } - private static string SessionElapsed(IReadOnlyList snapshots) + private static long SessionElapsedMilliseconds(IReadOnlyList snapshots, bool isTerminal) { - var elapsedMs = snapshots.Sum(snapshot => snapshot.Run.TotalDurationMs - ?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - snapshot.Run.StartedUtc).TotalMilliseconds))); - return FormatDuration(elapsedMs); + if (snapshots.Count == 0) + { + return 0; + } + + var started = snapshots.Min(snapshot => snapshot.Run.StartedUtc); + var ended = isTerminal + ? snapshots + .Select(snapshot => snapshot.Run.CompletedUtc ?? snapshot.Run.UpdatedUtc) + .DefaultIfEmpty(DateTime.UtcNow) + .Max() + : DateTime.UtcNow; + return Math.Max(0, Convert.ToInt64((ended - started).TotalMilliseconds)); } private static string FormatDuration(long milliseconds) diff --git a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs index ad2cf49..9fb9ff8 100644 --- a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs +++ b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs @@ -5,6 +5,7 @@ public sealed class StoryIntelligenceExperiencePrototypeViewModel public string Mode { get; init; } = "simulation"; public int? RunId { get; init; } public int? ImportSessionId { get; init; } + public int? BookId { get; init; } public string? RunStatus { get; init; } public string? ChangeToken { get; init; } public DateTime GeneratedUtc { get; init; } = DateTime.UtcNow; diff --git a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml index a4a27d5..cab9b0c 100644 --- a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml +++ b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml @@ -24,6 +24,7 @@ Mode = Model.Mode, RunId = Model.RunId, ImportSessionId = Model.ImportSessionId, + BookId = Model.BookId, RunStatus = Model.RunStatus, GeneratedUtc = Model.GeneratedUtc, IsTerminal = false, @@ -50,6 +51,7 @@ data-image-diagnostics="true" data-mode="@Model.Mode" data-run-id="@Model.RunId" + data-book-id="@Model.BookId" data-import-session-id="@Model.ImportSessionId" data-snapshot-url="@(Model.ImportSessionId.HasValue ? $"/api/story-intelligence/import-sessions/{Model.ImportSessionId.Value}/visualisation-snapshot" : Model.RunId.HasValue ? $"/api/story-intelligence/runs/{Model.RunId.Value}/visualisation-snapshot" : string.Empty)" data-change-token="@(Model.Mode == "real" ? string.Empty : Model.ChangeToken)" @@ -66,7 +68,14 @@ Preparing Scene 1 - Simulation + @if (Model.Mode == "real" && Model.BookId.HasValue) + { + Return to Import + } + else + { + Simulation + } @if (Model.AvailableRuns.Count > 0) @@ -181,20 +190,23 @@ - + @if (Model.Mode == "simulation") + { + + }
Diagnostics diff --git a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css index 4bf20aa..369f963 100644 --- a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css +++ b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css @@ -288,10 +288,10 @@ input:focus-visible { .story-exp-shell { display: grid; - grid-template-columns: minmax(206px, 0.54fr) minmax(760px, 2.44fr) minmax(214px, 0.56fr); - gap: 16px; + grid-template-columns: minmax(180px, 0.46fr) minmax(820px, 2.72fr) minmax(188px, 0.48fr); + gap: 12px; min-height: 0; - padding: 14px 22px 74px; + padding: 12px 18px 58px; } .story-exp-left, @@ -565,7 +565,7 @@ input:focus-visible { .story-exp-connections { position: absolute; inset: 0; - z-index: 1; + z-index: 60; width: 100%; height: 100%; pointer-events: none; @@ -641,7 +641,7 @@ input:focus-visible { .story-exp-zone { position: absolute; inset: 0; - z-index: 2; + z-index: 20; min-width: 0; min-height: 0; pointer-events: none; @@ -683,13 +683,13 @@ input:focus-visible { height: var(--node-size, 150px); transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1)); opacity: var(--opacity, 1); - z-index: var(--z, 3); + z-index: calc(var(--z, 3) + 30); pointer-events: auto; transition: transform 1050ms cubic-bezier(.16, .84, .28, 1), opacity 780ms ease; } .story-character.is-pov { - z-index: 7; + z-index: 48; } .story-character.is-context .story-character__image { @@ -699,6 +699,8 @@ input:focus-visible { .story-character__image, .story-location__image, .story-asset__image { + position: relative; + z-index: 1; display: block; overflow: hidden; border: 1px solid rgba(169, 205, 240, 0.36); @@ -727,6 +729,7 @@ input:focus-visible { .story-character__text { position: absolute; + z-index: 80; top: calc(var(--node-size, 150px) + 9px); left: 50%; display: grid; @@ -817,7 +820,7 @@ input:focus-visible { gap: 10px; width: var(--node-size, 190px); transform: translate(var(--x, 0), var(--y, 0)); - z-index: 4; + z-index: 42; pointer-events: auto; transition: opacity 560ms ease, transform 850ms cubic-bezier(.16, .84, .28, 1); } @@ -841,6 +844,8 @@ input:focus-visible { } .story-location__text { + position: relative; + z-index: 80; display: grid; justify-items: center; gap: 3px; @@ -887,7 +892,7 @@ input:focus-visible { width: max(var(--node-size, 78px), 104px); opacity: var(--opacity, 1); transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1)); - z-index: var(--z, 3); + z-index: calc(var(--z, 3) + 30); pointer-events: auto; transition: transform 860ms cubic-bezier(.16, .84, .28, 1), opacity 620ms ease; } @@ -901,6 +906,8 @@ input:focus-visible { } .story-asset__text { + position: relative; + z-index: 80; display: grid; justify-items: center; gap: 2px; diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index a8555a7..18bc896 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -1920,6 +1920,21 @@ let scene = null; let totalWordCount = 0; let detectedHeadingChapters = 0; + let manuscriptStarted = false; + + const isFrontMatterHeading = (value) => { + const clean = normalizeTitleForResolve(value || "", "").toLocaleLowerCase(); + return [ + "title page", + "copyright", + "dedication", + "contents", + "table of contents", + "acknowledgements", + "acknowledgments", + "about the author" + ].includes(clean); + }; const finishScene = () => { scene = null; @@ -1982,10 +1997,15 @@ return; } - documentText.push(text); const words = countWords(text); if (isBuiltInHeading(paragraph, 1)) { + if (!manuscriptStarted && isFrontMatterHeading(text)) { + return; + } + + manuscriptStarted = true; + documentText.push(text); detectedHeadingChapters += 1; startChapter(paragraph, text); totalWordCount += words; @@ -1995,9 +2015,15 @@ } if (!chapter) { + if (!manuscriptStarted) { + return; + } + startChapter(paragraph, "Opening pages", true); } + documentText.push(text); + if (isBuiltInHeading(paragraph, 2)) { startScene(paragraph, text); totalWordCount += words;