(() => { const storageKeys = { projectId: "plotdirector.word.projectId", bookId: "plotdirector.word.bookId", activeTab: "plotdirector.word.activeTab" }; const validTabNames = new Set(["scene", "links", "structure", "diagnostics"]); const shell = document.querySelector(".word-companion-shell"); const tabButtons = [...document.querySelectorAll("[data-tab-button]")]; const tabPanels = [...document.querySelectorAll("[data-tab-panel]")]; 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 refreshCurrentSceneButton = document.querySelector("[data-refresh-current-scene]"); 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 sceneTrackingStatus = document.querySelector("[data-scene-tracking-status]"); const documentMessage = document.querySelector("[data-document-message]"); const syncNote = document.querySelector("[data-sync-note]"); const documentOutline = document.querySelector("[data-document-outline]"); const analyseManuscriptStructureButton = document.querySelector("[data-analyse-manuscript-structure]"); const applyStructureSyncButton = document.querySelector("[data-apply-structure-sync]"); const structurePreviewStatus = document.querySelector("[data-structure-preview-status]"); const structurePreviewResults = document.querySelector("[data-structure-preview-results]"); const refreshPlotDirectorSceneButton = document.querySelector("[data-refresh-plotdirector-scene]"); 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 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 syncSceneProgressButton = document.querySelector("[data-sync-scene-progress]"); const syncWordCount = document.querySelector("[data-sync-word-count]"); const syncPlotDirectorCount = document.querySelector("[data-sync-plotdirector-count]"); const syncLastSynced = document.querySelector("[data-sync-last-synced]"); const syncStatus = document.querySelector("[data-sync-status]"); 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 primaryLocationSelect = document.querySelector("[data-primary-location-select]"); const saveSceneLinksButton = document.querySelector("[data-save-scene-links]"); const sceneLinksStatus = document.querySelector("[data-scene-links-status]"); const linksEditingScene = document.querySelector("[data-links-editing-scene]"); const chapterCreateLink = document.querySelector("[data-chapter-create-link]"); const chapterCreateLinkChapter = document.querySelector("[data-chapter-create-link-chapter]"); const createPlotDirectorChapterButton = document.querySelector("[data-create-plotdirector-chapter]"); const linkExistingChapterButton = document.querySelector("[data-link-existing-chapter]"); const chapterCreateLinkStatus = document.querySelector("[data-chapter-create-link-status]"); const sceneCreateLink = document.querySelector("[data-scene-create-link]"); const createLinkChapter = document.querySelector("[data-create-link-chapter]"); const createLinkScene = document.querySelector("[data-create-link-scene]"); const createPlotDirectorSceneButton = document.querySelector("[data-create-plotdirector-scene]"); const linkExistingSceneButton = document.querySelector("[data-link-existing-scene]"); const createLinkStatus = document.querySelector("[data-create-link-status]"); const linkExistingDialog = document.querySelector("[data-link-existing-dialog]"); const linkSceneSearch = document.querySelector("[data-link-scene-search]"); const linkSceneOptions = document.querySelector("[data-link-scene-options]"); const linkSelectedSceneButton = document.querySelector("[data-link-selected-scene]"); const linkDialogCancel = document.querySelector("[data-link-dialog-cancel]"); const linkDialogStatus = document.querySelector("[data-link-dialog-status]"); const createSceneDialog = document.querySelector("[data-create-scene-dialog]"); const createDialogScene = document.querySelector("[data-create-dialog-scene]"); const createDialogChapter = document.querySelector("[data-create-dialog-chapter]"); const createDialogConfirm = document.querySelector("[data-create-dialog-confirm]"); const createDialogCancel = document.querySelector("[data-create-dialog-cancel]"); const linkExistingChapterDialog = document.querySelector("[data-link-existing-chapter-dialog]"); const linkChapterSearch = document.querySelector("[data-link-chapter-search]"); const linkChapterOptions = document.querySelector("[data-link-chapter-options]"); const linkSelectedChapterButton = document.querySelector("[data-link-selected-chapter]"); const linkChapterDialogCancel = document.querySelector("[data-link-chapter-dialog-cancel]"); const linkChapterDialogStatus = document.querySelector("[data-link-chapter-dialog-status]"); const createChapterDialog = document.querySelector("[data-create-chapter-dialog]"); const createChapterDialogChapter = document.querySelector("[data-create-chapter-dialog-chapter]"); const createChapterDialogBook = document.querySelector("[data-create-chapter-dialog-book]"); const createChapterDialogConfirm = document.querySelector("[data-create-chapter-dialog-confirm]"); const createChapterDialogCancel = document.querySelector("[data-create-chapter-dialog-cancel]"); const applyStructureSyncDialog = document.querySelector("[data-apply-structure-sync-dialog]"); const applyStructureSyncSummary = document.querySelector("[data-apply-structure-sync-summary]"); const applyStructureSyncConfirm = document.querySelector("[data-apply-structure-sync-confirm]"); const applyStructureSyncCancel = document.querySelector("[data-apply-structure-sync-cancel]"); 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 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 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 = ""; let detectedSceneWordCount = null; let detectedChapterIndex = null; let detectedChapterAnchorId = null; let detectedSceneAnchorId = null; let resolvedSceneId = null; let resolvedChapterId = null; let currentResolutionMethod = ""; let currentResolutionAnchorType = "Title Match"; let storedAnchorInvalid = false; let linkDialogScenes = []; let selectedLinkSceneId = null; let pendingCreateSceneConfirmation = null; let linkDialogChapters = []; let selectedLinkChapterId = null; let pendingCreateChapterConfirmation = null; let structurePreviewModel = null; let isApplyingStructureSync = false; let pendingApplyStructureSyncConfirmation = null; let sceneTrackingMode = "Manual"; let sceneContextStale = false; let selectionRefreshTimer = null; let selectionHandlerRegistered = false; let isAutoRefreshingScene = false; let lastLinkedSceneTitle = ""; let lastLinkedChapterTitle = ""; let currentSceneLinks = { characters: [], assets: [], locations: [], primaryLocationId: null }; const setOfficeStatus = (message) => { if (officeStatus) { officeStatus.textContent = message; } }; const readStoredText = (key) => { try { return window.localStorage.getItem(key) || ""; } catch { return ""; } }; const writeStoredText = (key, value) => { try { if (value) { window.localStorage.setItem(key, String(value)); } else { window.localStorage.removeItem(key); } } catch { // Some Office hosts can restrict storage. The pane still falls back to the Scene tab. } }; const selectTab = (tabName, persist = true) => { const targetTab = validTabNames.has(tabName) && tabButtons.some((button) => button.dataset.tabButton === tabName) ? tabName : "scene"; for (const button of tabButtons) { const active = button.dataset.tabButton === targetTab; button.classList.toggle("is-active", active); button.setAttribute("aria-selected", active ? "true" : "false"); } for (const panel of tabPanels) { const active = panel.dataset.tabPanel === targetTab; panel.classList.toggle("is-active", active); panel.hidden = !active; } if (persist) { writeStoredText(storageKeys.activeTab, targetTab); } }; const restoreActiveTab = () => { const storedTab = readStoredText(storageKeys.activeTab); selectTab(validTabNames.has(storedTab) ? storedTab : "scene", false); }; 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 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, chapterIndex = null) => { detectedChapterTitle = chapterTitle || ""; detectedSceneTitle = sceneTitle || ""; detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null; detectedChapterIndex = Number.isInteger(chapterIndex) && chapterIndex > 0 ? chapterIndex : null; detectedChapterAnchorId = Number.isInteger(chapterAnchorId) && chapterAnchorId > 0 ? chapterAnchorId : null; detectedSceneAnchorId = Number.isInteger(sceneAnchorId) && sceneAnchorId > 0 ? sceneAnchorId : null; if (currentChapter) { currentChapter.textContent = chapterTitle || "Not detected"; } if (currentScene) { currentScene.textContent = sceneTitle || "Not detected"; } if (currentWordCount) { currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-"; } setText(syncWordCount, Number.isInteger(wordCount) ? String(wordCount) : "-"); updateAnchorDisplay(); }; const setHidden = (element, hidden) => { if (element) { element.hidden = hidden; } }; const setSyncStatus = (message) => { setText(syncStatus, message); }; const setStructurePreviewStatus = (message) => { setText(structurePreviewStatus, message); }; const resetStructurePreview = (message = "Select a project and book to analyse manuscript structure.") => { structurePreviewModel = null; if (structurePreviewResults) { structurePreviewResults.innerHTML = ""; const empty = document.createElement("p"); empty.textContent = "No preview generated."; structurePreviewResults.append(empty); } setStructurePreviewStatus(message); updateApplyStructureSyncButton(); }; const previewItems = (model = structurePreviewModel) => previewGroups.flatMap((group) => Array.isArray(model?.[group.key]) ? model[group.key] : []); const actionablePreviewItems = (model = structurePreviewModel) => previewItems(model) .filter((item) => item.category === "couldLink" || item.category === "wouldCreateChapters" || item.category === "wouldCreateScenes"); const updateApplyStructureSyncButton = () => { if (applyStructureSyncButton) { applyStructureSyncButton.disabled = isApplyingStructureSync || actionablePreviewItems().length === 0; } }; const setAnalyseButtonEnabled = () => { if (analyseManuscriptStructureButton) { analyseManuscriptStructureButton.disabled = isApplyingStructureSync || !selectedProject() || !selectedBook(); } updateApplyStructureSyncButton(); }; const updateLinksEditingScene = () => { if (!linksEditingScene) { return; } linksEditingScene.textContent = resolvedSceneId ? `Editing links for: ${lastLinkedSceneTitle || detectedSceneTitle || "Current scene"}` : "Link a PlotDirector scene first."; }; const updateSceneActionButtons = () => { const blocked = sceneContextStale || !resolvedSceneId; if (syncSceneProgressButton) { syncSceneProgressButton.disabled = blocked; } if (saveSceneLinksButton) { saveSceneLinksButton.disabled = blocked; } if (primaryLocationSelect) { primaryLocationSelect.disabled = blocked; } for (const container of [characterChips, assetChips]) { for (const checkbox of container ? [...container.querySelectorAll("input[type='checkbox']")] : []) { checkbox.disabled = blocked; } } }; const setSceneTrackingState = (status) => { if (status === "Live" || status === "Manual") { sceneTrackingMode = status; } setText(sceneTrackingStatus, status || sceneTrackingMode || "Manual"); }; const setSceneContextStale = (isStale, message = "") => { sceneContextStale = !!isStale; setText(sceneTrackingStatus, sceneContextStale ? "Stale" : sceneTrackingMode); updateSceneActionButtons(); if (message) { setDocumentMessage(message); setSceneLinksStatus(message); } }; const setResolvedScene = (sceneId, chapterId = null, resolutionMethod = "") => { resolvedSceneId = Number.isInteger(sceneId) && sceneId > 0 ? sceneId : null; resolvedChapterId = Number.isInteger(chapterId) && chapterId > 0 ? chapterId : null; currentResolutionMethod = resolutionMethod || ""; updateSceneActionButtons(); setSyncStatus(sceneContextStale ? "Refresh Current Scene before syncing." : resolvedSceneId ? "Ready to sync." : "No resolved PlotDirector scene."); setSceneLinksStatus(sceneContextStale ? "Refresh Current Scene before saving." : resolvedSceneId ? "Ready to save." : "Link a PlotDirector scene first."); updateLinksEditingScene(); updateAnchorDisplay(); }; const setRefreshCurrentSceneBusy = (isBusy) => { if (!refreshCurrentSceneButton) { return; } refreshCurrentSceneButton.disabled = isBusy; refreshCurrentSceneButton.textContent = isBusy ? "Refreshing..." : "Refresh Current Scene"; }; const displayValue = (value) => { if (value === null || value === undefined || value === "") { return "-"; } return String(value); }; const setSceneLinksStatus = (message) => { setText(sceneLinksStatus, message); }; const setCreateLinkStatus = (message) => { setText(createLinkStatus, message); }; const setChapterCreateLinkStatus = (message) => { setText(chapterCreateLinkStatus, message); }; const canUseChapterCreateLinkActions = () => !!selectedBook() && !!detectedChapterTitle && !resolvedChapterId; const canUseCreateLinkActions = () => !!selectedBook() && !!detectedSceneTitle && Number.isInteger(resolvedChapterId) && resolvedChapterId > 0; const hideChapterCreateLink = () => { setHidden(chapterCreateLink, true); setChapterCreateLinkStatus(""); if (createPlotDirectorChapterButton) { createPlotDirectorChapterButton.disabled = true; createPlotDirectorChapterButton.textContent = "Create Chapter in PlotDirector"; } if (linkExistingChapterButton) { linkExistingChapterButton.disabled = true; linkExistingChapterButton.textContent = "Link to Existing Chapter"; } }; const showChapterCreateLink = (message = "") => { setText(chapterCreateLinkChapter, displayValue(detectedChapterTitle)); setHidden(chapterCreateLink, false); setChapterCreateLinkStatus(message); const enabled = canUseChapterCreateLinkActions(); if (createPlotDirectorChapterButton) { createPlotDirectorChapterButton.disabled = !enabled; createPlotDirectorChapterButton.textContent = "Create Chapter in PlotDirector"; } if (linkExistingChapterButton) { linkExistingChapterButton.disabled = !enabled; linkExistingChapterButton.textContent = "Link to Existing Chapter"; } }; const hideSceneCreateLink = () => { setHidden(sceneCreateLink, true); setCreateLinkStatus(""); if (createPlotDirectorSceneButton) { createPlotDirectorSceneButton.disabled = true; createPlotDirectorSceneButton.textContent = "Create Scene in PlotDirector"; } if (linkExistingSceneButton) { linkExistingSceneButton.disabled = true; linkExistingSceneButton.textContent = "Link to Existing Scene"; } }; const showSceneCreateLink = (message = "") => { setText(createLinkChapter, displayValue(detectedChapterTitle)); setText(createLinkScene, displayValue(detectedSceneTitle)); setHidden(sceneCreateLink, false); setCreateLinkStatus(message); const enabled = canUseCreateLinkActions(); if (createPlotDirectorSceneButton) { createPlotDirectorSceneButton.disabled = !enabled; createPlotDirectorSceneButton.textContent = "Create Scene in PlotDirector"; } if (linkExistingSceneButton) { linkExistingSceneButton.disabled = !enabled; linkExistingSceneButton.textContent = "Link to Existing Scene"; } }; const resetPlotDirectorScene = (message) => { setPlotDirectorSceneStatus(message); setHidden(plotDirectorSceneDetails, true); setHidden(writingBriefBlock, true); setHidden(plotDirectorLinkGroups, false); hideChapterCreateLink(); hideSceneCreateLink(); currentResolutionMethod = ""; lastLinkedSceneTitle = ""; lastLinkedChapterTitle = ""; currentResolutionAnchorType = detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match"; storedAnchorInvalid = false; currentSceneLinks = { characters: [], assets: [], locations: [], primaryLocationId: null }; renderSceneLinkList(characterChips, [], "character"); renderSceneLinkList(assetChips, [], "asset"); renderPrimaryLocationOptions([], null, false); setResolvedScene(null); setText(syncPlotDirectorCount, "-"); setText(plotDirectorSceneTitle, "-"); setText(plotDirectorRevisionStatus, "-"); setText(plotDirectorEstimatedWords, "-"); setText(plotDirectorActualWords, "-"); setText(plotDirectorBlockedStatus, "-"); setText(plotDirectorBlockedReason, "-"); setHidden(plotDirectorBlockedReasonRow, true); updateAnchorDisplay(); updateLinksEditingScene(); }; const linkItemId = (item, type) => { if (type === "asset") { return item?.assetId || item?.AssetId || item?.storyAssetId || item?.StoryAssetId || 0; } if (type === "location") { return item?.locationId || item?.LocationId || 0; } return item?.characterId || item?.CharacterId || 0; }; const renderSceneLinkList = (container, items, type, enabled = false) => { if (!container) { return; } container.innerHTML = ""; const rows = Array.isArray(items) ? items : []; if (rows.length === 0) { const empty = document.createElement("p"); empty.className = "word-companion-empty-list"; empty.textContent = "None"; container.append(empty); return; } for (const item of rows) { const id = Number.parseInt(linkItemId(item, type), 10); if (!Number.isInteger(id) || id <= 0) { continue; } const label = document.createElement("label"); label.className = "word-companion-check-item"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = String(id); checkbox.checked = !!item.linked; checkbox.disabled = !enabled; const text = document.createElement("span"); text.textContent = item.name || "Unnamed"; label.append(checkbox, text); container.append(label); } }; const renderPrimaryLocationOptions = (items, selectedLocationId, enabled = false) => { if (!primaryLocationSelect) { return; } primaryLocationSelect.innerHTML = ""; const none = document.createElement("option"); none.value = ""; none.textContent = "None"; primaryLocationSelect.append(none); const selectedId = Number.parseInt(selectedLocationId || "", 10); for (const item of Array.isArray(items) ? items : []) { const id = Number.parseInt(linkItemId(item, "location"), 10); if (!Number.isInteger(id) || id <= 0) { continue; } const option = document.createElement("option"); option.value = String(id); option.textContent = item.name || "Unnamed"; option.selected = id === selectedId; primaryLocationSelect.append(option); } primaryLocationSelect.disabled = !enabled; }; const renderCompanionData = (data) => { lastLinkedSceneTitle = data?.sceneTitle || detectedSceneTitle || ""; lastLinkedChapterTitle = data?.chapterTitle || detectedChapterTitle || ""; 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(syncPlotDirectorCount, 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 has been added for this scene."; } currentSceneLinks = { characters: Array.isArray(data?.characters) ? data.characters : [], assets: Array.isArray(data?.assets) ? data.assets : [], locations: Array.isArray(data?.locations) ? data.locations : [], primaryLocationId: data?.primaryLocationId ?? null }; renderSceneLinkList(characterChips, currentSceneLinks.characters, "character", !!resolvedSceneId && !sceneContextStale); renderSceneLinkList(assetChips, currentSceneLinks.assets, "asset", !!resolvedSceneId && !sceneContextStale); renderPrimaryLocationOptions(currentSceneLinks.locations, currentSceneLinks.primaryLocationId, !!resolvedSceneId && !sceneContextStale); updateLinksEditingScene(); setSceneLinksStatus(sceneContextStale ? "Refresh Current Scene before saving." : resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene."); 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 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 getBookStructureChapters = async (bookId) => { const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`); return Array.isArray(structure?.chapters) ? structure.chapters : []; }; const fetchBookChapters = async (bookId) => { const response = await fetchJson(`/api/word-companion/books/${bookId}/chapters`); return Array.isArray(response?.chapters) ? response.chapters : []; }; const findCurrentChapterInChapters = (chapters) => { if (Number.isInteger(resolvedChapterId) && resolvedChapterId > 0) { const anchoredChapter = chapters.find((chapter) => chapter.chapterId === resolvedChapterId); if (anchoredChapter) { return anchoredChapter; } } const requestChapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter").toLocaleLowerCase(); if (!requestChapterTitle) { return null; } return chapters.find((chapter) => normalizeTitleForResolve(chapter.title, "chapter").toLocaleLowerCase() === requestChapterTitle) || null; }; const findCurrentChapter = async () => { const book = selectedBook(); if (!book) { return null; } return findCurrentChapterInChapters(await getBookStructureChapters(book.bookId)); }; 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; setSceneContextStale(false); setPlotDirectorSceneStatus("Linked to PlotDirector"); setDocumentMessage(""); hideChapterCreateLink(); hideSceneCreateLink(); setResolvedScene(sceneId, chapterId, resolutionMethod); renderCompanionData(companionData); }; 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("lastError")) { setText(diagnosticLastError, values.lastError); } 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 bookDisplayTitle = (book) => { const title = String(book?.title || "").trim(); const subtitle = String(book?.subtitle || "").trim(); return subtitle ? `${title}: ${subtitle}` : title; }; 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 ? bookDisplayTitle(book) : "(Not connected)"; } if (syncNote) { syncNote.textContent = book ? "" : "Select a PlotDirector book before refreshing."; } setAnalyseButtonEnabled(); }; 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 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 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, anchorId: paragraphAnchorId(paragraph, "PD-CHAPTER"), 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, anchorId: paragraphAnchorId(paragraph, "PD-SCENE"), 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 previewGroups = [ { key: "alreadyLinked", title: "Already Linked" }, { key: "couldLink", title: "Could Link" }, { key: "wouldCreateChapters", title: "Would Create Chapters" }, { key: "wouldCreateScenes", title: "Would Create Scenes" }, { key: "manualReview", title: "Manual Review Required" } ]; const previewActionLabels = { alreadyLinked: "Already Linked", couldLink: "Could Link", wouldCreateChapter: "Would Create Chapter", wouldCreateScene: "Would Create Scene", manualReview: "Manual Review Required" }; const previewActionSymbols = { alreadyLinked: "✓", couldLink: "~", wouldCreateChapter: "+", wouldCreateScene: "+", manualReview: "?" }; const titleKey = (title, type) => normalizeTitleForResolve(title, type).toLocaleLowerCase(); const createPreviewItem = ({ category, action, type, wordTitle, match = "", anchorStatus = "None", matches = [], pdChapter = null, pdScene = null, wordChapter = null, wordScene = null, parentItem = null }) => ({ id: `${type}-${wordChapter?.index || 0}-${wordScene?.index || 0}-${wordChapter?.paragraphIndex ?? wordScene?.paragraphIndex ?? 0}`, category, action, type, wordTitle, match, anchorStatus, matches, pdChapter, pdScene, wordChapter, wordScene, parentItem, result: "Pending", error: "" }); const addPreviewItem = (model, item) => { if (Array.isArray(model[item.category])) { model[item.category].push(item); } }; const sceneRowsForChapter = (chapter) => Array.isArray(chapter?.scenes) ? chapter.scenes : []; const findSceneById = (chapters, sceneId) => { for (const chapter of chapters) { const scene = sceneRowsForChapter(chapter).find((item) => item.sceneId === sceneId); if (scene) { return { chapter, scene }; } } return null; }; const compareChapterForPreview = (wordChapter, plotDirectorChapters, model) => { const anchorStatus = wordChapter.anchorId ? `PD-CHAPTER-${wordChapter.anchorId}` : "None"; if (wordChapter.anchorId) { const anchoredChapter = plotDirectorChapters.find((chapter) => chapter.chapterId === wordChapter.anchorId); if (anchoredChapter) { const item = createPreviewItem({ category: "alreadyLinked", action: "alreadyLinked", type: "Chapter", wordTitle: wordChapter.title, match: `Chapter ${anchoredChapter.chapterId}: ${anchoredChapter.title}`, anchorStatus, pdChapter: anchoredChapter, wordChapter }); addPreviewItem(model, item); return item; } const item = createPreviewItem({ category: "manualReview", action: "manualReview", type: "Chapter", wordTitle: wordChapter.title, anchorStatus: `${anchorStatus} not found`, matches: [], pdChapter: null, wordChapter }); addPreviewItem(model, item); return item; } const matches = plotDirectorChapters.filter((chapter) => titleKey(chapter.title, "chapter") === titleKey(wordChapter.title, "chapter")); if (matches.length === 1) { const item = createPreviewItem({ category: "couldLink", action: "couldLink", type: "Chapter", wordTitle: wordChapter.title, match: `Chapter ${matches[0].chapterId}: ${matches[0].title}`, anchorStatus, pdChapter: matches[0], wordChapter }); addPreviewItem(model, item); return item; } if (matches.length > 1) { const item = createPreviewItem({ category: "manualReview", action: "manualReview", type: "Chapter", wordTitle: wordChapter.title, anchorStatus, matches: matches.map((chapter) => `Chapter ${chapter.chapterId}: ${chapter.title}`), pdChapter: null, wordChapter }); addPreviewItem(model, item); return item; } const item = createPreviewItem({ category: "wouldCreateChapters", action: "wouldCreateChapter", type: "Chapter", wordTitle: wordChapter.title, anchorStatus, pdChapter: null, wordChapter }); addPreviewItem(model, item); return item; }; const compareSceneForPreview = (wordScene, chapterPreview, plotDirectorChapters, model) => { const anchorStatus = wordScene.anchorId ? `PD-SCENE-${wordScene.anchorId}` : "None"; if (wordScene.anchorId) { const anchored = findSceneById(plotDirectorChapters, wordScene.anchorId); if (anchored) { addPreviewItem(model, createPreviewItem({ category: "alreadyLinked", action: "alreadyLinked", type: "Scene", wordTitle: wordScene.title, match: `Scene ${anchored.scene.sceneId}: ${anchored.scene.title}`, anchorStatus, pdChapter: anchored.chapter, pdScene: anchored.scene, wordChapter: chapterPreview?.wordChapter, wordScene, parentItem: chapterPreview })); return; } addPreviewItem(model, createPreviewItem({ category: "manualReview", action: "manualReview", type: "Scene", wordTitle: wordScene.title, anchorStatus: `${anchorStatus} not found`, wordChapter: chapterPreview?.wordChapter, wordScene, parentItem: chapterPreview })); return; } if (!chapterPreview?.pdChapter && chapterPreview?.category === "manualReview") { addPreviewItem(model, createPreviewItem({ category: "manualReview", action: "manualReview", type: "Scene", wordTitle: wordScene.title, anchorStatus, matches: ["Parent chapter requires manual review."], wordChapter: chapterPreview?.wordChapter, wordScene, parentItem: chapterPreview })); return; } if (!chapterPreview?.pdChapter) { addPreviewItem(model, createPreviewItem({ category: "wouldCreateScenes", action: "wouldCreateScene", type: "Scene", wordTitle: wordScene.title, anchorStatus, wordChapter: chapterPreview?.wordChapter, wordScene, parentItem: chapterPreview })); return; } const matches = sceneRowsForChapter(chapterPreview.pdChapter) .filter((scene) => titleKey(scene.title, "scene") === titleKey(wordScene.title, "scene")); if (matches.length === 1) { addPreviewItem(model, createPreviewItem({ category: "couldLink", action: "couldLink", type: "Scene", wordTitle: wordScene.title, match: `Scene ${matches[0].sceneId}: ${matches[0].title}`, anchorStatus, pdChapter: chapterPreview.pdChapter, pdScene: matches[0], wordChapter: chapterPreview.wordChapter, wordScene, parentItem: chapterPreview })); return; } if (matches.length > 1) { addPreviewItem(model, createPreviewItem({ category: "manualReview", action: "manualReview", type: "Scene", wordTitle: wordScene.title, anchorStatus, matches: matches.map((scene) => `Scene ${scene.sceneId}: ${scene.title}`), wordChapter: chapterPreview.wordChapter, wordScene, parentItem: chapterPreview })); return; } addPreviewItem(model, createPreviewItem({ category: "wouldCreateScenes", action: "wouldCreateScene", type: "Scene", wordTitle: wordScene.title, anchorStatus, pdChapter: chapterPreview.pdChapter, wordChapter: chapterPreview.wordChapter, wordScene, parentItem: chapterPreview })); }; const buildStructurePreviewModel = (wordStructure, plotDirectorStructure) => { const model = { alreadyLinked: [], couldLink: [], wouldCreateChapters: [], wouldCreateScenes: [], manualReview: [] }; const plotDirectorChapters = Array.isArray(plotDirectorStructure?.chapters) ? plotDirectorStructure.chapters : []; for (const wordChapter of Array.isArray(wordStructure?.chapters) ? wordStructure.chapters : []) { const chapterPreview = compareChapterForPreview(wordChapter, plotDirectorChapters, model); for (const wordScene of sceneRowsForChapter(wordChapter)) { compareSceneForPreview(wordScene, chapterPreview, plotDirectorChapters, model); } } return model; }; const renderPreviewItem = (item) => { const row = document.createElement("article"); row.className = "word-companion-preview-item"; const title = document.createElement("strong"); title.textContent = `${previewActionSymbols[item.action] || ""} ${item.type}: ${item.wordTitle}`; row.append(title); const anchor = document.createElement("span"); anchor.textContent = `Anchor: ${item.anchorStatus || "None"}`; row.append(anchor); if (item.match) { const match = document.createElement("span"); match.textContent = `Match: ${item.match}`; row.append(match); } if (Array.isArray(item.matches) && item.matches.length > 0) { const matches = document.createElement("div"); matches.className = "word-companion-preview-matches"; const label = document.createElement("span"); label.textContent = "Matches:"; matches.append(label); const list = document.createElement("ul"); for (const matchText of item.matches) { const li = document.createElement("li"); li.textContent = matchText; list.append(li); } matches.append(list); row.append(matches); } const action = document.createElement("span"); action.textContent = `Action: ${previewActionLabels[item.action] || item.action}`; row.append(action); if (item.result && item.result !== "Pending") { const result = document.createElement("span"); result.textContent = item.error ? `Result: ${item.result} - ${item.error}` : `Result: ${item.result}`; row.append(result); } return row; }; const renderStructurePreview = (model) => { if (!structurePreviewResults) { return; } structurePreviewResults.innerHTML = ""; for (const group of previewGroups) { const section = document.createElement("section"); section.className = "word-companion-preview-group"; const heading = document.createElement("strong"); heading.textContent = group.title; section.append(heading); const items = Array.isArray(model?.[group.key]) ? model[group.key] : []; if (items.length === 0) { const empty = document.createElement("p"); empty.className = "word-companion-empty-list"; empty.textContent = "None"; section.append(empty); } else { for (const item of items) { section.append(renderPreviewItem(item)); } } structurePreviewResults.append(section); } updateApplyStructureSyncButton(); }; 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 () => { setDocumentMessage(""); if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setCurrentStructure(null, null, null); setDocumentMessage("Word document access is unavailable."); setDiagnostics({ lastScan: "Failure", lastError: "Word document access is unavailable." }); return false; } if (refreshDocumentButton) { refreshDocumentButton.disabled = true; refreshDocumentButton.textContent = "Scanning..."; } setDocumentMessage("Scanning document structure..."); try { const structure = await window.Word.run(async (context) => { return await readDocumentStructure(context); }); setCurrentStructure( structure.currentChapter?.title, structure.currentScene?.title, structure.currentScene?.wordCount, structure.currentChapter?.anchorId, structure.currentScene?.anchorId, structure.currentChapter?.index ); resetPlotDirectorScene("Not detected"); renderOutline(structure); setDocumentMessage(""); setDiagnostics({ lastScan: "Success", lastError: "-", paragraphs: String(structure.paragraphCount), heading1: String(structure.heading1Count), heading2: String(structure.heading2Count) }); return true; } 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", lastError: message }); return false; } finally { if (refreshDocumentButton) { refreshDocumentButton.disabled = false; refreshDocumentButton.textContent = "Refresh Document Structure"; } } }; const analyseManuscriptStructure = async () => { const book = selectedBook(); if (!selectedProject() || !book) { setStructurePreviewStatus("Select a project and book to analyse manuscript structure."); return false; } if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setStructurePreviewStatus("Word document access is unavailable."); setDiagnostics({ lastScan: "Failure", lastError: "Word document access is unavailable." }); return false; } if (analyseManuscriptStructureButton) { analyseManuscriptStructureButton.disabled = true; analyseManuscriptStructureButton.textContent = "Analysing..."; } setStructurePreviewStatus("Analysing manuscript structure..."); try { const wordStructure = await window.Word.run(async (context) => { return await readDocumentStructure(context); }); const plotDirectorStructure = await fetchJson(`/api/word-companion/books/${book.bookId}/structure`); setCurrentStructure( wordStructure.currentChapter?.title, wordStructure.currentScene?.title, wordStructure.currentScene?.wordCount, wordStructure.currentChapter?.anchorId, wordStructure.currentScene?.anchorId, wordStructure.currentChapter?.index ); renderOutline(wordStructure); structurePreviewModel = buildStructurePreviewModel(wordStructure, plotDirectorStructure); renderStructurePreview(structurePreviewModel); setStructurePreviewStatus("Preview generated. No changes have been applied."); setDiagnostics({ lastScan: "Success", lastError: "-", paragraphs: String(wordStructure.paragraphCount), heading1: String(wordStructure.heading1Count), heading2: String(wordStructure.heading2Count) }); return true; } catch (error) { console.error("Unable to analyse manuscript structure.", error); setStructurePreviewStatus("Unable to analyse manuscript structure."); setDiagnostics({ lastScan: "Failure", lastError: errorText(error) }); return false; } finally { if (analyseManuscriptStructureButton) { analyseManuscriptStructureButton.textContent = "Analyse Manuscript Structure"; setAnalyseButtonEnabled(); } } }; const applyCounts = (model = structurePreviewModel) => { const actionable = actionablePreviewItems(model); const linkChapters = actionable.filter((item) => item.category === "couldLink" && item.type === "Chapter").length; const linkScenes = actionable.filter((item) => item.category === "couldLink" && item.type === "Scene").length; const createChapters = actionable.filter((item) => item.category === "wouldCreateChapters").length; const createScenes = actionable.filter((item) => item.category === "wouldCreateScenes").length; const manualReview = Array.isArray(model?.manualReview) ? model.manualReview.length : 0; return { linkChapters, linkScenes, createChapters, createScenes, manualReview }; }; const renderApplySummary = (counts) => { if (!applyStructureSyncSummary) { return; } applyStructureSyncSummary.innerHTML = ""; const lines = [ `Link ${counts.linkChapters + counts.linkScenes} chapters/scenes`, `Create ${counts.createChapters} chapters`, `Create ${counts.createScenes} scenes` ]; const list = document.createElement("ul"); for (const line of lines) { const item = document.createElement("li"); item.textContent = line; list.append(item); } applyStructureSyncSummary.append(list); }; const closeApplyStructureSyncDialog = (confirmed) => { if (pendingApplyStructureSyncConfirmation) { pendingApplyStructureSyncConfirmation(confirmed); pendingApplyStructureSyncConfirmation = null; } if (applyStructureSyncDialog?.close) { applyStructureSyncDialog.close(); } else if (applyStructureSyncDialog) { applyStructureSyncDialog.hidden = true; } }; const confirmApplyStructureSync = () => { const counts = applyCounts(); renderApplySummary(counts); if (!applyStructureSyncDialog) { return Promise.resolve(window.confirm( `Apply Structure Sync?\n\nThis will:\n\n- Link ${counts.linkChapters + counts.linkScenes} chapters/scenes\n- Create ${counts.createChapters} chapters\n- Create ${counts.createScenes} scenes\n\nManual review items will be skipped.` )); } return new Promise((resolve) => { pendingApplyStructureSyncConfirmation = resolve; if (applyStructureSyncDialog.showModal) { applyStructureSyncDialog.showModal(); } else { applyStructureSyncDialog.hidden = false; } }); }; const markPreviewItem = (item, result, error = "") => { item.result = result; item.error = error; }; const previewChapterSortOrder = (item) => Number.isInteger(item?.wordChapter?.index) && item.wordChapter.index > 0 ? item.wordChapter.index * 10 : null; const previewSceneSortOrder = (item) => Number.isInteger(item?.wordScene?.index) && item.wordScene.index > 0 ? item.wordScene.index * 10 : null; const createChapterFromPreviewItem = async (bookId, item) => { const response = await postJson(`/api/word-companion/books/${bookId}/chapters`, { title: normalizeWhitespace(item.wordTitle), sortOrder: previewChapterSortOrder(item) }); if (!response?.chapterId) { throw new Error("Chapter creation did not return a chapter ID."); } item.pdChapter = { chapterId: response.chapterId, title: response.title || item.wordTitle, sortOrder: response.sortOrder || previewChapterSortOrder(item) || 0, scenes: [] }; item.match = `Chapter ${item.pdChapter.chapterId}: ${item.pdChapter.title}`; }; const createSceneFromPreviewItem = async (bookId, item) => { const chapter = item.pdChapter || item.parentItem?.pdChapter; if (!chapter?.chapterId) { throw new Error("Scene parent chapter is not available."); } const response = await postJson(`/api/word-companion/books/${bookId}/chapters/${chapter.chapterId}/scenes`, { sceneTitle: normalizeWhitespace(item.wordTitle), sortOrder: previewSceneSortOrder(item) }); if (!response?.sceneId) { throw new Error("Scene creation did not return a scene ID."); } item.pdChapter = chapter; item.pdScene = { sceneId: response.sceneId, title: item.wordTitle, sortOrder: previewSceneSortOrder(item) || 0 }; item.match = `Scene ${item.pdScene.sceneId}: ${item.pdScene.title}`; }; const attachAnchorForPreviewItem = async (item) => { if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { throw new Error("Word document access is unavailable."); } await window.Word.run(async (context) => { const structure = await readDocumentStructure(context); const source = item.type === "Chapter" ? item.wordChapter : item.wordScene; const paragraph = Number.isInteger(source?.paragraphIndex) ? structure.bodyParagraphs[source.paragraphIndex] : null; if (!paragraph) { throw new Error("Unable to locate the Word heading."); } if (item.type === "Chapter") { const chapterId = item.pdChapter?.chapterId; if (!chapterId) { throw new Error("Chapter ID is not available."); } ensureAnchorControl(paragraph, "PlotDirector Chapter", `PD-CHAPTER-${chapterId}`, "PD-CHAPTER"); } else { const sceneId = item.pdScene?.sceneId; if (!sceneId) { throw new Error("Scene ID is not available."); } ensureAnchorControl(paragraph, "PlotDirector Scene", `PD-SCENE-${sceneId}`, "PD-SCENE"); } await context.sync(); }); }; const resultSummaryText = (summary) => `Structure Sync Complete. Created: ${summary.createdChapters} Chapters, ${summary.createdScenes} Scenes. Linked: ${summary.linkedChapters} Chapters, ${summary.linkedScenes} Scenes. Skipped: ${summary.skipped} Manual Review items. Failed: ${summary.failed} Items.`; const applyStructureSync = async () => { const book = selectedBook(); if (!book || actionablePreviewItems().length === 0 || isApplyingStructureSync) { updateApplyStructureSyncButton(); return; } const confirmed = await confirmApplyStructureSync(); if (!confirmed) { return; } isApplyingStructureSync = true; setAnalyseButtonEnabled(); setStructurePreviewStatus("Applying structure sync..."); const summary = { createdChapters: 0, createdScenes: 0, linkedChapters: 0, linkedScenes: 0, skipped: Array.isArray(structurePreviewModel?.manualReview) ? structurePreviewModel.manualReview.length : 0, failed: 0 }; const linkChapters = actionablePreviewItems().filter((item) => item.category === "couldLink" && item.type === "Chapter"); const createChapters = actionablePreviewItems().filter((item) => item.category === "wouldCreateChapters"); const linkScenes = actionablePreviewItems().filter((item) => item.category === "couldLink" && item.type === "Scene"); const createScenes = actionablePreviewItems().filter((item) => item.category === "wouldCreateScenes"); const anchorQueue = []; let finalStatus = ""; const queueAnchor = (item, counterName) => { anchorQueue.push({ item, counterName }); }; const attachQueuedItem = async (item, counterName) => { try { await attachAnchorForPreviewItem(item); markPreviewItem(item, "Success"); summary[counterName] += 1; } catch (error) { markPreviewItem(item, "Failed", errorText(error)); summary.failed += 1; } }; try { for (const item of linkChapters) { queueAnchor(item, "linkedChapters"); } for (const item of createChapters) { try { await createChapterFromPreviewItem(book.bookId, item); queueAnchor(item, "createdChapters"); } catch (error) { markPreviewItem(item, "Failed", errorText(error)); summary.failed += 1; } } for (const item of linkScenes) { queueAnchor(item, "linkedScenes"); } for (const item of createScenes) { try { await createSceneFromPreviewItem(book.bookId, item); queueAnchor(item, "createdScenes"); } catch (error) { markPreviewItem(item, "Failed", errorText(error)); summary.failed += 1; } } for (const queued of anchorQueue) { await attachQueuedItem(queued.item, queued.counterName); } for (const item of structurePreviewModel.manualReview || []) { markPreviewItem(item, "Skipped"); } renderStructurePreview(structurePreviewModel); finalStatus = resultSummaryText(summary); setStructurePreviewStatus(finalStatus); } finally { isApplyingStructureSync = false; setAnalyseButtonEnabled(); if (finalStatus) { await analyseManuscriptStructure(); setStructurePreviewStatus(`${finalStatus} Preview refreshed.`); } } }; 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", lastError: "Word document access is unavailable.", 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", lastError: "-", paragraphs: String(paragraphCount) }); } catch (error) { console.error("Word Companion diagnostics failed.", error); const message = errorText(error); setDiagnostics({ lastScan: "Failure", lastError: message }); setDocumentMessage(`Unable to scan the Word document: ${message}`); } 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 patchJson = (url, body) => fetchJson(url, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const putJson = (url, body) => fetchJson(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const selectedLinkIds = (container) => { if (!container) { return []; } return [...container.querySelectorAll("input[type='checkbox']:checked")] .map((checkbox) => Number.parseInt(checkbox.value || "", 10)) .filter((id) => Number.isInteger(id) && id > 0); }; const selectedPrimaryLocationId = () => { const id = Number.parseInt(primaryLocationSelect?.value || "", 10); return Number.isInteger(id) && id > 0 ? id : null; }; const updateCurrentSceneLinkState = () => { const selectedCharacters = new Set(selectedLinkIds(characterChips)); const selectedAssets = new Set(selectedLinkIds(assetChips)); currentSceneLinks = { characters: currentSceneLinks.characters.map((item) => ({ ...item, linked: selectedCharacters.has(Number.parseInt(linkItemId(item, "character"), 10)) })), assets: currentSceneLinks.assets.map((item) => ({ ...item, linked: selectedAssets.has(Number.parseInt(linkItemId(item, "asset"), 10)) })), locations: currentSceneLinks.locations, primaryLocationId: selectedPrimaryLocationId() }; }; const verifyCurrentWordSceneMatchesLinkedScene = async () => { if (!resolvedSceneId) { return false; } if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setSceneContextStale(true, "The Word cursor is now in a different scene. Refresh Current Scene before saving."); return false; } try { const structure = await window.Word.run(async (context) => { return await readDocumentStructure(context); }); const currentChapterTitle = structure.currentChapter?.title || ""; const currentSceneTitle = structure.currentScene?.title || ""; const currentSceneAnchorId = structure.currentScene?.anchorId || null; const currentChapterAnchorId = structure.currentChapter?.anchorId || null; const anchoredSceneMatches = Number.isInteger(currentSceneAnchorId) && currentSceneAnchorId === resolvedSceneId; const anchoredChapterMatches = Number.isInteger(currentChapterAnchorId) && currentChapterAnchorId === resolvedChapterId; const titleSceneMatches = normalizeTitleForResolve(currentSceneTitle, "scene").toLocaleLowerCase() === normalizeTitleForResolve(lastLinkedSceneTitle || detectedSceneTitle, "scene").toLocaleLowerCase(); const titleChapterMatches = normalizeTitleForResolve(currentChapterTitle, "chapter").toLocaleLowerCase() === normalizeTitleForResolve(lastLinkedChapterTitle || detectedChapterTitle, "chapter").toLocaleLowerCase(); const matches = anchoredSceneMatches || (titleSceneMatches && (anchoredChapterMatches || titleChapterMatches)); if (!matches) { setSceneContextStale(true, "The Word cursor is now in a different scene. Refresh Current Scene before saving."); return false; } setSceneContextStale(false); return true; } catch (error) { console.error("Unable to verify current Word scene before saving.", error); setDiagnostics({ lastError: errorText(error) }); setSceneContextStale(true, "The Word cursor is now in a different scene. Refresh Current Scene before saving."); return false; } }; const saveSceneLinks = async () => { if (!resolvedSceneId) { setSceneLinksStatus("Link a PlotDirector scene first."); return; } if (sceneContextStale || !(await verifyCurrentWordSceneMatchesLinkedScene())) { setSceneLinksStatus("The Word cursor is now in a different scene. Refresh Current Scene before saving."); return; } if (saveSceneLinksButton) { saveSceneLinksButton.disabled = true; saveSceneLinksButton.textContent = "Saving..."; } try { await putJson(`/api/word-companion/scenes/${resolvedSceneId}/links`, { characterIds: selectedLinkIds(characterChips), assetIds: selectedLinkIds(assetChips), locationIds: [], primaryLocationId: selectedPrimaryLocationId() }); updateCurrentSceneLinkState(); setSceneLinksStatus("Scene links saved successfully."); setDiagnostics({ lastError: "-" }); } catch (error) { setSceneLinksStatus("Unable to save scene links."); setDiagnostics({ lastError: errorText(error) }); } finally { if (saveSceneLinksButton) { saveSceneLinksButton.disabled = !resolvedSceneId || sceneContextStale; saveSceneLinksButton.textContent = "Save Scene Links"; } } }; 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 attachResolvedPlotDirectorIds = async (successMessage = "PlotDirector IDs attached successfully.") => { if (!resolvedSceneId || !resolvedChapterId) { setDocumentMessage("No resolved PlotDirector scene."); return false; } const replacingExistingAnchor = (detectedSceneAnchorId && detectedSceneAnchorId !== resolvedSceneId) || (detectedChapterAnchorId && detectedChapterAnchorId !== resolvedChapterId); if (replacingExistingAnchor && !window.confirm("This scene is already linked. Replace the existing PlotDirector link?")) { return false; } if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setDocumentMessage("Word document access is unavailable."); return false; } 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(successMessage); setDiagnostics({ lastError: "-" }); updateAnchorDisplay(); return true; } catch (error) { console.error("Unable to attach PlotDirector IDs.", error); setDocumentMessage("Unable to attach PlotDirector IDs."); setDiagnostics({ lastError: errorText(error) }); updateAnchorDisplay(); return false; } finally { if (attachPlotDirectorIdsButton) { attachPlotDirectorIdsButton.textContent = detectedSceneAnchorId === resolvedSceneId ? "Attach PlotDirector IDs" : "Reattach PlotDirector IDs"; updateAnchorDisplay(); } } }; const attachResolvedChapterId = async (successMessage) => { if (!resolvedChapterId) { setChapterCreateLinkStatus("No resolved PlotDirector chapter."); return false; } if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setChapterCreateLinkStatus("Word document access is unavailable."); return false; } try { await window.Word.run(async (context) => { const structure = await readDocumentStructure(context); if (!structure.currentChapter) { throw new Error("No chapter detected in Word. Use Heading 1 for chapters."); } const chapterParagraph = structure.bodyParagraphs[structure.currentChapter.paragraphIndex]; if (!chapterParagraph) { throw new Error("Unable to locate the current Word chapter heading."); } ensureAnchorControl(chapterParagraph, "PlotDirector Chapter", `PD-CHAPTER-${resolvedChapterId}`, "PD-CHAPTER"); await context.sync(); }); detectedChapterAnchorId = resolvedChapterId; storedAnchorInvalid = false; currentResolutionAnchorType = "ChapterID Anchor"; currentResolutionMethod = "Chapter anchor"; setChapterCreateLinkStatus(successMessage); setDocumentMessage(successMessage); setDiagnostics({ lastError: "-" }); updateAnchorDisplay(); return true; } catch (error) { console.error("Unable to attach PlotDirector chapter ID.", error); setChapterCreateLinkStatus("Unable to attach PlotDirector chapter ID."); setDiagnostics({ lastError: errorText(error) }); updateAnchorDisplay(); return false; } }; const attachPlotDirectorIds = async () => { await attachResolvedPlotDirectorIds(); }; const refreshAfterChapterLink = async (message) => { hideChapterCreateLink(); await refreshPlotDirectorScene(); setDocumentMessage(message); setChapterCreateLinkStatus(message); }; const closeCreateChapterDialog = (confirmed) => { if (pendingCreateChapterConfirmation) { pendingCreateChapterConfirmation(confirmed); pendingCreateChapterConfirmation = null; } if (createChapterDialog?.close) { createChapterDialog.close(); } else if (createChapterDialog) { createChapterDialog.hidden = true; } }; const confirmCreateChapter = (chapterTitle, bookTitle) => { setText(createChapterDialogChapter, `"${chapterTitle}"`); setText(createChapterDialogBook, `"${bookTitle}"`); if (!createChapterDialog) { return Promise.resolve(window.confirm(`Create PlotDirector chapter:\n\n"${chapterTitle}"\n\nin book:\n\n"${bookTitle}"\n\n?`)); } return new Promise((resolve) => { pendingCreateChapterConfirmation = resolve; if (createChapterDialog.showModal) { createChapterDialog.showModal(); } else { createChapterDialog.hidden = false; } }); }; const createPlotDirectorChapter = async () => { const book = selectedBook(); if (!book || !detectedChapterTitle || resolvedChapterId) { showChapterCreateLink("Chapter not found in PlotDirector."); return; } const chapterTitle = normalizeWhitespace(detectedChapterTitle); const confirmed = await confirmCreateChapter(chapterTitle, bookDisplayTitle(book) || "selected book"); if (!confirmed) { return; } if (createPlotDirectorChapterButton) { createPlotDirectorChapterButton.disabled = true; createPlotDirectorChapterButton.textContent = "Creating..."; } try { const sortOrder = Number.isInteger(detectedChapterIndex) && detectedChapterIndex > 0 ? detectedChapterIndex * 10 : null; const response = await postJson(`/api/word-companion/books/${book.bookId}/chapters`, { title: chapterTitle, sortOrder }); if (!response?.chapterId) { throw new Error("Chapter creation did not return a chapter ID."); } setResolvedScene(null, response.chapterId, "Manual chapter link"); const anchored = await attachResolvedChapterId("Chapter created and linked successfully."); if (!anchored) { return; } await refreshAfterChapterLink("Chapter created and linked successfully."); } catch (error) { setChapterCreateLinkStatus("Unable to create chapter."); setDiagnostics({ lastError: errorText(error) }); } finally { if (createPlotDirectorChapterButton) { createPlotDirectorChapterButton.disabled = !canUseChapterCreateLinkActions(); createPlotDirectorChapterButton.textContent = "Create Chapter in PlotDirector"; } } }; const renderLinkChapterOptions = () => { if (!linkChapterOptions) { return; } const query = normalizeWhitespace(linkChapterSearch?.value || "").toLocaleLowerCase(); const rows = linkDialogChapters.filter((chapter) => !query || String(chapter.title || "").toLocaleLowerCase().includes(query)); linkChapterOptions.innerHTML = ""; selectedLinkChapterId = null; if (linkSelectedChapterButton) { linkSelectedChapterButton.disabled = true; } if (rows.length === 0) { const empty = document.createElement("p"); empty.className = "word-companion-empty-list"; empty.textContent = "No chapters found."; linkChapterOptions.append(empty); return; } for (const chapter of rows) { const chapterId = Number.parseInt(chapter.chapterId || "", 10); if (!Number.isInteger(chapterId) || chapterId <= 0) { continue; } const label = document.createElement("label"); label.className = "word-companion-scene-option"; const radio = document.createElement("input"); radio.type = "radio"; radio.name = "word-companion-link-chapter"; radio.value = String(chapterId); radio.addEventListener("change", () => { selectedLinkChapterId = chapterId; if (linkSelectedChapterButton) { linkSelectedChapterButton.disabled = false; } }); const text = document.createElement("span"); text.textContent = chapter.title || "Untitled chapter"; label.append(radio, text); linkChapterOptions.append(label); } }; const openLinkExistingChapterDialog = async () => { const book = selectedBook(); if (!book || !detectedChapterTitle || resolvedChapterId) { showChapterCreateLink("Chapter not found in PlotDirector."); return; } if (linkExistingChapterButton) { linkExistingChapterButton.disabled = true; linkExistingChapterButton.textContent = "Loading..."; } try { linkDialogChapters = await fetchBookChapters(book.bookId); selectedLinkChapterId = null; if (linkChapterSearch) { linkChapterSearch.value = ""; } setText(linkChapterDialogStatus, ""); renderLinkChapterOptions(); if (linkExistingChapterDialog?.showModal) { linkExistingChapterDialog.showModal(); } else if (linkExistingChapterDialog) { linkExistingChapterDialog.hidden = false; } } catch (error) { setChapterCreateLinkStatus("Unable to load chapters."); setDiagnostics({ lastError: errorText(error) }); } finally { if (linkExistingChapterButton) { linkExistingChapterButton.disabled = !canUseChapterCreateLinkActions(); linkExistingChapterButton.textContent = "Link to Existing Chapter"; } } }; const closeLinkExistingChapterDialog = () => { if (linkExistingChapterDialog?.close) { linkExistingChapterDialog.close(); } else if (linkExistingChapterDialog) { linkExistingChapterDialog.hidden = true; } }; const linkSelectedExistingChapter = async () => { const chapter = linkDialogChapters.find((item) => item.chapterId === selectedLinkChapterId); if (!chapter) { setText(linkChapterDialogStatus, "Select a chapter."); return; } if (linkSelectedChapterButton) { linkSelectedChapterButton.disabled = true; linkSelectedChapterButton.textContent = "Linking..."; } try { setResolvedScene(null, chapter.chapterId, "Manual chapter link"); const anchored = await attachResolvedChapterId("Chapter linked successfully."); if (!anchored) { return; } closeLinkExistingChapterDialog(); await refreshAfterChapterLink("Chapter linked successfully."); } catch (error) { setText(linkChapterDialogStatus, "Unable to link chapter."); setChapterCreateLinkStatus("Unable to link chapter."); setDiagnostics({ lastError: errorText(error) }); } finally { if (linkSelectedChapterButton) { linkSelectedChapterButton.disabled = !selectedLinkChapterId; linkSelectedChapterButton.textContent = "Link Chapter"; } } }; const resolveCreatedOrLinkedScene = async (sceneId, chapterId, successMessage) => { const book = selectedBook(); if (!book) { setCreateLinkStatus("Select a PlotDirector book first."); return false; } await loadResolvedCompanionData(book.bookId, sceneId, chapterId, "Manual link", "Title Match"); const anchored = await attachResolvedPlotDirectorIds(successMessage); if (!anchored) { return false; } setCreateLinkStatus(successMessage); hideSceneCreateLink(); return true; }; const closeCreateSceneDialog = (confirmed) => { if (pendingCreateSceneConfirmation) { pendingCreateSceneConfirmation(confirmed); pendingCreateSceneConfirmation = null; } if (createSceneDialog?.close) { createSceneDialog.close(); } else if (createSceneDialog) { createSceneDialog.hidden = true; } }; const confirmCreateScene = (sceneTitle, chapterTitle) => { setText(createDialogScene, `"${sceneTitle}"`); setText(createDialogChapter, `"${chapterTitle}"`); if (!createSceneDialog) { return Promise.resolve(window.confirm(`Create PlotDirector scene:\n\n"${sceneTitle}"\n\nwithin chapter:\n\n"${chapterTitle}"\n\n?`)); } return new Promise((resolve) => { pendingCreateSceneConfirmation = resolve; if (createSceneDialog.showModal) { createSceneDialog.showModal(); } else { createSceneDialog.hidden = false; } }); }; const createPlotDirectorScene = async () => { const book = selectedBook(); if (!book || !detectedSceneTitle) { showSceneCreateLink("Scene not found in PlotDirector."); return; } const sceneTitle = normalizeTitleForResolve(detectedSceneTitle, "scene"); const chapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter"); const confirmed = await confirmCreateScene(sceneTitle, chapterTitle); if (!confirmed) { return; } if (createPlotDirectorSceneButton) { createPlotDirectorSceneButton.disabled = true; createPlotDirectorSceneButton.textContent = "Creating..."; } try { const chapter = await findCurrentChapter(); if (!chapter) { showSceneCreateLink("This chapter does not exist in PlotDirector. Create or link the chapter first."); return; } const response = await postJson(`/api/word-companion/books/${book.bookId}/chapters/${chapter.chapterId}/scenes`, { sceneTitle }); if (!response?.sceneId || !response?.chapterId) { throw new Error("Scene creation did not return a scene ID."); } await resolveCreatedOrLinkedScene(response.sceneId, response.chapterId, "Scene created and linked successfully."); } catch (error) { setCreateLinkStatus("Unable to create scene."); setDiagnostics({ lastError: errorText(error) }); } finally { if (createPlotDirectorSceneButton) { createPlotDirectorSceneButton.disabled = !canUseCreateLinkActions(); createPlotDirectorSceneButton.textContent = "Create Scene in PlotDirector"; } } }; const renderLinkSceneOptions = () => { if (!linkSceneOptions) { return; } const query = normalizeWhitespace(linkSceneSearch?.value || "").toLocaleLowerCase(); const rows = linkDialogScenes.filter((scene) => !query || String(scene.title || "").toLocaleLowerCase().includes(query)); linkSceneOptions.innerHTML = ""; selectedLinkSceneId = null; if (linkSelectedSceneButton) { linkSelectedSceneButton.disabled = true; } if (rows.length === 0) { const empty = document.createElement("p"); empty.className = "word-companion-empty-list"; empty.textContent = "No scenes found."; linkSceneOptions.append(empty); return; } for (const scene of rows) { const sceneId = Number.parseInt(scene.sceneId || "", 10); if (!Number.isInteger(sceneId) || sceneId <= 0) { continue; } const label = document.createElement("label"); label.className = "word-companion-scene-option"; const radio = document.createElement("input"); radio.type = "radio"; radio.name = "word-companion-link-scene"; radio.value = String(sceneId); radio.addEventListener("change", () => { selectedLinkSceneId = sceneId; if (linkSelectedSceneButton) { linkSelectedSceneButton.disabled = false; } }); const text = document.createElement("span"); text.textContent = scene.title || "Untitled scene"; label.append(radio, text); linkSceneOptions.append(label); } }; const openLinkExistingSceneDialog = async () => { const book = selectedBook(); if (!book || !detectedSceneTitle) { showSceneCreateLink("Scene not found in PlotDirector."); return; } if (linkExistingSceneButton) { linkExistingSceneButton.disabled = true; linkExistingSceneButton.textContent = "Loading..."; } try { const chapters = await getBookStructureChapters(book.bookId); const chapter = findCurrentChapterInChapters(chapters); if (!chapter) { showSceneCreateLink("This chapter does not exist in PlotDirector. Create or link the chapter first."); return; } linkDialogScenes = Array.isArray(chapter.scenes) ? chapter.scenes : []; selectedLinkSceneId = null; if (linkSceneSearch) { linkSceneSearch.value = ""; } setText(linkDialogStatus, ""); renderLinkSceneOptions(); if (linkExistingDialog?.showModal) { linkExistingDialog.showModal(); } else if (linkExistingDialog) { linkExistingDialog.hidden = false; } } catch (error) { setCreateLinkStatus("Unable to load scenes."); setDiagnostics({ lastError: errorText(error) }); } finally { if (linkExistingSceneButton) { linkExistingSceneButton.disabled = !canUseCreateLinkActions(); linkExistingSceneButton.textContent = "Link to Existing Scene"; } } }; const closeLinkExistingSceneDialog = () => { if (linkExistingDialog?.close) { linkExistingDialog.close(); } else if (linkExistingDialog) { linkExistingDialog.hidden = true; } }; const linkSelectedExistingScene = async () => { const book = selectedBook(); const scene = linkDialogScenes.find((item) => item.sceneId === selectedLinkSceneId); if (!book || !scene) { setText(linkDialogStatus, "Select a scene."); return; } if (linkSelectedSceneButton) { linkSelectedSceneButton.disabled = true; linkSelectedSceneButton.textContent = "Linking..."; } try { const chapter = await findCurrentChapter(); if (!chapter) { throw new Error("This chapter does not exist in PlotDirector. Create or link the chapter first."); } closeLinkExistingSceneDialog(); await resolveCreatedOrLinkedScene(scene.sceneId, chapter.chapterId, "Scene linked successfully."); } catch (error) { setText(linkDialogStatus, "Unable to link scene."); setCreateLinkStatus("Unable to link scene."); setDiagnostics({ lastError: errorText(error) }); } finally { if (linkSelectedSceneButton) { linkSelectedSceneButton.disabled = !selectedLinkSceneId; linkSelectedSceneButton.textContent = "Link Scene"; } } }; const syncSceneProgress = async () => { if (!resolvedSceneId) { setSyncStatus("No resolved PlotDirector scene."); return; } if (sceneContextStale) { setSyncStatus("Refresh Current Scene before syncing."); return; } if (!Number.isInteger(detectedSceneWordCount)) { setSyncStatus("Unable to sync progress."); return; } if (syncSceneProgressButton) { syncSceneProgressButton.disabled = true; syncSceneProgressButton.textContent = "Syncing..."; } const lastWorkedOn = new Date().toISOString(); try { const response = await patchJson(`/api/word-companion/scenes/${resolvedSceneId}/word-count`, { actualWordCount: detectedSceneWordCount, lastWorkedOn }); setText(syncPlotDirectorCount, displayValue(response?.actualWords ?? detectedSceneWordCount)); setText(plotDirectorActualWords, displayValue(response?.actualWords ?? detectedSceneWordCount)); setText(syncLastSynced, new Date().toLocaleString()); setSyncStatus("Progress synced successfully."); } catch { setSyncStatus("Unable to sync progress."); } finally { if (syncSceneProgressButton) { syncSceneProgressButton.disabled = !resolvedSceneId || sceneContextStale; syncSceneProgressButton.textContent = "Sync Progress"; } } }; const refreshPlotDirectorChapter = async () => { const project = selectedProject(); const book = selectedBook(); const requestChapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter"); setResolveDiagnostics({ projectId: project?.projectId, projectTitle: project?.title, bookId: book?.bookId, bookTitle: book ? bookDisplayTitle(book) : undefined, detectedChapter: detectedChapterTitle, detectedScene: detectedSceneTitle, requestChapter: requestChapterTitle, requestScene: "", result: "Not run", chapterId: null, sceneId: null }); if (!book) { resetPlotDirectorScene("Not detected"); setDocumentMessage("Select a PlotDirector book first."); return false; } if (!detectedChapterTitle) { resetPlotDirectorScene("Not detected"); setDocumentMessage("No chapter detected in Word. Use Heading 1 for chapters."); return false; } resetPlotDirectorScene("Not detected"); try { const chapters = await fetchBookChapters(book.bookId); const anchoredChapter = detectedChapterAnchorId ? chapters.find((chapter) => chapter.chapterId === detectedChapterAnchorId) : null; const requestChapterKey = requestChapterTitle.toLocaleLowerCase(); const titleChapter = requestChapterKey ? chapters.find((chapter) => normalizeTitleForResolve(chapter.title, "chapter").toLocaleLowerCase() === requestChapterKey) : null; const chapter = anchoredChapter || titleChapter; if (chapter) { hideChapterCreateLink(); setResolvedScene(null, chapter.chapterId, anchoredChapter ? "Chapter anchor" : "Chapter title"); currentResolutionAnchorType = anchoredChapter ? "ChapterID Anchor" : "Title Match"; setPlotDirectorSceneStatus("Chapter linked to PlotDirector"); setDocumentMessage("No scene detected in Word. Use Heading 2 for scenes."); setResolveDiagnostics({ result: "Chapter matched", chapterId: chapter.chapterId, sceneId: null }); return true; } setResolvedScene(null); setPlotDirectorSceneStatus("Chapter not found in PlotDirector."); setDocumentMessage("Chapter not found in PlotDirector."); setResolveDiagnostics({ result: "Chapter not matched", chapterId: null, sceneId: null }); showChapterCreateLink("Chapter not found in PlotDirector."); return false; } catch (error) { resetPlotDirectorScene("Not found in PlotDirector"); setDiagnostics({ lastError: errorText(error) }); setDocumentMessage("Unable to load PlotDirector chapter data."); return false; } }; 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 ? bookDisplayTitle(book) : undefined, detectedChapter: detectedChapterTitle, detectedScene: detectedSceneTitle, requestChapter: requestChapterTitle, requestScene: requestSceneTitle, result: "Not run", chapterId: null, sceneId: null }); setAnchorDiagnostics({ anchorType: detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match", storedSceneId: detectedSceneAnchorId, storedChapterId: detectedChapterAnchorId, resolutionMethod: "-" }); if (!book) { resetPlotDirectorScene("Not detected"); setDocumentMessage("Select a PlotDirector book first."); setResolveDiagnostics({ result: "Error" }); return false; } if (!detectedChapterTitle || !detectedSceneTitle) { resetPlotDirectorScene("Not detected"); setDocumentMessage("No scene detected in Word. Use Heading 2 for scenes."); setResolveDiagnostics({ result: "Error" }); return false; } if (refreshPlotDirectorSceneButton) { refreshPlotDirectorSceneButton.disabled = true; refreshPlotDirectorSceneButton.textContent = "Refreshing..."; } const restorePlotDirectorSceneButton = () => { if (refreshPlotDirectorSceneButton) { refreshPlotDirectorSceneButton.disabled = false; refreshPlotDirectorSceneButton.textContent = "Refresh PlotDirector Scene"; } }; resetPlotDirectorScene("Not detected"); setDocumentMessage("Resolving PlotDirector scene..."); 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 chapters = await getBookStructureChapters(book.bookId); const anchoredChapter = chapters.find((chapter) => chapter.chapterId === detectedChapterAnchorId); const anchoredChapterScene = anchoredChapter ? (Array.isArray(anchoredChapter.scenes) ? anchoredChapter.scenes : []) .find((scene) => normalizeTitleForResolve(scene.title, "scene").toLocaleLowerCase() === requestSceneTitle.toLocaleLowerCase()) : null; if (anchoredChapterScene) { setResolveDiagnostics({ result: "Matched", chapterId: anchoredChapter.chapterId, sceneId: anchoredChapterScene.sceneId }); await loadResolvedCompanionData(book.bookId, anchoredChapterScene.sceneId, anchoredChapter.chapterId, "Anchor", "ChapterID Anchor"); return true; } if (anchoredChapter && !detectedSceneAnchorId) { currentResolutionAnchorType = "ChapterID Anchor"; setResolvedScene(null, detectedChapterAnchorId, "Chapter anchor"); setResolveDiagnostics({ result: "Chapter matched", chapterId: detectedChapterAnchorId, sceneId: null }); } else if (!detectedSceneAnchorId) { currentResolutionAnchorType = "ChapterID Anchor"; setResolveDiagnostics({ result: "Chapter anchor not found", chapterId: detectedChapterAnchorId, sceneId: null }); } } const resolveResponse = await postJson(`/api/word-companion/books/${book.bookId}/resolve-scene`, { chapterTitle: requestChapterTitle, sceneTitle: requestSceneTitle }); if (!resolveResponse?.matched || !resolveResponse.sceneId) { const hadInvalidAnchor = storedAnchorInvalid; const matchedChapterId = Number.parseInt(resolveResponse?.chapterId || "", 10); const anchorChapterId = Number.isInteger(resolvedChapterId) && resolvedChapterId > 0 ? resolvedChapterId : null; const unmatchedChapterId = Number.isInteger(matchedChapterId) && matchedChapterId > 0 ? matchedChapterId : anchorChapterId; const chapterMatched = !!resolveResponse?.chapterMatched || !!unmatchedChapterId; resetPlotDirectorScene("Not found in PlotDirector"); storedAnchorInvalid = hadInvalidAnchor; if (chapterMatched) { setResolvedScene(null, unmatchedChapterId, unmatchedChapterId === anchorChapterId ? "Chapter anchor" : "Chapter title"); setPlotDirectorSceneStatus("Scene not found in PlotDirector."); } setDocumentMessage(hadInvalidAnchor ? "Stored PlotDirector ID is invalid." : chapterMatched ? "Scene not found in PlotDirector." : "This chapter does not exist in PlotDirector. Create or link the chapter first."); setResolveDiagnostics({ result: "Not matched", chapterId: chapterMatched ? unmatchedChapterId : resolveResponse?.chapterId, sceneId: resolveResponse?.sceneId }); if (!hadInvalidAnchor) { if (chapterMatched) { hideChapterCreateLink(); showSceneCreateLink("Scene not found in PlotDirector."); } else { hideSceneCreateLink(); showChapterCreateLink("Chapter not found in PlotDirector."); } } restorePlotDirectorSceneButton(); return false; } const hadInvalidAnchor = storedAnchorInvalid; setResolveDiagnostics({ result: hadInvalidAnchor ? "Matched by title fallback" : "Matched", chapterId: resolveResponse.chapterId, sceneId: resolveResponse.sceneId }); await loadResolvedCompanionData(book.bookId, resolveResponse.sceneId, resolveResponse.chapterId, hadInvalidAnchor ? "Title fallback" : "Title", "Title Match"); if (hadInvalidAnchor) { storedAnchorInvalid = true; setDocumentMessage("Stored PlotDirector ID is invalid."); updateAnchorDisplay(); } return true; } catch (error) { const hadInvalidAnchor = storedAnchorInvalid; setDiagnostics({ lastError: errorText(error) }); resetPlotDirectorScene("Not found in PlotDirector"); storedAnchorInvalid = hadInvalidAnchor; setDocumentMessage(hadInvalidAnchor ? "Stored PlotDirector ID is invalid." : "Unable to load PlotDirector scene data."); setResolveDiagnostics({ result: "Error" }); return false; } finally { restorePlotDirectorSceneButton(); } }; const hasCurrentWordContextChanged = (structure) => { const chapterTitle = structure.currentChapter?.title || ""; const sceneTitle = structure.currentScene?.title || ""; const chapterAnchorId = structure.currentChapter?.anchorId || null; const sceneAnchorId = structure.currentScene?.anchorId || null; return normalizeTitleForResolve(chapterTitle, "chapter") !== normalizeTitleForResolve(detectedChapterTitle, "chapter") || normalizeTitleForResolve(sceneTitle, "scene") !== normalizeTitleForResolve(detectedSceneTitle, "scene") || chapterAnchorId !== detectedChapterAnchorId || sceneAnchorId !== detectedSceneAnchorId; }; const refreshCurrentSceneFromSelection = async () => { if (isAutoRefreshingScene || !isAuthenticated || !selectedBook()) { return; } if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setSceneTrackingState("Manual"); setDocumentMessage("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."); return; } isAutoRefreshingScene = true; setSceneTrackingState("Refreshing"); setDocumentMessage("Updating current scene..."); try { const structure = await window.Word.run(async (context) => { return await readDocumentStructure(context); }); const changed = hasCurrentWordContextChanged(structure); setCurrentStructure( structure.currentChapter?.title, structure.currentScene?.title, structure.currentScene?.wordCount, structure.currentChapter?.anchorId, structure.currentScene?.anchorId, structure.currentChapter?.index ); renderOutline(structure); setDiagnostics({ lastScan: "Success", lastError: "-", paragraphs: String(structure.paragraphCount), heading1: String(structure.heading1Count), heading2: String(structure.heading2Count) }); if (!changed) { setSceneContextStale(false); setSceneTrackingState("Live"); setDocumentMessage("Current scene updated."); return; } if (!detectedSceneTitle) { await refreshPlotDirectorChapter(); } else { await refreshPlotDirectorScene(); } setSceneContextStale(false); setSceneTrackingState("Live"); setDocumentMessage("Current scene updated."); } catch (error) { console.error("Unable to refresh current scene from Word selection.", error); setDiagnostics({ lastScan: "Failure", lastError: errorText(error) }); setSceneContextStale(true, "The Word cursor is now in a different scene. Refresh Current Scene before saving."); } finally { isAutoRefreshingScene = false; } }; const scheduleSelectionRefresh = () => { if (selectionRefreshTimer) { window.clearTimeout(selectionRefreshTimer); } selectionRefreshTimer = window.setTimeout(refreshCurrentSceneFromSelection, 850); }; const handleDocumentSelectionChanged = () => { scheduleSelectionRefresh(); }; const removeSelectionTracking = () => { if (!selectionHandlerRegistered || !window.Office?.context?.document || typeof window.Office.context.document.removeHandlerAsync !== "function" || !window.Office?.EventType?.DocumentSelectionChanged) { return; } try { window.Office.context.document.removeHandlerAsync( window.Office.EventType.DocumentSelectionChanged, { handler: handleDocumentSelectionChanged } ); selectionHandlerRegistered = false; } catch (error) { console.warn("Unable to remove Word selection tracking handler.", error); } }; const setupSelectionTracking = () => { if (!isAuthenticated) { return; } if (!window.Office?.context?.document || typeof window.Office.context.document.addHandlerAsync !== "function" || !window.Office?.EventType?.DocumentSelectionChanged) { setSceneTrackingState("Manual"); setDocumentMessage("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."); return; } try { window.Office.context.document.addHandlerAsync( window.Office.EventType.DocumentSelectionChanged, handleDocumentSelectionChanged, (result) => { if (result?.status === window.Office.AsyncResultStatus.Succeeded) { selectionHandlerRegistered = true; setSceneTrackingState("Live"); } else { setSceneTrackingState("Manual"); setDocumentMessage("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."); } } ); window.addEventListener("beforeunload", removeSelectionTracking); } catch (error) { console.warn("Unable to register Word selection tracking handler.", error); setSceneTrackingState("Manual"); setDocumentMessage("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."); } }; const refreshCurrentScene = async () => { if (!selectedBook()) { setDocumentMessage("Select a PlotDirector book first."); resetPlotDirectorScene("Not detected"); return; } setRefreshCurrentSceneBusy(true); try { const scanned = await refreshDocumentStructure(); if (!scanned) { return; } setSceneContextStale(false); if (!detectedSceneTitle) { await refreshPlotDirectorChapter(); return; } await refreshPlotDirectorScene(); } finally { setRefreshCurrentSceneBusy(false); } }; const loadBooks = async (projectId, preferredBookId) => { if (!projectId) { books = []; resetSelect(bookSelect, "Select a project first"); setBookMessage(""); writeStoredId(storageKeys.bookId, null); updateSummary(); resetPlotDirectorScene("Not detected"); resetStructurePreview(); return; } resetSelect(bookSelect, "Loading books..."); setBookMessage(""); resetPlotDirectorScene("Not detected"); resetStructurePreview("Select a book to analyse manuscript structure."); 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.map((book) => ({ ...book, displayTitle: bookDisplayTitle(book) })), "bookId", "displayTitle"); 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() ? "Not detected" : "Not detected"); resetStructurePreview(selectedBook() ? "Ready to analyse manuscript structure." : "Select a book to analyse manuscript structure."); } catch { books = []; resetSelect(bookSelect, "Unable to load books."); setBookMessage("Unable to load books."); writeStoredId(storageKeys.bookId, null); updateSummary(); resetPlotDirectorScene("Not detected"); resetStructurePreview("Unable to load books."); } }; 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(); } }; for (const button of tabButtons) { button.addEventListener("click", () => selectTab(button.dataset.tabButton)); } restoreActiveTab(); 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("Not detected"); resetStructurePreview(); 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("Not detected"); resetStructurePreview(selectedBook() ? "Ready to analyse manuscript structure." : "Select a book to analyse manuscript structure."); }); refreshDocumentButton?.addEventListener("click", refreshDocumentStructure); analyseManuscriptStructureButton?.addEventListener("click", analyseManuscriptStructure); applyStructureSyncButton?.addEventListener("click", applyStructureSync); applyStructureSyncConfirm?.addEventListener("click", () => closeApplyStructureSyncDialog(true)); applyStructureSyncCancel?.addEventListener("click", () => closeApplyStructureSyncDialog(false)); applyStructureSyncDialog?.addEventListener("cancel", (event) => { event.preventDefault(); closeApplyStructureSyncDialog(false); }); refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene); refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene); syncSceneProgressButton?.addEventListener("click", syncSceneProgress); saveSceneLinksButton?.addEventListener("click", saveSceneLinks); attachPlotDirectorIdsButton?.addEventListener("click", attachPlotDirectorIds); createPlotDirectorChapterButton?.addEventListener("click", createPlotDirectorChapter); linkExistingChapterButton?.addEventListener("click", openLinkExistingChapterDialog); linkChapterSearch?.addEventListener("input", renderLinkChapterOptions); linkSelectedChapterButton?.addEventListener("click", linkSelectedExistingChapter); linkChapterDialogCancel?.addEventListener("click", closeLinkExistingChapterDialog); createChapterDialogConfirm?.addEventListener("click", () => closeCreateChapterDialog(true)); createChapterDialogCancel?.addEventListener("click", () => closeCreateChapterDialog(false)); createChapterDialog?.addEventListener("cancel", (event) => { event.preventDefault(); closeCreateChapterDialog(false); }); createPlotDirectorSceneButton?.addEventListener("click", createPlotDirectorScene); linkExistingSceneButton?.addEventListener("click", openLinkExistingSceneDialog); linkSceneSearch?.addEventListener("input", renderLinkSceneOptions); linkSelectedSceneButton?.addEventListener("click", linkSelectedExistingScene); linkDialogCancel?.addEventListener("click", closeLinkExistingSceneDialog); createDialogConfirm?.addEventListener("click", () => closeCreateSceneDialog(true)); createDialogCancel?.addEventListener("click", () => closeCreateSceneDialog(false)); createSceneDialog?.addEventListener("cancel", (event) => { event.preventDefault(); closeCreateSceneDialog(false); }); 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."); setSceneTrackingState("Manual"); setDiagnostics({ host: "Unavailable", platform: "Unavailable", ready: "No", wordApi: "No" }); return; } updateOfficeReadiness() .then(() => { setOfficeStatus("Word Companion ready"); if (!wordHostAvailable) { setDocumentMessage("Word document access is unavailable."); setSceneTrackingState("Manual"); } else { setupSelectionTracking(); } }) .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" }); }); })();