Phase 10E: Scene Resolution and Writing Brief Display

This commit is contained in:
Nick Beckley 2026-06-13 19:23:29 +01:00
parent 07da6ff8b5
commit 8c4525d3b3
3 changed files with 298 additions and 2 deletions

View File

@ -75,6 +75,59 @@
</div>
</section>
<section class="word-companion-section word-companion-plotdirector-scene" aria-label="PlotDirector Scene">
<div class="word-companion-section-header">
<span>PlotDirector Scene</span>
<button type="button" data-refresh-plotdirector-scene>Refresh PlotDirector Scene</button>
</div>
<span>Scene Status</span>
<p class="word-companion-message" data-plotdirector-scene-status>Select a PlotDirector book.</p>
<div class="word-companion-scene-details" data-plotdirector-scene-details hidden>
<div>
<span>Scene Title</span>
<strong data-plotdirector-scene-title>-</strong>
</div>
<div>
<span>Revision Status</span>
<strong data-plotdirector-revision-status>-</strong>
</div>
<div>
<span>Estimated Words</span>
<strong data-plotdirector-estimated-words>-</strong>
</div>
<div>
<span>Actual Words</span>
<strong data-plotdirector-actual-words>-</strong>
</div>
<div>
<span>Blocked Status</span>
<strong data-plotdirector-blocked-status>-</strong>
</div>
<div data-plotdirector-blocked-reason-row hidden>
<span>Blocked Reason</span>
<strong data-plotdirector-blocked-reason>-</strong>
</div>
</div>
<div class="word-companion-brief" data-writing-brief-block hidden>
<span>Writing Brief</span>
<p data-writing-brief></p>
</div>
<div class="word-companion-link-groups" data-plotdirector-link-groups hidden>
<div>
<span>Characters</span>
<div class="word-companion-chip-list" data-character-chips></div>
</div>
<div>
<span>Assets</span>
<div class="word-companion-chip-list" data-asset-chips></div>
</div>
<div>
<span>Locations</span>
<div class="word-companion-chip-list" data-location-chips></div>
</div>
</div>
</section>
<section class="word-companion-section" aria-label="Diagnostics">
<div class="word-companion-section-header">
<span>Diagnostics</span>

View File

@ -174,6 +174,60 @@ body {
font-size: 0.88rem;
}
.word-companion-scene-details,
.word-companion-link-groups {
display: grid;
gap: 8px;
}
.word-companion-scene-details div,
.word-companion-link-groups div {
display: grid;
gap: 3px;
}
.word-companion-brief {
display: grid;
gap: 5px;
border-top: 1px solid var(--companion-line);
padding-top: 8px;
}
.word-companion-brief p {
margin: 0;
border: 1px solid var(--companion-line);
border-radius: 6px;
padding: 8px;
background: var(--companion-soft);
color: var(--companion-ink);
font-size: 0.9rem;
line-height: 1.35;
white-space: pre-wrap;
}
.word-companion-chip-list {
display: flex;
flex-wrap: wrap;
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-empty-chip {
border-color: var(--companion-line);
color: var(--companion-muted);
font-weight: 600;
}
.word-companion-outline {
display: grid;
gap: 8px;

View File

@ -21,6 +21,22 @@
const documentMessage = document.querySelector("[data-document-message]");
const syncNote = document.querySelector("[data-sync-note]");
const documentOutline = document.querySelector("[data-document-outline]");
const refreshPlotDirectorSceneButton = document.querySelector("[data-refresh-plotdirector-scene]");
const plotDirectorSceneStatus = document.querySelector("[data-plotdirector-scene-status]");
const plotDirectorSceneDetails = document.querySelector("[data-plotdirector-scene-details]");
const plotDirectorSceneTitle = document.querySelector("[data-plotdirector-scene-title]");
const plotDirectorRevisionStatus = document.querySelector("[data-plotdirector-revision-status]");
const plotDirectorEstimatedWords = document.querySelector("[data-plotdirector-estimated-words]");
const plotDirectorActualWords = document.querySelector("[data-plotdirector-actual-words]");
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
const plotDirectorBlockedReason = document.querySelector("[data-plotdirector-blocked-reason]");
const writingBriefBlock = document.querySelector("[data-writing-brief-block]");
const writingBrief = document.querySelector("[data-writing-brief]");
const plotDirectorLinkGroups = document.querySelector("[data-plotdirector-link-groups]");
const characterChips = document.querySelector("[data-character-chips]");
const assetChips = document.querySelector("[data-asset-chips]");
const locationChips = document.querySelector("[data-location-chips]");
const runDiagnosticsButton = document.querySelector("[data-run-diagnostics]");
const diagnosticHost = document.querySelector("[data-diagnostic-host]");
const diagnosticPlatform = document.querySelector("[data-diagnostic-platform]");
@ -37,6 +53,8 @@
let wordHostAvailable = false;
let officeHost = "Unknown";
let officePlatform = "Unknown";
let detectedChapterTitle = "";
let detectedSceneTitle = "";
const setOfficeStatus = (message) => {
if (officeStatus) {
@ -71,7 +89,16 @@
}
};
const setPlotDirectorSceneStatus = (message) => {
if (plotDirectorSceneStatus) {
plotDirectorSceneStatus.textContent = message;
}
};
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => {
detectedChapterTitle = chapterTitle || "";
detectedSceneTitle = sceneTitle || "";
if (currentChapter) {
currentChapter.textContent = chapterTitle || "Not detected";
}
@ -83,6 +110,91 @@
}
};
const setHidden = (element, hidden) => {
if (element) {
element.hidden = hidden;
}
};
const displayValue = (value) => {
if (value === null || value === undefined || value === "") {
return "-";
}
return String(value);
};
const resetPlotDirectorScene = (message) => {
setPlotDirectorSceneStatus(message);
setHidden(plotDirectorSceneDetails, true);
setHidden(writingBriefBlock, true);
setHidden(plotDirectorLinkGroups, true);
};
const renderLinkedItems = (container, items) => {
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";
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);
}
};
const renderCompanionData = (data) => {
setText(plotDirectorSceneTitle, displayValue(data?.sceneTitle));
setText(plotDirectorRevisionStatus, displayValue(data?.revisionStatus || data?.revisionStatusName || "Not provided"));
setText(plotDirectorEstimatedWords, displayValue(data?.estimatedWords));
setText(plotDirectorActualWords, displayValue(data?.actualWords));
setText(plotDirectorBlockedStatus, data?.blocked ? "Blocked" : "Not blocked");
if (data?.blockedReason) {
setText(plotDirectorBlockedReason, data.blockedReason);
setHidden(plotDirectorBlockedReasonRow, false);
} else {
setText(plotDirectorBlockedReason, "-");
setHidden(plotDirectorBlockedReasonRow, true);
}
if (writingBrief) {
writingBrief.textContent = data?.writingBrief || "No writing brief available.";
}
renderLinkedItems(characterChips, data?.characters);
renderLinkedItems(assetChips, data?.assets);
renderLinkedItems(locationChips, data?.locations);
setHidden(plotDirectorSceneDetails, false);
setHidden(writingBriefBlock, false);
setHidden(plotDirectorLinkGroups, false);
};
const findSceneRevisionStatus = async (bookId, sceneId) => {
const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`);
const chapters = Array.isArray(structure?.chapters) ? structure.chapters : [];
for (const chapter of chapters) {
const scenes = Array.isArray(chapter.scenes) ? chapter.scenes : [];
const scene = scenes.find((item) => item.sceneId === sceneId);
if (scene) {
return scene.revisionStatus || "";
}
}
return "";
};
const setText = (element, value) => {
if (element) {
element.textContent = value;
@ -477,10 +589,14 @@
}
};
const fetchJson = async (url) => {
const fetchJson = async (url, options = {}) => {
const response = await window.fetch(url, {
credentials: "same-origin",
headers: { Accept: "application/json" }
...options,
headers: {
Accept: "application/json",
...(options.headers || {})
}
});
if (!response.ok) {
@ -490,6 +606,72 @@
return await response.json();
};
const postJson = (url, body) => fetchJson(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const refreshPlotDirectorScene = async () => {
const book = selectedBook();
if (!book) {
resetPlotDirectorScene("Select a PlotDirector book.");
return;
}
if (!detectedChapterTitle || !detectedSceneTitle) {
resetPlotDirectorScene("No scene detected in Word.");
return;
}
if (refreshPlotDirectorSceneButton) {
refreshPlotDirectorSceneButton.disabled = true;
refreshPlotDirectorSceneButton.textContent = "Refreshing...";
}
const restorePlotDirectorSceneButton = () => {
if (refreshPlotDirectorSceneButton) {
refreshPlotDirectorSceneButton.disabled = false;
refreshPlotDirectorSceneButton.textContent = "Refresh PlotDirector Scene";
}
};
resetPlotDirectorScene("Resolving PlotDirector scene...");
let resolvedSceneId = null;
try {
const resolveResponse = await postJson(`/api/word-companion/books/${book.bookId}/resolve-scene`, {
chapterTitle: detectedChapterTitle,
sceneTitle: detectedSceneTitle
});
if (!resolveResponse?.matched || !resolveResponse.sceneId) {
resetPlotDirectorScene("Scene not found in PlotDirector.");
restorePlotDirectorSceneButton();
return;
}
resolvedSceneId = resolveResponse.sceneId;
} catch {
resetPlotDirectorScene("Unable to resolve scene.");
restorePlotDirectorSceneButton();
return;
}
try {
const companionData = await fetchJson(`/api/word-companion/scenes/${resolvedSceneId}/companion`);
try {
companionData.revisionStatus = await findSceneRevisionStatus(book.bookId, resolvedSceneId);
} catch {
companionData.revisionStatus = "";
}
setPlotDirectorSceneStatus("Linked to PlotDirector");
renderCompanionData(companionData);
} catch {
resetPlotDirectorScene("Unable to load PlotDirector scene data.");
} finally {
restorePlotDirectorSceneButton();
}
};
const loadBooks = async (projectId, preferredBookId) => {
if (!projectId) {
books = [];
@ -497,11 +679,13 @@
setBookMessage("");
writeStoredId(storageKeys.bookId, null);
updateSummary();
resetPlotDirectorScene("Select a PlotDirector book.");
return;
}
resetSelect(bookSelect, "Loading books...");
setBookMessage("");
resetPlotDirectorScene("Select a PlotDirector book.");
try {
const data = await fetchJson(`/api/word-companion/projects/${projectId}/books`);
@ -524,12 +708,14 @@
writeStoredId(storageKeys.bookId, null);
}
updateSummary();
resetPlotDirectorScene(selectedBook() ? "No scene detected in Word." : "Select a PlotDirector book.");
} catch {
books = [];
resetSelect(bookSelect, "Unable to load books.");
setBookMessage("Unable to load books.");
writeStoredId(storageKeys.bookId, null);
updateSummary();
resetPlotDirectorScene("Select a PlotDirector book.");
}
};
@ -581,6 +767,7 @@
const projectId = Number.parseInt(projectSelect.value || "", 10);
writeStoredId(storageKeys.projectId, Number.isInteger(projectId) && projectId > 0 ? projectId : null);
writeStoredId(storageKeys.bookId, null);
resetPlotDirectorScene("Select a PlotDirector book.");
await loadBooks(projectId, null);
});
@ -588,9 +775,11 @@
const bookId = Number.parseInt(bookSelect.value || "", 10);
writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null);
updateSummary();
resetPlotDirectorScene(selectedBook() ? "No scene detected in Word." : "Select a PlotDirector book.");
});
refreshDocumentButton?.addEventListener("click", refreshDocumentStructure);
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
if (isAuthenticated) {