Phase 10D.1: Word Companion Diagnostics

This commit is contained in:
Nick Beckley 2026-06-13 17:17:40 +01:00
parent 2b77e85bbc
commit 234176b3ae
3 changed files with 227 additions and 11 deletions

View File

@ -75,6 +75,47 @@
</div>
</section>
<section class="word-companion-section" aria-label="Diagnostics">
<div class="word-companion-section-header">
<span>Diagnostics</span>
<button type="button" data-run-diagnostics>Run Diagnostics</button>
</div>
<div class="word-companion-diagnostics-grid">
<div>
<span>Office Host</span>
<strong data-diagnostic-host>Unknown</strong>
</div>
<div>
<span>Office Platform</span>
<strong data-diagnostic-platform>Unknown</strong>
</div>
<div>
<span>Office Ready</span>
<strong data-diagnostic-ready>No</strong>
</div>
<div>
<span>Word API Available</span>
<strong data-diagnostic-word-api>No</strong>
</div>
<div>
<span>Last Scan Result</span>
<strong data-diagnostic-last-scan>Not run</strong>
</div>
<div>
<span>Paragraphs Found</span>
<strong data-diagnostic-paragraphs>-</strong>
</div>
<div>
<span>Heading 1 Found</span>
<strong data-diagnostic-heading1>-</strong>
</div>
<div>
<span>Heading 2 Found</span>
<strong data-diagnostic-heading2>-</strong>
</div>
</div>
</section>
<section class="word-companion-summary" aria-label="Connection summary">
<div>
<span>Connection Status</span>

View File

@ -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;

View File

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