Phase 21B - Story Intelligence Experience Prototype

This commit is contained in:
Nick Beckley 2026-07-11 20:00:06 +00:00
parent f4e6af844b
commit 0ebccf9ac8
22 changed files with 1805 additions and 0 deletions

View File

@ -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());
}
}

View File

@ -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<StoryIntelligenceExperienceCharacterState> characters,
IReadOnlyList<StoryIntelligenceExperienceAssetState> assets,
IReadOnlyList<StoryIntelligenceExperienceRelationshipState> relationships,
IReadOnlyList<StoryIntelligenceExperienceTextItem> threads,
IReadOnlyList<StoryIntelligenceExperienceTextItem> observations,
IReadOnlyList<string> discoveries,
IReadOnlyList<string> 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<StoryIntelligenceExperienceCharacterState> 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<StoryIntelligenceExperienceAssetState> 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<StoryIntelligenceExperienceRelationshipState> 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<StoryIntelligenceExperienceTextItem> Threads(params (string Id, string Title, string Detail, string Tone)[] items)
=> TextItems(items);
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> Observations(params (string Id, string Title, string Detail, string Tone)[] items)
=> TextItems(items);
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> 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<string> Discoveries(params string[] items) => items;
private static IReadOnlyList<string> Feed(params string[] items) => items;
}

View File

@ -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<StoryIntelligenceExperienceSceneState> 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<StoryIntelligenceExperienceCharacterState> Characters { get; init; } = [];
public IReadOnlyList<StoryIntelligenceExperienceAssetState> Assets { get; init; } = [];
public IReadOnlyList<StoryIntelligenceExperienceRelationshipState> Relationships { get; init; } = [];
public IReadOnlyList<StoryIntelligenceExperienceTextItem> KnowledgeThreads { get; init; } = [];
public IReadOnlyList<StoryIntelligenceExperienceTextItem> Observations { get; init; } = [];
public IReadOnlyList<string> LatestDiscoveries { get; init; } = [];
public IReadOnlyList<string> 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;
}

View File

@ -0,0 +1,135 @@
@using System.Text.Json
@model StoryIntelligenceExperiencePrototypeViewModel
@{
Layout = null;
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
}
<!DOCTYPE html>
<html lang="en" data-theme="dark" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Story Intelligence Experience Prototype - PlotDirector</title>
<link rel="stylesheet" href="~/css/story-intelligence-experience-prototype.css" asp-append-version="true" />
</head>
<body>
<script id="storyExperienceData" type="application/json">@Html.Raw(JsonSerializer.Serialize(Model, jsonOptions))</script>
<main class="story-exp" data-story-experience>
<header class="story-exp-header" aria-label="Story Intelligence status">
<div class="story-exp-brand">
<strong>PlotDirector</strong>
<span>Story Intelligence</span>
</div>
<div class="story-exp-context">
<span>@Model.ProjectName</span>
<span>@Model.BookTitle</span>
<span data-stage-label>Preparing</span>
<span>Scene <strong data-current-scene>1</strong></span>
</div>
<button class="story-exp-background" type="button">Continue in Background</button>
</header>
<section class="story-exp-shell" aria-label="Story Intelligence visual prototype">
<aside class="story-exp-left" aria-label="Analysis progress">
<div class="story-exp-progress">
<p class="story-exp-eyebrow">Overall progress</p>
<div class="story-exp-progress-row">
<strong data-progress-percent>0%</strong>
<span data-estimated-remaining>Calculating</span>
</div>
<div class="story-exp-progress-track" aria-hidden="true">
<span data-progress-bar></span>
</div>
</div>
<dl class="story-exp-stats">
<div>
<dt>Chapters analysed</dt>
<dd data-chapter-count>0 of 5</dd>
</div>
<div>
<dt>Scenes analysed</dt>
<dd data-scene-count>0 of 12</dd>
</div>
<div>
<dt>Current stage</dt>
<dd data-current-stage>Preparing</dd>
</div>
</dl>
<section class="story-exp-panel" aria-labelledby="latest-discoveries-title">
<h2 id="latest-discoveries-title">Latest discoveries</h2>
<ul class="story-exp-list" data-discoveries></ul>
</section>
<section class="story-exp-panel" aria-labelledby="activity-feed-title">
<h2 id="activity-feed-title">Live activity</h2>
<ol class="story-exp-feed" data-activity-feed></ol>
</section>
</aside>
<section class="story-exp-stage" aria-labelledby="living-story-title">
<div class="story-exp-stage-copy">
<p class="story-exp-eyebrow">Living story view</p>
<h1 id="living-story-title" data-scene-title>Reading the manuscript</h1>
<p data-scene-summary></p>
</div>
<div class="story-exp-visual" data-visual-stage>
<svg class="story-exp-connections" data-connections aria-hidden="true"></svg>
<div class="story-exp-zone story-exp-zone--characters" aria-label="Characters">
<p class="story-exp-zone-label">Characters</p>
<div class="story-exp-characters" data-characters></div>
</div>
<div class="story-exp-zone story-exp-zone--location" aria-label="Active location">
<p class="story-exp-zone-label">Location</p>
<div class="story-exp-location" data-location></div>
</div>
<div class="story-exp-zone story-exp-zone--assets" aria-label="Significant assets">
<p class="story-exp-zone-label">Assets</p>
<div class="story-exp-assets" data-assets></div>
</div>
</div>
<section class="story-exp-ribbon-wrap" aria-label="Recent scenes and timeline">
<div class="story-exp-ribbon" data-scene-ribbon></div>
<div class="story-exp-timeline" data-timeline></div>
</section>
</section>
<aside class="story-exp-right" aria-label="Story understanding">
<section class="story-exp-panel" aria-labelledby="relationships-title">
<h2 id="relationships-title">Relationship changes</h2>
<div class="story-exp-insight-list" data-relationships></div>
</section>
<section class="story-exp-panel" aria-labelledby="knowledge-title">
<h2 id="knowledge-title">Knowledge threads</h2>
<div class="story-exp-insight-list" data-knowledge></div>
</section>
<section class="story-exp-panel" aria-labelledby="observations-title">
<h2 id="observations-title">Story observations</h2>
<div class="story-exp-insight-list" data-observations></div>
</section>
</aside>
</section>
<aside class="story-exp-controls" aria-label="Prototype controls">
<div>
<span>Prototype controls</span>
<small>Simulation only</small>
</div>
<button type="button" data-control="restart">Restart</button>
<button type="button" data-control="previous">Previous</button>
<button type="button" data-control="play">Pause</button>
<button type="button" data-control="next">Next</button>
<label>
Speed
<input type="range" min="3500" max="10000" step="500" value="6200" data-speed-control />
</label>
</aside>
</main>
<script src="~/js/story-intelligence-experience-prototype.js" asp-append-version="true"></script>
</body>
</html>

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
<rect width="220" height="220" rx="24" fill="#102035"/>
<circle cx="84" cy="84" r="34" fill="none" stroke="#d7a85d" stroke-width="12"/>
<path d="M108 108l62 62M142 142l18-18M158 158l18-18" stroke="#d7a85d" stroke-width="13" stroke-linecap="round"/>
<path d="M55 150c24 20 61 25 94 12" stroke="#8fb4d4" stroke-width="5" fill="none" opacity=".55"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
<rect width="220" height="220" rx="24" fill="#102035"/>
<path d="M42 70h136v90H42z" fill="#f0d8a8" stroke="#c49b5b" stroke-width="5"/>
<path d="M42 72l68 54 68-54M42 160l54-48M178 160l-54-48" fill="none" stroke="#9d7440" stroke-width="5"/>
<circle cx="110" cy="126" r="14" fill="#9c2f2f"/>
</svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
<rect width="220" height="220" rx="24" fill="#102035"/>
<rect x="62" y="36" width="104" height="148" rx="8" fill="#274257" stroke="#b9c9d6" stroke-width="5"/>
<path d="M82 66h58M82 92h58M82 118h46M82 144h52" stroke="#d7a85d" stroke-width="5" stroke-linecap="round"/>
<path d="M62 58h-16M62 90h-16M62 122h-16M62 154h-16" stroke="#d7e3ee" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
<rect width="220" height="220" rx="24" fill="#102035"/>
<rect x="70" y="34" width="80" height="152" rx="16" fill="#111820" stroke="#8fb4d4" stroke-width="6"/>
<rect x="82" y="58" width="56" height="92" rx="6" fill="#24445e"/>
<circle cx="110" cy="166" r="7" fill="#d7a85d"/>
<path d="M92 92c12-11 24-11 36 0M84 78c18-18 34-18 52 0" stroke="#d7a85d" stroke-width="5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
<rect width="220" height="220" rx="24" fill="#102035"/>
<path d="M38 118h144l18 24v28H20v-30z" fill="#9f2525"/>
<path d="M68 88h78l28 30H44z" fill="#be3a32"/>
<rect x="78" y="94" width="34" height="24" fill="#dce9f4" opacity=".75"/>
<rect x="118" y="94" width="28" height="24" fill="#dce9f4" opacity=".55"/>
<circle cx="58" cy="170" r="20" fill="#050914" stroke="#aabdd0" stroke-width="6"/>
<circle cx="162" cy="170" r="20" fill="#050914" stroke="#aabdd0" stroke-width="6"/>
</svg>

After

Width:  |  Height:  |  Size: 559 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
<rect width="220" height="220" rx="24" fill="#102035"/>
<path d="M76 80c2-35 66-35 68 0" fill="none" stroke="#d7a85d" stroke-width="9"/>
<rect x="58" y="70" width="104" height="112" rx="20" fill="#33495d" stroke="#8aa8bf" stroke-width="5"/>
<rect x="78" y="118" width="64" height="42" rx="10" fill="#1c2c3a"/>
<path d="M80 94h60" stroke="#d7a85d" stroke-width="6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#183a5a"/><stop offset="1" stop-color="#c28b58"/></linearGradient></defs>
<rect width="300" height="300" fill="url(#g)"/>
<circle cx="150" cy="118" r="62" fill="#f0c7a4"/>
<path d="M82 132c5-70 41-94 85-82 34 9 52 36 51 82-28-30-76-34-136 0z" fill="#3b251d"/>
<path d="M58 300c14-74 50-111 92-111s78 37 92 111z" fill="#1b2638"/>
<path d="M110 157c23 22 57 22 80 0" fill="none" stroke="#8f5b48" stroke-width="8" stroke-linecap="round"/>
<circle cx="126" cy="120" r="6" fill="#1c2530"/><circle cx="174" cy="120" r="6" fill="#1c2530"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#1d2330"/><stop offset="1" stop-color="#714033"/></linearGradient></defs>
<rect width="300" height="300" fill="url(#g)"/>
<circle cx="150" cy="116" r="59" fill="#d7ad91"/>
<path d="M91 93c12-39 41-58 76-50 27 6 47 23 54 52-42-15-84-16-130-2z" fill="#2b2423"/>
<path d="M58 300c15-72 50-108 92-108s77 36 92 108z" fill="#2a2f38"/>
<path d="M119 158c18 5 44 5 62 0" fill="none" stroke="#6f463e" stroke-width="7" stroke-linecap="round"/>
<circle cx="126" cy="120" r="6" fill="#111820"/><circle cx="174" cy="120" r="6" fill="#111820"/>
</svg>

After

Width:  |  Height:  |  Size: 690 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#11243b"/><stop offset="1" stop-color="#755a8c"/></linearGradient></defs>
<rect width="300" height="300" fill="url(#g)"/>
<circle cx="150" cy="120" r="60" fill="#e8bea3"/>
<path d="M78 126c-2-55 26-92 72-92s74 37 72 92c-23-38-49-52-72-52s-49 14-72 52z" fill="#d8d1c3"/>
<path d="M62 300c17-70 52-105 88-105s71 35 88 105z" fill="#473a56"/>
<path d="M118 157c19 16 45 16 64 0" fill="none" stroke="#8d5b50" stroke-width="7" stroke-linecap="round"/>
<circle cx="127" cy="121" r="5" fill="#28313d"/><circle cx="173" cy="121" r="5" fill="#28313d"/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#163d48"/><stop offset="1" stop-color="#b45d75"/></linearGradient></defs>
<rect width="300" height="300" fill="url(#g)"/>
<circle cx="150" cy="120" r="58" fill="#f1c9a8"/>
<path d="M84 118c9-52 42-80 80-70 33 9 54 36 55 75-46-28-90-29-135-5z" fill="#8a4f35"/>
<path d="M66 300c19-68 51-103 84-103s65 35 84 103z" fill="#1d4f58"/>
<path d="M119 158c19 15 43 15 62 0" fill="none" stroke="#8f5b48" stroke-width="7" stroke-linecap="round"/>
<circle cx="127" cy="121" r="5" fill="#1c2530"/><circle cx="173" cy="121" r="5" fill="#1c2530"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
<rect width="420" height="520" fill="#07111f"/>
<path d="M0 365c120-50 300-50 420 0v155H0z" fill="#1a2430"/>
<path d="M70 376h280" stroke="#d7a85d" stroke-width="7" stroke-dasharray="28 20" opacity=".7"/>
<path d="M82 300h252l48 52v46H42v-48z" fill="#8e2424"/>
<circle cx="114" cy="398" r="34" fill="#050914" stroke="#9ab3c7" stroke-width="7"/>
<circle cx="310" cy="398" r="34" fill="#050914" stroke="#9ab3c7" stroke-width="7"/>
<rect x="142" y="314" width="92" height="40" fill="#dce9f4" opacity=".7"/>
<rect x="250" y="316" width="62" height="38" fill="#dce9f4" opacity=".55"/>
<circle cx="58" cy="86" r="34" fill="#d7a85d" opacity=".35"/>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
<defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#10243d"/><stop offset="1" stop-color="#050914"/></linearGradient></defs>
<rect width="420" height="520" fill="url(#sky)"/>
<path d="M45 470h330V210L210 95 45 210z" fill="#1b2b3a" stroke="#596f84" stroke-width="5"/>
<path d="M80 220h260v250H80z" fill="#243648"/>
<path d="M138 470V330h72v140z" fill="#111923"/>
<g fill="#d5a65a"><rect x="104" y="250" width="48" height="58"/><rect x="268" y="250" width="48" height="58"/><rect x="218" y="350" width="52" height="64"/></g>
<path d="M0 472c95-31 190-31 420 0v48H0z" fill="#07111f"/>
<g stroke="#8fb4d4" stroke-width="2" opacity=".35"><path d="M40 30v90"/><path d="M95 10v100"/><path d="M340 22v120"/></g>
</svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
<rect width="420" height="520" fill="#101a2a"/>
<rect x="46" y="66" width="328" height="388" rx="18" fill="#29384a"/>
<rect x="74" y="96" width="138" height="110" fill="#c79b5a"/>
<rect x="234" y="96" width="110" height="230" fill="#172130"/>
<rect x="94" y="250" width="112" height="56" fill="#5e4234"/>
<rect x="103" y="230" width="84" height="28" fill="#f0d2a5"/>
<path d="M72 398c75-36 175-34 276 0" fill="none" stroke="#d7a85d" stroke-width="5" opacity=".45"/>
<circle cx="296" cy="388" r="34" fill="#d7a85d" opacity=".28"/>
</svg>

After

Width:  |  Height:  |  Size: 616 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
<rect width="420" height="520" fill="#091522"/>
<rect x="50" y="82" width="320" height="354" rx="16" fill="#1c3341"/>
<circle cx="146" cy="286" r="64" fill="#0e1b25" stroke="#9db7c9" stroke-width="8"/>
<circle cx="146" cy="286" r="34" fill="#5e8fa0" opacity=".65"/>
<rect x="232" y="222" width="86" height="130" rx="8" fill="#d7a85d" opacity=".72"/>
<path d="M80 148h260" stroke="#d8e7f2" stroke-width="5" opacity=".35"/>
<path d="M96 164c42 24 86 24 132 0" stroke="#f2d8a4" stroke-width="6" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 590 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
<rect width="420" height="520" fill="#07111f"/>
<path d="M64 90h292v360H64z" fill="#162333"/>
<path d="M86 424h248M108 380h226M130 336h204M152 292h182M174 248h160" stroke="#8da8bf" stroke-width="18" opacity=".55"/>
<path d="M246 80v390" stroke="#d7a85d" stroke-width="8" opacity=".7"/>
<rect x="270" y="112" width="54" height="92" fill="#c9904f" opacity=".68"/>
<path d="M0 520L420 80v440z" fill="#000" opacity=".22"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@ -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 = `
<div class="story-location__image"><img alt="${escapeHtml(scene.location.name)} prototype location" /></div>
<div>
<h2></h2>
<p></p>
</div>`;
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 = `<span>Scene ${item.sceneNumber}</span><strong></strong><small></small>`;
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 = `<span></span>`;
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 = `<span></span><strong></strong><p></p>`;
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 = `
<div class="story-character__image"><img loading="eager" /></div>
<div class="story-character__text">
<strong data-character-name></strong>
<span data-character-role></span>
</div>`;
return node;
}
function createAssetNode(asset) {
const node = document.createElement("article");
node.className = "story-asset";
node.dataset.assetId = asset.id;
node.innerHTML = `
<div class="story-asset__image"><img loading="eager" /></div>
<div>
<strong data-asset-name></strong>
<span data-asset-label></span>
</div>`;
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) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#039;"
}[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();
})();

View File

@ -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.