Add progressive evidence cards to Story Intelligence
This commit is contained in:
parent
f02569b7d4
commit
d7fe153816
@ -1108,6 +1108,10 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
||||
Assert(onboarding.Contains("name=\"primaryAgeGroup\"", StringComparison.Ordinal), "Import setup must ask for primary age group.");
|
||||
Assert(onboarding.Contains("name=\"storyEra\"", StringComparison.Ordinal), "Import setup must ask for era.");
|
||||
Assert(onboarding.Contains("name=\"storyLocation\"", StringComparison.Ordinal), "Import setup must ask for location.");
|
||||
Assert(onboarding.Contains("<select class=\"form-control\" name=\"appearancePreference\"", StringComparison.Ordinal), "Appearance guidance must be a dropdown, not a radio list.");
|
||||
Assert(onboarding.Contains("<select class=\"form-control\" name=\"primaryAgeGroup\"", StringComparison.Ordinal), "Primary age group must be a dropdown.");
|
||||
Assert(onboarding.Contains("<select class=\"form-control\" name=\"storyEra\"", StringComparison.Ordinal), "Story era must be a dropdown.");
|
||||
Assert(onboarding.Contains("<select class=\"form-control\" name=\"storyLocation\"", StringComparison.Ordinal), "Story location must be a dropdown.");
|
||||
Assert(storyMemory.Contains("CharacterAppearanceExtractionService.Extract", StringComparison.Ordinal), "Durable Story Memory must consume dedicated appearance extraction.");
|
||||
Assert(storyMemory.Contains("SkinToneInstruction(skinTone)", StringComparison.Ordinal), "Demand generation must use hard skin-tone wording.");
|
||||
Assert(storyMemory.Contains("HairColours.Red", StringComparison.Ordinal) && storyMemory.Contains("SkinTones.Light", StringComparison.Ordinal), "Red-hair demand evidence must force light skin when skin is otherwise unknown.");
|
||||
@ -1145,9 +1149,13 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly()
|
||||
Assert(script.Contains("if (!realMode || !snapshotUrl || state.terminal) return;", StringComparison.Ordinal), "Replay must not poll live snapshot endpoints.");
|
||||
Assert(css.Contains(".story-exp-replay-browser", StringComparison.Ordinal), "Replay browser styling was not found.");
|
||||
Assert(css.Contains("scrollbar-width: none", StringComparison.Ordinal), "Side-panel native scrollbar tracks should be suppressed.");
|
||||
Assert(css.Contains(".story-exp-rebuild {\n position: fixed;", StringComparison.Ordinal), "Replay rebuild controls should be fixed so they do not resize the whole screen.");
|
||||
Assert(css.Contains(".story-exp-panel:has(.story-exp-insight-list:empty)", StringComparison.Ordinal), "Empty side panels should collapse instead of reserving large blank space.");
|
||||
Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks.");
|
||||
Assert(!css.Contains("grid-template-rows: auto auto minmax(170px, 1fr) minmax(96px, 0.56fr)", StringComparison.Ordinal), "Left rail must not use fixed-height panel tracks.");
|
||||
Assert(script.Contains("shouldUseCharacterPortrait", StringComparison.Ordinal), "Characters should use evidence cards until portrait evidence is strong enough.");
|
||||
Assert(script.Contains("shouldUseAssetImage", StringComparison.Ordinal), "Assets should use text cards until an object-specific image is confident enough.");
|
||||
Assert(script.Contains("appearance pending", StringComparison.Ordinal), "Character evidence cards should expose unresolved appearance state.");
|
||||
Assert(!css.Contains(".story-exp-insight-list {\n display: grid;\n gap: 9px;\n min-height: 0;\n overflow: auto", StringComparison.Ordinal), "Nested insight lists should not create native scrollbars.");
|
||||
Assert(progressViewModel.Contains("public int? ImportSessionID", StringComparison.Ordinal), "Import progress page should carry the import session for replay links.");
|
||||
}
|
||||
|
||||
@ -84,27 +84,61 @@
|
||||
<fieldset>
|
||||
<legend>Unresolved character appearance</legend>
|
||||
<p>Explicit manuscript evidence always wins. This only guides characters whose appearance is not described.</p>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.ExplicitEvidenceOnly" /> Use explicit manuscript evidence only</label>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.Balanced" checked /> Balanced diverse cast</label>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyLight" /> Predominantly light-skinned cast</label>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyMedium" /> Predominantly medium-skinned cast</label>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyDark" /> Predominantly dark-skinned cast</label>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.CustomMix" /> Custom mix</label>
|
||||
<label>
|
||||
Appearance baseline
|
||||
<select class="form-control" name="appearancePreference">
|
||||
<option value="@StoryMemoryAppearancePreferences.Balanced">Balanced diverse cast</option>
|
||||
<option value="@StoryMemoryAppearancePreferences.ExplicitEvidenceOnly">Explicit evidence only</option>
|
||||
<option value="@StoryMemoryAppearancePreferences.PredominantlyLight">Predominantly light-skinned cast</option>
|
||||
<option value="@StoryMemoryAppearancePreferences.PredominantlyMedium">Predominantly medium-skinned cast</option>
|
||||
<option value="@StoryMemoryAppearancePreferences.PredominantlyDark">Predominantly dark-skinned cast</option>
|
||||
<option value="@StoryMemoryAppearancePreferences.CustomMix">Custom mix</option>
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Story context for generated portraits</legend>
|
||||
<p>These guide generated images when the manuscript is silent. Explicit manuscript evidence still wins.</p>
|
||||
<label>
|
||||
Primary character age group
|
||||
<input class="form-control" type="text" name="primaryAgeGroup" placeholder="Late teen / early 20s" />
|
||||
<select class="form-control" name="primaryAgeGroup">
|
||||
<option value="">No baseline</option>
|
||||
<option value="Late teen / early 20s">Late teen / early 20s</option>
|
||||
<option value="Young teen">Young teen</option>
|
||||
<option value="Older teen">Older teen</option>
|
||||
<option value="Young adult">Young adult</option>
|
||||
<option value="Adult">Adult</option>
|
||||
<option value="Middle-aged adult">Middle-aged adult</option>
|
||||
<option value="Senior">Senior</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Era
|
||||
<input class="form-control" type="text" name="storyEra" placeholder="1980s" />
|
||||
<select class="form-control" name="storyEra">
|
||||
<option value="">No baseline</option>
|
||||
<option value="1980s">1980s</option>
|
||||
<option value="1970s">1970s</option>
|
||||
<option value="1990s">1990s</option>
|
||||
<option value="2000s">2000s</option>
|
||||
<option value="Contemporary">Contemporary</option>
|
||||
<option value="Victorian">Victorian</option>
|
||||
<option value="Medieval">Medieval</option>
|
||||
<option value="Near future">Near future</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Location
|
||||
<input class="form-control" type="text" name="storyLocation" placeholder="UK" />
|
||||
<select class="form-control" name="storyLocation">
|
||||
<option value="">No baseline</option>
|
||||
<option value="UK">UK</option>
|
||||
<option value="United States">United States</option>
|
||||
<option value="Europe">Europe</option>
|
||||
<option value="Urban">Urban</option>
|
||||
<option value="Suburban">Suburban</option>
|
||||
<option value="Rural">Rural</option>
|
||||
<option value="Fantasy world">Fantasy world</option>
|
||||
<option value="Science fiction setting">Science fiction setting</option>
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button class="btn btn-primary" type="submit" disabled="@(!Model.CanStart)">Analyse manuscript</button>
|
||||
|
||||
@ -287,16 +287,26 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-exp-rebuild {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 66px;
|
||||
z-index: 18;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 9px;
|
||||
margin: 0 18px 10px;
|
||||
width: clamp(320px, calc(100vw - 520px), 720px);
|
||||
min-height: 42px;
|
||||
max-height: 74px;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(151, 185, 218, 0.12);
|
||||
border-radius: 999px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(4, 12, 24, 0.74);
|
||||
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.22);
|
||||
transform: translateX(-50%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.story-exp-rebuild label {
|
||||
@ -307,6 +317,7 @@ input:focus-visible {
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.story-exp-rebuild select,
|
||||
@ -319,7 +330,7 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-exp-rebuild select {
|
||||
max-width: min(280px, 40vw);
|
||||
max-width: min(240px, 24vw);
|
||||
padding: 6px 22px 6px 9px;
|
||||
}
|
||||
|
||||
@ -886,6 +897,66 @@ input:focus-visible {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card {
|
||||
width: min(172px, calc(var(--node-size, 150px) + 44px));
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card .story-character__image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card .story-character__text {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: min(172px, calc(var(--node-size, 150px) + 44px));
|
||||
min-height: 86px;
|
||||
justify-items: start;
|
||||
gap: 5px;
|
||||
border-color: rgba(169, 205, 240, 0.22);
|
||||
border-radius: 12px;
|
||||
padding: 10px 11px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(16, 34, 58, 0.94), rgba(5, 13, 25, 0.9));
|
||||
text-align: left;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card.is-pov .story-character__text {
|
||||
border-color: rgba(215, 168, 93, 0.68);
|
||||
box-shadow: 0 0 0 5px rgba(215, 168, 93, 0.08), 0 24px 56px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card .story-character__text strong,
|
||||
.story-character.is-evidence-card .story-character__text span {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.story-entity-facts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.story-entity-facts span {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
border: 1px solid rgba(151, 185, 218, 0.12);
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
color: var(--story-soft);
|
||||
font-size: 0.61rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.story-location {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
@ -994,6 +1065,30 @@ input:focus-visible {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card {
|
||||
width: 132px;
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card .story-asset__image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card .story-asset__text {
|
||||
justify-items: start;
|
||||
width: 132px;
|
||||
min-height: 64px;
|
||||
border-color: rgba(215, 168, 93, 0.18);
|
||||
padding: 8px 9px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(34, 29, 18, 0.88), rgba(7, 13, 21, 0.84));
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card .story-asset__text strong,
|
||||
.story-asset.is-evidence-card .story-asset__text span {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.story-asset strong,
|
||||
.story-asset span {
|
||||
display: block;
|
||||
@ -1013,6 +1108,15 @@ input:focus-visible {
|
||||
font-size: 0.64rem;
|
||||
}
|
||||
|
||||
.story-asset .story-entity-facts span {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
border-radius: 999px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.story-exp-ribbon-wrap {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(104px, auto) 56px;
|
||||
|
||||
@ -231,6 +231,13 @@
|
||||
function setEntityImage(image, url, resolution, altText) {
|
||||
if (!image) return;
|
||||
image.alt = altText;
|
||||
if (!url) {
|
||||
image.removeAttribute("src");
|
||||
image.dataset.fallbackUrl = "";
|
||||
image.dataset.entityId = resolution?.entityId || "";
|
||||
image.dataset.libraryCode = resolution?.stableCode || "";
|
||||
return;
|
||||
}
|
||||
const fallbackUrl = resolution?.fallbackImageUrl || url;
|
||||
const nextUrl = failedImageUrls.has(url) ? fallbackUrl : url;
|
||||
image.dataset.fallbackUrl = fallbackUrl || "";
|
||||
@ -249,6 +256,115 @@
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseCharacterPortrait(character) {
|
||||
const resolution = character?.imageResolution || {};
|
||||
if (resolution.fallbackUsed || !resolution.finalImageUrl) return false;
|
||||
const facts = characterVisualFacts(character);
|
||||
if (facts.length < 2) return false;
|
||||
const confidence = Number(resolution.assignmentConfidence || 0);
|
||||
const score = Number(resolution.matchingScore || 0);
|
||||
return confidence >= 0.78 || score >= 82;
|
||||
}
|
||||
|
||||
function shouldUseAssetImage(asset) {
|
||||
const resolution = asset?.imageResolution || {};
|
||||
if (resolution.fallbackUsed || !resolution.finalImageUrl) return false;
|
||||
const confidence = Number(resolution.assignmentConfidence || 0);
|
||||
const score = Number(resolution.matchingScore || 0);
|
||||
const code = String(resolution.stableCode || asset?.libraryCode || "").toLowerCase();
|
||||
if (!code || code.includes("generic") || code.includes("unknown")) return false;
|
||||
return confidence >= 0.86 || score >= 90;
|
||||
}
|
||||
|
||||
function renderFactChips(container, facts) {
|
||||
if (!container) return;
|
||||
container.replaceChildren(...facts.slice(0, 4).map((fact) => {
|
||||
const chip = document.createElement("span");
|
||||
chip.textContent = fact;
|
||||
return chip;
|
||||
}));
|
||||
}
|
||||
|
||||
function characterFacts(character, isPov) {
|
||||
const facts = [];
|
||||
if (isPov) facts.push("POV");
|
||||
facts.push(...characterVisualFacts(character));
|
||||
const role = conciseCharacterLabel(character, isPov);
|
||||
if (role && !facts.some((fact) => sameFact(fact, role))) facts.push(role);
|
||||
if (!facts.length) facts.push(character.weight >= 70 ? "significant" : "identified");
|
||||
if (!shouldUseCharacterPortrait(character)) facts.push("appearance pending");
|
||||
return uniqueFacts(facts);
|
||||
}
|
||||
|
||||
function characterVisualFacts(character) {
|
||||
const resolution = character?.imageResolution || {};
|
||||
const evidence = parseEvidence(resolution.currentEvidence);
|
||||
const facts = [
|
||||
displayEvidenceValue(evidence.age || resolution.resolvedAgeBand),
|
||||
displayEvidenceValue(evidence.presentation || resolution.resolvedPresentation),
|
||||
displayHairFact(evidence.hair, evidence.hairLength),
|
||||
displaySkinFact(evidence.skin)
|
||||
];
|
||||
return uniqueFacts(facts.filter(Boolean));
|
||||
}
|
||||
|
||||
function assetFacts(asset) {
|
||||
const facts = [];
|
||||
const label = conciseAssetLabel(asset);
|
||||
if (label) facts.push(label);
|
||||
if (!shouldUseAssetImage(asset)) facts.push("text card");
|
||||
return uniqueFacts(facts);
|
||||
}
|
||||
|
||||
function parseEvidence(value) {
|
||||
const result = {};
|
||||
String(value || "").split(";").forEach((part) => {
|
||||
const [key, raw] = part.split("=");
|
||||
if (!key || raw === undefined) return;
|
||||
result[key.trim().toLowerCase()] = raw.trim();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function displayEvidenceValue(value) {
|
||||
const clean = String(value || "").trim();
|
||||
if (!clean || clean.toLowerCase() === "unknown") return "";
|
||||
return clean
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.replace(/\bOlder Teen\b/i, "late teen")
|
||||
.replace(/\bYoung Adult\b/i, "young adult")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function displayHairFact(colour, length) {
|
||||
const hairColour = displayEvidenceValue(colour);
|
||||
if (!hairColour) return "";
|
||||
const hairLength = displayEvidenceValue(length);
|
||||
return `${hairLength && hairLength !== "medium" ? `${hairLength} ` : ""}${hairColour} hair`;
|
||||
}
|
||||
|
||||
function displaySkinFact(value) {
|
||||
const skin = displayEvidenceValue(value);
|
||||
return skin ? `${skin} skin` : "";
|
||||
}
|
||||
|
||||
function uniqueFacts(facts) {
|
||||
const seen = new Set();
|
||||
return facts
|
||||
.map((fact) => String(fact || "").trim())
|
||||
.filter((fact) => {
|
||||
if (!fact) return false;
|
||||
const key = fact.toLowerCase();
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function sameFact(a, b) {
|
||||
return String(a || "").trim().toLowerCase() === String(b || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function preloadSceneImages(scene) {
|
||||
if (!scene) return;
|
||||
collectSceneImages(scene).forEach((url) => {
|
||||
@ -319,10 +435,13 @@
|
||||
node.style.setProperty("--opacity", String(position.opacity));
|
||||
node.style.setProperty("--z", String(position.z));
|
||||
node.dataset.layoutRole = position.layoutRole;
|
||||
setEntityImage(node.querySelector("img"), character.imagePath, character.imageResolution, `${character.name} prototype portrait`);
|
||||
const portraitReady = shouldUseCharacterPortrait(character);
|
||||
node.classList.toggle("is-evidence-card", !portraitReady);
|
||||
setEntityImage(node.querySelector("img"), portraitReady ? character.imagePath : "", character.imageResolution, `${character.name} prototype portrait`);
|
||||
text(node.querySelector("[data-character-name]"), character.name);
|
||||
const label = conciseCharacterLabel(character, character.id === scene.povCharacterId);
|
||||
text(node.querySelector("[data-character-role]"), label);
|
||||
renderFactChips(node.querySelector("[data-character-facts]"), characterFacts(character, character.id === scene.povCharacterId));
|
||||
node.title = `${character.name}: ${character.role}${character.relevance ? ` - ${character.relevance}` : ""}`;
|
||||
});
|
||||
|
||||
@ -389,9 +508,12 @@
|
||||
node.style.setProperty("--scale", String(position.scale));
|
||||
node.style.setProperty("--z", String(position.z));
|
||||
node.style.setProperty("--opacity", "1");
|
||||
setEntityImage(node.querySelector("img"), asset.imagePath, asset.imageResolution, `${asset.name} prototype asset`);
|
||||
const assetImageReady = shouldUseAssetImage(asset);
|
||||
node.classList.toggle("is-evidence-card", !assetImageReady);
|
||||
setEntityImage(node.querySelector("img"), assetImageReady ? asset.imagePath : "", asset.imageResolution, `${asset.name} prototype asset`);
|
||||
text(node.querySelector("[data-asset-name]"), asset.name);
|
||||
text(node.querySelector("[data-asset-label]"), conciseAssetLabel(asset));
|
||||
renderFactChips(node.querySelector("[data-asset-facts]"), assetFacts(asset));
|
||||
node.title = `${asset.name}: ${asset.label}`;
|
||||
});
|
||||
|
||||
@ -708,6 +830,7 @@
|
||||
<div class="story-character__text">
|
||||
<strong data-character-name></strong>
|
||||
<span data-character-role></span>
|
||||
<div class="story-entity-facts" data-character-facts></div>
|
||||
</div>`;
|
||||
return node;
|
||||
}
|
||||
@ -721,6 +844,7 @@
|
||||
<div class="story-asset__text">
|
||||
<strong data-asset-name></strong>
|
||||
<span data-asset-label></span>
|
||||
<div class="story-entity-facts" data-asset-facts></div>
|
||||
</div>`;
|
||||
return node;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user