diff --git a/PlotLine/Views/WordCompanionHost/Index.cshtml b/PlotLine/Views/WordCompanionHost/Index.cshtml index 8eaf9a7..0758a89 100644 --- a/PlotLine/Views/WordCompanionHost/Index.cshtml +++ b/PlotLine/Views/WordCompanionHost/Index.cshtml @@ -75,6 +75,47 @@ +
+
+ Diagnostics + +
+
+
+ Office Host + Unknown +
+
+ Office Platform + Unknown +
+
+ Office Ready + No +
+
+ Word API Available + No +
+
+ Last Scan Result + Not run +
+
+ Paragraphs Found + - +
+
+ Heading 1 Found + - +
+
+ Heading 2 Found + - +
+
+
+
Connection Status diff --git a/PlotLine/wwwroot/css/word-companion.css b/PlotLine/wwwroot/css/word-companion.css index db3b6fc..11fbe7b 100644 --- a/PlotLine/wwwroot/css/word-companion.css +++ b/PlotLine/wwwroot/css/word-companion.css @@ -158,11 +158,22 @@ body { gap: 8px; } -.word-companion-structure-grid div { +.word-companion-structure-grid div, +.word-companion-diagnostics-grid div { display: grid; gap: 2px; } +.word-companion-diagnostics-grid { + display: grid; + gap: 8px; + grid-template-columns: 1fr 1fr; +} + +.word-companion-diagnostics-grid strong { + font-size: 0.88rem; +} + .word-companion-outline { display: grid; gap: 8px; diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index 15a0504..837975e 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -21,11 +21,22 @@ const documentMessage = document.querySelector("[data-document-message]"); const syncNote = document.querySelector("[data-sync-note]"); const documentOutline = document.querySelector("[data-document-outline]"); + const runDiagnosticsButton = document.querySelector("[data-run-diagnostics]"); + const diagnosticHost = document.querySelector("[data-diagnostic-host]"); + const diagnosticPlatform = document.querySelector("[data-diagnostic-platform]"); + const diagnosticReady = document.querySelector("[data-diagnostic-ready]"); + const diagnosticWordApi = document.querySelector("[data-diagnostic-word-api]"); + const diagnosticLastScan = document.querySelector("[data-diagnostic-last-scan]"); + const diagnosticParagraphs = document.querySelector("[data-diagnostic-paragraphs]"); + const diagnosticHeading1 = document.querySelector("[data-diagnostic-heading1]"); + const diagnosticHeading2 = document.querySelector("[data-diagnostic-heading2]"); const isAuthenticated = shell?.dataset.authenticated === "true"; let projects = []; let books = []; let officeReady = false; let wordHostAvailable = false; + let officeHost = "Unknown"; + let officePlatform = "Unknown"; const setOfficeStatus = (message) => { if (officeStatus) { @@ -72,6 +83,91 @@ } }; + const setText = (element, value) => { + if (element) { + element.textContent = value; + } + }; + + const setDiagnostics = (values) => { + const hasValue = (name) => Object.prototype.hasOwnProperty.call(values, name); + + if (hasValue("host")) { + setText(diagnosticHost, values.host); + } + if (hasValue("platform")) { + setText(diagnosticPlatform, values.platform); + } + if (hasValue("ready")) { + setText(diagnosticReady, values.ready); + } + if (hasValue("wordApi")) { + setText(diagnosticWordApi, values.wordApi); + } + if (hasValue("lastScan")) { + setText(diagnosticLastScan, values.lastScan); + } + if (hasValue("paragraphs")) { + setText(diagnosticParagraphs, values.paragraphs); + } + if (hasValue("heading1")) { + setText(diagnosticHeading1, values.heading1); + } + if (hasValue("heading2")) { + setText(diagnosticHeading2, values.heading2); + } + }; + + const errorText = (error) => { + const parts = []; + + if (error?.name) { + parts.push(error.name); + } + if (error?.code && error.code !== error.name) { + parts.push(error.code); + } + if (error?.message) { + parts.push(error.message); + } + if (error?.debugInfo?.errorLocation) { + parts.push(`Location: ${error.debugInfo.errorLocation}`); + } + + return parts.length > 0 ? parts.join(": ") : String(error); + }; + + const updateOfficeReadiness = async () => { + if (!window.Office || typeof window.Office.onReady !== "function") { + officeReady = false; + wordHostAvailable = false; + officeHost = "Unavailable"; + officePlatform = "Unavailable"; + setDiagnostics({ + host: officeHost, + platform: officePlatform, + ready: "No", + wordApi: "No" + }); + return null; + } + + const info = await window.Office.onReady(); + officeReady = true; + officeHost = info?.host || "Unknown"; + officePlatform = info?.platform || "Unknown"; + wordHostAvailable = !!window.Word + && !!window.Office.HostType + && info?.host === window.Office.HostType.Word; + setDiagnostics({ + host: officeHost, + platform: officePlatform, + ready: "Yes", + wordApi: wordHostAvailable ? "Yes" : "No" + }); + return info; + }; + const readStoredId = (key) => { try { const value = window.localStorage.getItem(key); @@ -175,6 +271,8 @@ const buildDocumentStructure = (paragraphs, selectionParagraphs) => { const chapters = []; + let heading1Count = 0; + let heading2Count = 0; let chapter = null; let scene = null; @@ -185,6 +283,7 @@ } if (isBuiltInHeading(paragraph, 1)) { + heading1Count += 1; chapter = { index: chapters.length + 1, paragraphIndex: index, @@ -197,6 +296,7 @@ } if (isBuiltInHeading(paragraph, 2)) { + heading2Count += 1; if (!chapter) { return; } @@ -230,7 +330,10 @@ return { chapters, currentChapter: detectedChapter, - currentScene: detectedScene + currentScene: detectedScene, + paragraphCount: paragraphs.length, + heading1Count, + heading2Count }; }; @@ -283,6 +386,7 @@ if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { setCurrentStructure(null, null, null); setDocumentMessage("Word document access is unavailable."); + setDiagnostics({ lastScan: "Failure" }); return; } @@ -311,9 +415,18 @@ ); renderOutline(structure); setDocumentMessage(""); - } catch { + setDiagnostics({ + lastScan: "Success", + paragraphs: String(structure.paragraphCount), + heading1: String(structure.heading1Count), + heading2: String(structure.heading2Count) + }); + } catch (error) { + const message = errorText(error); + console.error("Unable to scan the Word document.", error); setCurrentStructure(null, null, null); - setDocumentMessage("Unable to scan the Word document."); + setDocumentMessage(`Unable to scan the Word document: ${message}`); + setDiagnostics({ lastScan: "Failure" }); } finally { if (refreshDocumentButton) { refreshDocumentButton.disabled = false; @@ -322,6 +435,48 @@ } }; + const runDiagnostics = async () => { + if (runDiagnosticsButton) { + runDiagnosticsButton.disabled = true; + runDiagnosticsButton.textContent = "Running..."; + } + + try { + await updateOfficeReadiness(); + + if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") { + setDiagnostics({ + lastScan: "Failure", + paragraphs: "-", + heading1: "-", + heading2: "-" + }); + return; + } + + const paragraphCount = await window.Word.run(async (context) => { + const paragraphs = context.document.body.paragraphs; + paragraphs.load("items"); + await context.sync(); + return paragraphs.items.length; + }); + + setDiagnostics({ + lastScan: "Success", + paragraphs: String(paragraphCount) + }); + } catch (error) { + console.error("Word Companion diagnostics failed.", error); + setDiagnostics({ lastScan: "Failure" }); + setDocumentMessage(`Unable to scan the Word document: ${errorText(error)}`); + } finally { + if (runDiagnosticsButton) { + runDiagnosticsButton.disabled = false; + runDiagnosticsButton.textContent = "Run Diagnostics"; + } + } + }; + const fetchJson = async (url) => { const response = await window.fetch(url, { credentials: "same-origin", @@ -436,6 +591,7 @@ }); refreshDocumentButton?.addEventListener("click", refreshDocumentStructure); + runDiagnosticsButton?.addEventListener("click", runDiagnostics); if (isAuthenticated) { loadProjects(); @@ -448,23 +604,31 @@ if (!window.Office || typeof window.Office.onReady !== "function") { setOfficeStatus("Word Companion ready"); setDocumentMessage("Word document access is unavailable."); + setDiagnostics({ + host: "Unavailable", + platform: "Unavailable", + ready: "No", + wordApi: "No" + }); return; } - window.Office.onReady() - .then((info) => { - officeReady = true; - wordHostAvailable = !!window.Word - && !!window.Office.HostType - && info?.host === window.Office.HostType.Word; + updateOfficeReadiness() + .then(() => { setOfficeStatus("Word Companion ready"); if (!wordHostAvailable) { setDocumentMessage("Word document access is unavailable."); } }) - .catch(() => { + .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" + }); }); })();