Phase 10H: Scene Links Editing

This commit is contained in:
Nick Beckley 2026-06-13 20:50:41 +01:00
parent 8d6399358e
commit 500c70cb58
3 changed files with 168 additions and 32 deletions

View File

@ -110,17 +110,22 @@
</section>
<section class="word-companion-section word-companion-link-groups" data-plotdirector-link-groups hidden aria-label="Scene Links">
<div class="word-companion-section-header">
<span>Scene Links</span>
<button type="button" data-save-scene-links disabled>Save Scene Links</button>
</div>
<p class="word-companion-message" data-scene-links-status>No resolved PlotDirector scene.</p>
<div>
<span>Characters</span>
<div class="word-companion-chip-list" data-character-chips></div>
<div class="word-companion-check-list" data-character-chips></div>
</div>
<div>
<span>Assets</span>
<div class="word-companion-chip-list" data-asset-chips></div>
<div class="word-companion-check-list" data-asset-chips></div>
</div>
<div>
<span>Locations</span>
<div class="word-companion-chip-list" data-location-chips></div>
<div class="word-companion-check-list" data-location-chips></div>
</div>
</section>

View File

@ -284,27 +284,48 @@ body {
white-space: pre-wrap;
}
.word-companion-chip-list {
display: flex;
flex-wrap: wrap;
.word-companion-check-list {
display: grid;
gap: 5px;
}
.word-companion-chip,
.word-companion-empty-chip {
border: 1px solid rgba(47, 111, 99, 0.25);
border-radius: 999px;
padding: 3px 7px;
background: var(--companion-soft);
color: var(--companion-accent-dark);
font-size: 0.82rem;
font-weight: 700;
.word-companion-check-item {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 7px;
min-height: 30px;
border: 1px solid rgba(47, 111, 99, 0.18);
border-radius: 6px;
padding: 5px 7px;
background: #ffffff;
color: var(--companion-ink);
font-size: 0.9rem;
}
.word-companion-empty-chip {
border-color: var(--companion-line);
color: var(--companion-muted);
.word-companion-check-item input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--companion-accent);
}
.word-companion-check-item span {
color: var(--companion-ink);
font-size: 0.9rem;
font-weight: 600;
text-transform: none;
}
.word-companion-check-item:has(input:disabled) {
background: var(--companion-soft);
color: var(--companion-muted);
}
.word-companion-empty-list {
margin: 0;
color: var(--companion-muted);
font-size: 0.88rem;
}
.word-companion-outline {

View File

@ -43,6 +43,8 @@
const characterChips = document.querySelector("[data-character-chips]");
const assetChips = document.querySelector("[data-asset-chips]");
const locationChips = document.querySelector("[data-location-chips]");
const saveSceneLinksButton = document.querySelector("[data-save-scene-links]");
const sceneLinksStatus = document.querySelector("[data-scene-links-status]");
const resolveProjectId = document.querySelector("[data-resolve-project-id]");
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
const resolveBookId = document.querySelector("[data-resolve-book-id]");
@ -75,6 +77,11 @@
let detectedSceneTitle = "";
let detectedSceneWordCount = null;
let resolvedSceneId = null;
let currentSceneLinks = {
characters: [],
assets: [],
locations: []
};
const setOfficeStatus = (message) => {
if (officeStatus) {
@ -147,7 +154,11 @@
if (syncSceneProgressButton) {
syncSceneProgressButton.disabled = !resolvedSceneId;
}
if (saveSceneLinksButton) {
saveSceneLinksButton.disabled = !resolvedSceneId;
}
setSyncStatus(resolvedSceneId ? "Ready to sync." : "No resolved PlotDirector scene.");
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
};
const setRefreshCurrentSceneBusy = (isBusy) => {
@ -167,11 +178,19 @@
return String(value);
};
const setSceneLinksStatus = (message) => {
setText(sceneLinksStatus, message);
};
const resetPlotDirectorScene = (message) => {
setPlotDirectorSceneStatus(message);
setHidden(plotDirectorSceneDetails, true);
setHidden(writingBriefBlock, true);
setHidden(plotDirectorLinkGroups, true);
currentSceneLinks = { characters: [], assets: [], locations: [] };
renderSceneLinkList(characterChips, [], "character");
renderSceneLinkList(assetChips, [], "asset");
renderSceneLinkList(locationChips, [], "location");
setResolvedScene(null);
setText(syncPlotDirectorCount, "-");
setText(plotDirectorSceneTitle, "-");
@ -183,26 +202,53 @@
setHidden(plotDirectorBlockedReasonRow, true);
};
const renderLinkedItems = (container, items) => {
const linkItemId = (item, type) => {
if (type === "asset") {
return item?.assetId || item?.AssetId || item?.storyAssetId || item?.StoryAssetId || 0;
}
if (type === "location") {
return item?.locationId || item?.LocationId || 0;
}
return item?.characterId || item?.CharacterId || 0;
};
const renderSceneLinkList = (container, items, type, enabled = false) => {
if (!container) {
return;
}
container.innerHTML = "";
const linkedItems = Array.isArray(items) ? items.filter((item) => item.linked) : [];
if (linkedItems.length === 0) {
const empty = document.createElement("span");
empty.className = "word-companion-empty-chip";
const rows = Array.isArray(items) ? items : [];
if (rows.length === 0) {
const empty = document.createElement("p");
empty.className = "word-companion-empty-list";
empty.textContent = "None";
container.append(empty);
return;
}
for (const item of linkedItems) {
const chip = document.createElement("span");
chip.className = "word-companion-chip";
chip.textContent = item.name || "Unnamed";
container.append(chip);
for (const item of rows) {
const id = Number.parseInt(linkItemId(item, type), 10);
if (!Number.isInteger(id) || id <= 0) {
continue;
}
const label = document.createElement("label");
label.className = "word-companion-check-item";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = String(id);
checkbox.checked = !!item.linked;
checkbox.disabled = !enabled;
const text = document.createElement("span");
text.textContent = item.name || "Unnamed";
label.append(checkbox, text);
container.append(label);
}
};
@ -226,9 +272,15 @@
writingBrief.textContent = data?.writingBrief || "No writing brief has been added for this scene.";
}
renderLinkedItems(characterChips, data?.characters);
renderLinkedItems(assetChips, data?.assets);
renderLinkedItems(locationChips, data?.locations);
currentSceneLinks = {
characters: Array.isArray(data?.characters) ? data.characters : [],
assets: Array.isArray(data?.assets) ? data.assets : [],
locations: Array.isArray(data?.locations) ? data.locations : []
};
renderSceneLinkList(characterChips, currentSceneLinks.characters, "character", !!resolvedSceneId);
renderSceneLinkList(assetChips, currentSceneLinks.assets, "asset", !!resolvedSceneId);
renderSceneLinkList(locationChips, currentSceneLinks.locations, "location", !!resolvedSceneId);
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
setHidden(plotDirectorSceneDetails, false);
setHidden(writingBriefBlock, false);
setHidden(plotDirectorLinkGroups, false);
@ -711,6 +763,63 @@
body: JSON.stringify(body)
});
const putJson = (url, body) => fetchJson(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const selectedLinkIds = (container) => {
if (!container) {
return [];
}
return [...container.querySelectorAll("input[type='checkbox']:checked")]
.map((checkbox) => Number.parseInt(checkbox.value || "", 10))
.filter((id) => Number.isInteger(id) && id > 0);
};
const updateCurrentSceneLinkState = () => {
const selectedCharacters = new Set(selectedLinkIds(characterChips));
const selectedAssets = new Set(selectedLinkIds(assetChips));
const selectedLocations = new Set(selectedLinkIds(locationChips));
currentSceneLinks = {
characters: currentSceneLinks.characters.map((item) => ({ ...item, linked: selectedCharacters.has(Number.parseInt(linkItemId(item, "character"), 10)) })),
assets: currentSceneLinks.assets.map((item) => ({ ...item, linked: selectedAssets.has(Number.parseInt(linkItemId(item, "asset"), 10)) })),
locations: currentSceneLinks.locations.map((item) => ({ ...item, linked: selectedLocations.has(Number.parseInt(linkItemId(item, "location"), 10)) }))
};
};
const saveSceneLinks = async () => {
if (!resolvedSceneId) {
setSceneLinksStatus("No resolved PlotDirector scene.");
return;
}
if (saveSceneLinksButton) {
saveSceneLinksButton.disabled = true;
saveSceneLinksButton.textContent = "Saving...";
}
try {
await putJson(`/api/word-companion/scenes/${resolvedSceneId}/links`, {
characterIds: selectedLinkIds(characterChips),
assetIds: selectedLinkIds(assetChips),
locationIds: selectedLinkIds(locationChips)
});
updateCurrentSceneLinkState();
setSceneLinksStatus("Scene links saved successfully.");
} catch {
setSceneLinksStatus("Unable to save scene links.");
} finally {
if (saveSceneLinksButton) {
saveSceneLinksButton.disabled = !resolvedSceneId;
saveSceneLinksButton.textContent = "Save Scene Links";
}
}
};
const syncSceneProgress = async () => {
if (!resolvedSceneId) {
setSyncStatus("No resolved PlotDirector scene.");
@ -836,8 +945,8 @@
}
setPlotDirectorSceneStatus("Linked to PlotDirector");
setDocumentMessage("");
renderCompanionData(companionData);
setResolvedScene(resolvedSceneId);
renderCompanionData(companionData);
return true;
} catch {
resetPlotDirectorScene("Not found in PlotDirector");
@ -985,6 +1094,7 @@
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene);
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
if (isAuthenticated) {