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=\"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=\"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("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("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("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.");
|
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(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(".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("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(".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: 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(!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(!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.");
|
Assert(progressViewModel.Contains("public int? ImportSessionID", StringComparison.Ordinal), "Import progress page should carry the import session for replay links.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,27 +84,61 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Unresolved character appearance</legend>
|
<legend>Unresolved character appearance</legend>
|
||||||
<p>Explicit manuscript evidence always wins. This only guides characters whose appearance is not described.</p>
|
<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>
|
||||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.Balanced" checked /> Balanced diverse cast</label>
|
Appearance baseline
|
||||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyLight" /> Predominantly light-skinned cast</label>
|
<select class="form-control" name="appearancePreference">
|
||||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyMedium" /> Predominantly medium-skinned cast</label>
|
<option value="@StoryMemoryAppearancePreferences.Balanced">Balanced diverse cast</option>
|
||||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyDark" /> Predominantly dark-skinned cast</label>
|
<option value="@StoryMemoryAppearancePreferences.ExplicitEvidenceOnly">Explicit evidence only</option>
|
||||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.CustomMix" /> Custom mix</label>
|
<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>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Story context for generated portraits</legend>
|
<legend>Story context for generated portraits</legend>
|
||||||
<p>These guide generated images when the manuscript is silent. Explicit manuscript evidence still wins.</p>
|
<p>These guide generated images when the manuscript is silent. Explicit manuscript evidence still wins.</p>
|
||||||
<label>
|
<label>
|
||||||
Primary character age group
|
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>
|
||||||
<label>
|
<label>
|
||||||
Era
|
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>
|
||||||
<label>
|
<label>
|
||||||
Location
|
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>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button class="btn btn-primary" type="submit" disabled="@(!Model.CanStart)">Analyse manuscript</button>
|
<button class="btn btn-primary" type="submit" disabled="@(!Model.CanStart)">Analyse manuscript</button>
|
||||||
|
|||||||
@ -287,16 +287,26 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-rebuild {
|
.story-exp-rebuild {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 66px;
|
||||||
|
z-index: 18;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 9px;
|
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: 1px solid rgba(151, 185, 218, 0.12);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: rgba(4, 12, 24, 0.74);
|
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 {
|
.story-exp-rebuild label {
|
||||||
@ -307,6 +317,7 @@ input:focus-visible {
|
|||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-rebuild select,
|
.story-exp-rebuild select,
|
||||||
@ -319,7 +330,7 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-rebuild select {
|
.story-exp-rebuild select {
|
||||||
max-width: min(280px, 40vw);
|
max-width: min(240px, 24vw);
|
||||||
padding: 6px 22px 6px 9px;
|
padding: 6px 22px 6px 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -886,6 +897,66 @@ input:focus-visible {
|
|||||||
opacity: 0.9;
|
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 {
|
.story-location {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -994,6 +1065,30 @@ input:focus-visible {
|
|||||||
text-align: center;
|
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 strong,
|
||||||
.story-asset span {
|
.story-asset span {
|
||||||
display: block;
|
display: block;
|
||||||
@ -1013,6 +1108,15 @@ input:focus-visible {
|
|||||||
font-size: 0.64rem;
|
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 {
|
.story-exp-ribbon-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(104px, auto) 56px;
|
grid-template-rows: auto minmax(104px, auto) 56px;
|
||||||
|
|||||||
@ -231,6 +231,13 @@
|
|||||||
function setEntityImage(image, url, resolution, altText) {
|
function setEntityImage(image, url, resolution, altText) {
|
||||||
if (!image) return;
|
if (!image) return;
|
||||||
image.alt = altText;
|
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 fallbackUrl = resolution?.fallbackImageUrl || url;
|
||||||
const nextUrl = failedImageUrls.has(url) ? fallbackUrl : url;
|
const nextUrl = failedImageUrls.has(url) ? fallbackUrl : url;
|
||||||
image.dataset.fallbackUrl = fallbackUrl || "";
|
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) {
|
function preloadSceneImages(scene) {
|
||||||
if (!scene) return;
|
if (!scene) return;
|
||||||
collectSceneImages(scene).forEach((url) => {
|
collectSceneImages(scene).forEach((url) => {
|
||||||
@ -319,10 +435,13 @@
|
|||||||
node.style.setProperty("--opacity", String(position.opacity));
|
node.style.setProperty("--opacity", String(position.opacity));
|
||||||
node.style.setProperty("--z", String(position.z));
|
node.style.setProperty("--z", String(position.z));
|
||||||
node.dataset.layoutRole = position.layoutRole;
|
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);
|
text(node.querySelector("[data-character-name]"), character.name);
|
||||||
const label = conciseCharacterLabel(character, character.id === scene.povCharacterId);
|
const label = conciseCharacterLabel(character, character.id === scene.povCharacterId);
|
||||||
text(node.querySelector("[data-character-role]"), label);
|
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}` : ""}`;
|
node.title = `${character.name}: ${character.role}${character.relevance ? ` - ${character.relevance}` : ""}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -389,9 +508,12 @@
|
|||||||
node.style.setProperty("--scale", String(position.scale));
|
node.style.setProperty("--scale", String(position.scale));
|
||||||
node.style.setProperty("--z", String(position.z));
|
node.style.setProperty("--z", String(position.z));
|
||||||
node.style.setProperty("--opacity", "1");
|
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-name]"), asset.name);
|
||||||
text(node.querySelector("[data-asset-label]"), conciseAssetLabel(asset));
|
text(node.querySelector("[data-asset-label]"), conciseAssetLabel(asset));
|
||||||
|
renderFactChips(node.querySelector("[data-asset-facts]"), assetFacts(asset));
|
||||||
node.title = `${asset.name}: ${asset.label}`;
|
node.title = `${asset.name}: ${asset.label}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -708,6 +830,7 @@
|
|||||||
<div class="story-character__text">
|
<div class="story-character__text">
|
||||||
<strong data-character-name></strong>
|
<strong data-character-name></strong>
|
||||||
<span data-character-role></span>
|
<span data-character-role></span>
|
||||||
|
<div class="story-entity-facts" data-character-facts></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -721,6 +844,7 @@
|
|||||||
<div class="story-asset__text">
|
<div class="story-asset__text">
|
||||||
<strong data-asset-name></strong>
|
<strong data-asset-name></strong>
|
||||||
<span data-asset-label></span>
|
<span data-asset-label></span>
|
||||||
|
<div class="story-entity-facts" data-asset-facts></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user