(() => { const roots = [...document.querySelectorAll("[data-word-companion-presence]")] .filter((node) => node.dataset.wordCompanionPresence !== "false"); if (roots.length === 0 || !window.signalR) { return; } const labelFor = (status) => (status?.isConnected || status?.IsConnected ? "Connected" : status?.status || status?.Status || "Offline"); const field = (status, camel, pascal) => status?.[camel] ?? status?.[pascal] ?? null; const formatTime = (value) => { if (!value) { return "No heartbeat yet"; } const date = new Date(value); return Number.isNaN(date.getTime()) ? "No heartbeat yet" : `Last heartbeat ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`; }; const applyStatus = (status) => { const connected = !!field(status, "isConnected", "IsConnected"); const version = field(status, "companionVersion", "CompanionVersion") || "Waiting"; const documentName = field(status, "currentDocumentName", "CurrentDocumentName") || "No document reported yet"; const label = connected ? "Connected" : labelFor(status); const title = [ `Word Companion: ${label}`, version && version !== "Waiting" ? `Version ${version}` : "", documentName && documentName !== "No document reported yet" ? documentName : "", formatTime(field(status, "lastHeartbeatUtc", "LastHeartbeatUtc")) ].filter(Boolean).join("\n"); for (const root of roots) { root.classList.toggle("is-connected", connected); root.classList.toggle("is-offline", !connected); root.dataset.wordCompanionStatus = label; root.title = title; } document.querySelectorAll("[data-word-companion-status-label]").forEach((node) => { node.textContent = label; }); document.querySelectorAll("[data-word-companion-waiting-status]").forEach((node) => { node.textContent = connected ? "Word Companion Connected" : "Word Companion disconnected. Waiting for reconnection..."; }); document.querySelectorAll("[data-word-companion-connected-copy]").forEach((node) => { node.textContent = connected ? "Your Companion is connected. Continue when you are ready." : "Open Word and start the Companion. This page will update automatically."; }); document.querySelectorAll("[data-word-companion-version]").forEach((node) => { node.textContent = version; }); document.querySelectorAll("[data-word-companion-document]").forEach((node) => { node.textContent = documentName; }); document.querySelectorAll("[data-word-companion-dashboard-title]").forEach((node) => { node.textContent = connected ? "Word Companion Connected" : "Connect your Word Companion"; }); document.querySelectorAll("[data-word-companion-dashboard-description]").forEach((node) => { node.textContent = connected ? "Your Word Companion is ready for this story setup." : "Open Word, start the Companion, and sign in with your PlotDirector account."; }); }; const connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/word-companion-follow") .withAutomaticReconnect() .build(); connection.on("WordCompanionPresenceChanged", applyStatus); connection.onreconnecting(() => applyStatus({ status: "Connecting", isConnected: false })); connection.onreconnected(() => connection.invoke("WatchCompanionPresence").then(applyStatus).catch(() => {})); connection.onclose(() => applyStatus({ status: "Offline", isConnected: false })); connection.start() .then(() => connection.invoke("WatchCompanionPresence")) .then(applyStatus) .catch(() => applyStatus({ status: "Offline", isConnected: false })); })();