(() => { 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" }); }); })();