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>
<section class="word-companion-section word-companion-link-groups" data-plotdirector-link-groups hidden aria-label="Scene Links"> <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> <div>
<span>Characters</span> <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>
<div> <div>
<span>Assets</span> <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>
<div> <div>
<span>Locations</span> <span>Locations</span>
<div class="word-companion-chip-list" data-location-chips></div> <div class="word-companion-check-list" data-location-chips></div>
</div> </div>
</section> </section>

View File

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

View File

@ -43,6 +43,8 @@
const characterChips = document.querySelector("[data-character-chips]"); const characterChips = document.querySelector("[data-character-chips]");
const assetChips = document.querySelector("[data-asset-chips]"); const assetChips = document.querySelector("[data-asset-chips]");
const locationChips = document.querySelector("[data-location-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 resolveProjectId = document.querySelector("[data-resolve-project-id]");
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]"); const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
const resolveBookId = document.querySelector("[data-resolve-book-id]"); const resolveBookId = document.querySelector("[data-resolve-book-id]");
@ -75,6 +77,11 @@
let detectedSceneTitle = ""; let detectedSceneTitle = "";
let detectedSceneWordCount = null; let detectedSceneWordCount = null;
let resolvedSceneId = null; let resolvedSceneId = null;
let currentSceneLinks = {
characters: [],
assets: [],
locations: []
};
const setOfficeStatus = (message) => { const setOfficeStatus = (message) => {
if (officeStatus) { if (officeStatus) {
@ -147,7 +154,11 @@
if (syncSceneProgressButton) { if (syncSceneProgressButton) {
syncSceneProgressButton.disabled = !resolvedSceneId; syncSceneProgressButton.disabled = !resolvedSceneId;
} }
if (saveSceneLinksButton) {
saveSceneLinksButton.disabled = !resolvedSceneId;
}
setSyncStatus(resolvedSceneId ? "Ready to sync." : "No resolved PlotDirector scene."); setSyncStatus(resolvedSceneId ? "Ready to sync." : "No resolved PlotDirector scene.");
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
}; };
const setRefreshCurrentSceneBusy = (isBusy) => { const setRefreshCurrentSceneBusy = (isBusy) => {
@ -167,11 +178,19 @@
return String(value); return String(value);
}; };
const setSceneLinksStatus = (message) => {
setText(sceneLinksStatus, message);
};
const resetPlotDirectorScene = (message) => { const resetPlotDirectorScene = (message) => {
setPlotDirectorSceneStatus(message); setPlotDirectorSceneStatus(message);
setHidden(plotDirectorSceneDetails, true); setHidden(plotDirectorSceneDetails, true);
setHidden(writingBriefBlock, true); setHidden(writingBriefBlock, true);
setHidden(plotDirectorLinkGroups, true); setHidden(plotDirectorLinkGroups, true);
currentSceneLinks = { characters: [], assets: [], locations: [] };
renderSceneLinkList(characterChips, [], "character");
renderSceneLinkList(assetChips, [], "asset");
renderSceneLinkList(locationChips, [], "location");
setResolvedScene(null); setResolvedScene(null);
setText(syncPlotDirectorCount, "-"); setText(syncPlotDirectorCount, "-");
setText(plotDirectorSceneTitle, "-"); setText(plotDirectorSceneTitle, "-");
@ -183,26 +202,53 @@
setHidden(plotDirectorBlockedReasonRow, true); 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) { if (!container) {
return; return;
} }
container.innerHTML = ""; container.innerHTML = "";
const linkedItems = Array.isArray(items) ? items.filter((item) => item.linked) : []; const rows = Array.isArray(items) ? items : [];
if (linkedItems.length === 0) { if (rows.length === 0) {
const empty = document.createElement("span"); const empty = document.createElement("p");
empty.className = "word-companion-empty-chip"; empty.className = "word-companion-empty-list";
empty.textContent = "None"; empty.textContent = "None";
container.append(empty); container.append(empty);
return; return;
} }
for (const item of linkedItems) { for (const item of rows) {
const chip = document.createElement("span"); const id = Number.parseInt(linkItemId(item, type), 10);
chip.className = "word-companion-chip"; if (!Number.isInteger(id) || id <= 0) {
chip.textContent = item.name || "Unnamed"; continue;
container.append(chip); }
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."; writingBrief.textContent = data?.writingBrief || "No writing brief has been added for this scene.";
} }
renderLinkedItems(characterChips, data?.characters); currentSceneLinks = {
renderLinkedItems(assetChips, data?.assets); characters: Array.isArray(data?.characters) ? data.characters : [],
renderLinkedItems(locationChips, data?.locations); 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(plotDirectorSceneDetails, false);
setHidden(writingBriefBlock, false); setHidden(writingBriefBlock, false);
setHidden(plotDirectorLinkGroups, false); setHidden(plotDirectorLinkGroups, false);
@ -711,6 +763,63 @@
body: JSON.stringify(body) 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 () => { const syncSceneProgress = async () => {
if (!resolvedSceneId) { if (!resolvedSceneId) {
setSyncStatus("No resolved PlotDirector scene."); setSyncStatus("No resolved PlotDirector scene.");
@ -836,8 +945,8 @@
} }
setPlotDirectorSceneStatus("Linked to PlotDirector"); setPlotDirectorSceneStatus("Linked to PlotDirector");
setDocumentMessage(""); setDocumentMessage("");
renderCompanionData(companionData);
setResolvedScene(resolvedSceneId); setResolvedScene(resolvedSceneId);
renderCompanionData(companionData);
return true; return true;
} catch { } catch {
resetPlotDirectorScene("Not found in PlotDirector"); resetPlotDirectorScene("Not found in PlotDirector");
@ -985,6 +1094,7 @@
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene); refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene); refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene);
syncSceneProgressButton?.addEventListener("click", syncSceneProgress); syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
runDiagnosticsButton?.addEventListener("click", runDiagnostics); runDiagnosticsButton?.addEventListener("click", runDiagnostics);
if (isAuthenticated) { if (isAuthenticated) {