898 lines
35 KiB
JavaScript
898 lines
35 KiB
JavaScript
(() => {
|
|
const storageKeys = {
|
|
projectId: "plotdirector.word.projectId",
|
|
bookId: "plotdirector.word.bookId"
|
|
};
|
|
|
|
const shell = document.querySelector(".word-companion-shell");
|
|
const officeStatus = document.querySelector("[data-office-status]");
|
|
const connectionStatus = document.querySelector("[data-connection-status]");
|
|
const summaryConnection = document.querySelector("[data-summary-connection]");
|
|
const summaryProject = document.querySelector("[data-summary-project]");
|
|
const summaryBook = document.querySelector("[data-summary-book]");
|
|
const projectSelect = document.querySelector("[data-project-select]");
|
|
const bookSelect = document.querySelector("[data-book-select]");
|
|
const projectMessage = document.querySelector("[data-project-message]");
|
|
const bookMessage = document.querySelector("[data-book-message]");
|
|
const refreshDocumentButton = document.querySelector("[data-refresh-document]");
|
|
const currentChapter = document.querySelector("[data-current-chapter]");
|
|
const currentScene = document.querySelector("[data-current-scene]");
|
|
const currentWordCount = document.querySelector("[data-current-word-count]");
|
|
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 resolveProjectId = document.querySelector("[data-resolve-project-id]");
|
|
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
|
|
const resolveBookId = document.querySelector("[data-resolve-book-id]");
|
|
const resolveBookTitle = document.querySelector("[data-resolve-book-title]");
|
|
const resolveDetectedChapter = document.querySelector("[data-resolve-detected-chapter]");
|
|
const resolveDetectedScene = document.querySelector("[data-resolve-detected-scene]");
|
|
const resolveRequestChapter = document.querySelector("[data-resolve-request-chapter]");
|
|
const resolveRequestScene = document.querySelector("[data-resolve-request-scene]");
|
|
const resolveResult = document.querySelector("[data-resolve-result]");
|
|
const resolveChapterId = document.querySelector("[data-resolve-chapter-id]");
|
|
const resolveSceneId = document.querySelector("[data-resolve-scene-id]");
|
|
const runDiagnosticsButton = document.querySelector("[data-run-diagnostics]");
|
|
const diagnosticHost = document.querySelector("[data-diagnostic-host]");
|
|
const diagnosticPlatform = document.querySelector("[data-diagnostic-platform]");
|
|
const diagnosticReady = document.querySelector("[data-diagnostic-ready]");
|
|
const diagnosticWordApi = document.querySelector("[data-diagnostic-word-api]");
|
|
const diagnosticLastScan = document.querySelector("[data-diagnostic-last-scan]");
|
|
const diagnosticParagraphs = document.querySelector("[data-diagnostic-paragraphs]");
|
|
const diagnosticHeading1 = document.querySelector("[data-diagnostic-heading1]");
|
|
const diagnosticHeading2 = document.querySelector("[data-diagnostic-heading2]");
|
|
const isAuthenticated = shell?.dataset.authenticated === "true";
|
|
let projects = [];
|
|
let books = [];
|
|
let officeReady = false;
|
|
let wordHostAvailable = false;
|
|
let officeHost = "Unknown";
|
|
let officePlatform = "Unknown";
|
|
let detectedChapterTitle = "";
|
|
let detectedSceneTitle = "";
|
|
|
|
const setOfficeStatus = (message) => {
|
|
if (officeStatus) {
|
|
officeStatus.textContent = message;
|
|
}
|
|
};
|
|
|
|
const setConnectionStatus = (message) => {
|
|
if (connectionStatus) {
|
|
connectionStatus.textContent = message;
|
|
}
|
|
if (summaryConnection) {
|
|
summaryConnection.textContent = message;
|
|
}
|
|
};
|
|
|
|
const setProjectMessage = (message) => {
|
|
if (projectMessage) {
|
|
projectMessage.textContent = message;
|
|
}
|
|
};
|
|
|
|
const setBookMessage = (message) => {
|
|
if (bookMessage) {
|
|
bookMessage.textContent = message;
|
|
}
|
|
};
|
|
|
|
const setDocumentMessage = (message) => {
|
|
if (documentMessage) {
|
|
documentMessage.textContent = message;
|
|
}
|
|
};
|
|
|
|
const setPlotDirectorSceneStatus = (message) => {
|
|
if (plotDirectorSceneStatus) {
|
|
plotDirectorSceneStatus.textContent = message;
|
|
}
|
|
};
|
|
|
|
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => {
|
|
detectedChapterTitle = chapterTitle || "";
|
|
detectedSceneTitle = sceneTitle || "";
|
|
|
|
if (currentChapter) {
|
|
currentChapter.textContent = chapterTitle || "Not detected";
|
|
}
|
|
if (currentScene) {
|
|
currentScene.textContent = sceneTitle || "Not detected";
|
|
}
|
|
if (currentWordCount) {
|
|
currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-";
|
|
}
|
|
};
|
|
|
|
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 normalizeWhitespace = (value) => String(value || "").trim().replace(/\s+/g, " ");
|
|
|
|
const normalizeTitleForResolve = (title, type) => {
|
|
const original = normalizeWhitespace(title);
|
|
if (!original) {
|
|
return "";
|
|
}
|
|
|
|
const label = type === "scene" ? "scene" : "chapter";
|
|
const ordinal = "(?:\\d+|[ivxlcdm]+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)";
|
|
const prefix = new RegExp(`^${label}\\s+${ordinal}\\s*(?::|-|\\u2013|\\u2014)\\s*`, "i");
|
|
const normalized = normalizeWhitespace(original.replace(prefix, ""));
|
|
return normalized || original;
|
|
};
|
|
|
|
const setResolveDiagnostics = (values) => {
|
|
const hasValue = (name) => Object.prototype.hasOwnProperty.call(values, name);
|
|
|
|
if (hasValue("projectId")) setText(resolveProjectId, displayValue(values.projectId));
|
|
if (hasValue("projectTitle")) setText(resolveProjectTitle, displayValue(values.projectTitle));
|
|
if (hasValue("bookId")) setText(resolveBookId, displayValue(values.bookId));
|
|
if (hasValue("bookTitle")) setText(resolveBookTitle, displayValue(values.bookTitle));
|
|
if (hasValue("detectedChapter")) setText(resolveDetectedChapter, displayValue(values.detectedChapter));
|
|
if (hasValue("detectedScene")) setText(resolveDetectedScene, displayValue(values.detectedScene));
|
|
if (hasValue("requestChapter")) setText(resolveRequestChapter, displayValue(values.requestChapter));
|
|
if (hasValue("requestScene")) setText(resolveRequestScene, displayValue(values.requestScene));
|
|
if (hasValue("result")) setText(resolveResult, displayValue(values.result));
|
|
if (hasValue("chapterId")) setText(resolveChapterId, displayValue(values.chapterId));
|
|
if (hasValue("sceneId")) setText(resolveSceneId, displayValue(values.sceneId));
|
|
};
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
const setDiagnostics = (values) => {
|
|
const hasValue = (name) => Object.prototype.hasOwnProperty.call(values, name);
|
|
|
|
if (hasValue("host")) {
|
|
setText(diagnosticHost, values.host);
|
|
}
|
|
if (hasValue("platform")) {
|
|
setText(diagnosticPlatform, values.platform);
|
|
}
|
|
if (hasValue("ready")) {
|
|
setText(diagnosticReady, values.ready);
|
|
}
|
|
if (hasValue("wordApi")) {
|
|
setText(diagnosticWordApi, values.wordApi);
|
|
}
|
|
if (hasValue("lastScan")) {
|
|
setText(diagnosticLastScan, values.lastScan);
|
|
}
|
|
if (hasValue("paragraphs")) {
|
|
setText(diagnosticParagraphs, values.paragraphs);
|
|
}
|
|
if (hasValue("heading1")) {
|
|
setText(diagnosticHeading1, values.heading1);
|
|
}
|
|
if (hasValue("heading2")) {
|
|
setText(diagnosticHeading2, values.heading2);
|
|
}
|
|
};
|
|
|
|
const errorText = (error) => {
|
|
const parts = [];
|
|
|
|
if (error?.name) {
|
|
parts.push(error.name);
|
|
}
|
|
if (error?.code && error.code !== error.name) {
|
|
parts.push(error.code);
|
|
}
|
|
if (error?.message) {
|
|
parts.push(error.message);
|
|
}
|
|
if (error?.debugInfo?.errorLocation) {
|
|
parts.push(`Location: ${error.debugInfo.errorLocation}`);
|
|
}
|
|
|
|
return parts.length > 0 ? parts.join(": ") : String(error);
|
|
};
|
|
|
|
const updateOfficeReadiness = async () => {
|
|
if (!window.Office || typeof window.Office.onReady !== "function") {
|
|
officeReady = false;
|
|
wordHostAvailable = false;
|
|
officeHost = "Unavailable";
|
|
officePlatform = "Unavailable";
|
|
setDiagnostics({
|
|
host: officeHost,
|
|
platform: officePlatform,
|
|
ready: "No",
|
|
wordApi: "No"
|
|
});
|
|
return null;
|
|
}
|
|
|
|
const info = await window.Office.onReady();
|
|
officeReady = true;
|
|
officeHost = info?.host || "Unknown";
|
|
officePlatform = info?.platform || "Unknown";
|
|
wordHostAvailable = !!window.Word
|
|
&& !!window.Office.HostType
|
|
&& info?.host === window.Office.HostType.Word;
|
|
setDiagnostics({
|
|
host: officeHost,
|
|
platform: officePlatform,
|
|
ready: "Yes",
|
|
wordApi: wordHostAvailable ? "Yes" : "No"
|
|
});
|
|
return info;
|
|
};
|
|
|
|
const readStoredId = (key) => {
|
|
try {
|
|
const value = window.localStorage.getItem(key);
|
|
const id = Number.parseInt(value || "", 10);
|
|
return Number.isInteger(id) && id > 0 ? id : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const writeStoredId = (key, value) => {
|
|
try {
|
|
if (value) {
|
|
window.localStorage.setItem(key, String(value));
|
|
} else {
|
|
window.localStorage.removeItem(key);
|
|
}
|
|
} catch {
|
|
// Some Office hosts can restrict storage. Selection still works for the current pane session.
|
|
}
|
|
};
|
|
|
|
const resetSelect = (select, message) => {
|
|
if (!select) {
|
|
return;
|
|
}
|
|
|
|
select.innerHTML = "";
|
|
const option = document.createElement("option");
|
|
option.value = "";
|
|
option.textContent = message;
|
|
select.append(option);
|
|
select.disabled = true;
|
|
};
|
|
|
|
const addOptions = (select, placeholder, rows, idKey, titleKey) => {
|
|
if (!select) {
|
|
return;
|
|
}
|
|
|
|
select.innerHTML = "";
|
|
const placeholderOption = document.createElement("option");
|
|
placeholderOption.value = "";
|
|
placeholderOption.textContent = placeholder;
|
|
select.append(placeholderOption);
|
|
|
|
for (const row of rows) {
|
|
const option = document.createElement("option");
|
|
option.value = String(row[idKey]);
|
|
option.textContent = row[titleKey];
|
|
select.append(option);
|
|
}
|
|
|
|
select.disabled = rows.length === 0;
|
|
};
|
|
|
|
const selectedProject = () => {
|
|
const projectId = Number.parseInt(projectSelect?.value || "", 10);
|
|
return projects.find((project) => project.projectId === projectId) || null;
|
|
};
|
|
|
|
const selectedBook = () => {
|
|
const bookId = Number.parseInt(bookSelect?.value || "", 10);
|
|
return books.find((book) => book.bookId === bookId) || null;
|
|
};
|
|
|
|
const updateSummary = () => {
|
|
const project = selectedProject();
|
|
const book = selectedBook();
|
|
|
|
if (summaryProject) {
|
|
summaryProject.textContent = project?.title || "(Not connected)";
|
|
}
|
|
if (summaryBook) {
|
|
summaryBook.textContent = book?.title || "(Not connected)";
|
|
}
|
|
if (syncNote) {
|
|
syncNote.textContent = book ? "" : "Select a PlotDirector book before syncing in a future step.";
|
|
}
|
|
};
|
|
|
|
const normalizedStyleName = (paragraph) => {
|
|
const style = paragraph?.styleBuiltIn || paragraph?.style || "";
|
|
return String(style).replace(/\s+/g, "").toLowerCase();
|
|
};
|
|
|
|
const isBuiltInHeading = (paragraph, level) => {
|
|
const style = normalizedStyleName(paragraph);
|
|
return style === `heading${level}` || style.endsWith(`.heading${level}`) || style.includes(`heading${level}`);
|
|
};
|
|
|
|
const countWords = (text) => {
|
|
const words = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
return words.length;
|
|
};
|
|
|
|
const paragraphMatches = (left, right) => {
|
|
return String(left?.text || "").trim() === String(right?.text || "").trim()
|
|
&& normalizedStyleName(left) === normalizedStyleName(right);
|
|
};
|
|
|
|
const buildDocumentStructure = (paragraphs, selectionParagraphs) => {
|
|
const chapters = [];
|
|
let heading1Count = 0;
|
|
let heading2Count = 0;
|
|
let chapter = null;
|
|
let scene = null;
|
|
|
|
paragraphs.forEach((paragraph, index) => {
|
|
const title = String(paragraph.text || "").trim();
|
|
if (!title) {
|
|
return;
|
|
}
|
|
|
|
if (isBuiltInHeading(paragraph, 1)) {
|
|
heading1Count += 1;
|
|
chapter = {
|
|
index: chapters.length + 1,
|
|
paragraphIndex: index,
|
|
title,
|
|
scenes: []
|
|
};
|
|
chapters.push(chapter);
|
|
scene = null;
|
|
return;
|
|
}
|
|
|
|
if (isBuiltInHeading(paragraph, 2)) {
|
|
heading2Count += 1;
|
|
if (!chapter) {
|
|
return;
|
|
}
|
|
|
|
scene = {
|
|
index: chapter.scenes.length + 1,
|
|
paragraphIndex: index,
|
|
title,
|
|
wordCount: 0
|
|
};
|
|
chapter.scenes.push(scene);
|
|
return;
|
|
}
|
|
|
|
if (scene) {
|
|
scene.wordCount += countWords(title);
|
|
}
|
|
});
|
|
|
|
const selectedParagraph = selectionParagraphs.find((paragraph) => String(paragraph.text || "").trim());
|
|
const selectedIndex = selectedParagraph
|
|
? paragraphs.findIndex((paragraph) => paragraphMatches(paragraph, selectedParagraph))
|
|
: -1;
|
|
const detectedChapter = selectedIndex >= 0
|
|
? chapters.filter((item) => item.paragraphIndex <= selectedIndex).at(-1) || null
|
|
: null;
|
|
const detectedScene = detectedChapter && selectedIndex >= 0
|
|
? detectedChapter.scenes.filter((item) => item.paragraphIndex <= selectedIndex).at(-1) || null
|
|
: null;
|
|
|
|
return {
|
|
chapters,
|
|
currentChapter: detectedChapter,
|
|
currentScene: detectedScene,
|
|
paragraphCount: paragraphs.length,
|
|
heading1Count,
|
|
heading2Count
|
|
};
|
|
};
|
|
|
|
const renderOutline = (structure) => {
|
|
if (!documentOutline) {
|
|
return;
|
|
}
|
|
|
|
documentOutline.innerHTML = "";
|
|
|
|
if (structure.chapters.length === 0) {
|
|
const message = document.createElement("p");
|
|
message.textContent = "No chapters detected. Use Heading 1 for chapter titles.";
|
|
documentOutline.append(message);
|
|
return;
|
|
}
|
|
|
|
const hasScenes = structure.chapters.some((chapter) => chapter.scenes.length > 0);
|
|
if (!hasScenes) {
|
|
const message = document.createElement("p");
|
|
message.textContent = "No scenes detected. Use Heading 2 for scene titles.";
|
|
documentOutline.append(message);
|
|
}
|
|
|
|
for (const chapter of structure.chapters) {
|
|
const chapterBlock = document.createElement("div");
|
|
chapterBlock.className = "word-companion-outline-chapter";
|
|
|
|
const chapterTitle = document.createElement("strong");
|
|
chapterTitle.textContent = chapter.title;
|
|
chapterBlock.append(chapterTitle);
|
|
|
|
if (chapter.scenes.length > 0) {
|
|
const sceneList = document.createElement("ul");
|
|
for (const sceneItem of chapter.scenes) {
|
|
const sceneRow = document.createElement("li");
|
|
sceneRow.textContent = sceneItem.title;
|
|
sceneList.append(sceneRow);
|
|
}
|
|
chapterBlock.append(sceneList);
|
|
}
|
|
|
|
documentOutline.append(chapterBlock);
|
|
}
|
|
};
|
|
|
|
const refreshDocumentStructure = async () => {
|
|
setDocumentMessage("");
|
|
|
|
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
|
setCurrentStructure(null, null, null);
|
|
setDocumentMessage("Word document access is unavailable.");
|
|
setDiagnostics({ lastScan: "Failure" });
|
|
return;
|
|
}
|
|
|
|
if (refreshDocumentButton) {
|
|
refreshDocumentButton.disabled = true;
|
|
refreshDocumentButton.textContent = "Scanning...";
|
|
}
|
|
setDocumentMessage("Scanning document structure...");
|
|
|
|
try {
|
|
const structure = await window.Word.run(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();
|
|
|
|
return buildDocumentStructure(bodyParagraphs.items, selectionParagraphs.items);
|
|
});
|
|
|
|
setCurrentStructure(
|
|
structure.currentChapter?.title,
|
|
structure.currentScene?.title,
|
|
structure.currentScene?.wordCount
|
|
);
|
|
renderOutline(structure);
|
|
setDocumentMessage("");
|
|
setDiagnostics({
|
|
lastScan: "Success",
|
|
paragraphs: String(structure.paragraphCount),
|
|
heading1: String(structure.heading1Count),
|
|
heading2: String(structure.heading2Count)
|
|
});
|
|
} catch (error) {
|
|
const message = errorText(error);
|
|
console.error("Unable to scan the Word document.", error);
|
|
setCurrentStructure(null, null, null);
|
|
setDocumentMessage(`Unable to scan the Word document: ${message}`);
|
|
setDiagnostics({ lastScan: "Failure" });
|
|
} finally {
|
|
if (refreshDocumentButton) {
|
|
refreshDocumentButton.disabled = false;
|
|
refreshDocumentButton.textContent = "Refresh Document Structure";
|
|
}
|
|
}
|
|
};
|
|
|
|
const runDiagnostics = async () => {
|
|
if (runDiagnosticsButton) {
|
|
runDiagnosticsButton.disabled = true;
|
|
runDiagnosticsButton.textContent = "Running...";
|
|
}
|
|
|
|
try {
|
|
await updateOfficeReadiness();
|
|
|
|
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
|
setDiagnostics({
|
|
lastScan: "Failure",
|
|
paragraphs: "-",
|
|
heading1: "-",
|
|
heading2: "-"
|
|
});
|
|
return;
|
|
}
|
|
|
|
const paragraphCount = await window.Word.run(async (context) => {
|
|
const paragraphs = context.document.body.paragraphs;
|
|
paragraphs.load("items");
|
|
await context.sync();
|
|
return paragraphs.items.length;
|
|
});
|
|
|
|
setDiagnostics({
|
|
lastScan: "Success",
|
|
paragraphs: String(paragraphCount)
|
|
});
|
|
} catch (error) {
|
|
console.error("Word Companion diagnostics failed.", error);
|
|
setDiagnostics({ lastScan: "Failure" });
|
|
setDocumentMessage(`Unable to scan the Word document: ${errorText(error)}`);
|
|
} finally {
|
|
if (runDiagnosticsButton) {
|
|
runDiagnosticsButton.disabled = false;
|
|
runDiagnosticsButton.textContent = "Run Diagnostics";
|
|
}
|
|
}
|
|
};
|
|
|
|
const fetchJson = async (url, options = {}) => {
|
|
const response = await window.fetch(url, {
|
|
credentials: "same-origin",
|
|
...options,
|
|
headers: {
|
|
Accept: "application/json",
|
|
...(options.headers || {})
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Request failed: ${response.status}`);
|
|
}
|
|
|
|
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 project = selectedProject();
|
|
const book = selectedBook();
|
|
const requestChapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter");
|
|
const requestSceneTitle = normalizeTitleForResolve(detectedSceneTitle, "scene");
|
|
|
|
setResolveDiagnostics({
|
|
projectId: project?.projectId,
|
|
projectTitle: project?.title,
|
|
bookId: book?.bookId,
|
|
bookTitle: book?.title,
|
|
detectedChapter: detectedChapterTitle,
|
|
detectedScene: detectedSceneTitle,
|
|
requestChapter: requestChapterTitle,
|
|
requestScene: requestSceneTitle,
|
|
result: "Not run",
|
|
chapterId: null,
|
|
sceneId: null
|
|
});
|
|
|
|
if (!book) {
|
|
resetPlotDirectorScene("Select a PlotDirector book.");
|
|
setResolveDiagnostics({ result: "Error" });
|
|
return;
|
|
}
|
|
|
|
if (!detectedChapterTitle || !detectedSceneTitle) {
|
|
resetPlotDirectorScene("No scene detected in Word.");
|
|
setResolveDiagnostics({ result: "Error" });
|
|
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: requestChapterTitle,
|
|
sceneTitle: requestSceneTitle
|
|
});
|
|
|
|
if (!resolveResponse?.matched || !resolveResponse.sceneId) {
|
|
resetPlotDirectorScene("No exact match found for this chapter/scene in the selected book.");
|
|
setResolveDiagnostics({
|
|
result: "Not matched",
|
|
chapterId: resolveResponse?.chapterId,
|
|
sceneId: resolveResponse?.sceneId
|
|
});
|
|
restorePlotDirectorSceneButton();
|
|
return;
|
|
}
|
|
|
|
resolvedSceneId = resolveResponse.sceneId;
|
|
setResolveDiagnostics({
|
|
result: "Matched",
|
|
chapterId: resolveResponse.chapterId,
|
|
sceneId: resolveResponse.sceneId
|
|
});
|
|
} catch {
|
|
resetPlotDirectorScene("Unable to resolve scene.");
|
|
setResolveDiagnostics({ result: "Error" });
|
|
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.");
|
|
setResolveDiagnostics({ result: "Error" });
|
|
} finally {
|
|
restorePlotDirectorSceneButton();
|
|
}
|
|
};
|
|
|
|
const loadBooks = async (projectId, preferredBookId) => {
|
|
if (!projectId) {
|
|
books = [];
|
|
resetSelect(bookSelect, "Select a project first");
|
|
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`);
|
|
books = Array.isArray(data.books) ? data.books : [];
|
|
|
|
if (books.length === 0) {
|
|
resetSelect(bookSelect, "No books available.");
|
|
setBookMessage("No books available.");
|
|
writeStoredId(storageKeys.bookId, null);
|
|
updateSummary();
|
|
return;
|
|
}
|
|
|
|
addOptions(bookSelect, "Choose a book", books, "bookId", "title");
|
|
const validBook = books.find((book) => book.bookId === preferredBookId);
|
|
if (validBook && bookSelect) {
|
|
bookSelect.value = String(validBook.bookId);
|
|
writeStoredId(storageKeys.bookId, validBook.bookId);
|
|
} else {
|
|
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.");
|
|
}
|
|
};
|
|
|
|
const loadProjects = async () => {
|
|
resetSelect(projectSelect, "Loading projects...");
|
|
resetSelect(bookSelect, "Select a project first");
|
|
setProjectMessage("");
|
|
setBookMessage("");
|
|
|
|
try {
|
|
const data = await fetchJson("/api/word-companion/projects");
|
|
projects = Array.isArray(data.projects) ? data.projects : [];
|
|
|
|
if (projects.length === 0) {
|
|
resetSelect(projectSelect, "No projects available.");
|
|
setProjectMessage("No projects available.");
|
|
writeStoredId(storageKeys.projectId, null);
|
|
writeStoredId(storageKeys.bookId, null);
|
|
updateSummary();
|
|
return;
|
|
}
|
|
|
|
addOptions(projectSelect, "Choose a project", projects, "projectId", "title");
|
|
const storedProjectId = readStoredId(storageKeys.projectId);
|
|
const validProject = projects.find((project) => project.projectId === storedProjectId);
|
|
if (validProject && projectSelect) {
|
|
projectSelect.value = String(validProject.projectId);
|
|
writeStoredId(storageKeys.projectId, validProject.projectId);
|
|
await loadBooks(validProject.projectId, readStoredId(storageKeys.bookId));
|
|
} else {
|
|
writeStoredId(storageKeys.projectId, null);
|
|
writeStoredId(storageKeys.bookId, null);
|
|
updateSummary();
|
|
}
|
|
} catch {
|
|
projects = [];
|
|
books = [];
|
|
resetSelect(projectSelect, "Unable to connect to PlotDirector.");
|
|
resetSelect(bookSelect, "Select a project first");
|
|
setConnectionStatus("Unable to connect to PlotDirector.");
|
|
setProjectMessage("Unable to connect to PlotDirector.");
|
|
writeStoredId(storageKeys.projectId, null);
|
|
writeStoredId(storageKeys.bookId, null);
|
|
updateSummary();
|
|
}
|
|
};
|
|
|
|
projectSelect?.addEventListener("change", async () => {
|
|
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);
|
|
});
|
|
|
|
bookSelect?.addEventListener("change", () => {
|
|
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) {
|
|
loadProjects();
|
|
} else {
|
|
resetSelect(projectSelect, "Please sign in to PlotDirector.");
|
|
resetSelect(bookSelect, "Please sign in to PlotDirector.");
|
|
updateSummary();
|
|
}
|
|
|
|
if (!window.Office || typeof window.Office.onReady !== "function") {
|
|
setOfficeStatus("Word Companion ready");
|
|
setDocumentMessage("Word document access is unavailable.");
|
|
setDiagnostics({
|
|
host: "Unavailable",
|
|
platform: "Unavailable",
|
|
ready: "No",
|
|
wordApi: "No"
|
|
});
|
|
return;
|
|
}
|
|
|
|
updateOfficeReadiness()
|
|
.then(() => {
|
|
setOfficeStatus("Word Companion ready");
|
|
if (!wordHostAvailable) {
|
|
setDocumentMessage("Word document access is unavailable.");
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Office readiness check failed.", error);
|
|
setConnectionStatus("Unable to connect to PlotDirector.");
|
|
setOfficeStatus("Word Companion unavailable");
|
|
setDocumentMessage("Word document access is unavailable.");
|
|
setDiagnostics({
|
|
ready: "No",
|
|
wordApi: "No",
|
|
lastScan: "Failure"
|
|
});
|
|
});
|
|
})();
|