Phase 21B.2 - Story Intelligence Experience Composition Refinement
This commit is contained in:
parent
e3d18b4a6b
commit
57df5646e6
@ -21,6 +21,7 @@
|
||||
<strong>PlotDirector</strong>
|
||||
<span>Story Intelligence</span>
|
||||
</div>
|
||||
<ol class="story-exp-pipeline" data-analysis-stages aria-label="Analysis stages"></ol>
|
||||
<div class="story-exp-context">
|
||||
<span>@Model.ProjectName</span>
|
||||
<span>@Model.BookTitle</span>
|
||||
|
||||
@ -54,7 +54,7 @@ input:focus-visible {
|
||||
.story-exp {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 70px minmax(0, 1fr);
|
||||
grid-template-rows: 66px minmax(0, 1fr);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
isolation: isolate;
|
||||
@ -87,10 +87,10 @@ input:focus-visible {
|
||||
|
||||
.story-exp-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(210px, 0.65fr) minmax(460px, 1.35fr) auto;
|
||||
grid-template-columns: minmax(210px, 0.52fr) minmax(400px, 1.35fr) minmax(290px, 0.78fr) auto;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
padding: 14px 24px;
|
||||
gap: 18px;
|
||||
padding: 10px 24px;
|
||||
border-bottom: 1px solid var(--story-line);
|
||||
background: rgba(4, 10, 20, 0.68);
|
||||
backdrop-filter: blur(18px);
|
||||
@ -102,6 +102,139 @@ input:focus-visible {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.story-exp-pipeline {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(11, minmax(38px, 1fr));
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.story-exp-pipeline::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4%;
|
||||
right: 4%;
|
||||
top: 10px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(109, 181, 255, 0.54), rgba(215, 168, 93, 0.24), rgba(255, 255, 255, 0.18));
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step {
|
||||
position: relative;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
color: rgba(196, 214, 235, 0.54);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step span {
|
||||
z-index: 1;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid rgba(196, 214, 235, 0.36);
|
||||
border-radius: 50%;
|
||||
background: rgba(8, 18, 32, 0.98);
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step strong {
|
||||
overflow: hidden;
|
||||
max-width: 78px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step.is-complete span {
|
||||
border-color: rgba(109, 181, 255, 0.9);
|
||||
background: var(--story-blue);
|
||||
box-shadow: 0 0 14px rgba(109, 181, 255, 0.35);
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step.is-current {
|
||||
color: var(--story-text);
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step.is-current span {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-top: -2px;
|
||||
border-color: rgba(215, 168, 93, 0.9);
|
||||
background: var(--story-gold);
|
||||
box-shadow: 0 0 0 5px rgba(215, 168, 93, 0.12), 0 0 22px rgba(215, 168, 93, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.story-exp-header {
|
||||
grid-template-columns: minmax(188px, 0.44fr) minmax(360px, 1.2fr) minmax(230px, 0.66fr) auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.story-exp-pipeline-step strong {
|
||||
max-width: 45px;
|
||||
}
|
||||
|
||||
.story-exp-context {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.story-exp-context span {
|
||||
max-width: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 820px) and (min-width: 1181px) {
|
||||
.story-exp-shell {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.story-exp-stage {
|
||||
grid-template-rows: auto minmax(0, 1fr) 126px;
|
||||
}
|
||||
|
||||
.story-exp-stage-copy {
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.story-exp-ribbon-wrap {
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.story-exp-ribbon {
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.story-scene-card {
|
||||
flex-basis: 126px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.story-scene-card.is-current {
|
||||
flex-basis: 172px;
|
||||
}
|
||||
|
||||
.story-exp-timeline {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.story-exp-controls {
|
||||
bottom: 10px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.story-exp-brand strong {
|
||||
font-size: 1.12rem;
|
||||
letter-spacing: 0.02em;
|
||||
@ -121,8 +254,8 @@ input:focus-visible {
|
||||
|
||||
.story-exp-context {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@ -154,7 +287,7 @@ input:focus-visible {
|
||||
grid-template-columns: minmax(206px, 0.54fr) minmax(760px, 2.44fr) minmax(214px, 0.56fr);
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 18px 22px 84px;
|
||||
padding: 14px 22px 74px;
|
||||
}
|
||||
|
||||
.story-exp-left,
|
||||
@ -363,8 +496,8 @@ input:focus-visible {
|
||||
|
||||
.story-exp-stage {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) 166px;
|
||||
gap: 14px;
|
||||
grid-template-rows: auto minmax(0, 1fr) 158px;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@ -373,14 +506,14 @@ input:focus-visible {
|
||||
grid-template-columns: 1fr minmax(240px, 0.72fr);
|
||||
align-items: end;
|
||||
gap: 22px;
|
||||
min-height: 86px;
|
||||
min-height: 62px;
|
||||
}
|
||||
|
||||
.story-exp-stage-copy h1 {
|
||||
grid-column: 1;
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 3vw, 3.8rem);
|
||||
line-height: 1.02;
|
||||
font-size: clamp(1.55rem, 2.35vw, 2.75rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@ -388,38 +521,37 @@ input:focus-visible {
|
||||
grid-column: 2;
|
||||
margin: 0;
|
||||
color: var(--story-soft);
|
||||
font-size: clamp(0.9rem, 1.2vw, 1.05rem);
|
||||
line-height: 1.45;
|
||||
font-size: clamp(0.86rem, 1vw, 1rem);
|
||||
line-height: 1.38;
|
||||
}
|
||||
|
||||
.story-exp-visual {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(470px, 1.22fr) minmax(320px, 0.9fr) minmax(238px, 0.58fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
display: block;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(151, 185, 218, 0.11);
|
||||
border-radius: 30px;
|
||||
padding: 22px;
|
||||
border: 1px solid rgba(151, 185, 218, 0.045);
|
||||
border-radius: 24px;
|
||||
padding: 0;
|
||||
background:
|
||||
radial-gradient(circle at 22% 40%, rgba(109, 181, 255, 0.18), transparent 24rem),
|
||||
radial-gradient(circle at 58% 49%, rgba(215, 168, 93, 0.15), transparent 20rem),
|
||||
radial-gradient(circle at 86% 30%, rgba(227, 140, 168, 0.08), transparent 18rem),
|
||||
linear-gradient(180deg, rgba(11, 24, 43, 0.66), rgba(4, 10, 20, 0.54));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), var(--story-shadow);
|
||||
radial-gradient(circle at 20% 22%, rgba(109, 181, 255, 0.14), transparent 16rem),
|
||||
radial-gradient(circle at 58% 46%, rgba(109, 181, 255, 0.13), transparent 18rem),
|
||||
radial-gradient(circle at 78% 55%, rgba(215, 168, 93, 0.11), transparent 18rem),
|
||||
linear-gradient(180deg, rgba(11, 24, 43, 0.34), rgba(4, 10, 20, 0.18));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025), 0 22px 72px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.story-exp-visual::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 28px 34px;
|
||||
border-radius: 28px;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background:
|
||||
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent),
|
||||
linear-gradient(180deg, transparent, rgba(109, 181, 255, 0.04), transparent);
|
||||
opacity: 0.52;
|
||||
radial-gradient(circle, rgba(255, 255, 255, 0.28) 0 1px, transparent 1.6px),
|
||||
linear-gradient(122deg, transparent 0 34%, rgba(109, 181, 255, 0.08) 34.05% 34.18%, transparent 34.28%),
|
||||
linear-gradient(28deg, transparent 0 62%, rgba(215, 168, 93, 0.08) 62.05% 62.18%, transparent 62.28%);
|
||||
background-size: 112px 112px, auto, auto;
|
||||
opacity: 0.34;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -433,30 +565,93 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-exp-connection {
|
||||
stroke: rgba(215, 168, 93, 0.48);
|
||||
stroke: rgba(215, 168, 93, 0.5);
|
||||
stroke-linecap: round;
|
||||
stroke-width: 2;
|
||||
stroke-width: 1.8;
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
filter: drop-shadow(0 0 8px rgba(215, 168, 93, 0.28));
|
||||
animation: drawLine 900ms ease forwards;
|
||||
fill: none;
|
||||
filter: drop-shadow(0 0 7px rgba(215, 168, 93, 0.22));
|
||||
animation: drawLine 950ms cubic-bezier(.16, .84, .28, 1) forwards;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--presence .story-exp-connection,
|
||||
.story-exp-connection-group--presence-soft .story-exp-connection {
|
||||
stroke: rgba(125, 193, 255, 0.58);
|
||||
}
|
||||
|
||||
.story-exp-connection-group--presence-soft .story-exp-connection,
|
||||
.story-exp-connection-group--knowledge .story-exp-connection,
|
||||
.story-exp-connection-group--asset-link .story-exp-connection {
|
||||
stroke-dasharray: 0.12 0.08;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--support .story-exp-connection {
|
||||
stroke: rgba(121, 210, 166, 0.64);
|
||||
}
|
||||
|
||||
.story-exp-connection-group--conflict .story-exp-connection {
|
||||
stroke: rgba(227, 140, 168, 0.76);
|
||||
stroke-dasharray: 0.18 0.1;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--uncertain .story-exp-connection {
|
||||
stroke: rgba(182, 162, 255, 0.56);
|
||||
stroke-dasharray: 0.08 0.09;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--knowledge .story-exp-connection {
|
||||
stroke: rgba(182, 162, 255, 0.5);
|
||||
}
|
||||
|
||||
.story-exp-connection-label {
|
||||
paint-order: stroke;
|
||||
stroke: rgba(4, 10, 20, 0.86);
|
||||
stroke-width: 5px;
|
||||
fill: var(--story-soft);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: lowercase;
|
||||
filter: drop-shadow(0 0 9px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
.story-exp-zone {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.story-exp-zone-label {
|
||||
margin: 0 0 8px;
|
||||
opacity: 0.72;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
color: rgba(158, 177, 199, 0.54);
|
||||
font-size: 0.64rem;
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.story-exp-zone--characters .story-exp-zone-label {
|
||||
left: 24px;
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
.story-exp-zone--location .story-exp-zone-label {
|
||||
left: 55%;
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
.story-exp-zone--assets .story-exp-zone-label {
|
||||
right: 8%;
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
.story-exp-characters {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.story-character {
|
||||
@ -466,6 +661,7 @@ input:focus-visible {
|
||||
width: var(--node-size, 150px);
|
||||
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||
opacity: var(--opacity, 1);
|
||||
pointer-events: auto;
|
||||
transition: transform 1050ms cubic-bezier(.16, .84, .28, 1), opacity 780ms ease;
|
||||
}
|
||||
|
||||
@ -478,9 +674,9 @@ input:focus-visible {
|
||||
.story-asset__image {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(169, 205, 240, 0.2);
|
||||
border: 1px solid rgba(169, 205, 240, 0.36);
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
box-shadow: 0 24px 54px rgba(0, 0, 0, 0.28);
|
||||
box-shadow: 0 24px 54px rgba(0, 0, 0, 0.32), 0 0 28px rgba(109, 181, 255, 0.08);
|
||||
}
|
||||
|
||||
.story-character__image {
|
||||
@ -499,7 +695,7 @@ input:focus-visible {
|
||||
|
||||
.story-character.is-pov .story-character__image {
|
||||
border-color: rgba(215, 168, 93, 0.82);
|
||||
box-shadow: 0 0 0 7px rgba(215, 168, 93, 0.1), 0 34px 78px rgba(215, 168, 93, 0.2);
|
||||
box-shadow: 0 0 0 7px rgba(215, 168, 93, 0.1), 0 34px 78px rgba(215, 168, 93, 0.22), 0 0 36px rgba(215, 168, 93, 0.22);
|
||||
}
|
||||
|
||||
.story-character__text {
|
||||
@ -521,12 +717,14 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-location {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 14px;
|
||||
height: calc(100% - 28px);
|
||||
padding: 8px 0;
|
||||
transition: opacity 560ms ease, transform 800ms cubic-bezier(.16, .84, .28, 1);
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
width: var(--node-size, 190px);
|
||||
transform: translate(var(--x, 0), var(--y, 0));
|
||||
pointer-events: auto;
|
||||
transition: opacity 560ms ease, transform 850ms cubic-bezier(.16, .84, .28, 1);
|
||||
}
|
||||
|
||||
.story-location.is-changing {
|
||||
@ -535,11 +733,11 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-location__image {
|
||||
width: 100%;
|
||||
max-height: 360px;
|
||||
aspect-ratio: 4 / 5;
|
||||
border-radius: 32px;
|
||||
box-shadow: 0 28px 78px rgba(0, 0, 0, 0.34), 0 0 54px rgba(109, 181, 255, 0.1);
|
||||
width: var(--node-size, 190px);
|
||||
height: var(--node-size, 190px);
|
||||
border-radius: 50%;
|
||||
border-color: rgba(125, 193, 255, 0.82);
|
||||
box-shadow: 0 0 0 7px rgba(109, 181, 255, 0.1), 0 30px 82px rgba(0, 0, 0, 0.38), 0 0 58px rgba(109, 181, 255, 0.22);
|
||||
}
|
||||
|
||||
.story-location h2,
|
||||
@ -548,35 +746,47 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-location h2 {
|
||||
font-size: clamp(1.45rem, 2vw, 2.1rem);
|
||||
font-size: clamp(1.12rem, 1.55vw, 1.55rem);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-location p {
|
||||
color: var(--story-muted);
|
||||
font-size: 0.86rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-exp-assets {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 14px;
|
||||
height: calc(100% - 28px);
|
||||
padding-top: 24px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.story-asset {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
grid-template-columns: 66px 1fr;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
justify-items: center;
|
||||
gap: 7px;
|
||||
width: max(var(--node-size, 86px), 112px);
|
||||
opacity: var(--opacity, 1);
|
||||
transform: translateY(var(--y, 0)) scale(var(--scale, 1));
|
||||
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||
pointer-events: auto;
|
||||
transition: transform 860ms cubic-bezier(.16, .84, .28, 1), opacity 620ms ease;
|
||||
}
|
||||
|
||||
.story-asset__image {
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
border-radius: 17px;
|
||||
width: var(--node-size, 86px);
|
||||
height: var(--node-size, 86px);
|
||||
border-radius: 50%;
|
||||
border-color: rgba(215, 168, 93, 0.68);
|
||||
box-shadow: 0 0 0 5px rgba(215, 168, 93, 0.08), 0 22px 48px rgba(0, 0, 0, 0.3), 0 0 28px rgba(215, 168, 93, 0.14);
|
||||
}
|
||||
|
||||
.story-asset__text {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-asset strong,
|
||||
@ -586,7 +796,7 @@ input:focus-visible {
|
||||
|
||||
.story-asset span {
|
||||
color: var(--story-muted);
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.story-exp-ribbon-wrap {
|
||||
@ -781,13 +991,29 @@ input:focus-visible {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.story-exp-header,
|
||||
.story-exp-shell,
|
||||
.story-exp-stage-copy,
|
||||
.story-exp-visual {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.story-exp-header {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.story-exp-pipeline {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.story-exp-context {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 3;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.story-exp-shell {
|
||||
padding-bottom: 110px;
|
||||
}
|
||||
|
||||
@ -14,9 +14,23 @@
|
||||
intervalMs: 6200,
|
||||
timer: null
|
||||
};
|
||||
const analysisStages = [
|
||||
{ key: "reading", label: "Reading" },
|
||||
{ key: "chapters", label: "Chapters" },
|
||||
{ key: "scenes", label: "Scenes" },
|
||||
{ key: "content", label: "Content" },
|
||||
{ key: "characters", label: "Characters" },
|
||||
{ key: "locations", label: "Locations" },
|
||||
{ key: "assets", label: "Assets" },
|
||||
{ key: "relationships", label: "Relationships" },
|
||||
{ key: "knowledge", label: "Knowledge" },
|
||||
{ key: "continuity", label: "Continuity" },
|
||||
{ key: "finalising", label: "Finalising" }
|
||||
];
|
||||
|
||||
const dom = {
|
||||
stageLabel: root.querySelector("[data-stage-label]"),
|
||||
stageRail: root.querySelector("[data-analysis-stages]"),
|
||||
currentScene: root.querySelector("[data-current-scene]"),
|
||||
progressPercent: root.querySelector("[data-progress-percent]"),
|
||||
elapsedTime: root.querySelector("[data-elapsed-time]"),
|
||||
@ -53,6 +67,7 @@
|
||||
const current = loadSceneState(state.index);
|
||||
|
||||
updateProgress(current);
|
||||
updateAnalysisStages(current);
|
||||
updateSceneCopy(current);
|
||||
reconcileCharacters(current, previous);
|
||||
reconcileLocation(current, previous);
|
||||
@ -77,6 +92,37 @@
|
||||
if (dom.progressRing) dom.progressRing.style.setProperty("--progress-value", `${scene.progressPercent * 3.6}deg`);
|
||||
}
|
||||
|
||||
function updateAnalysisStages(scene) {
|
||||
if (!dom.stageRail) return;
|
||||
const currentIndex = stageIndex(scene);
|
||||
dom.stageRail.innerHTML = "";
|
||||
analysisStages.forEach((stage, index) => {
|
||||
const node = document.createElement("li");
|
||||
node.className = "story-exp-pipeline-step";
|
||||
node.classList.toggle("is-complete", index < currentIndex || scene.progressPercent >= 100);
|
||||
node.classList.toggle("is-current", index === currentIndex && scene.progressPercent < 100);
|
||||
node.classList.toggle("is-future", index > currentIndex && scene.progressPercent < 100);
|
||||
node.innerHTML = "<span></span><strong></strong>";
|
||||
text(node.querySelector("strong"), stage.label);
|
||||
dom.stageRail.append(node);
|
||||
});
|
||||
}
|
||||
|
||||
function stageIndex(scene) {
|
||||
const stage = String(scene.stage || "").toLowerCase();
|
||||
if (scene.progressPercent >= 100 || stage.includes("review")) return analysisStages.length - 1;
|
||||
if (stage.includes("continuity")) return 9;
|
||||
if (stage.includes("knowledge")) return 8;
|
||||
if (stage.includes("relationship")) return 7;
|
||||
if (stage.includes("asset")) return 6;
|
||||
if (stage.includes("location")) return 5;
|
||||
if (stage.includes("character")) return 4;
|
||||
if (stage.includes("content") || stage.includes("inferring")) return 3;
|
||||
if (stage.includes("scene")) return 2;
|
||||
if (stage.includes("chapter")) return 1;
|
||||
return Math.max(0, Math.min(analysisStages.length - 1, Math.floor(scene.progressPercent / 10)));
|
||||
}
|
||||
|
||||
function updateSceneCopy(scene) {
|
||||
text(dom.sceneTitle, scene.title);
|
||||
text(dom.sceneSummary, scene.summary);
|
||||
@ -108,7 +154,10 @@
|
||||
if (!activeIds.has(node.dataset.characterId)) {
|
||||
node.style.setProperty("--opacity", "0");
|
||||
node.style.setProperty("--scale", "0.86");
|
||||
window.setTimeout(() => node.remove(), reducedMotion ? 0 : 620);
|
||||
window.setTimeout(() => {
|
||||
const currentIds = new Set(loadSceneState(state.index).characters.map((character) => character.id));
|
||||
if (!currentIds.has(node.dataset.characterId)) node.remove();
|
||||
}, reducedMotion ? 0 : 620);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -126,14 +175,19 @@
|
||||
node.dataset.locationId = scene.location.id;
|
||||
node.innerHTML = `
|
||||
<div class="story-location__image"><img alt="${escapeHtml(scene.location.name)} prototype location" /></div>
|
||||
<div>
|
||||
<div class="story-location__text">
|
||||
<h2></h2>
|
||||
<p></p>
|
||||
</div>`;
|
||||
const position = locationPosition();
|
||||
node.style.setProperty("--node-size", `${position.size}px`);
|
||||
node.style.setProperty("--x", `${position.x}px`);
|
||||
node.style.setProperty("--y", `${position.y}px`);
|
||||
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);
|
||||
scheduleConnectionDraw();
|
||||
}, changed && !reducedMotion ? 260 : 0);
|
||||
}
|
||||
|
||||
@ -146,8 +200,11 @@
|
||||
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));
|
||||
const position = assetPosition(asset, index, scene.assets.length);
|
||||
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", "1");
|
||||
node.querySelector("img").src = asset.imagePath;
|
||||
node.querySelector("img").alt = `${asset.name} prototype asset`;
|
||||
@ -159,7 +216,10 @@
|
||||
if (!activeIds.has(node.dataset.assetId)) {
|
||||
node.style.setProperty("--opacity", "0");
|
||||
node.style.setProperty("--scale", "0.8");
|
||||
window.setTimeout(() => node.remove(), reducedMotion ? 0 : 520);
|
||||
window.setTimeout(() => {
|
||||
const currentIds = new Set(loadSceneState(state.index).assets.map((asset) => asset.id));
|
||||
if (!currentIds.has(node.dataset.assetId)) node.remove();
|
||||
}, reducedMotion ? 0 : 520);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -272,35 +332,80 @@
|
||||
if (!dom.connections || !dom.visualStage) return;
|
||||
const stageRect = dom.visualStage.getBoundingClientRect();
|
||||
dom.connections.innerHTML = "";
|
||||
const drawn = [];
|
||||
|
||||
scene.relationships.forEach((relationship) => {
|
||||
scene.relationships.forEach((relationship, index) => {
|
||||
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 start = connectionPoint(source, stageRect, target);
|
||||
const end = connectionPoint(target, stageRect, source);
|
||||
drawn.push(drawConnection(start, end, relationshipTone(relationship), relationship.weight, index === 0 ? inlineRelationshipLabel(relationship) : ""));
|
||||
});
|
||||
|
||||
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);
|
||||
drawn.push(drawConnection(connectionPoint(pov, stageRect, location), connectionPoint(location, stageRect, pov), "presence", 72, ""));
|
||||
}
|
||||
|
||||
scene.characters
|
||||
.filter((character) => character.id !== scene.povCharacterId && character.weight >= 65)
|
||||
.slice(0, 2)
|
||||
.forEach((character) => {
|
||||
const node = dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`);
|
||||
if (node && location) {
|
||||
drawn.push(drawConnection(connectionPoint(node, stageRect, location), connectionPoint(location, stageRect, node), "presence-soft", character.weight, ""));
|
||||
}
|
||||
});
|
||||
|
||||
scene.assets.forEach((asset, index) => {
|
||||
const assetNode = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`);
|
||||
if (!assetNode || !location) return;
|
||||
drawn.push(drawConnection(connectionPoint(location, stageRect, assetNode), connectionPoint(assetNode, stageRect, location), "evidence", asset.weight, index === 0 ? assetConnectionLabel(asset) : ""));
|
||||
if (pov && index < 2) {
|
||||
drawn.push(drawConnection(connectionPoint(pov, stageRect, assetNode), connectionPoint(assetNode, stageRect, pov), "knowledge", Math.max(45, asset.weight - 18), ""));
|
||||
}
|
||||
});
|
||||
|
||||
if (scene.assets.length > 1) {
|
||||
const first = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(scene.assets[0].id)}"]`);
|
||||
const second = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(scene.assets[1].id)}"]`);
|
||||
if (first && second) {
|
||||
drawn.push(drawConnection(connectionPoint(first, stageRect, second), connectionPoint(second, stageRect, first), "asset-link", 48, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawConnection(start, end, tone, weight, label) {
|
||||
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.setAttribute("class", `story-exp-connection-group story-exp-connection-group--${tone}`);
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const curve = Math.max(34, Math.min(126, Math.abs(dx) * 0.24 + Math.abs(dy) * 0.1));
|
||||
const bend = start.x < end.x ? curve : -curve;
|
||||
const d = `M ${start.x} ${start.y} C ${start.x + bend} ${start.y - curve * 0.28}, ${end.x - bend} ${end.y + curve * 0.28}, ${end.x} ${end.y}`;
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("pathLength", "1");
|
||||
path.setAttribute("class", "story-exp-connection");
|
||||
path.style.strokeWidth = String(1.1 + Math.min(weight, 100) / 56);
|
||||
group.append(path);
|
||||
|
||||
if (label) {
|
||||
const labelNode = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
labelNode.setAttribute("class", "story-exp-connection-label");
|
||||
labelNode.setAttribute("x", String((start.x + end.x) / 2));
|
||||
labelNode.setAttribute("y", String((start.y + end.y) / 2 - 8));
|
||||
labelNode.setAttribute("text-anchor", "middle");
|
||||
labelNode.textContent = label;
|
||||
group.append(labelNode);
|
||||
}
|
||||
|
||||
dom.connections.append(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
function createCharacterNode(character) {
|
||||
@ -322,7 +427,7 @@
|
||||
node.dataset.assetId = asset.id;
|
||||
node.innerHTML = `
|
||||
<div class="story-asset__image"><img loading="eager" /></div>
|
||||
<div>
|
||||
<div class="story-asset__text">
|
||||
<strong data-asset-name></strong>
|
||||
<span data-asset-label></span>
|
||||
</div>`;
|
||||
@ -330,15 +435,15 @@
|
||||
}
|
||||
|
||||
function characterPosition(character, index, povId) {
|
||||
const bounds = dom.characters?.getBoundingClientRect();
|
||||
const width = Math.max(520, bounds?.width || 620);
|
||||
const height = Math.max(520, bounds?.height || 620);
|
||||
const povSize = Math.min(238, Math.max(206, width * 0.34));
|
||||
const bounds = dom.visualStage?.getBoundingClientRect();
|
||||
const width = Math.max(820, bounds?.width || 1040);
|
||||
const height = Math.max(500, bounds?.height || 560);
|
||||
const povSize = Math.min(210, Math.max(166, width * 0.16));
|
||||
|
||||
if (character.id === povId) {
|
||||
return {
|
||||
x: Math.round(width * 0.36 - povSize / 2),
|
||||
y: Math.round(height * 0.46 - povSize / 2),
|
||||
x: Math.round(width * 0.25 - povSize / 2),
|
||||
y: Math.round(height * 0.25 - povSize / 2),
|
||||
size: Math.round(povSize),
|
||||
scale: 1,
|
||||
opacity: 1
|
||||
@ -348,14 +453,14 @@
|
||||
const important = character.weight >= 70;
|
||||
const positions = important
|
||||
? [
|
||||
{ x: width * 0.08, y: height * 0.18 },
|
||||
{ x: width * 0.62, y: height * 0.15 },
|
||||
{ x: width * 0.58, y: height * 0.58 }
|
||||
{ x: width * 0.12, y: height * 0.16 },
|
||||
{ x: width * 0.38, y: height * 0.38 },
|
||||
{ x: width * 0.16, y: height * 0.56 }
|
||||
]
|
||||
: [
|
||||
{ x: width * 0.12, y: height * 0.62 },
|
||||
{ x: width * 0.73, y: height * 0.48 },
|
||||
{ x: width * 0.06, y: height * 0.42 }
|
||||
{ x: width * 0.08, y: height * 0.45 },
|
||||
{ x: width * 0.39, y: height * 0.66 },
|
||||
{ x: width * 0.18, y: height * 0.34 }
|
||||
];
|
||||
const slot = positions[index % positions.length];
|
||||
const size = important
|
||||
@ -370,6 +475,38 @@
|
||||
};
|
||||
}
|
||||
|
||||
function locationPosition() {
|
||||
const bounds = dom.visualStage?.getBoundingClientRect();
|
||||
const width = Math.max(820, bounds?.width || 1040);
|
||||
const height = Math.max(500, bounds?.height || 560);
|
||||
const size = Math.min(220, Math.max(176, width * 0.18));
|
||||
return {
|
||||
x: Math.round(width * 0.62 - size / 2),
|
||||
y: Math.round(height * 0.46 - size / 2),
|
||||
size
|
||||
};
|
||||
}
|
||||
|
||||
function assetPosition(asset, index, total) {
|
||||
const bounds = dom.visualStage?.getBoundingClientRect();
|
||||
const width = Math.max(820, bounds?.width || 1040);
|
||||
const height = Math.max(500, bounds?.height || 560);
|
||||
const anchors = [
|
||||
{ x: 0.83, y: 0.26 },
|
||||
{ x: 0.84, y: 0.51 },
|
||||
{ x: 0.82, y: 0.74 },
|
||||
{ x: 0.72, y: 0.19 }
|
||||
];
|
||||
const anchor = anchors[index % anchors.length];
|
||||
const size = Math.min(116, Math.max(76, 64 + Math.min(asset.weight, 100) * 0.42));
|
||||
return {
|
||||
x: Math.round(width * anchor.x - size / 2),
|
||||
y: Math.round(height * anchor.y - size / 2),
|
||||
size: Math.round(size),
|
||||
scale: 1
|
||||
};
|
||||
}
|
||||
|
||||
function centrePoint(node, parentRect) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
return {
|
||||
@ -378,6 +515,45 @@
|
||||
};
|
||||
}
|
||||
|
||||
function connectionPoint(node, parentRect, targetNode) {
|
||||
const centre = centrePoint(node, parentRect);
|
||||
const rect = node.getBoundingClientRect();
|
||||
const target = targetNode ? centrePoint(targetNode, parentRect) : centre;
|
||||
const radius = Math.min(rect.width, rect.height) * 0.44;
|
||||
const angle = Math.atan2(target.y - centre.y, target.x - centre.x);
|
||||
return {
|
||||
x: centre.x + Math.cos(angle) * radius,
|
||||
y: centre.y + Math.sin(angle) * radius
|
||||
};
|
||||
}
|
||||
|
||||
function relationshipTone(relationship) {
|
||||
const stateText = `${relationship.label} ${relationship.state}`.toLowerCase();
|
||||
if (stateText.includes("conflict") || stateText.includes("threat") || stateText.includes("obstruction") || stateText.includes("suspect")) return "conflict";
|
||||
if (stateText.includes("trust") || stateText.includes("ally") || stateText.includes("protect")) return "support";
|
||||
return relationship.weight >= 80 ? "strong" : "uncertain";
|
||||
}
|
||||
|
||||
function inlineRelationshipLabel(relationship) {
|
||||
const stateText = `${relationship.label} ${relationship.state}`.toLowerCase();
|
||||
if (stateText.includes("trust")) return "trust strengthened";
|
||||
if (stateText.includes("ally") || stateText.includes("alliance")) return "ally";
|
||||
if (stateText.includes("conflict") || stateText.includes("threat")) return "tension increasing";
|
||||
if (stateText.includes("suspect") || stateText.includes("question")) return "questions";
|
||||
if (stateText.includes("family") || stateText.includes("grief")) return "family";
|
||||
if (stateText.includes("motive")) return "motive exposed";
|
||||
return relationship.state || relationship.label;
|
||||
}
|
||||
|
||||
function assetConnectionLabel(asset) {
|
||||
const value = `${asset.name} ${asset.label}`.toLowerCase();
|
||||
if (value.includes("letter")) return "evidence";
|
||||
if (value.includes("key")) return "access";
|
||||
if (value.includes("tr6") || value.includes("car")) return "blocks escape";
|
||||
if (value.includes("phone")) return "recorded";
|
||||
return "found here";
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
stopTimer();
|
||||
if (!state.playing) return;
|
||||
@ -441,6 +617,8 @@
|
||||
window.addEventListener("resize", () => {
|
||||
const current = loadSceneState(state.index);
|
||||
reconcileCharacters(current);
|
||||
reconcileLocation(current, current);
|
||||
reconcileAssets(current);
|
||||
scheduleConnectionDraw();
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
# Phase 21B.2 - Story Intelligence Experience Composition Refinement
|
||||
|
||||
## Purpose
|
||||
|
||||
Phase 21B.2 refines the development-only Story Intelligence Experience prototype so the Living Story View reads as one connected narrative map rather than separate character, location and asset regions.
|
||||
|
||||
This phase remains visual-only. It does not connect real Story Intelligence data, add SignalR, alter the import workflow, expose production navigation, create SQL, or modify the database.
|
||||
|
||||
## Composition Changes
|
||||
|
||||
- The central map container border and background were greatly softened.
|
||||
- Characters, the active location and significant assets now share a single visual canvas.
|
||||
- The scene title was reduced so the map has more vertical room.
|
||||
- Category labels remain only as faint positional guides.
|
||||
- The background now carries a subtler star/network texture behind the entities.
|
||||
|
||||
## Circular Location
|
||||
|
||||
The active location is now rendered as a circular node with the same broad visual language as character and asset nodes:
|
||||
|
||||
- Circular image mask
|
||||
- Soft illuminated border
|
||||
- Location name and secondary description below the node
|
||||
- Stable `data-location-id` connection target
|
||||
|
||||
The existing prototype SVG location artwork is still used and masked with `object-fit: cover`, so future curated images can replace it without changing the layout contract.
|
||||
|
||||
## Circular Assets
|
||||
|
||||
Assets in the main Living Story View now render as circular image-first nodes:
|
||||
|
||||
- Circular image/avatar
|
||||
- Label and state text underneath
|
||||
- Size influenced by simulated narrative weight
|
||||
- Stable `data-asset-id` connection targets
|
||||
|
||||
The compact discovery feed indicators were left unchanged because they serve a different scanning purpose in the left panel.
|
||||
|
||||
## Connection Lines
|
||||
|
||||
The SVG connection layer now draws multiple current-scene connection categories:
|
||||
|
||||
- Character relationship
|
||||
- Character present at location
|
||||
- Asset found at location
|
||||
- Character knowledge or evidence connection to assets
|
||||
- Asset-to-asset supporting evidence link
|
||||
|
||||
Connections use curved paths and category-specific styling. Supportive, uncertain, conflict, presence, evidence and knowledge lines are distinguishable by colour and line style rather than colour alone.
|
||||
|
||||
## Inline Relationship Labels
|
||||
|
||||
The central map now renders one or two inline relationship labels per scene on selected character-to-character connections. These labels summarize the active relationship state, while the right-hand Relationship Changes panel continues to show the latest detected changes.
|
||||
|
||||
Examples include:
|
||||
|
||||
- `trust strengthened`
|
||||
- `tension increasing`
|
||||
- `family`
|
||||
- `questions`
|
||||
- `motive exposed`
|
||||
|
||||
## Analysis Stage Progress
|
||||
|
||||
A compact analysis-stage rail was added to the header. It advances with simulated scene state and uses live-analysis stage labels:
|
||||
|
||||
- Reading
|
||||
- Chapters
|
||||
- Scenes
|
||||
- Content
|
||||
- Characters
|
||||
- Locations
|
||||
- Assets
|
||||
- Relationships
|
||||
- Knowledge
|
||||
- Continuity
|
||||
- Finalising
|
||||
|
||||
The real Story Intelligence pipeline classes were inspected, but not changed. This rail is prototype-only and maps the existing simulated scene `stage` and `progressPercent` values onto a compact display.
|
||||
|
||||
## Responsive Behaviour
|
||||
|
||||
The main desktop target remains 1920 x 1080 through 1366 x 768. At narrower widths the stage rail can scroll horizontally and the existing stacked layout is retained. Full mobile optimisation remains deferred.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The map positions are still authored prototype positions, not graph-solved layout.
|
||||
- SVG connection labels are intentionally sparse to avoid turning the experience into a dense generic graph.
|
||||
- Location and asset artwork is still prototype SVG artwork, not final curated imagery.
|
||||
- No historical replay/event store exists yet.
|
||||
|
||||
## Deferred
|
||||
|
||||
- Real data binding
|
||||
- Durable Story Intelligence snapshot contract
|
||||
- Production route/navigation
|
||||
- Final image library
|
||||
- Mobile-specific composition pass
|
||||
- Phase 21C real-data integration
|
||||
Loading…
x
Reference in New Issue
Block a user