Phase 10I: Word Companion Manuscript Anchors (Hidden PlotDirector IDs)
This commit is contained in:
parent
6dd46c2632
commit
cda6279d9f
@ -66,6 +66,22 @@
|
|||||||
<span>Link Status</span>
|
<span>Link Status</span>
|
||||||
<strong data-plotdirector-scene-status>Not detected</strong>
|
<strong data-plotdirector-scene-status>Not detected</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Anchor Status</span>
|
||||||
|
<strong data-anchor-status>Not attached</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Book ID</span>
|
||||||
|
<strong data-anchor-book-id>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Chapter ID</span>
|
||||||
|
<strong data-anchor-chapter-id>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Scene ID</span>
|
||||||
|
<strong data-anchor-scene-id>-</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Word Count in Word</span>
|
<span>Word Count in Word</span>
|
||||||
<strong data-current-word-count>-</strong>
|
<strong data-current-word-count>-</strong>
|
||||||
@ -101,6 +117,7 @@
|
|||||||
<strong data-sync-plotdirector-count>-</strong>
|
<strong data-sync-plotdirector-count>-</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" data-attach-plotdirector-ids disabled>Attach PlotDirector IDs</button>
|
||||||
<p class="word-companion-message" data-sync-note></p>
|
<p class="word-companion-message" data-sync-note></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -189,6 +206,22 @@
|
|||||||
<span>Scene ID</span>
|
<span>Scene ID</span>
|
||||||
<strong data-resolve-scene-id>-</strong>
|
<strong data-resolve-scene-id>-</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Anchor Type</span>
|
||||||
|
<strong data-diagnostic-anchor-type>Title Match</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Stored Scene ID</span>
|
||||||
|
<strong data-diagnostic-stored-scene-id>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Stored Chapter ID</span>
|
||||||
|
<strong data-diagnostic-stored-chapter-id>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Current Resolution Method</span>
|
||||||
|
<strong data-diagnostic-resolution-method>-</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Office Host</span>
|
<span>Office Host</span>
|
||||||
<strong data-diagnostic-host>Unknown</strong>
|
<strong data-diagnostic-host>Unknown</strong>
|
||||||
|
|||||||
@ -177,6 +177,23 @@ body {
|
|||||||
color: var(--companion-muted);
|
color: var(--companion-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.word-companion-current-scene > button {
|
||||||
|
min-height: 34px;
|
||||||
|
border: 1px solid var(--companion-line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--companion-accent-dark);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-companion-current-scene > button:disabled {
|
||||||
|
background: var(--companion-soft);
|
||||||
|
color: var(--companion-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.word-companion-message:empty {
|
.word-companion-message:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,11 @@
|
|||||||
const documentOutline = document.querySelector("[data-document-outline]");
|
const documentOutline = document.querySelector("[data-document-outline]");
|
||||||
const refreshPlotDirectorSceneButton = document.querySelector("[data-refresh-plotdirector-scene]");
|
const refreshPlotDirectorSceneButton = document.querySelector("[data-refresh-plotdirector-scene]");
|
||||||
const plotDirectorSceneStatus = document.querySelector("[data-plotdirector-scene-status]");
|
const plotDirectorSceneStatus = document.querySelector("[data-plotdirector-scene-status]");
|
||||||
|
const anchorStatus = document.querySelector("[data-anchor-status]");
|
||||||
|
const anchorBookId = document.querySelector("[data-anchor-book-id]");
|
||||||
|
const anchorChapterId = document.querySelector("[data-anchor-chapter-id]");
|
||||||
|
const anchorSceneId = document.querySelector("[data-anchor-scene-id]");
|
||||||
|
const attachPlotDirectorIdsButton = document.querySelector("[data-attach-plotdirector-ids]");
|
||||||
const plotDirectorSceneDetails = document.querySelector("[data-plotdirector-scene-details]");
|
const plotDirectorSceneDetails = document.querySelector("[data-plotdirector-scene-details]");
|
||||||
const plotDirectorSceneTitle = document.querySelector("[data-plotdirector-scene-title]");
|
const plotDirectorSceneTitle = document.querySelector("[data-plotdirector-scene-title]");
|
||||||
const plotDirectorRevisionStatus = document.querySelector("[data-plotdirector-revision-status]");
|
const plotDirectorRevisionStatus = document.querySelector("[data-plotdirector-revision-status]");
|
||||||
@ -64,6 +69,10 @@
|
|||||||
const diagnosticWordApi = document.querySelector("[data-diagnostic-word-api]");
|
const diagnosticWordApi = document.querySelector("[data-diagnostic-word-api]");
|
||||||
const diagnosticLastScan = document.querySelector("[data-diagnostic-last-scan]");
|
const diagnosticLastScan = document.querySelector("[data-diagnostic-last-scan]");
|
||||||
const diagnosticLastError = document.querySelector("[data-diagnostic-last-error]");
|
const diagnosticLastError = document.querySelector("[data-diagnostic-last-error]");
|
||||||
|
const diagnosticAnchorType = document.querySelector("[data-diagnostic-anchor-type]");
|
||||||
|
const diagnosticStoredSceneId = document.querySelector("[data-diagnostic-stored-scene-id]");
|
||||||
|
const diagnosticStoredChapterId = document.querySelector("[data-diagnostic-stored-chapter-id]");
|
||||||
|
const diagnosticResolutionMethod = document.querySelector("[data-diagnostic-resolution-method]");
|
||||||
const diagnosticParagraphs = document.querySelector("[data-diagnostic-paragraphs]");
|
const diagnosticParagraphs = document.querySelector("[data-diagnostic-paragraphs]");
|
||||||
const diagnosticHeading1 = document.querySelector("[data-diagnostic-heading1]");
|
const diagnosticHeading1 = document.querySelector("[data-diagnostic-heading1]");
|
||||||
const diagnosticHeading2 = document.querySelector("[data-diagnostic-heading2]");
|
const diagnosticHeading2 = document.querySelector("[data-diagnostic-heading2]");
|
||||||
@ -77,7 +86,13 @@
|
|||||||
let detectedChapterTitle = "";
|
let detectedChapterTitle = "";
|
||||||
let detectedSceneTitle = "";
|
let detectedSceneTitle = "";
|
||||||
let detectedSceneWordCount = null;
|
let detectedSceneWordCount = null;
|
||||||
|
let detectedChapterAnchorId = null;
|
||||||
|
let detectedSceneAnchorId = null;
|
||||||
let resolvedSceneId = null;
|
let resolvedSceneId = null;
|
||||||
|
let resolvedChapterId = null;
|
||||||
|
let currentResolutionMethod = "";
|
||||||
|
let currentResolutionAnchorType = "Title Match";
|
||||||
|
let storedAnchorInvalid = false;
|
||||||
let currentSceneLinks = {
|
let currentSceneLinks = {
|
||||||
characters: [],
|
characters: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
@ -124,10 +139,51 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => {
|
const setAnchorDiagnostics = (values) => {
|
||||||
|
const hasValue = (name) => Object.prototype.hasOwnProperty.call(values, name);
|
||||||
|
|
||||||
|
if (hasValue("anchorType")) setText(diagnosticAnchorType, displayValue(values.anchorType));
|
||||||
|
if (hasValue("storedSceneId")) setText(diagnosticStoredSceneId, displayValue(values.storedSceneId));
|
||||||
|
if (hasValue("storedChapterId")) setText(diagnosticStoredChapterId, displayValue(values.storedChapterId));
|
||||||
|
if (hasValue("resolutionMethod")) setText(diagnosticResolutionMethod, displayValue(values.resolutionMethod));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAnchorDisplay = () => {
|
||||||
|
const book = selectedBook();
|
||||||
|
const hasMatchingSceneAnchor = Number.isInteger(resolvedSceneId)
|
||||||
|
&& resolvedSceneId > 0
|
||||||
|
&& detectedSceneAnchorId === resolvedSceneId;
|
||||||
|
const hasMatchingChapterAnchor = !Number.isInteger(resolvedChapterId)
|
||||||
|
|| resolvedChapterId <= 0
|
||||||
|
|| detectedChapterAnchorId === resolvedChapterId;
|
||||||
|
const attached = hasMatchingSceneAnchor && hasMatchingChapterAnchor;
|
||||||
|
|
||||||
|
setText(anchorStatus, attached ? "Attached to PlotDirector" : "Not attached");
|
||||||
|
setText(anchorBookId, book?.bookId ? String(book.bookId) : "-");
|
||||||
|
setText(anchorChapterId, Number.isInteger(resolvedChapterId) && resolvedChapterId > 0 ? String(resolvedChapterId) : "-");
|
||||||
|
setText(anchorSceneId, Number.isInteger(resolvedSceneId) && resolvedSceneId > 0 ? String(resolvedSceneId) : "-");
|
||||||
|
|
||||||
|
if (attachPlotDirectorIdsButton) {
|
||||||
|
attachPlotDirectorIdsButton.disabled = !resolvedSceneId || (attached && !storedAnchorInvalid);
|
||||||
|
attachPlotDirectorIdsButton.textContent = detectedSceneAnchorId && !attached
|
||||||
|
? "Reattach PlotDirector IDs"
|
||||||
|
: "Attach PlotDirector IDs";
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnchorDiagnostics({
|
||||||
|
anchorType: currentResolutionAnchorType || "Title Match",
|
||||||
|
storedSceneId: detectedSceneAnchorId,
|
||||||
|
storedChapterId: detectedChapterAnchorId,
|
||||||
|
resolutionMethod: currentResolutionMethod
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount, chapterAnchorId = null, sceneAnchorId = null) => {
|
||||||
detectedChapterTitle = chapterTitle || "";
|
detectedChapterTitle = chapterTitle || "";
|
||||||
detectedSceneTitle = sceneTitle || "";
|
detectedSceneTitle = sceneTitle || "";
|
||||||
detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null;
|
detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null;
|
||||||
|
detectedChapterAnchorId = Number.isInteger(chapterAnchorId) && chapterAnchorId > 0 ? chapterAnchorId : null;
|
||||||
|
detectedSceneAnchorId = Number.isInteger(sceneAnchorId) && sceneAnchorId > 0 ? sceneAnchorId : null;
|
||||||
|
|
||||||
if (currentChapter) {
|
if (currentChapter) {
|
||||||
currentChapter.textContent = chapterTitle || "Not detected";
|
currentChapter.textContent = chapterTitle || "Not detected";
|
||||||
@ -139,6 +195,7 @@
|
|||||||
currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-";
|
currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-";
|
||||||
}
|
}
|
||||||
setText(syncWordCount, Number.isInteger(wordCount) ? String(wordCount) : "-");
|
setText(syncWordCount, Number.isInteger(wordCount) ? String(wordCount) : "-");
|
||||||
|
updateAnchorDisplay();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHidden = (element, hidden) => {
|
const setHidden = (element, hidden) => {
|
||||||
@ -151,8 +208,10 @@
|
|||||||
setText(syncStatus, message);
|
setText(syncStatus, message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setResolvedScene = (sceneId) => {
|
const setResolvedScene = (sceneId, chapterId = null, resolutionMethod = "") => {
|
||||||
resolvedSceneId = Number.isInteger(sceneId) && sceneId > 0 ? sceneId : null;
|
resolvedSceneId = Number.isInteger(sceneId) && sceneId > 0 ? sceneId : null;
|
||||||
|
resolvedChapterId = Number.isInteger(chapterId) && chapterId > 0 ? chapterId : null;
|
||||||
|
currentResolutionMethod = resolutionMethod || "";
|
||||||
if (syncSceneProgressButton) {
|
if (syncSceneProgressButton) {
|
||||||
syncSceneProgressButton.disabled = !resolvedSceneId;
|
syncSceneProgressButton.disabled = !resolvedSceneId;
|
||||||
}
|
}
|
||||||
@ -161,6 +220,7 @@
|
|||||||
}
|
}
|
||||||
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.");
|
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
|
||||||
|
updateAnchorDisplay();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRefreshCurrentSceneBusy = (isBusy) => {
|
const setRefreshCurrentSceneBusy = (isBusy) => {
|
||||||
@ -189,6 +249,9 @@
|
|||||||
setHidden(plotDirectorSceneDetails, true);
|
setHidden(plotDirectorSceneDetails, true);
|
||||||
setHidden(writingBriefBlock, true);
|
setHidden(writingBriefBlock, true);
|
||||||
setHidden(plotDirectorLinkGroups, true);
|
setHidden(plotDirectorLinkGroups, true);
|
||||||
|
currentResolutionMethod = "";
|
||||||
|
currentResolutionAnchorType = detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match";
|
||||||
|
storedAnchorInvalid = false;
|
||||||
currentSceneLinks = { characters: [], assets: [], locations: [], primaryLocationId: null };
|
currentSceneLinks = { characters: [], assets: [], locations: [], primaryLocationId: null };
|
||||||
renderSceneLinkList(characterChips, [], "character");
|
renderSceneLinkList(characterChips, [], "character");
|
||||||
renderSceneLinkList(assetChips, [], "asset");
|
renderSceneLinkList(assetChips, [], "asset");
|
||||||
@ -202,6 +265,7 @@
|
|||||||
setText(plotDirectorBlockedStatus, "-");
|
setText(plotDirectorBlockedStatus, "-");
|
||||||
setText(plotDirectorBlockedReason, "-");
|
setText(plotDirectorBlockedReason, "-");
|
||||||
setHidden(plotDirectorBlockedReasonRow, true);
|
setHidden(plotDirectorBlockedReasonRow, true);
|
||||||
|
updateAnchorDisplay();
|
||||||
};
|
};
|
||||||
|
|
||||||
const linkItemId = (item, type) => {
|
const linkItemId = (item, type) => {
|
||||||
@ -362,6 +426,36 @@
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findSceneInBookStructure = async (bookId, predicate) => {
|
||||||
|
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 : [];
|
||||||
|
for (const scene of scenes) {
|
||||||
|
if (predicate(chapter, scene)) {
|
||||||
|
return { chapter, scene };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadResolvedCompanionData = async (bookId, sceneId, chapterId, resolutionMethod, anchorType) => {
|
||||||
|
const companionData = await fetchJson(`/api/word-companion/scenes/${sceneId}/companion`);
|
||||||
|
try {
|
||||||
|
companionData.revisionStatus = await findSceneRevisionStatus(bookId, sceneId);
|
||||||
|
} catch {
|
||||||
|
companionData.revisionStatus = "";
|
||||||
|
}
|
||||||
|
currentResolutionAnchorType = anchorType || "Title Match";
|
||||||
|
storedAnchorInvalid = false;
|
||||||
|
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
||||||
|
setDocumentMessage("");
|
||||||
|
setResolvedScene(sceneId, chapterId, resolutionMethod);
|
||||||
|
renderCompanionData(companionData);
|
||||||
|
};
|
||||||
|
|
||||||
const setText = (element, value) => {
|
const setText = (element, value) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
element.textContent = value;
|
element.textContent = value;
|
||||||
@ -551,6 +645,28 @@
|
|||||||
&& normalizedStyleName(left) === normalizedStyleName(right);
|
&& normalizedStyleName(left) === normalizedStyleName(right);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const anchorIdFromTag = (tag, prefix) => {
|
||||||
|
const match = String(tag || "").match(new RegExp(`^${prefix}-(\\d+)$`, "i"));
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number.parseInt(match[1], 10);
|
||||||
|
return Number.isInteger(id) && id > 0 ? id : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const paragraphAnchorId = (paragraph, prefix) => {
|
||||||
|
const controls = Array.isArray(paragraph?.contentControls?.items) ? paragraph.contentControls.items : [];
|
||||||
|
for (const control of controls) {
|
||||||
|
const id = anchorIdFromTag(control.tag, prefix);
|
||||||
|
if (id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const buildDocumentStructure = (paragraphs, selectionParagraphs) => {
|
const buildDocumentStructure = (paragraphs, selectionParagraphs) => {
|
||||||
const chapters = [];
|
const chapters = [];
|
||||||
let heading1Count = 0;
|
let heading1Count = 0;
|
||||||
@ -570,6 +686,7 @@
|
|||||||
index: chapters.length + 1,
|
index: chapters.length + 1,
|
||||||
paragraphIndex: index,
|
paragraphIndex: index,
|
||||||
title,
|
title,
|
||||||
|
anchorId: paragraphAnchorId(paragraph, "PD-CHAPTER"),
|
||||||
scenes: []
|
scenes: []
|
||||||
};
|
};
|
||||||
chapters.push(chapter);
|
chapters.push(chapter);
|
||||||
@ -587,6 +704,7 @@
|
|||||||
index: chapter.scenes.length + 1,
|
index: chapter.scenes.length + 1,
|
||||||
paragraphIndex: index,
|
paragraphIndex: index,
|
||||||
title,
|
title,
|
||||||
|
anchorId: paragraphAnchorId(paragraph, "PD-SCENE"),
|
||||||
wordCount: 0
|
wordCount: 0
|
||||||
};
|
};
|
||||||
chapter.scenes.push(scene);
|
chapter.scenes.push(scene);
|
||||||
@ -662,6 +780,26 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readDocumentStructure = async (context) => {
|
||||||
|
const bodyParagraphs = context.document.body.paragraphs;
|
||||||
|
const selectionParagraphs = context.document.getSelection().paragraphs;
|
||||||
|
|
||||||
|
bodyParagraphs.load("text,styleBuiltIn,style");
|
||||||
|
selectionParagraphs.load("text,styleBuiltIn,style");
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
|
for (const paragraph of bodyParagraphs.items) {
|
||||||
|
if (isBuiltInHeading(paragraph, 1) || isBuiltInHeading(paragraph, 2)) {
|
||||||
|
paragraph.contentControls.load("items/tag,title");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
|
const structure = buildDocumentStructure(bodyParagraphs.items, selectionParagraphs.items);
|
||||||
|
structure.bodyParagraphs = bodyParagraphs.items;
|
||||||
|
return structure;
|
||||||
|
};
|
||||||
|
|
||||||
const refreshDocumentStructure = async () => {
|
const refreshDocumentStructure = async () => {
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
|
|
||||||
@ -680,20 +818,15 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const structure = await window.Word.run(async (context) => {
|
const structure = await window.Word.run(async (context) => {
|
||||||
const bodyParagraphs = context.document.body.paragraphs;
|
return await readDocumentStructure(context);
|
||||||
const selectionParagraphs = context.document.getSelection().paragraphs;
|
|
||||||
|
|
||||||
bodyParagraphs.load("text,styleBuiltIn,style");
|
|
||||||
selectionParagraphs.load("text,styleBuiltIn,style");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
return buildDocumentStructure(bodyParagraphs.items, selectionParagraphs.items);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setCurrentStructure(
|
setCurrentStructure(
|
||||||
structure.currentChapter?.title,
|
structure.currentChapter?.title,
|
||||||
structure.currentScene?.title,
|
structure.currentScene?.title,
|
||||||
structure.currentScene?.wordCount
|
structure.currentScene?.wordCount,
|
||||||
|
structure.currentChapter?.anchorId,
|
||||||
|
structure.currentScene?.anchorId
|
||||||
);
|
);
|
||||||
resetPlotDirectorScene("Not detected");
|
resetPlotDirectorScene("Not detected");
|
||||||
renderOutline(structure);
|
renderOutline(structure);
|
||||||
@ -859,6 +992,83 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findAnchorControl = (paragraph, prefix) => {
|
||||||
|
const controls = Array.isArray(paragraph?.contentControls?.items) ? paragraph.contentControls.items : [];
|
||||||
|
return controls.find((control) => anchorIdFromTag(control.tag, prefix)) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureAnchorControl = (paragraph, title, tag, prefix) => {
|
||||||
|
const existing = findAnchorControl(paragraph, prefix);
|
||||||
|
const control = existing || paragraph.getRange().insertContentControl();
|
||||||
|
control.title = title;
|
||||||
|
control.tag = tag;
|
||||||
|
control.appearance = "Hidden";
|
||||||
|
return control;
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachPlotDirectorIds = async () => {
|
||||||
|
if (!resolvedSceneId || !resolvedChapterId) {
|
||||||
|
setDocumentMessage("No resolved PlotDirector scene.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacingExistingAnchor = (detectedSceneAnchorId && detectedSceneAnchorId !== resolvedSceneId)
|
||||||
|
|| (detectedChapterAnchorId && detectedChapterAnchorId !== resolvedChapterId);
|
||||||
|
if (replacingExistingAnchor && !window.confirm("This scene is already linked. Replace the existing PlotDirector link?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||||
|
setDocumentMessage("Word document access is unavailable.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachPlotDirectorIdsButton) {
|
||||||
|
attachPlotDirectorIdsButton.disabled = true;
|
||||||
|
attachPlotDirectorIdsButton.textContent = "Attaching...";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.Word.run(async (context) => {
|
||||||
|
const structure = await readDocumentStructure(context);
|
||||||
|
if (!structure.currentChapter || !structure.currentScene) {
|
||||||
|
throw new Error("No scene detected in Word. Use Heading 2 for scenes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const chapterParagraph = structure.bodyParagraphs[structure.currentChapter.paragraphIndex];
|
||||||
|
const sceneParagraph = structure.bodyParagraphs[structure.currentScene.paragraphIndex];
|
||||||
|
if (!chapterParagraph || !sceneParagraph) {
|
||||||
|
throw new Error("Unable to locate the current Word headings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAnchorControl(chapterParagraph, "PlotDirector Chapter", `PD-CHAPTER-${resolvedChapterId}`, "PD-CHAPTER");
|
||||||
|
ensureAnchorControl(sceneParagraph, "PlotDirector Scene", `PD-SCENE-${resolvedSceneId}`, "PD-SCENE");
|
||||||
|
await context.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
detectedChapterAnchorId = resolvedChapterId;
|
||||||
|
detectedSceneAnchorId = resolvedSceneId;
|
||||||
|
storedAnchorInvalid = false;
|
||||||
|
currentResolutionAnchorType = "SceneID Anchor";
|
||||||
|
currentResolutionMethod = "Anchor";
|
||||||
|
setDocumentMessage("PlotDirector IDs attached successfully.");
|
||||||
|
setDiagnostics({ lastError: "-" });
|
||||||
|
updateAnchorDisplay();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to attach PlotDirector IDs.", error);
|
||||||
|
setDocumentMessage("Unable to attach PlotDirector IDs.");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
updateAnchorDisplay();
|
||||||
|
} finally {
|
||||||
|
if (attachPlotDirectorIdsButton) {
|
||||||
|
attachPlotDirectorIdsButton.textContent = detectedSceneAnchorId === resolvedSceneId
|
||||||
|
? "Attach PlotDirector IDs"
|
||||||
|
: "Reattach PlotDirector IDs";
|
||||||
|
updateAnchorDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const syncSceneProgress = async () => {
|
const syncSceneProgress = async () => {
|
||||||
if (!resolvedSceneId) {
|
if (!resolvedSceneId) {
|
||||||
setSyncStatus("No resolved PlotDirector scene.");
|
setSyncStatus("No resolved PlotDirector scene.");
|
||||||
@ -914,6 +1124,12 @@
|
|||||||
chapterId: null,
|
chapterId: null,
|
||||||
sceneId: null
|
sceneId: null
|
||||||
});
|
});
|
||||||
|
setAnchorDiagnostics({
|
||||||
|
anchorType: detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match",
|
||||||
|
storedSceneId: detectedSceneAnchorId,
|
||||||
|
storedChapterId: detectedChapterAnchorId,
|
||||||
|
resolutionMethod: "-"
|
||||||
|
});
|
||||||
|
|
||||||
if (!book) {
|
if (!book) {
|
||||||
resetPlotDirectorScene("Not detected");
|
resetPlotDirectorScene("Not detected");
|
||||||
@ -942,16 +1158,80 @@
|
|||||||
resetPlotDirectorScene("Not detected");
|
resetPlotDirectorScene("Not detected");
|
||||||
setDocumentMessage("Resolving PlotDirector scene...");
|
setDocumentMessage("Resolving PlotDirector scene...");
|
||||||
|
|
||||||
let resolvedSceneId = null;
|
|
||||||
try {
|
try {
|
||||||
|
if (detectedSceneAnchorId) {
|
||||||
|
const anchored = await findSceneInBookStructure(
|
||||||
|
book.bookId,
|
||||||
|
(_chapter, scene) => scene.sceneId === detectedSceneAnchorId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (anchored) {
|
||||||
|
setResolveDiagnostics({
|
||||||
|
result: "Matched",
|
||||||
|
chapterId: anchored.chapter.chapterId,
|
||||||
|
sceneId: anchored.scene.sceneId
|
||||||
|
});
|
||||||
|
await loadResolvedCompanionData(book.bookId, anchored.scene.sceneId, anchored.chapter.chapterId, "Anchor", "SceneID Anchor");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
storedAnchorInvalid = true;
|
||||||
|
currentResolutionAnchorType = "SceneID Anchor";
|
||||||
|
setPlotDirectorSceneStatus("Not found in PlotDirector");
|
||||||
|
setDocumentMessage("Stored PlotDirector ID is invalid.");
|
||||||
|
setResolveDiagnostics({
|
||||||
|
result: "Error",
|
||||||
|
chapterId: detectedChapterAnchorId,
|
||||||
|
sceneId: detectedSceneAnchorId
|
||||||
|
});
|
||||||
|
setAnchorDiagnostics({
|
||||||
|
anchorType: "SceneID Anchor",
|
||||||
|
storedSceneId: detectedSceneAnchorId,
|
||||||
|
storedChapterId: detectedChapterAnchorId,
|
||||||
|
resolutionMethod: "Anchor"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectedChapterAnchorId) {
|
||||||
|
const anchoredChapterScene = await findSceneInBookStructure(
|
||||||
|
book.bookId,
|
||||||
|
(chapter, scene) => chapter.chapterId === detectedChapterAnchorId
|
||||||
|
&& normalizeTitleForResolve(scene.title, "scene").toLocaleLowerCase() === requestSceneTitle.toLocaleLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (anchoredChapterScene) {
|
||||||
|
setResolveDiagnostics({
|
||||||
|
result: "Matched",
|
||||||
|
chapterId: anchoredChapterScene.chapter.chapterId,
|
||||||
|
sceneId: anchoredChapterScene.scene.sceneId
|
||||||
|
});
|
||||||
|
await loadResolvedCompanionData(book.bookId, anchoredChapterScene.scene.sceneId, anchoredChapterScene.chapter.chapterId, "Anchor", "ChapterID Anchor");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detectedSceneAnchorId) {
|
||||||
|
storedAnchorInvalid = true;
|
||||||
|
currentResolutionAnchorType = "ChapterID Anchor";
|
||||||
|
setPlotDirectorSceneStatus("Not found in PlotDirector");
|
||||||
|
setDocumentMessage("Stored PlotDirector ID is invalid.");
|
||||||
|
setResolveDiagnostics({
|
||||||
|
result: "Error",
|
||||||
|
chapterId: detectedChapterAnchorId,
|
||||||
|
sceneId: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolveResponse = await postJson(`/api/word-companion/books/${book.bookId}/resolve-scene`, {
|
const resolveResponse = await postJson(`/api/word-companion/books/${book.bookId}/resolve-scene`, {
|
||||||
chapterTitle: requestChapterTitle,
|
chapterTitle: requestChapterTitle,
|
||||||
sceneTitle: requestSceneTitle
|
sceneTitle: requestSceneTitle
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resolveResponse?.matched || !resolveResponse.sceneId) {
|
if (!resolveResponse?.matched || !resolveResponse.sceneId) {
|
||||||
|
const hadInvalidAnchor = storedAnchorInvalid;
|
||||||
resetPlotDirectorScene("Not found in PlotDirector");
|
resetPlotDirectorScene("Not found in PlotDirector");
|
||||||
setDocumentMessage("Scene not found in PlotDirector.");
|
storedAnchorInvalid = hadInvalidAnchor;
|
||||||
|
setDocumentMessage(hadInvalidAnchor ? "Stored PlotDirector ID is invalid." : "Scene not found in PlotDirector.");
|
||||||
setResolveDiagnostics({
|
setResolveDiagnostics({
|
||||||
result: "Not matched",
|
result: "Not matched",
|
||||||
chapterId: resolveResponse?.chapterId,
|
chapterId: resolveResponse?.chapterId,
|
||||||
@ -961,35 +1241,26 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedSceneId = resolveResponse.sceneId;
|
const hadInvalidAnchor = storedAnchorInvalid;
|
||||||
|
|
||||||
setResolveDiagnostics({
|
setResolveDiagnostics({
|
||||||
result: "Matched",
|
result: hadInvalidAnchor ? "Matched by title fallback" : "Matched",
|
||||||
chapterId: resolveResponse.chapterId,
|
chapterId: resolveResponse.chapterId,
|
||||||
sceneId: resolveResponse.sceneId
|
sceneId: resolveResponse.sceneId
|
||||||
});
|
});
|
||||||
} catch {
|
await loadResolvedCompanionData(book.bookId, resolveResponse.sceneId, resolveResponse.chapterId, hadInvalidAnchor ? "Title fallback" : "Title", "Title Match");
|
||||||
resetPlotDirectorScene("Not found in PlotDirector");
|
if (hadInvalidAnchor) {
|
||||||
setDocumentMessage("Scene not found in PlotDirector.");
|
storedAnchorInvalid = true;
|
||||||
setResolveDiagnostics({ result: "Error" });
|
setDocumentMessage("Stored PlotDirector ID is invalid.");
|
||||||
restorePlotDirectorSceneButton();
|
updateAnchorDisplay();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
setDocumentMessage("");
|
|
||||||
setResolvedScene(resolvedSceneId);
|
|
||||||
renderCompanionData(companionData);
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
const hadInvalidAnchor = storedAnchorInvalid;
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
resetPlotDirectorScene("Not found in PlotDirector");
|
resetPlotDirectorScene("Not found in PlotDirector");
|
||||||
setDocumentMessage("Unable to load PlotDirector scene data.");
|
storedAnchorInvalid = hadInvalidAnchor;
|
||||||
|
setDocumentMessage(hadInvalidAnchor ? "Stored PlotDirector ID is invalid." : "Unable to load PlotDirector scene data.");
|
||||||
setResolveDiagnostics({ result: "Error" });
|
setResolveDiagnostics({ result: "Error" });
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@ -1134,6 +1405,7 @@
|
|||||||
refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene);
|
refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene);
|
||||||
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
||||||
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
|
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
|
||||||
|
attachPlotDirectorIdsButton?.addEventListener("click", attachPlotDirectorIds);
|
||||||
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user