From 0ebccf9ac8625194dc639a4d63bb836fa6c5f4ff Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sat, 11 Jul 2026 20:00:06 +0000 Subject: [PATCH] Phase 21B - Story Intelligence Experience Prototype --- PlotLine/Controllers/DevelopmentController.cs | 21 + ...toryIntelligenceExperiencePrototypeData.cs | 200 +++++ ...telligenceExperiencePrototypeViewModels.cs | 79 ++ .../StoryIntelligenceExperience.cshtml | 135 ++++ ...tory-intelligence-experience-prototype.css | 721 ++++++++++++++++++ .../prototype/asset-keys.svg | 6 + .../prototype/asset-letter.svg | 6 + .../prototype/asset-notebook.svg | 6 + .../prototype/asset-phone.svg | 7 + .../prototype/asset-red-tr6.svg | 9 + .../prototype/asset-rucksack.svg | 7 + .../prototype/character-beth.svg | 9 + .../prototype/character-graham.svg | 9 + .../prototype/character-maggie.svg | 9 + .../prototype/character-rosie.svg | 9 + .../prototype/location-car-park.svg | 11 + .../prototype/location-doweries.svg | 10 + .../prototype/location-flat.svg | 10 + .../prototype/location-laundry.svg | 9 + .../prototype/location-stairwell.svg | 8 + ...story-intelligence-experience-prototype.js | 401 ++++++++++ ...Story-Intelligence-Experience-Prototype.md | 123 +++ 22 files changed, 1805 insertions(+) create mode 100644 PlotLine/Controllers/DevelopmentController.cs create mode 100644 PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs create mode 100644 PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs create mode 100644 PlotLine/Views/Development/StoryIntelligenceExperience.cshtml create mode 100644 PlotLine/wwwroot/css/story-intelligence-experience-prototype.css create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/asset-keys.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/asset-letter.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/asset-notebook.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/asset-phone.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/asset-red-tr6.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/asset-rucksack.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/character-beth.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/character-graham.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/character-maggie.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/character-rosie.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/location-car-park.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/location-doweries.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/location-flat.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/location-laundry.svg create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/location-stairwell.svg create mode 100644 PlotLine/wwwroot/js/story-intelligence-experience-prototype.js create mode 100644 docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md diff --git a/PlotLine/Controllers/DevelopmentController.cs b/PlotLine/Controllers/DevelopmentController.cs new file mode 100644 index 0000000..1a30691 --- /dev/null +++ b/PlotLine/Controllers/DevelopmentController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PlotLine.Services; + +namespace PlotLine.Controllers; + +[Authorize(Policy = "AdminOnly")] +[Route("Development")] +public sealed class DevelopmentController(IWebHostEnvironment environment) : Controller +{ + [HttpGet("StoryIntelligenceExperience")] + public IActionResult StoryIntelligenceExperience() + { + if (!environment.IsDevelopment()) + { + return NotFound(); + } + + return View(StoryIntelligenceExperiencePrototypeData.Build()); + } +} diff --git a/PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs b/PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs new file mode 100644 index 0000000..ace8afb --- /dev/null +++ b/PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs @@ -0,0 +1,200 @@ +using PlotLine.ViewModels; + +namespace PlotLine.Services; + +public static class StoryIntelligenceExperiencePrototypeData +{ + private const string AssetRoot = "/images/story-intelligence/prototype"; + + public static StoryIntelligenceExperiencePrototypeViewModel Build() + => new() + { + ProjectName = "Alpha Flame", + BookTitle = "The Doweries Manuscript", + Scenes = + [ + Scene( + "scene-01", 1, "A key turns in the rain", "Beth arrives at The Doweries and finds Maggie's letter waiting where it should not be.", + "Detecting scene structure", "Fri 18 Oct", "Beth reaches The Doweries", 8, 1, 12, "42 min", + "beth", Location("doweries", "The Doweries", "Rain-darkened estate house", "location-doweries.svg"), + Characters(("beth", "Beth", "POV character", "Searching", 100, "character-beth.svg"), ("maggie", "Maggie", "Absent presence", "Remembered", 42, "character-maggie.svg"), ("graham", "Graham", "Name in the letter", "Unseen", 26, "character-graham.svg")), + Assets(("letter", "Letter", "Unopened evidence", 88, "asset-letter.svg"), ("notebook", "Notebook", "Beth's notes", 54, "asset-notebook.svg")), + Relationships(("rel-beth-maggie", "beth", "maggie", "Family tie detected", "grief and loyalty", 72)), + Threads(("thread-maggie-secret", "Why did Maggie hide the letter?", "A secret begins around the flat and the missing key.", "mystery")), + Observations(("obs-arrival", "The story opens with withheld knowledge.", "The reader knows the letter matters before Beth understands why.", "insight")), + Discoveries("Possible character found: Maggie", "Location found: The Doweries", "Asset found: Letter"), + Feed("Chapter 1 queued", "Scene boundary detected", "Beth identified as point of view")), + Scene( + "scene-02", 2, "Maggie's flat", "Inside Maggie's flat, Beth finds signs that someone searched the room before her.", + "Reading scenes", "Fri 18 Oct", "Flat searched before Beth arrives", 17, 1, 12, "38 min", + "beth", Location("maggie-flat", "Maggie's flat", "A small flat inside The Doweries", "location-flat.svg"), + Characters(("beth", "Beth", "POV character", "Investigating", 100, "character-beth.svg"), ("maggie", "Maggie", "Absent presence", "Everywhere in the room", 64, "character-maggie.svg"), ("rosie", "Rosie", "Neighbour", "Possible witness", 38, "character-rosie.svg")), + Assets(("letter", "Letter", "Hidden in plain sight", 92, "asset-letter.svg"), ("rucksack", "Rucksack", "Recently moved", 72, "asset-rucksack.svg"), ("notebook", "Notebook", "Fresh page torn out", 58, "asset-notebook.svg")), + Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship strengthened", "protective grief", 82), ("rel-beth-rosie", "beth", "rosie", "Witness connection", "cautious trust", 45)), + Threads(("thread-maggie-secret", "Maggie's missing page", "A torn notebook page suggests Maggie prepared for Beth's arrival.", "mystery"), ("thread-reader-knows", "Reader knows the flat was searched.", "Beth notices the disturbed objects but not yet the pattern.", "secret")), + Observations(("obs-flat", "The location narrows from estate to flat.", "PlotDirector treats Maggie's flat as the active location rather than showing the parent estate.", "insight")), + Discoveries("Location refined: Maggie's flat", "Possible asset found: Rucksack", "Knowledge thread opened: missing page"), + Feed("Scene 2 analysis started", "Flat recognised as child location", "Beth-Maggie relationship evidence increased")), + Scene( + "scene-03", 3, "Rosie at the landing", "Rosie interrupts Beth and reveals Graham visited Maggie two nights before.", + "Reading scenes", "Fri 18 Oct", "Graham placed near Maggie", 26, 2, 12, "34 min", + "beth", Location("maggie-flat", "Maggie's flat", "Landing light spilling through the open door", "location-flat.svg"), + Characters(("beth", "Beth", "POV character", "Questioning", 100, "character-beth.svg"), ("rosie", "Rosie", "Witness", "Nervous but helpful", 76, "character-rosie.svg"), ("graham", "Graham", "Named suspect", "Recently present", 58, "character-graham.svg"), ("maggie", "Maggie", "Absent presence", "Connected to Graham", 44, "character-maggie.svg")), + Assets(("letter", "Letter", "Still unread", 70, "asset-letter.svg"), ("notebook", "Notebook", "Confirms missing page", 62, "asset-notebook.svg")), + Relationships(("rel-beth-rosie", "beth", "rosie", "Relationship discovered", "cautious trust", 64), ("rel-graham-maggie", "graham", "maggie", "Possible conflict detected", "recent visit", 70)), + Threads(("thread-graham", "Why did Graham visit Maggie?", "Rosie's memory puts Graham at the flat before the search.", "question"), ("thread-maggie-secret", "Maggie's missing page", "The missing page may describe Graham's visit.", "mystery")), + Observations(("obs-rosie", "A witness changes the centre of gravity.", "Rosie grows because she supplies new causal information.", "insight")), + Discoveries("Possible relationship found: Graham and Maggie", "Character relevance increased: Rosie", "Timeline clue found: two nights before"), + Feed("Rosie promoted to important scene character", "Graham inferred from dialogue", "Relationship signal found")), + Scene( + "scene-04", 4, "The red TR6", "Beth spots Graham's red TR6 outside the estate and realises he has returned.", + "Extracting assets", "Fri 18 Oct", "Red TR6 returns to The Doweries", 36, 2, 12, "30 min", + "beth", Location("doweries", "The Doweries", "Driveway under sodium light", "location-doweries.svg"), + Characters(("beth", "Beth", "POV character", "Watching", 100, "character-beth.svg"), ("graham", "Graham", "Active threat", "Returned", 82, "character-graham.svg"), ("rosie", "Rosie", "Witness", "Off to the side", 36, "character-rosie.svg")), + Assets(("red-tr6", "Red TR6", "Graham's car", 96, "asset-red-tr6.svg"), ("letter", "Letter", "Unopened evidence", 56, "asset-letter.svg"), ("keys", "Keys", "Estate access", 48, "asset-keys.svg")), + Relationships(("rel-beth-graham", "beth", "graham", "Possible conflict detected", "avoidance", 78), ("rel-graham-maggie", "graham", "maggie", "Relationship strengthened", "unexplained visit", 74)), + Threads(("thread-graham", "Graham returns before Beth can leave.", "The car turns a memory into an immediate threat.", "question")), + Observations(("obs-tr6", "An asset becomes narrative evidence.", "The car is not background colour; it proves Graham is physically present.", "insight")), + Discoveries("Asset found: Red TR6", "Relationship signal found: Beth and Graham", "Location changed: The Doweries driveway"), + Feed("Scene 4 analysed", "Vehicle classified as significant asset", "Conflict confidence increased")), + Scene( + "scene-05", 5, "Graham on the stairs", "Graham blocks Beth on the stairwell and asks whether she has opened Maggie's letter.", + "Inferring relationships", "Fri 18 Oct", "Graham confronts Beth", 47, 3, 12, "26 min", + "beth", Location("stairwell", "The Doweries stairwell", "Narrow stairwell between flat and exit", "location-stairwell.svg"), + Characters(("beth", "Beth", "POV character", "Cornered", 100, "character-beth.svg"), ("graham", "Graham", "Opposing force", "Pressing", 96, "character-graham.svg"), ("maggie", "Maggie", "Absent pressure", "The reason for the question", 46, "character-maggie.svg")), + Assets(("letter", "Letter", "Graham wants it", 96, "asset-letter.svg"), ("keys", "Keys", "Exit blocked", 55, "asset-keys.svg")), + Relationships(("rel-beth-graham", "beth", "graham", "Relationship changed", "open conflict", 95), ("rel-graham-maggie", "graham", "maggie", "Suspicion strengthened", "knows about letter", 84)), + Threads(("thread-letter", "What is in Maggie's letter?", "Graham's question proves the letter contains something he fears.", "mystery"), ("thread-reader-knows", "Reader knows Graham is afraid of the letter.", "Beth now knows the letter has leverage.", "secret")), + Observations(("obs-stairs", "The relationship graph tightens.", "The scene is now Beth versus Graham, with the letter acting as pressure point.", "insight")), + Discoveries("Relationship changed: Beth and Graham", "Knowledge thread opened: letter contents", "Asset ownership pressure detected: Letter"), + Feed("Graham promoted to primary opposing character", "Letter confidence increased", "Conflict classified")), + Scene( + "scene-06", 6, "Rosie's warning", "Rosie pulls Beth into the laundry room and warns her not to trust Graham's version of Maggie.", + "Reading scenes", "Fri 18 Oct", "Rosie contradicts Graham", 58, 3, 12, "22 min", + "rosie", Location("laundry", "Laundry room", "Low-lit utility room behind the stairwell", "location-laundry.svg"), + Characters(("rosie", "Rosie", "POV character", "Warning Beth", 100, "character-rosie.svg"), ("beth", "Beth", "Protected listener", "Reassessing", 82, "character-beth.svg"), ("graham", "Graham", "Absent threat", "His story is challenged", 58, "character-graham.svg"), ("maggie", "Maggie", "Remembered", "Misrepresented", 42, "character-maggie.svg")), + Assets(("notebook", "Notebook", "Rosie saw it", 78, "asset-notebook.svg"), ("letter", "Letter", "Still decisive", 70, "asset-letter.svg")), + Relationships(("rel-beth-rosie", "beth", "rosie", "Relationship strengthened", "protective trust", 86), ("rel-rosie-graham", "rosie", "graham", "Possible conflict detected", "fear and mistrust", 74)), + Threads(("thread-graham", "Graham's account is unreliable.", "Rosie introduces contradiction and a motive to lie.", "question"), ("thread-letter", "The letter may expose Graham.", "Both Rosie and Graham orbit the same hidden evidence.", "mystery")), + Observations(("obs-pov-shift", "The POV changes to Rosie.", "The camera follows the emotional source of new information, not just the protagonist.", "insight")), + Discoveries("POV changed: Rosie", "Relationship strengthened: Beth and Rosie", "Contradictory evidence detected"), + Feed("Scene 6 started", "POV character changed", "Knowledge thread updated")), + Scene( + "scene-07", 7, "Notebook page", "Beth reconstructs the torn notebook page and links the missing line to the red TR6.", + "Building knowledge", "Fri 18 Oct", "Notebook and car clue connect", 69, 4, 12, "18 min", + "beth", Location("maggie-flat", "Maggie's flat", "Desk, lamplight and torn paper edge", "location-flat.svg"), + Characters(("beth", "Beth", "POV character", "Connecting clues", 100, "character-beth.svg"), ("maggie", "Maggie", "Absent author", "Left a trail", 70, "character-maggie.svg"), ("graham", "Graham", "Implied target", "Named by evidence", 50, "character-graham.svg")), + Assets(("notebook", "Notebook", "Torn page reconstructed", 96, "asset-notebook.svg"), ("red-tr6", "Red TR6", "Matches hidden note", 84, "asset-red-tr6.svg"), ("letter", "Letter", "Next evidence", 72, "asset-letter.svg")), + Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship strengthened", "Maggie left Beth a trail", 90), ("rel-graham-maggie", "graham", "maggie", "Relationship changed", "direct evidence", 88)), + Threads(("thread-letter", "The letter is part of a trail.", "Maggie appears to have staged evidence for Beth to find.", "mystery"), ("thread-tr6", "The car links Graham to the missing page.", "Asset evidence connects two earlier scenes.", "secret")), + Observations(("obs-thread", "Knowledge threads begin to braid.", "PlotDirector links asset, relationship and location evidence into one story question.", "insight")), + Discoveries("Knowledge thread opened: Red TR6 link", "Relationship updated: Graham and Maggie", "Asset association found: Notebook and Red TR6"), + Feed("Scene 7 analysed", "Notebook promoted to key asset", "Cross-scene link inferred")), + Scene( + "scene-08", 8, "The call", "Beth hears Maggie's recorded message and realises the warning was meant for Rosie too.", + "Building knowledge", "Fri 18 Oct", "Recorded message expands the secret", 79, 4, 12, "14 min", + "beth", Location("maggie-flat", "Maggie's flat", "Phone speaker glowing on the table", "location-flat.svg"), + Characters(("beth", "Beth", "POV character", "Listening", 100, "character-beth.svg"), ("maggie", "Maggie", "Voice on recording", "Direct presence", 88, "character-maggie.svg"), ("rosie", "Rosie", "Named ally", "Also warned", 64, "character-rosie.svg"), ("graham", "Graham", "Threat", "Closing in", 54, "character-graham.svg")), + Assets(("letter", "Letter", "Names the danger", 86, "asset-letter.svg"), ("phone", "Phone", "Recorded message", 92, "asset-phone.svg"), ("notebook", "Notebook", "Supports warning", 66, "asset-notebook.svg")), + Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship changed", "message from beyond absence", 94), ("rel-maggie-rosie", "maggie", "rosie", "Relationship discovered", "trusted ally", 72)), + Threads(("thread-letter", "Maggie's warning has two recipients.", "The secret is bigger than Beth's inheritance of the letter.", "mystery"), ("thread-reader-knows", "Reader now knows Rosie was part of Maggie's plan.", "Beth understands Rosie differently.", "secret")), + Observations(("obs-message", "An absent character becomes visually central.", "Maggie's portrait grows because the scene gives her direct agency.", "insight")), + Discoveries("Relationship discovered: Maggie and Rosie", "Asset found: Phone recording", "Observation created: absent character has agency"), + Feed("Scene 8 analysed", "Maggie promoted through recorded evidence", "Knowledge thread expanded")), + Scene( + "scene-09", 9, "Car park exit", "Beth and Rosie try to leave, but the red TR6 is parked across the only exit.", + "Inferring continuity", "Fri 18 Oct", "Exit blocked", 89, 5, 12, "9 min", + "beth", Location("car-park", "The Doweries car park", "Wet tarmac and one blocked exit", "location-car-park.svg"), + Characters(("beth", "Beth", "POV character", "Deciding", 100, "character-beth.svg"), ("rosie", "Rosie", "Ally", "At Beth's side", 84, "character-rosie.svg"), ("graham", "Graham", "Threat", "Controls the exit", 78, "character-graham.svg")), + Assets(("red-tr6", "Red TR6", "Blocks escape", 98, "asset-red-tr6.svg"), ("letter", "Letter", "Motive for pursuit", 74, "asset-letter.svg"), ("keys", "Keys", "Possible escape route", 62, "asset-keys.svg")), + Relationships(("rel-beth-rosie", "beth", "rosie", "Relationship strengthened", "active alliance", 92), ("rel-beth-graham", "beth", "graham", "Relationship intensified", "physical obstruction", 98)), + Threads(("thread-exit", "How do Beth and Rosie leave?", "The location and asset combine into an immediate continuity problem.", "question"), ("thread-letter", "Graham's pursuit confirms the letter's value.", "The hidden evidence now drives action.", "mystery")), + Observations(("obs-blocked", "Location, asset and relationship align.", "The car park is important because the red TR6 changes what characters can do.", "insight")), + Discoveries("Continuity observation: exit blocked", "Relationship intensified: Beth and Graham", "Asset state changed: Red TR6 blocks exit"), + Feed("Scene 9 analysed", "Location changed to car park", "Asset movement inferred")), + Scene( + "scene-10", 10, "The opened letter", "Beth opens Maggie's letter and the hidden pattern around Graham finally becomes explicit.", + "Preparing review", "Fri 18 Oct", "Letter reveals Graham's motive", 100, 5, 12, "Ready to review", + "beth", Location("car-park", "The Doweries car park", "Car lights, rain and an opened letter", "location-car-park.svg"), + Characters(("beth", "Beth", "POV character", "Understands", 100, "character-beth.svg"), ("maggie", "Maggie", "Truth teller", "Her plan revealed", 90, "character-maggie.svg"), ("rosie", "Rosie", "Ally", "Confirmed", 78, "character-rosie.svg"), ("graham", "Graham", "Exposed", "Motive visible", 74, "character-graham.svg")), + Assets(("letter", "Letter", "Opened evidence", 100, "asset-letter.svg"), ("red-tr6", "Red TR6", "Proof of presence", 76, "asset-red-tr6.svg"), ("notebook", "Notebook", "Corroborates letter", 68, "asset-notebook.svg")), + Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship resolved", "trusted inheritance", 96), ("rel-beth-rosie", "beth", "rosie", "Relationship confirmed", "alliance", 90), ("rel-beth-graham", "beth", "graham", "Conflict confirmed", "motive exposed", 98)), + Threads(("thread-letter", "Maggie's letter explains Graham's motive.", "The central question becomes review-ready.", "mystery"), ("thread-tr6", "The Red TR6 evidence is confirmed.", "The car, notebook and letter point to the same conclusion.", "secret")), + Observations(("obs-complete", "PlotDirector has a coherent story map for this sequence.", "Characters, location, assets and relationships now tell one readable story.", "insight")), + Discoveries("Analysis complete: 10 scenes ready for review", "Relationship confirmed: Beth and Rosie", "Knowledge thread resolved: Letter contents"), + Feed("Scene 10 analysed", "Chapter sequence ready", "Review data prepared")) + ] + }; + + private static StoryIntelligenceExperienceSceneState Scene( + string id, + int number, + string title, + string summary, + string stage, + string timelineLabel, + string timelineEvent, + int progress, + int chapters, + int sceneTotal, + string remaining, + string pov, + StoryIntelligenceExperienceLocationState location, + IReadOnlyList characters, + IReadOnlyList assets, + IReadOnlyList relationships, + IReadOnlyList threads, + IReadOnlyList observations, + IReadOnlyList discoveries, + IReadOnlyList feed) + => new() + { + Id = id, + SceneNumber = number, + Title = title, + Summary = summary, + Stage = stage, + TimelineLabel = timelineLabel, + TimelineEvent = timelineEvent, + ProgressPercent = progress, + ChaptersAnalysed = chapters, + ChapterTotal = 5, + ScenesAnalysed = number, + SceneTotal = sceneTotal, + EstimatedRemaining = remaining, + PovCharacterId = pov, + Location = location, + Characters = characters, + Assets = assets, + Relationships = relationships, + KnowledgeThreads = threads, + Observations = observations, + LatestDiscoveries = discoveries, + ActivityFeed = feed + }; + + private static StoryIntelligenceExperienceLocationState Location(string id, string name, string label, string image) + => new() { Id = id, Name = name, Label = label, ImagePath = $"{AssetRoot}/{image}" }; + + private static IReadOnlyList Characters(params (string Id, string Name, string Role, string Relevance, int Weight, string Image)[] items) + => items.Select(item => new StoryIntelligenceExperienceCharacterState { Id = item.Id, Name = item.Name, Role = item.Role, Relevance = item.Relevance, Weight = item.Weight, ImagePath = $"{AssetRoot}/{item.Image}" }).ToList(); + + private static IReadOnlyList Assets(params (string Id, string Name, string Label, int Weight, string Image)[] items) + => items.Select(item => new StoryIntelligenceExperienceAssetState { Id = item.Id, Name = item.Name, Label = item.Label, Weight = item.Weight, ImagePath = $"{AssetRoot}/{item.Image}" }).ToList(); + + private static IReadOnlyList Relationships(params (string Id, string Source, string Target, string Label, string State, int Weight)[] items) + => items.Select(item => new StoryIntelligenceExperienceRelationshipState { Id = item.Id, SourceId = item.Source, TargetId = item.Target, Label = item.Label, State = item.State, Weight = item.Weight }).ToList(); + + private static IReadOnlyList Threads(params (string Id, string Title, string Detail, string Tone)[] items) + => TextItems(items); + + private static IReadOnlyList Observations(params (string Id, string Title, string Detail, string Tone)[] items) + => TextItems(items); + + private static IReadOnlyList TextItems(params (string Id, string Title, string Detail, string Tone)[] items) + => items.Select(item => new StoryIntelligenceExperienceTextItem { Id = item.Id, Title = item.Title, Detail = item.Detail, Tone = item.Tone }).ToList(); + + private static IReadOnlyList Discoveries(params string[] items) => items; + + private static IReadOnlyList Feed(params string[] items) => items; +} diff --git a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs new file mode 100644 index 0000000..ea5c4e4 --- /dev/null +++ b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs @@ -0,0 +1,79 @@ +namespace PlotLine.ViewModels; + +public sealed class StoryIntelligenceExperiencePrototypeViewModel +{ + public string ProjectName { get; init; } = string.Empty; + public string BookTitle { get; init; } = string.Empty; + public IReadOnlyList Scenes { get; init; } = []; +} + +public sealed class StoryIntelligenceExperienceSceneState +{ + public string Id { get; init; } = string.Empty; + public int SceneNumber { get; init; } + public string Title { get; init; } = string.Empty; + public string Summary { get; init; } = string.Empty; + public string Stage { get; init; } = string.Empty; + public string TimelineLabel { get; init; } = string.Empty; + public string TimelineEvent { get; init; } = string.Empty; + public int ProgressPercent { get; init; } + public int ChaptersAnalysed { get; init; } + public int ChapterTotal { get; init; } + public int ScenesAnalysed { get; init; } + public int SceneTotal { get; init; } + public string EstimatedRemaining { get; init; } = string.Empty; + public string PovCharacterId { get; init; } = string.Empty; + public StoryIntelligenceExperienceLocationState Location { get; init; } = new(); + public IReadOnlyList Characters { get; init; } = []; + public IReadOnlyList Assets { get; init; } = []; + public IReadOnlyList Relationships { get; init; } = []; + public IReadOnlyList KnowledgeThreads { get; init; } = []; + public IReadOnlyList Observations { get; init; } = []; + public IReadOnlyList LatestDiscoveries { get; init; } = []; + public IReadOnlyList ActivityFeed { get; init; } = []; +} + +public sealed class StoryIntelligenceExperienceCharacterState +{ + public string Id { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string Role { get; init; } = string.Empty; + public string Relevance { get; init; } = string.Empty; + public int Weight { get; init; } + public string ImagePath { get; init; } = string.Empty; +} + +public sealed class StoryIntelligenceExperienceLocationState +{ + public string Id { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string Label { get; init; } = string.Empty; + public string ImagePath { get; init; } = string.Empty; +} + +public sealed class StoryIntelligenceExperienceAssetState +{ + public string Id { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string Label { get; init; } = string.Empty; + public int Weight { get; init; } + public string ImagePath { get; init; } = string.Empty; +} + +public sealed class StoryIntelligenceExperienceRelationshipState +{ + public string Id { get; init; } = string.Empty; + public string SourceId { get; init; } = string.Empty; + public string TargetId { get; init; } = string.Empty; + public string Label { get; init; } = string.Empty; + public string State { get; init; } = string.Empty; + public int Weight { get; init; } +} + +public sealed class StoryIntelligenceExperienceTextItem +{ + public string Id { get; init; } = string.Empty; + public string Title { get; init; } = string.Empty; + public string Detail { get; init; } = string.Empty; + public string Tone { get; init; } = string.Empty; +} diff --git a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml new file mode 100644 index 0000000..dc8d63c --- /dev/null +++ b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml @@ -0,0 +1,135 @@ +@using System.Text.Json +@model StoryIntelligenceExperiencePrototypeViewModel +@{ + Layout = null; + var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; +} + + + + + + Story Intelligence Experience Prototype - PlotDirector + + + + + +
+
+
+ PlotDirector + Story Intelligence +
+
+ @Model.ProjectName + @Model.BookTitle + Preparing + Scene 1 +
+ +
+ +
+ + +
+
+

Living story view

+

Reading the manuscript

+

+
+ +
+ +
+

Characters

+
+
+
+

Location

+
+
+
+

Assets

+
+
+
+ +
+
+
+
+
+ + +
+ + +
+ + + + diff --git a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css new file mode 100644 index 0000000..e563782 --- /dev/null +++ b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css @@ -0,0 +1,721 @@ +:root { + color-scheme: dark; + --story-bg: #07111f; + --story-bg-deep: #030813; + --story-panel: rgba(10, 22, 39, 0.74); + --story-panel-strong: rgba(14, 30, 52, 0.9); + --story-line: rgba(151, 185, 218, 0.18); + --story-line-strong: rgba(169, 205, 240, 0.34); + --story-text: #f3f8ff; + --story-muted: #9eb1c7; + --story-soft: #c4d6eb; + --story-gold: #d7a85d; + --story-gold-soft: rgba(215, 168, 93, 0.18); + --story-blue: #6db5ff; + --story-green: #79d2a6; + --story-rose: #e38ca8; + --story-radius: 18px; + --story-shadow: 0 28px 80px rgba(0, 0, 0, 0.36); +} + +* { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; + margin: 0; +} + +body { + overflow: hidden; + background: + radial-gradient(circle at 50% 18%, rgba(47, 106, 168, 0.25), transparent 28rem), + radial-gradient(circle at 90% 80%, rgba(215, 168, 93, 0.12), transparent 22rem), + linear-gradient(145deg, var(--story-bg-deep), var(--story-bg) 48%, #101725); + color: var(--story-text); + font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; +} + +button, +input { + font: inherit; +} + +button:focus-visible, +input:focus-visible { + outline: 2px solid var(--story-blue); + outline-offset: 3px; +} + +.story-exp { + position: relative; + display: grid; + grid-template-rows: 70px minmax(0, 1fr); + width: 100vw; + height: 100vh; + isolation: isolate; +} + +.story-exp::before { + content: ""; + position: fixed; + inset: 0; + z-index: -2; + background: + linear-gradient(rgba(255, 255, 255, 0.026) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: radial-gradient(circle at 50% 50%, black, transparent 75%); +} + +.story-exp-header { + display: grid; + grid-template-columns: minmax(210px, 0.65fr) minmax(460px, 1.35fr) auto; + align-items: center; + gap: 22px; + padding: 14px 24px; + border-bottom: 1px solid var(--story-line); + background: rgba(4, 10, 20, 0.68); + backdrop-filter: blur(18px); +} + +.story-exp-brand { + display: flex; + align-items: baseline; + gap: 12px; +} + +.story-exp-brand strong { + font-size: 1.12rem; + letter-spacing: 0.02em; +} + +.story-exp-brand span, +.story-exp-context span, +.story-exp-eyebrow, +.story-exp-zone-label, +.story-exp-controls span { + color: var(--story-muted); + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.story-exp-context { + display: flex; + justify-content: center; + gap: 16px; + min-width: 0; +} + +.story-exp-context span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.story-exp-background, +.story-exp-controls button { + min-height: 38px; + border: 1px solid rgba(215, 168, 93, 0.46); + border-radius: 999px; + padding: 0 14px; + background: rgba(215, 168, 93, 0.13); + color: #fff5dc; + font-weight: 800; +} + +.story-exp-background:hover, +.story-exp-controls button:hover { + background: rgba(215, 168, 93, 0.2); +} + +.story-exp-shell { + display: grid; + grid-template-columns: minmax(250px, 0.72fr) minmax(620px, 1.92fr) minmax(280px, 0.86fr); + gap: 18px; + min-height: 0; + padding: 18px 24px 84px; +} + +.story-exp-left, +.story-exp-right { + display: grid; + align-content: start; + gap: 14px; + min-height: 0; +} + +.story-exp-left, +.story-exp-right, +.story-exp-stage { + min-width: 0; +} + +.story-exp-progress, +.story-exp-panel, +.story-exp-stats { + border: 1px solid var(--story-line); + border-radius: var(--story-radius); + background: linear-gradient(180deg, rgba(18, 34, 58, 0.84), rgba(7, 17, 31, 0.68)); + box-shadow: var(--story-shadow); +} + +.story-exp-progress, +.story-exp-panel { + padding: 16px; +} + +.story-exp-progress-row { + display: flex; + align-items: end; + justify-content: space-between; + gap: 12px; + margin: 4px 0 12px; +} + +.story-exp-progress-row strong { + font-size: clamp(2.1rem, 4vw, 3.2rem); + line-height: 0.95; +} + +.story-exp-progress-row span { + color: var(--story-soft); + font-weight: 700; + text-align: right; +} + +.story-exp-progress-track { + overflow: hidden; + height: 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.09); +} + +.story-exp-progress-track span { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--story-blue), var(--story-gold)); + box-shadow: 0 0 22px rgba(109, 181, 255, 0.42); + transition: width 900ms ease; +} + +.story-exp-stats { + display: grid; + gap: 1px; + overflow: hidden; + padding: 0; +} + +.story-exp-stats div { + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + padding: 12px 14px; + background: rgba(255, 255, 255, 0.025); +} + +.story-exp-stats dt { + color: var(--story-muted); + font-size: 0.78rem; + font-weight: 800; +} + +.story-exp-stats dd { + margin: 0; + color: var(--story-text); + font-weight: 800; +} + +.story-exp-panel h2 { + margin: 0 0 12px; + font-size: 1rem; + letter-spacing: 0.01em; +} + +.story-exp-list, +.story-exp-feed { + display: grid; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +.story-exp-list li, +.story-exp-feed li, +.story-exp-insight { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 10px 11px; + background: rgba(255, 255, 255, 0.045); + color: var(--story-soft); +} + +.story-exp-feed li { + color: var(--story-muted); +} + +.story-exp-stage { + display: grid; + grid-template-rows: auto minmax(0, 1fr) 160px; + gap: 14px; + min-height: 0; +} + +.story-exp-stage-copy { + display: grid; + grid-template-columns: 1fr minmax(240px, 0.72fr); + align-items: end; + gap: 22px; + min-height: 86px; +} + +.story-exp-stage-copy h1 { + grid-column: 1; + margin: 0; + font-size: clamp(1.8rem, 3vw, 3.8rem); + line-height: 1.02; + letter-spacing: 0; +} + +.story-exp-stage-copy p:last-child { + grid-column: 2; + margin: 0; + color: var(--story-soft); + font-size: clamp(0.9rem, 1.2vw, 1.05rem); + line-height: 1.45; +} + +.story-exp-visual { + position: relative; + display: grid; + grid-template-columns: minmax(360px, 1.1fr) minmax(260px, 0.92fr) minmax(230px, 0.68fr); + grid-template-rows: minmax(0, 1fr); + gap: 16px; + min-height: 0; + overflow: hidden; + border: 1px solid var(--story-line); + border-radius: 26px; + padding: 18px; + background: + radial-gradient(circle at 28% 34%, rgba(109, 181, 255, 0.13), transparent 20rem), + radial-gradient(circle at 69% 45%, rgba(215, 168, 93, 0.12), transparent 18rem), + linear-gradient(180deg, rgba(11, 24, 43, 0.82), rgba(4, 10, 20, 0.72)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), var(--story-shadow); +} + +.story-exp-connections { + position: absolute; + inset: 0; + z-index: 1; + width: 100%; + height: 100%; + pointer-events: none; +} + +.story-exp-connection { + stroke: rgba(215, 168, 93, 0.48); + stroke-linecap: round; + stroke-width: 2; + stroke-dasharray: 1; + stroke-dashoffset: 1; + filter: drop-shadow(0 0 8px rgba(215, 168, 93, 0.28)); + animation: drawLine 900ms ease forwards; +} + +.story-exp-zone { + position: relative; + z-index: 2; + min-width: 0; + min-height: 0; +} + +.story-exp-zone-label { + margin: 0 0 10px; +} + +.story-exp-characters { + position: relative; + min-height: 100%; +} + +.story-character { + position: absolute; + display: grid; + gap: 8px; + width: var(--node-size, 150px); + transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1)); + opacity: var(--opacity, 1); + transition: transform 900ms cubic-bezier(.2, .8, .2, 1), opacity 700ms ease; +} + +.story-character.is-pov { + z-index: 4; +} + +.story-character__image, +.story-location__image, +.story-asset__image { + display: block; + overflow: hidden; + border: 1px solid var(--story-line-strong); + background: rgba(255, 255, 255, 0.06); + box-shadow: 0 22px 48px rgba(0, 0, 0, 0.3); +} + +.story-character__image { + width: var(--node-size, 150px); + height: var(--node-size, 150px); + border-radius: 50%; +} + +.story-character__image img, +.story-location__image img, +.story-asset__image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.story-character.is-pov .story-character__image { + border-color: rgba(215, 168, 93, 0.82); + box-shadow: 0 0 0 6px rgba(215, 168, 93, 0.12), 0 28px 70px rgba(215, 168, 93, 0.2); +} + +.story-character__text { + display: grid; + justify-items: center; + gap: 2px; + text-align: center; +} + +.story-character__text strong { + font-size: 1rem; +} + +.story-character__text span { + max-width: 150px; + color: var(--story-muted); + font-size: 0.76rem; + font-weight: 700; +} + +.story-location { + display: grid; + align-content: center; + gap: 12px; + height: calc(100% - 28px); + transition: opacity 500ms ease, transform 700ms ease; +} + +.story-location.is-changing { + opacity: 0.35; + transform: scale(0.98); +} + +.story-location__image { + aspect-ratio: 4 / 5; + border-radius: 28px; +} + +.story-location h2, +.story-location p { + margin: 0; +} + +.story-location h2 { + font-size: clamp(1.45rem, 2vw, 2.1rem); +} + +.story-location p { + color: var(--story-muted); +} + +.story-exp-assets { + display: grid; + align-content: center; + gap: 12px; + height: calc(100% - 28px); +} + +.story-asset { + display: grid; + grid-template-columns: 72px 1fr; + align-items: center; + gap: 12px; + opacity: var(--opacity, 1); + transform: translateY(var(--y, 0)) scale(var(--scale, 1)); + transition: transform 760ms ease, opacity 560ms ease; +} + +.story-asset__image { + width: 72px; + height: 72px; + border-radius: 18px; +} + +.story-asset strong, +.story-asset span { + display: block; +} + +.story-asset span { + color: var(--story-muted); + font-size: 0.82rem; +} + +.story-exp-ribbon-wrap { + display: grid; + gap: 12px; + min-width: 0; + border: 1px solid var(--story-line); + border-radius: 22px; + padding: 14px; + background: rgba(6, 16, 30, 0.72); +} + +.story-exp-ribbon { + position: relative; + display: flex; + align-items: stretch; + justify-content: center; + gap: 10px; + min-height: 86px; + overflow: hidden; +} + +.story-scene-card { + flex: 0 0 130px; + display: grid; + align-content: space-between; + gap: 7px; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 16px; + padding: 10px; + background: rgba(255, 255, 255, 0.045); + color: var(--story-muted); + opacity: 0.62; + transform: scale(0.92); + transition: transform 760ms ease, opacity 760ms ease, background 760ms ease, border-color 760ms ease; +} + +.story-scene-card.is-current { + flex-basis: 170px; + border-color: rgba(215, 168, 93, 0.58); + background: rgba(215, 168, 93, 0.14); + color: var(--story-text); + opacity: 1; + transform: scale(1); +} + +.story-scene-card strong { + color: inherit; + font-size: 0.86rem; +} + +.story-scene-card span { + color: var(--story-muted); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.story-exp-timeline { + position: relative; + display: grid; + grid-template-columns: repeat(var(--timeline-count, 1), minmax(0, 1fr)); + gap: 0; + min-height: 42px; +} + +.story-exp-timeline::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 16px; + height: 2px; + background: linear-gradient(90deg, rgba(109, 181, 255, 0.28), rgba(215, 168, 93, 0.68)); +} + +.story-timeline-marker { + position: relative; + display: grid; + justify-items: center; + gap: 8px; + color: var(--story-muted); + font-size: 0.72rem; + text-align: center; +} + +.story-timeline-marker::before { + content: ""; + z-index: 1; + width: 12px; + height: 12px; + margin-top: 11px; + border: 2px solid rgba(215, 168, 93, 0.8); + border-radius: 50%; + background: var(--story-bg); +} + +.story-timeline-marker.is-current::before { + width: 18px; + height: 18px; + margin-top: 8px; + background: var(--story-gold); + box-shadow: 0 0 20px rgba(215, 168, 93, 0.5); +} + +.story-exp-insight-list { + display: grid; + gap: 10px; +} + +.story-exp-insight { + display: grid; + gap: 4px; + transition: opacity 500ms ease, transform 600ms ease; +} + +.story-exp-insight strong { + color: var(--story-text); + font-size: 0.92rem; +} + +.story-exp-insight span { + color: var(--story-muted); + font-size: 0.78rem; + font-weight: 800; + text-transform: uppercase; +} + +.story-exp-controls { + position: fixed; + right: 24px; + bottom: 18px; + z-index: 10; + display: flex; + align-items: center; + gap: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; + padding: 9px 10px 9px 14px; + background: rgba(4, 10, 20, 0.82); + box-shadow: var(--story-shadow); + backdrop-filter: blur(16px); +} + +.story-exp-controls div { + display: grid; + padding-right: 6px; +} + +.story-exp-controls small { + color: var(--story-muted); + font-size: 0.72rem; +} + +.story-exp-controls label { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--story-muted); + font-size: 0.78rem; + font-weight: 800; + text-transform: uppercase; +} + +.story-exp-controls input { + width: 104px; +} + +@keyframes drawLine { + to { + stroke-dashoffset: 0; + } +} + +@media (max-width: 1180px) { + body { + overflow: auto; + } + + .story-exp { + height: auto; + min-height: 100vh; + } + + .story-exp-header, + .story-exp-shell, + .story-exp-stage-copy, + .story-exp-visual { + grid-template-columns: 1fr; + } + + .story-exp-shell { + padding-bottom: 110px; + } + + .story-exp-stage { + grid-template-rows: auto auto auto; + } + + .story-exp-visual { + min-height: 760px; + } + + .story-exp-stage-copy p:last-child { + grid-column: 1; + } + + .story-exp-context { + justify-content: start; + flex-wrap: wrap; + } + + .story-exp-controls { + left: 14px; + right: 14px; + justify-content: center; + flex-wrap: wrap; + border-radius: 22px; + } +} + +@media (max-width: 760px) { + .story-exp-header, + .story-exp-shell { + padding-left: 14px; + padding-right: 14px; + } + + .story-exp-ribbon { + justify-content: start; + overflow-x: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 1ms !important; + animation-iteration-count: 1 !important; + transition-duration: 1ms !important; + } + + .story-exp-connection { + stroke-dasharray: none; + stroke-dashoffset: 0; + } +} diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/asset-keys.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-keys.svg new file mode 100644 index 0000000..0038e98 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-keys.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/asset-letter.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-letter.svg new file mode 100644 index 0000000..2efab89 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-letter.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/asset-notebook.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-notebook.svg new file mode 100644 index 0000000..d4310f4 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-notebook.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/asset-phone.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-phone.svg new file mode 100644 index 0000000..22664a0 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-phone.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/asset-red-tr6.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-red-tr6.svg new file mode 100644 index 0000000..535fa23 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-red-tr6.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/asset-rucksack.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-rucksack.svg new file mode 100644 index 0000000..0ef9eb7 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/asset-rucksack.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/character-beth.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/character-beth.svg new file mode 100644 index 0000000..c21ab7b --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/character-beth.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/character-graham.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/character-graham.svg new file mode 100644 index 0000000..bf1c349 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/character-graham.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/character-maggie.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/character-maggie.svg new file mode 100644 index 0000000..e1459e4 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/character-maggie.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/character-rosie.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/character-rosie.svg new file mode 100644 index 0000000..83977de --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/character-rosie.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/location-car-park.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/location-car-park.svg new file mode 100644 index 0000000..f3b77c9 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/location-car-park.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/location-doweries.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/location-doweries.svg new file mode 100644 index 0000000..3bbe7be --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/location-doweries.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/location-flat.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/location-flat.svg new file mode 100644 index 0000000..c218693 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/location-flat.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/location-laundry.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/location-laundry.svg new file mode 100644 index 0000000..290d3f2 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/location-laundry.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/location-stairwell.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/location-stairwell.svg new file mode 100644 index 0000000..e41a737 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/location-stairwell.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js new file mode 100644 index 0000000..4bf5948 --- /dev/null +++ b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js @@ -0,0 +1,401 @@ +(() => { + const root = document.querySelector("[data-story-experience]"); + const dataNode = document.getElementById("storyExperienceData"); + if (!root || !dataNode) return; + + const model = JSON.parse(dataNode.textContent || "{}"); + const scenes = model.scenes || []; + if (!scenes.length) return; + + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const state = { + index: 0, + playing: true, + intervalMs: 6200, + timer: null + }; + + const dom = { + stageLabel: root.querySelector("[data-stage-label]"), + currentScene: root.querySelector("[data-current-scene]"), + progressPercent: root.querySelector("[data-progress-percent]"), + estimatedRemaining: root.querySelector("[data-estimated-remaining]"), + progressBar: root.querySelector("[data-progress-bar]"), + chapterCount: root.querySelector("[data-chapter-count]"), + sceneCount: root.querySelector("[data-scene-count]"), + currentStage: root.querySelector("[data-current-stage]"), + discoveries: root.querySelector("[data-discoveries]"), + activityFeed: root.querySelector("[data-activity-feed]"), + sceneTitle: root.querySelector("[data-scene-title]"), + sceneSummary: root.querySelector("[data-scene-summary]"), + visualStage: root.querySelector("[data-visual-stage]"), + connections: root.querySelector("[data-connections]"), + characters: root.querySelector("[data-characters]"), + location: root.querySelector("[data-location]"), + assets: root.querySelector("[data-assets]"), + ribbon: root.querySelector("[data-scene-ribbon]"), + timeline: root.querySelector("[data-timeline]"), + relationships: root.querySelector("[data-relationships]"), + knowledge: root.querySelector("[data-knowledge]"), + observations: root.querySelector("[data-observations]"), + play: root.querySelector("[data-control='play']"), + speed: root.querySelector("[data-speed-control]") + }; + + function loadSceneState(index) { + return scenes[Math.max(0, Math.min(scenes.length - 1, index))]; + } + + function transitionToNextState(nextIndex) { + const previous = loadSceneState(state.index); + state.index = Math.max(0, Math.min(scenes.length - 1, nextIndex)); + const current = loadSceneState(state.index); + + updateProgress(current); + updateSceneCopy(current); + reconcileCharacters(current, previous); + reconcileLocation(current, previous); + reconcileAssets(current, previous); + reconcileRelationships(current); + updateKnowledgeThreads(current); + updateSceneRibbon(current); + updateTimeline(current); + updateFeeds(current); + scheduleConnectionDraw(); + } + + function updateProgress(scene) { + text(dom.stageLabel, scene.stage); + text(dom.currentScene, scene.sceneNumber); + text(dom.progressPercent, `${scene.progressPercent}%`); + text(dom.estimatedRemaining, scene.estimatedRemaining); + text(dom.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`); + text(dom.sceneCount, `${scene.scenesAnalysed} of ${scene.sceneTotal}`); + text(dom.currentStage, scene.stage); + if (dom.progressBar) dom.progressBar.style.width = `${scene.progressPercent}%`; + } + + function updateSceneCopy(scene) { + text(dom.sceneTitle, scene.title); + text(dom.sceneSummary, scene.summary); + } + + function reconcileCharacters(scene) { + const activeIds = new Set(scene.characters.map((character) => character.id)); + scene.characters.forEach((character, index) => { + let node = dom.characters.querySelector(`[data-character-id="${cssEscape(character.id)}"]`); + if (!node) { + node = createCharacterNode(character); + dom.characters.append(node); + } + + const position = characterPosition(character, index, scene.povCharacterId); + node.classList.toggle("is-pov", character.id === scene.povCharacterId); + node.style.setProperty("--node-size", `${position.size}px`); + node.style.setProperty("--x", `${position.x}px`); + node.style.setProperty("--y", `${position.y}px`); + node.style.setProperty("--scale", String(position.scale)); + node.style.setProperty("--opacity", String(position.opacity)); + node.querySelector("img").src = character.imagePath; + node.querySelector("img").alt = `${character.name} prototype portrait`; + text(node.querySelector("[data-character-name]"), character.name); + text(node.querySelector("[data-character-role]"), `${character.role} - ${character.relevance}`); + }); + + dom.characters.querySelectorAll("[data-character-id]").forEach((node) => { + if (!activeIds.has(node.dataset.characterId)) { + node.style.setProperty("--opacity", "0"); + node.style.setProperty("--scale", "0.86"); + window.setTimeout(() => node.remove(), reducedMotion ? 0 : 620); + } + }); + } + + function reconcileLocation(scene, previous) { + const changed = previous?.location?.id !== scene.location.id; + if (changed) { + dom.location.firstElementChild?.classList.add("is-changing"); + } + + window.setTimeout(() => { + dom.location.innerHTML = ""; + const node = document.createElement("article"); + node.className = "story-location"; + node.dataset.locationId = scene.location.id; + node.innerHTML = ` +
${escapeHtml(scene.location.name)} prototype location
+
+

+

+
`; + node.querySelector("img").src = scene.location.imagePath; + text(node.querySelector("h2"), scene.location.name); + text(node.querySelector("p"), scene.location.label); + dom.location.append(node); + }, changed && !reducedMotion ? 260 : 0); + } + + function reconcileAssets(scene) { + const activeIds = new Set(scene.assets.map((asset) => asset.id)); + scene.assets.forEach((asset, index) => { + let node = dom.assets.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`); + if (!node) { + node = createAssetNode(asset); + dom.assets.append(node); + } + + node.style.setProperty("--y", `${(index - 1) * 2}px`); + node.style.setProperty("--scale", String(0.92 + Math.min(asset.weight, 100) / 500)); + node.style.setProperty("--opacity", "1"); + node.querySelector("img").src = asset.imagePath; + node.querySelector("img").alt = `${asset.name} prototype asset`; + text(node.querySelector("[data-asset-name]"), asset.name); + text(node.querySelector("[data-asset-label]"), asset.label); + }); + + dom.assets.querySelectorAll("[data-asset-id]").forEach((node) => { + if (!activeIds.has(node.dataset.assetId)) { + node.style.setProperty("--opacity", "0"); + node.style.setProperty("--scale", "0.8"); + window.setTimeout(() => node.remove(), reducedMotion ? 0 : 520); + } + }); + } + + function reconcileRelationships(scene) { + renderInsights(dom.relationships, scene.relationships.map((relationship) => ({ + id: relationship.id, + title: relationship.label, + detail: relationship.state, + tone: relationship.weight >= 90 ? "confirmed" : relationship.weight >= 70 ? "strengthening" : "emerging" + }))); + } + + function updateKnowledgeThreads(scene) { + renderInsights(dom.knowledge, scene.knowledgeThreads); + renderInsights(dom.observations, scene.observations); + } + + function updateSceneRibbon(scene) { + const currentIndex = state.index; + const start = Math.max(0, Math.min(scenes.length - 7, currentIndex - 3)); + const visible = scenes.slice(start, start + 7); + dom.ribbon.innerHTML = ""; + visible.forEach((item) => { + const card = document.createElement("article"); + card.className = "story-scene-card"; + card.classList.toggle("is-current", item.id === scene.id); + card.innerHTML = `Scene ${item.sceneNumber}`; + text(card.querySelector("strong"), item.title); + text(card.querySelector("small"), item.timelineEvent); + dom.ribbon.append(card); + }); + } + + function updateTimeline(scene) { + const completed = scenes.slice(0, state.index + 1); + dom.timeline.style.setProperty("--timeline-count", String(completed.length)); + dom.timeline.innerHTML = ""; + completed.forEach((item) => { + const marker = document.createElement("div"); + marker.className = "story-timeline-marker"; + marker.classList.toggle("is-current", item.id === scene.id); + marker.innerHTML = ``; + text(marker.querySelector("span"), `${item.timelineLabel} - S${item.sceneNumber}`); + dom.timeline.append(marker); + }); + } + + function updateFeeds(scene) { + renderList(dom.discoveries, scene.latestDiscoveries); + renderList(dom.activityFeed, scene.activityFeed); + } + + function renderInsights(container, items) { + const activeIds = new Set(items.map((item) => item.id)); + items.forEach((item) => { + let node = container.querySelector(`[data-insight-id="${cssEscape(item.id)}"]`); + if (!node) { + node = document.createElement("article"); + node.className = "story-exp-insight"; + node.dataset.insightId = item.id; + node.innerHTML = `

`; + container.append(node); + } + + node.dataset.tone = item.tone || ""; + text(node.querySelector("span"), item.tone || "Signal"); + text(node.querySelector("strong"), item.title); + text(node.querySelector("p"), item.detail); + }); + + container.querySelectorAll("[data-insight-id]").forEach((node) => { + if (!activeIds.has(node.dataset.insightId)) { + node.style.opacity = "0"; + node.style.transform = "translateY(8px)"; + window.setTimeout(() => node.remove(), reducedMotion ? 0 : 360); + } + }); + } + + function renderList(container, items) { + container.innerHTML = ""; + items.forEach((item) => { + const node = document.createElement(container.tagName === "OL" ? "li" : "li"); + node.textContent = item; + container.append(node); + }); + } + + function scheduleConnectionDraw() { + window.requestAnimationFrame(() => drawConnections(loadSceneState(state.index))); + } + + function drawConnections(scene) { + if (!dom.connections || !dom.visualStage) return; + const stageRect = dom.visualStage.getBoundingClientRect(); + dom.connections.innerHTML = ""; + + scene.relationships.forEach((relationship) => { + const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`); + const target = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.targetId)}"]`); + if (!source || !target) return; + + const start = centrePoint(source, stageRect); + const end = centrePoint(target, stageRect); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + const midY = Math.min(start.y, end.y) - 36; + path.setAttribute("d", `M ${start.x} ${start.y} C ${start.x} ${midY}, ${end.x} ${midY}, ${end.x} ${end.y}`); + path.setAttribute("pathLength", "1"); + path.setAttribute("class", "story-exp-connection"); + path.style.strokeWidth = String(1.5 + Math.min(relationship.weight, 100) / 28); + dom.connections.append(path); + }); + + const pov = dom.visualStage.querySelector(`[data-character-id="${cssEscape(scene.povCharacterId)}"]`); + const location = dom.visualStage.querySelector("[data-location-id]"); + if (pov && location) { + const start = centrePoint(pov, stageRect); + const end = centrePoint(location, stageRect); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", `M ${start.x} ${start.y} C ${start.x + 120} ${start.y + 30}, ${end.x - 110} ${end.y - 30}, ${end.x} ${end.y}`); + path.setAttribute("pathLength", "1"); + path.setAttribute("class", "story-exp-connection"); + path.style.stroke = "rgba(109, 181, 255, 0.42)"; + dom.connections.append(path); + } + } + + function createCharacterNode(character) { + const node = document.createElement("article"); + node.className = "story-character"; + node.dataset.characterId = character.id; + node.innerHTML = ` +
+
+ + +
`; + return node; + } + + function createAssetNode(asset) { + const node = document.createElement("article"); + node.className = "story-asset"; + node.dataset.assetId = asset.id; + node.innerHTML = ` +
+
+ + +
`; + return node; + } + + function characterPosition(character, index, povId) { + if (character.id === povId) { + return { x: 145, y: 130, size: 210, scale: 1, opacity: 1 }; + } + + const important = character.weight >= 70; + const positions = important + ? [{ x: 18, y: 54 }, { x: 344, y: 44 }, { x: 322, y: 286 }] + : [{ x: 34, y: 300 }, { x: 396, y: 240 }, { x: 20, y: 198 }]; + const slot = positions[index % positions.length]; + const size = important ? 138 + (character.weight - 70) * 0.9 : 92 + Math.max(0, character.weight - 24) * 0.45; + return { x: slot.x, y: slot.y, size: Math.round(size), scale: 1, opacity: important ? 0.94 : 0.72 }; + } + + function centrePoint(node, parentRect) { + const rect = node.getBoundingClientRect(); + return { + x: rect.left - parentRect.left + rect.width / 2, + y: rect.top - parentRect.top + rect.height / 2 + }; + } + + function startTimer() { + stopTimer(); + if (!state.playing) return; + state.timer = window.setInterval(() => { + const next = state.index >= scenes.length - 1 ? 0 : state.index + 1; + transitionToNextState(next); + }, state.intervalMs); + } + + function stopTimer() { + if (state.timer) { + window.clearInterval(state.timer); + state.timer = null; + } + } + + function text(node, value) { + if (node) node.textContent = value ?? ""; + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (match) => ({ + "&": "&", + "<": "<", + ">": ">", + "\"": """, + "'": "'" + }[match])); + } + + function cssEscape(value) { + return window.CSS?.escape ? window.CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&"); + } + + root.querySelector("[data-control='restart']")?.addEventListener("click", () => { + transitionToNextState(0); + startTimer(); + }); + + root.querySelector("[data-control='previous']")?.addEventListener("click", () => { + transitionToNextState(state.index <= 0 ? scenes.length - 1 : state.index - 1); + startTimer(); + }); + + root.querySelector("[data-control='next']")?.addEventListener("click", () => { + transitionToNextState(state.index >= scenes.length - 1 ? 0 : state.index + 1); + startTimer(); + }); + + dom.play?.addEventListener("click", () => { + state.playing = !state.playing; + text(dom.play, state.playing ? "Pause" : "Play"); + if (state.playing) startTimer(); else stopTimer(); + }); + + dom.speed?.addEventListener("input", () => { + state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200; + startTimer(); + }); + + window.addEventListener("resize", () => scheduleConnectionDraw()); + + transitionToNextState(0); + startTimer(); +})(); diff --git a/docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md b/docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md new file mode 100644 index 0000000..ccad7b2 --- /dev/null +++ b/docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md @@ -0,0 +1,123 @@ +# Phase 21B - Story Intelligence Experience Prototype + +## Purpose + +Phase 21B creates an isolated, development-only visual prototype for the PlotDirector Story Intelligence Experience. The product specification is `PlotLine/Docs/Features/PlotDirector_Story_Intelligence_Experience_Design_v2.md`; this note records the implementation approach and limits. + +## Route + +- `/Development/StoryIntelligenceExperience` +- Implemented by `DevelopmentController`. +- Requires the existing `AdminOnly` policy. +- Returns `NotFound` unless `IWebHostEnvironment.IsDevelopment()` is true. +- Not linked from normal navigation. + +## Files added or changed + +- `PlotLine/Controllers/DevelopmentController.cs` +- `PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs` +- `PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs` +- `PlotLine/Views/Development/StoryIntelligenceExperience.cshtml` +- `PlotLine/wwwroot/css/story-intelligence-experience-prototype.css` +- `PlotLine/wwwroot/js/story-intelligence-experience-prototype.js` +- `PlotLine/wwwroot/images/story-intelligence/prototype/*.svg` +- `docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md` + +No existing Story Intelligence import, progress, SignalR, worker, persistence or database files were changed. + +## Prototype data structure + +The prototype uses `StoryIntelligenceExperiencePrototypeData.Build()` to provide simulated Alpha Flame-style scene states. Each state includes: + +- Scene number, title, summary and stage +- Progress figures and estimated remaining time +- POV character +- Important and incidental characters +- One active location +- Significant assets +- Relationship changes +- Knowledge threads +- Story observations +- Timeline label/event +- Latest discoveries and activity feed + +The Razor view serializes this model into a JSON script tag. The JavaScript renderer consumes that state and reconciles DOM elements by stable prototype IDs. + +## Rendering approach + +The prototype uses a hybrid renderer: + +- HTML/CSS for the full-screen shell, panels, imagery, progress, scene ribbon and timeline. +- SVG for relationship and location connection lines. +- JavaScript for current-to-next state reconciliation and playback controls. + +Cytoscape was not reused. It remains useful for the existing Relationship Map, but this prototype is not a free-form network graph. The experience needs authored composition around POV, location, assets, narrative rhythm and side-panel understanding, which is better tested here with HTML/CSS/SVG. + +## Animation approach + +The JavaScript updates from current scene state to next scene state and reconciles: + +- Character prominence, position and role changes +- POV changes +- Location crossfades +- Asset appearance/removal/prominence +- Relationship line drawing +- Insight panel updates +- Scene ribbon movement +- Timeline growth +- Progress/stat changes + +The default automatic playback interval is controlled by the prototype speed slider. Manual controls allow play/pause, previous, next and restart for development review only. + +## Imagery approach + +Prototype-only SVG artwork is stored under: + +`PlotLine/wwwroot/images/story-intelligence/prototype/` + +The artwork includes character portraits, locations and assets. It is intentionally isolated so it can later be replaced by curated canonical imagery and should not be treated as final artwork. + +## Accessibility and reduced motion + +The prototype provides: + +- Ordinary HTML text for progress, status, discoveries and insights. +- Button elements for all controls. +- Visible focus styles. +- Semantic sections and labelled side panels. +- `prefers-reduced-motion` support that shortens transitions and disables animated line drawing. + +## Responsive behaviour + +The primary target is desktop/laptop. At narrower widths, the layout stacks and the page becomes scrollable so the current-story view remains usable rather than shrinking into unreadable tiles. A polished mobile version is deferred. + +## Technical limitations + +- Uses simulated data only. +- No SignalR or API integration. +- No database storage. +- No durable event stream. +- No real import pipeline connection. +- No post-import Story Map. +- No frontend package manager or bundler was introduced. +- The prototype view includes its own full-screen document shell rather than the standard layout. + +## Items intentionally deferred + +- Real Story Intelligence data binding. +- Snapshot/replay endpoints. +- Durable provisional discovery IDs. +- Event-history tables. +- Production navigation. +- Sigma.js/Graphology evaluation. +- Permanent Story Map route. +- Mobile polish. + +## Recommendations for Phase 21C + +Phase 21C should keep the current progress page intact while adding the first real data seam behind the prototype: + +1. Define a read-only snapshot contract shaped like the prototype scene state. +2. Map existing `StoryIntelligenceRuns` and `StoryIntelligenceSceneResults` into that contract. +3. Add durable discovery/event storage only after the snapshot shape is validated. +4. Preserve the same browser-side state reconciliation API so simulated data can be replaced incrementally.